chrome-devtools-frontend
Version:
Chrome DevTools UI
138 lines (123 loc) • 4.55 kB
text/typescript
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
import { AsyncDirective,directive} from '../async-directive.js';
import { ElementPart,nothing} from '../lit-html.js';
/**
* Creates a new Ref object, which is container for a reference to an element.
*/
export const createRef = <T = Element>() => new Ref<T>();
class Ref<T = Element> {
/**
* The current Element value of the ref, or else `undefined` if the ref is no
* longer rendered.
*/
readonly value?: T;
}
interface RefInternal {
value: Element | undefined;
}
// When callbacks are used for refs, this map tracks the last value the callback
// was called with, for ensuring a directive doesn't clear the ref if the ref
// has already been rendered to a new spot
const lastElementForCallback: WeakMap<
Function,
Element | undefined
> = new WeakMap();
export type RefOrCallback = Ref | ((el: Element | undefined) => void);
class RefDirective extends AsyncDirective {
private _element?: Element;
private _ref?: RefOrCallback;
private _context: unknown;
render(_ref: RefOrCallback) {
return nothing;
}
update(part: ElementPart, [ref]: Parameters<this['render']>) {
const refChanged = ref !== this._ref;
if (refChanged && this._ref !== undefined) {
// The ref passed to the directive has changed;
// unset the previous ref's value
this._updateRefValue(undefined);
}
if (refChanged || this._lastElementForRef !== this._element) {
// We either got a new ref or this is the first render;
// store the ref/element & update the ref value
this._ref = ref;
this._context = part.options?.host;
this._updateRefValue((this._element = part.element));
}
return nothing;
}
private _updateRefValue(element: Element | undefined) {
if (typeof this._ref === 'function') {
// If the current ref was called with a previous value, call with
// `undefined`; We do this to ensure callbacks are called in a consistent
// way regardless of whether a ref might be moving up in the tree (in
// which case it would otherwise be called with the new value before the
// previous one unsets it) and down in the tree (where it would be unset
// before being set)
if (lastElementForCallback.get(this._ref) !== undefined) {
this._ref.call(this._context, undefined);
}
lastElementForCallback.set(this._ref, element);
// Call the ref with the new element value
if (element !== undefined) {
this._ref.call(this._context, element);
}
} else {
(this._ref as RefInternal)!.value = element;
}
}
private get _lastElementForRef() {
return typeof this._ref === 'function'
? lastElementForCallback.get(this._ref)
: this._ref?.value;
}
disconnected() {
// Only clear the box if our element is still the one in it (i.e. another
// directive instance hasn't rendered its element to it before us); that
// only happens in the event of the directive being cleared (not via manual
// disconnection)
if (this._lastElementForRef === this._element) {
this._updateRefValue(undefined);
}
}
reconnected() {
// If we were manually disconnected, we can safely put our element back in
// the box, since no rendering could have occurred to change its state
this._updateRefValue(this._element);
}
}
/**
* Sets the value of a Ref object or calls a ref callback with the element it's
* bound to.
*
* A Ref object acts as a container for a reference to an element. A ref
* callback is a function that takes an element as its only argument.
*
* The ref directive sets the value of the Ref object or calls the ref callback
* during rendering, if the referenced element changed.
*
* Note: If a ref callback is rendered to a different element position or is
* removed in a subsequent render, it will first be called with `undefined`,
* followed by another call with the new element it was rendered to (if any).
*
* @example
*
* // Using Ref object
* const inputRef = createRef();
* render(html`<input ${ref(inputRef)}>`, container);
* inputRef.value.focus();
*
* // Using callback
* const callback = (inputElement) => inputElement.focus();
* render(html`<input ${ref(callback)}>`, container);
*/
export const ref = directive(RefDirective);
/**
* The type of the class that powers this directive. Necessary for naming the
* directive's return type.
*/
export type {RefDirective};