UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

546 lines (519 loc) 17.2 kB
import { getOwner, setOwner } from '../@ember/-internals/owner/index.js'; import { g as guidFor } from './guid-Cbq2sNV_.js'; import { getViewElement, clearElementView, clearViewElement, addChildView, setViewElement, setElementView } from '../@ember/-internals/views/lib/system/utils.js'; import { _instrumentStart } from '../@ember/instrumentation/index.js'; import { a as createComputeRef, v as valueForRef, e as createPrimitiveRef, f as childRefFor, g as childRefFromParts, c as createConstRef, d as isUpdatableRef, u as updateRef } from './reference-BshxG6wn.js'; import { f as reifyPositional } from './debug-render-tree-CF5O4-WI.js'; import { E as EMPTY_ARRAY } from './array-utils-CZQxrdD3.js'; import { d as valueForTag, n as beginUntrackFrame, r as endUntrackFrame, m as beginTrackFrame, q as endTrackFrame, a as consumeTag, v as validateTag } from './cache-BIlOoPA7.js'; import { g as get } from './property_get-CAFdpRyu.js'; import { dasherize } from '../@ember/-internals/string/index.js'; import { registerDestructor } from '../@glimmer/destroyable/index.js'; import { MUTABLE_CELL } from '../@ember/-internals/views/lib/compat/attrs.js'; /** * @deprecated */ function unwrapTemplate(template) { if (template.result === 'error') { throw new Error(`Compile Error: ${template.problem} @ ${template.span.start}..${template.span.end}`); } return template; } function isTemplateFactory(template) { return typeof template === 'function'; } function referenceForParts(rootRef, parts) { let isAttrs = parts[0] === 'attrs'; // TODO deprecate this if (isAttrs) { parts.shift(); if (parts.length === 1) { return childRefFor(rootRef, parts[0]); } } return childRefFromParts(rootRef, parts); } function parseAttributeBinding(microsyntax) { let colonIndex = microsyntax.indexOf(':'); if (colonIndex === -1) { return [microsyntax, microsyntax, true]; } else { let prop = microsyntax.substring(0, colonIndex); let attribute = microsyntax.substring(colonIndex + 1); return [prop, attribute, false]; } } function installAttributeBinding(component, rootRef, parsed, operations) { let [prop, attribute, isSimple] = parsed; if (attribute === 'id') { // SAFETY: `get` could not infer the type of `prop` and just gave us `unknown`. // we may want to throw an error in the future if the value isn't string or null/undefined. let elementId = get(component, prop); if (elementId === undefined || elementId === null) { elementId = component.elementId; } let elementIdRef = createPrimitiveRef(elementId); operations.setAttribute('id', elementIdRef, true, null); return; } let isPath = prop.indexOf('.') > -1; let reference = isPath ? referenceForParts(rootRef, prop.split('.')) : childRefFor(rootRef, prop); operations.setAttribute(attribute, reference, false, null); } function createClassNameBindingRef(rootRef, microsyntax, operations) { let parts = microsyntax.split(':'); let [prop, truthy, falsy] = parts; let isStatic = prop === ''; if (isStatic) { operations.setAttribute('class', createPrimitiveRef(truthy), true, null); } else { let isPath = prop.indexOf('.') > -1; let parts = isPath ? prop.split('.') : []; let value = isPath ? referenceForParts(rootRef, parts) : childRefFor(rootRef, prop); let ref; if (truthy === undefined) { ref = createSimpleClassNameBindingRef(value, isPath ? parts[parts.length - 1] : prop); } else { ref = createColonClassNameBindingRef(value, truthy, falsy); } operations.setAttribute('class', ref, false, null); } } function createSimpleClassNameBindingRef(inner, path) { let dasherizedPath; return createComputeRef(() => { let value = valueForRef(inner); if (value === true) { return dasherizedPath || (dasherizedPath = dasherize(path)); } else if (value || value === 0) { return String(value); } else { return null; } }); } function createColonClassNameBindingRef(inner, truthy, falsy) { return createComputeRef(() => { return valueForRef(inner) ? truthy : falsy; }); } function NOOP() {} /** @module ember */ /** Represents the internal state of the component. @class ComponentStateBucket @private */ class ComponentStateBucket { classRef = null; rootRef; argsRevision; constructor(component, args, argsTag, finalizer, hasWrappedElement, isInteractive) { this.component = component; this.args = args; this.argsTag = argsTag; this.finalizer = finalizer; this.hasWrappedElement = hasWrappedElement; this.isInteractive = isInteractive; this.classRef = null; this.argsRevision = args === null ? 0 : valueForTag(argsTag); this.rootRef = createConstRef(component); registerDestructor(this, () => this.willDestroy(), true); registerDestructor(this, () => this.component.destroy()); } willDestroy() { let { component, isInteractive } = this; if (isInteractive) { beginUntrackFrame(); component.trigger('willDestroyElement'); component.trigger('willClearRender'); endUntrackFrame(); let element = getViewElement(component); if (element) { clearElementView(element); clearViewElement(component); } } component.renderer.unregister(component); } finalize() { let { finalizer } = this; finalizer(); this.finalizer = NOOP; } } // ComponentArgs takes EvaluatedNamedArgs and converts them into the // inputs needed by CurlyComponents (attrs and props, with mutable // cells, etc). function processComponentArgs(namedArgs) { let attrs = Object.create(null); let props = Object.create(null); for (let name in namedArgs) { let ref = namedArgs[name]; let value = valueForRef(ref); if (isUpdatableRef(ref)) { attrs[name] = new MutableCell(ref, value); } else { attrs[name] = value; } props[name] = value; } props.attrs = attrs; return props; } const REF = Symbol('REF'); class MutableCell { value; [MUTABLE_CELL]; [REF]; constructor(ref, value) { this[MUTABLE_CELL] = true; this[REF] = ref; this.value = value; } update(val) { updateRef(this[REF], val); } } const COMPONENT_ARGS_MAP = new WeakMap(); function getComponentCapturedArgs(component) { return COMPONENT_ARGS_MAP.get(component); } const DIRTY_TAG = Symbol('DIRTY_TAG'); const IS_DISPATCHING_ATTRS = Symbol('IS_DISPATCHING_ATTRS'); const BOUNDS = Symbol('BOUNDS'); const EMBER_VIEW_REF = createPrimitiveRef('ember-view'); function aliasIdToElementId(args, props) { if (args.named.has('id')) { props.elementId = props.id; } } // We must traverse the attributeBindings in reverse keeping track of // what has already been applied. This is essentially refining the concatenated // properties applying right to left. function applyAttributeBindings(attributeBindings, component, rootRef, operations) { let seen = []; let i = attributeBindings.length - 1; while (i !== -1) { let binding = attributeBindings[i]; let parsed = parseAttributeBinding(binding); let attribute = parsed[1]; if (seen.indexOf(attribute) === -1) { seen.push(attribute); installAttributeBinding(component, rootRef, parsed, operations); } i--; } if (seen.indexOf('id') === -1) { let id = component.elementId ? component.elementId : guidFor(component); operations.setAttribute('id', createPrimitiveRef(id), false, null); } } class CurlyComponentManager { templateFor(component) { let { layout, layoutName } = component; let owner = getOwner(component); let factory; if (layout === undefined) { if (layoutName !== undefined) { let _factory = owner.lookup(`template:${layoutName}`); factory = _factory; } else { return null; } } else if (isTemplateFactory(layout)) { factory = layout; } else { // no layout was found, use the default layout return null; } return unwrapTemplate(factory(owner)).asWrappedLayout(); } getDynamicLayout(bucket) { return this.templateFor(bucket.component); } getTagName(state) { let { component, hasWrappedElement } = state; if (!hasWrappedElement) { return null; } return component && component.tagName || 'div'; } getCapabilities() { return CURLY_CAPABILITIES; } prepareArgs(ComponentClass, args) { if (args.named.has('__ARGS__')) { let { __ARGS__, ...rest } = args.named.capture(); let __args__ = valueForRef(__ARGS__); let prepared = { positional: __args__.positional, named: { ...rest, ...__args__.named } }; return prepared; } const { positionalParams } = ComponentClass.class ?? ComponentClass; // early exits if (positionalParams === undefined || positionalParams === null || args.positional.length === 0) { return null; } let named; if (typeof positionalParams === 'string') { let captured = args.positional.capture(); named = { [positionalParams]: createComputeRef(() => reifyPositional(captured)) }; Object.assign(named, args.named.capture()); } else if (Array.isArray(positionalParams) && positionalParams.length > 0) { const count = Math.min(positionalParams.length, args.positional.length); named = {}; Object.assign(named, args.named.capture()); for (let i = 0; i < count; i++) { let name = positionalParams[i]; named[name] = args.positional.at(i); } } else { return null; } return { positional: EMPTY_ARRAY, named }; } /* * This hook is responsible for actually instantiating the component instance. * It also is where we perform additional bookkeeping to support legacy * features like exposed by view mixins like ChildViewSupport, ActionSupport, * etc. */ create(owner, ComponentClass, args, { isInteractive }, dynamicScope, callerSelfRef) { // Get the nearest concrete component instance from the scope. "Virtual" // components will be skipped. let parentView = dynamicScope.view; // Capture the arguments, which tells Glimmer to give us our own, stable // copy of the Arguments object that is safe to hold on to between renders. let capturedArgs = args.named.capture(); beginTrackFrame(); let props = processComponentArgs(capturedArgs); let argsTag = endTrackFrame(); // Alias `id` argument to `elementId` property on the component instance. aliasIdToElementId(args, props); // Set component instance's parentView property to point to nearest concrete // component. props.parentView = parentView; // Save the current `this` context of the template as the component's // `_target`, so bubbled actions are routed to the right place. props._target = valueForRef(callerSelfRef); setOwner(props, owner); // caller: // <FaIcon @name="bug" /> // // callee: // <i class="fa-{{@name}}"></i> // Now that we've built up all of the properties to set on the component instance, // actually create it. beginUntrackFrame(); let component = ComponentClass.create(props); // Store capturedArgs in a WeakMap keyed by the component instance so that // PROPERTY_DID_CHANGE can look them up COMPONENT_ARGS_MAP.set(component, capturedArgs); let finalizer = _instrumentStart('render.component', initialRenderInstrumentDetails, component); // We become the new parentView for downstream components, so save our // component off on the dynamic scope. dynamicScope.view = component; // Unless we're the root component, we need to add ourselves to our parent // component's childViews array. if (parentView !== null && parentView !== undefined) { addChildView(parentView, component); } component.trigger('didReceiveAttrs'); let hasWrappedElement = component.tagName !== ''; // We usually do this in the `didCreateElement`, but that hook doesn't fire for tagless components if (!hasWrappedElement) { if (isInteractive) { component.trigger('willRender'); } component._transitionTo('hasElement'); if (isInteractive) { component.trigger('willInsertElement'); } } // Track additional lifecycle metadata about this component in a state bucket. // Essentially we're saving off all the state we'll need in the future. let bucket = new ComponentStateBucket(component, capturedArgs, argsTag, finalizer, hasWrappedElement, isInteractive); if (args.named.has('class')) { bucket.classRef = args.named.get('class'); } if (isInteractive && hasWrappedElement) { component.trigger('willRender'); } endUntrackFrame(); // consume every argument so we always run again consumeTag(bucket.argsTag); consumeTag(component[DIRTY_TAG]); return bucket; } getDebugName(definition) { return definition.fullName || definition.normalizedName || definition.class?.name || definition.name; } getSelf({ rootRef }) { return rootRef; } didCreateElement({ component, classRef, isInteractive, rootRef }, element, operations) { setViewElement(component, element); setElementView(element, component); let { attributeBindings, classNames, classNameBindings } = component; if (attributeBindings && attributeBindings.length) { applyAttributeBindings(attributeBindings, component, rootRef, operations); } else { let id = component.elementId ? component.elementId : guidFor(component); operations.setAttribute('id', createPrimitiveRef(id), false, null); } if (classRef) { const ref = createSimpleClassNameBindingRef(classRef); operations.setAttribute('class', ref, false, null); } if (classNames && classNames.length) { classNames.forEach(name => { operations.setAttribute('class', createPrimitiveRef(name), false, null); }); } if (classNameBindings && classNameBindings.length) { classNameBindings.forEach(binding => { createClassNameBindingRef(rootRef, binding, operations); }); } operations.setAttribute('class', EMBER_VIEW_REF, false, null); if ('ariaRole' in component) { operations.setAttribute('role', childRefFor(rootRef, 'ariaRole'), false, null); } component._transitionTo('hasElement'); if (isInteractive) { beginUntrackFrame(); component.trigger('willInsertElement'); endUntrackFrame(); } } didRenderLayout(bucket, bounds) { bucket.component[BOUNDS] = bounds; bucket.finalize(); } didCreate({ component, isInteractive }) { if (isInteractive) { component._transitionTo('inDOM'); component.trigger('didInsertElement'); component.trigger('didRender'); } } update(bucket) { let { component, args, argsTag, argsRevision, isInteractive } = bucket; bucket.finalizer = _instrumentStart('render.component', rerenderInstrumentDetails, component); beginUntrackFrame(); if (args !== null && !validateTag(argsTag, argsRevision)) { beginTrackFrame(); let props = processComponentArgs(args); argsTag = bucket.argsTag = endTrackFrame(); bucket.argsRevision = valueForTag(argsTag); component[IS_DISPATCHING_ATTRS] = true; component.setProperties(props); component[IS_DISPATCHING_ATTRS] = false; component.trigger('didUpdateAttrs'); component.trigger('didReceiveAttrs'); } if (isInteractive) { component.trigger('willUpdate'); component.trigger('willRender'); } endUntrackFrame(); consumeTag(argsTag); consumeTag(component[DIRTY_TAG]); } didUpdateLayout(bucket) { bucket.finalize(); } didUpdate({ component, isInteractive }) { if (isInteractive) { component.trigger('didUpdate'); component.trigger('didRender'); } } getDestroyable(bucket) { return bucket; } } function initialRenderInstrumentDetails(component) { return component.instrumentDetails({ initialRender: true }); } function rerenderInstrumentDetails(component) { return component.instrumentDetails({ initialRender: false }); } const CURLY_CAPABILITIES = { dynamicLayout: true, dynamicTag: true, prepareArgs: true, createArgs: true, attributeHook: true, elementHook: true, createCaller: true, dynamicScope: true, updateHook: true, createInstance: true, wrapped: true, willDestroy: true, hasSubOwner: false }; const CURLY_COMPONENT_MANAGER = new CurlyComponentManager(); function isCurlyManager(manager) { return manager === CURLY_COMPONENT_MANAGER; } export { BOUNDS as B, CURLY_COMPONENT_MANAGER as C, DIRTY_TAG as D, IS_DISPATCHING_ATTRS as I, CurlyComponentManager as a, ComponentStateBucket as b, isCurlyManager as c, getComponentCapturedArgs as g, initialRenderInstrumentDetails as i, unwrapTemplate as u };