diff --git a/broccoli/amd-compat-entrypoints/ember.debug.js b/broccoli/amd-compat-entrypoints/ember.debug.js index c4ac39d47b3..e7f5cd33020 100644 --- a/broccoli/amd-compat-entrypoints/ember.debug.js +++ b/broccoli/amd-compat-entrypoints/ember.debug.js @@ -161,9 +161,6 @@ d('@ember/array/proxy', emberArrayProxy); import * as emberCanaryFeaturesIndex from '@ember/canary-features/index'; d('@ember/canary-features/index', emberCanaryFeaturesIndex); -import * as emberComponentHelper from '@ember/component/helper'; -d('@ember/component/helper', emberComponentHelper); - import * as emberComponentIndex from '@ember/component/index'; d('@ember/component/index', emberComponentIndex); diff --git a/node-tests/fixtures/helper/mu-helper.js b/node-tests/fixtures/helper/mu-helper.js deleted file mode 100644 index 88c8894ebd3..00000000000 --- a/node-tests/fixtures/helper/mu-helper.js +++ /dev/null @@ -1,7 +0,0 @@ -import { helper as buildHelper } from '@ember/component/helper'; - -export function fooBarBaz(positional /*, named*/) { - return positional; -} - -export const helper = buildHelper(fooBarBaz); diff --git a/package.json b/package.json index dca8e27a2e8..a2f4d9f6f56 100644 --- a/package.json +++ b/package.json @@ -239,7 +239,6 @@ "@ember/array/mutable.js": "ember-source/@ember/array/mutable.js", "@ember/array/proxy.js": "ember-source/@ember/array/proxy.js", "@ember/canary-features/index.js": "ember-source/@ember/canary-features/index.js", - "@ember/component/helper.js": "ember-source/@ember/component/helper.js", "@ember/component/index.js": "ember-source/@ember/component/index.js", "@ember/component/template-only.js": "ember-source/@ember/component/template-only.js", "@ember/controller/index.js": "ember-source/@ember/controller/index.js", @@ -404,4 +403,4 @@ } }, "packageManager": "pnpm@10.5.0" -} +} \ No newline at end of file diff --git a/packages/@ember/-internals/glimmer/index.ts b/packages/@ember/-internals/glimmer/index.ts index e529ab01ce0..8f15cc05e00 100644 --- a/packages/@ember/-internals/glimmer/index.ts +++ b/packages/@ember/-internals/glimmer/index.ts @@ -65,10 +65,7 @@ ``` Ember's built-in helpers are described under the [Ember.Templates.helpers](/ember/release/classes/Ember.Templates.helpers) - namespace. Documentation on creating custom helpers can be found under - [helper](/ember/release/functions/@ember%2Fcomponent%2Fhelper/helper) (or - under [Helper](/ember/release/classes/Helper) if a helper requires access to - dependency injection). + namespace. ### Invoking a Component @@ -447,16 +444,7 @@ export { templateFactory as template, templateCacheCounters } from '@glimmer/opcode-compiler'; export { default as RootTemplate } from './lib/templates/root'; -export { default as Input } from './lib/components/input'; export { default as LinkTo } from './lib/components/link-to'; -export { default as Textarea } from './lib/components/textarea'; -export { default as Component } from './lib/component'; -export { - default as Helper, - helper, - type FunctionBasedHelper, - type FunctionBasedHelperInstance, -} from './lib/helper'; export { TrustedHTML, SafeString, diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts deleted file mode 100644 index 0b0da6ec97d..00000000000 --- a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts +++ /dev/null @@ -1,556 +0,0 @@ -import { - type default as Owner, - type InternalFactory, - getOwner, - setOwner, -} from '@ember/-internals/owner'; -import { enumerableSymbol, guidFor } from '@ember/-internals/utils'; -import { addChildView, setElementView, setViewElement } from '@ember/-internals/views'; -import type { Nullable } from '@ember/-internals/utility-types'; -import { assert, debugFreeze } from '@ember/debug'; -import { _instrumentStart } from '@ember/instrumentation'; -import { DEBUG } from '@glimmer/env'; -import type { - Bounds, - CapturedArguments, - CompilableProgram, - Destroyable, - ElementOperations, - Environment, - InternalComponentCapabilities, - PreparedArguments, - TemplateFactory, - VMArguments, - WithCreateInstance, - WithDynamicLayout, - WithDynamicTagName, -} from '@glimmer/interfaces'; -import type { Reference } from '@glimmer/reference'; -import { childRefFor, createComputeRef, createPrimitiveRef, valueForRef } from '@glimmer/reference'; -import { reifyPositional } from '@glimmer/runtime'; -import { EMPTY_ARRAY } from '@glimmer/util'; -import { unwrapTemplate } from './unwrap-template'; -import { - beginTrackFrame, - beginUntrackFrame, - consumeTag, - endTrackFrame, - endUntrackFrame, - validateTag, - valueForTag, -} from '@glimmer/validator'; -import type Component from '../component'; -import type { DynamicScope } from '../renderer'; -import type RuntimeResolver from '../resolver'; -import { isTemplateFactory } from '../template'; -import { - createClassNameBindingRef, - createSimpleClassNameBindingRef, - installAttributeBinding, - parseAttributeBinding, -} from '../utils/bindings'; - -import ComponentStateBucket from '../utils/curly-component-state-bucket'; -import { processComponentArgs } from '../utils/process-args'; - -export const ARGS = enumerableSymbol('ARGS'); -export const HAS_BLOCK = enumerableSymbol('HAS_BLOCK'); - -export const DIRTY_TAG = Symbol('DIRTY_TAG'); -export const IS_DISPATCHING_ATTRS = Symbol('IS_DISPATCHING_ATTRS'); -export const BOUNDS = Symbol('BOUNDS'); - -const EMBER_VIEW_REF = createPrimitiveRef('ember-view'); - -function aliasIdToElementId(args: VMArguments, props: any) { - if (args.named.has('id')) { - assert( - `You cannot invoke a component with both 'id' and 'elementId' at the same time.`, - !args.named.has('elementId') - ); - 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: Array, - component: Component, - rootRef: Reference, - operations: ElementOperations -) { - let seen: string[] = []; - let i = attributeBindings.length - 1; - - while (i !== -1) { - let binding = attributeBindings[i]; - assert('has binding', binding); - let parsed: [string, string, boolean] = 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); - } -} - -const EMPTY_POSITIONAL_ARGS: Reference[] = []; - -debugFreeze(EMPTY_POSITIONAL_ARGS); - -type ComponentFactory = InternalFactory< - Component, - { - create(props?: any): Component; - positionalParams: string | string[] | undefined | null; - name: string; - } -> & { - name: string; - positionalParams: string | string[] | undefined | null; -}; - -export default class CurlyComponentManager - implements - WithCreateInstance, - WithDynamicLayout, - WithDynamicTagName -{ - protected templateFor(component: Component): CompilableProgram | null { - let { layout, layoutName } = component; - let owner = getOwner(component); - assert('Component is unexpectedly missing an owner', owner); - - let factory: TemplateFactory; - - if (layout === undefined) { - if (layoutName !== undefined) { - let _factory = owner.lookup(`template:${layoutName}`) as TemplateFactory; - assert(`Layout \`${layoutName}\` not found!`, _factory !== undefined); - 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: ComponentStateBucket): CompilableProgram | null { - return this.templateFor(bucket.component); - } - - getTagName(state: ComponentStateBucket): Nullable { - let { component, hasWrappedElement } = state; - - if (!hasWrappedElement) { - return null; - } - - return (component && component.tagName) || 'div'; - } - - getCapabilities(): InternalComponentCapabilities { - return CURLY_CAPABILITIES; - } - - prepareArgs(ComponentClass: ComponentFactory, args: VMArguments): Nullable { - if (args.named.has('__ARGS__')) { - assert( - '[BUG] cannot pass both __ARGS__ and positional arguments', - args.positional.length === 0 - ); - - let { __ARGS__, ...rest } = args.named.capture(); - assert('[BUG] unexpectedly missing __ARGS__ after check', __ARGS__); - - // does this need to be untracked? - let __args__ = valueForRef(__ARGS__) as CapturedArguments; - - 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: PreparedArguments['named']; - - if (typeof positionalParams === 'string') { - assert( - `You cannot specify positional parameters and the hash argument \`${positionalParams}\`.`, - !args.named.has(positionalParams) - ); - 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: string | undefined = positionalParams[i]; - assert('Expected at least one positional param', name); - - assert( - `You cannot specify both a positional param (at position ${i}) and the hash argument \`${name}\`.`, - !args.named.has(name) - ); - - named[name] = args.positional.at(i); - } - } else { - return null; - } - - return { positional: EMPTY_ARRAY as readonly Reference[], 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: Owner, - ComponentClass: ComponentFactory, - args: VMArguments, - { isInteractive }: Environment, - dynamicScope: DynamicScope, - callerSelfRef: Reference, - hasBlock: boolean - ): ComponentStateBucket { - // 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); - props[ARGS] = 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; - - // Set whether this component was invoked with a block - // (`{{#my-component}}{{/my-component}}`) or without one - // (`{{my-component}}`). - props[HAS_BLOCK] = hasBlock; - - // 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: - // - // - // callee: - // - - // 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); - - 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 (DEBUG) { - processComponentInitializationAssertions(component, props); - } - - 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: ComponentFactory): string { - return ( - definition.fullName || definition.normalizedName || definition.class?.name || definition.name - ); - } - - getSelf({ rootRef }: ComponentStateBucket): Reference { - return rootRef; - } - - didCreateElement( - { component, classRef, isInteractive, rootRef }: ComponentStateBucket, - element: Element, - operations: ElementOperations - ): void { - 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: string) => { - operations.setAttribute('class', createPrimitiveRef(name), false, null); - }); - } - - if (classNameBindings && classNameBindings.length) { - classNameBindings.forEach((binding: string) => { - 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: ComponentStateBucket, bounds: Bounds): void { - bucket.component[BOUNDS] = bounds; - bucket.finalize(); - } - - didCreate({ component, isInteractive }: ComponentStateBucket): void { - if (isInteractive) { - component._transitionTo('inDOM'); - component.trigger('didInsertElement'); - component.trigger('didRender'); - } - } - - update(bucket: ComponentStateBucket): void { - 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: ComponentStateBucket): void { - bucket.finalize(); - } - - didUpdate({ component, isInteractive }: ComponentStateBucket): void { - if (isInteractive) { - component.trigger('didUpdate'); - component.trigger('didRender'); - } - } - - getDestroyable(bucket: ComponentStateBucket): Nullable { - return bucket; - } -} - -export function processComponentInitializationAssertions(component: Component, props: any): void { - assert( - `classNameBindings must be non-empty strings: ${component}`, - (() => { - let { classNameBindings } = component; - for (let i = 0; i < classNameBindings.length; i++) { - let binding = classNameBindings[i]; - - if (typeof binding !== 'string' || binding.length === 0) { - return false; - } - } - return true; - })() - ); - - assert( - `classNameBindings must not have spaces in them: ${component}`, - (() => { - let { classNameBindings } = component; - for (let binding of classNameBindings) { - if (binding.split(' ').length > 1) { - return false; - } - } - return true; - })() - ); - - assert( - `You cannot use \`classNameBindings\` on a tag-less component: ${component}`, - component.tagName !== '' || - !component.classNameBindings || - component.classNameBindings.length === 0 - ); - - assert( - `You cannot use \`elementId\` on a tag-less component: ${component}`, - component.tagName !== '' || - props.id === component.elementId || - (!component.elementId && component.elementId !== '') - ); - - assert( - `You cannot use \`attributeBindings\` on a tag-less component: ${component}`, - component.tagName !== '' || - !component.attributeBindings || - component.attributeBindings.length === 0 - ); -} - -export function initialRenderInstrumentDetails(component: any): any { - return component.instrumentDetails({ initialRender: true }); -} - -export function rerenderInstrumentDetails(component: any): any { - return component.instrumentDetails({ initialRender: false }); -} - -export const CURLY_CAPABILITIES: InternalComponentCapabilities = { - 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, -}; - -export const CURLY_COMPONENT_MANAGER = new CurlyComponentManager(); - -export function isCurlyManager(manager: object): boolean { - return manager === CURLY_COMPONENT_MANAGER; -} diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts b/packages/@ember/-internals/glimmer/lib/component-managers/root.ts deleted file mode 100644 index 00aca8f633c..00000000000 --- a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { getFactoryFor } from '@ember/-internals/container'; -import { assert } from '@ember/debug'; -import { _instrumentStart } from '@ember/instrumentation'; -import { DEBUG } from '@glimmer/env'; -import type { - ComponentDefinition, - Environment, - InternalComponentCapabilities, - Owner, - VMArguments, -} from '@glimmer/interfaces'; -import type { Nullable } from '@ember/-internals/utility-types'; -import { capabilityFlagsFrom } from '@glimmer/manager'; -import { CONSTANT_TAG, consumeTag } from '@glimmer/validator'; -import type Component from '../component'; -import type { DynamicScope } from '../renderer'; -import ComponentStateBucket from '../utils/curly-component-state-bucket'; -import CurlyComponentManager, { - DIRTY_TAG, - initialRenderInstrumentDetails, - processComponentInitializationAssertions, -} from './curly'; - -class RootComponentManager extends CurlyComponentManager { - component: Component; - - constructor(component: Component) { - super(); - this.component = component; - } - - create( - _owner: Owner, - _state: unknown, - _args: Nullable, - { isInteractive }: Environment, - dynamicScope: DynamicScope - ) { - let component = this.component; - - let finalizer = _instrumentStart('render.component', initialRenderInstrumentDetails, component); - - dynamicScope.view = component; - - 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'); - } - } - - if (DEBUG) { - processComponentInitializationAssertions(component, {}); - } - - let bucket = new ComponentStateBucket( - component, - null, - CONSTANT_TAG, - finalizer, - hasWrappedElement, - isInteractive - ); - - consumeTag(component[DIRTY_TAG]); - - return bucket; - } -} - -// ROOT is the top-level template it has nothing but one yield. -// it is supposed to have a dummy element -export const ROOT_CAPABILITIES: InternalComponentCapabilities = { - dynamicLayout: true, - dynamicTag: true, - prepareArgs: false, - createArgs: false, - attributeHook: true, - elementHook: true, - createCaller: true, - dynamicScope: true, - updateHook: true, - createInstance: true, - wrapped: true, - willDestroy: false, - hasSubOwner: false, -}; - -export class RootComponentDefinition implements ComponentDefinition { - // handle is not used by this custom definition - handle = -1; - - resolvedName = '-top-level'; - state: object; - manager: RootComponentManager; - capabilities = capabilityFlagsFrom(ROOT_CAPABILITIES); - compilable = null; - - constructor(component: Component) { - this.manager = new RootComponentManager(component); - let factory = getFactoryFor(component); - assert('missing factory for component', factory !== undefined); - this.state = factory; - } -} diff --git a/packages/@ember/-internals/glimmer/lib/component.ts b/packages/@ember/-internals/glimmer/lib/component.ts deleted file mode 100644 index e106e05157a..00000000000 --- a/packages/@ember/-internals/glimmer/lib/component.ts +++ /dev/null @@ -1,1698 +0,0 @@ -import type { View } from '@ember/-internals/glimmer/lib/renderer'; -import { - descriptorForProperty, - get, - nativeDescDecorator, - PROPERTY_DID_CHANGE, -} from '@ember/-internals/metal'; -import type { PropertyDidChange } from '@ember/-internals/metal/lib/property_events'; -import { getOwner } from '@ember/-internals/owner'; -import { TargetActionSupport } from '@ember/-internals/runtime'; -import type { ViewStates } from '@ember/-internals/views'; -import { - ActionSupport, - addChildView, - CoreView, - EventDispatcher, - getChildViews, - getViewElement, -} from '@ember/-internals/views'; -import { guidFor } from '@ember/-internals/utils'; -import { assert } from '@ember/debug'; -import { DEBUG } from '@glimmer/env'; -import type { Environment, Template, TemplateFactory } from '@glimmer/interfaces'; -import { setInternalComponentManager } from '@glimmer/manager'; -import { isUpdatableRef, updateRef } from '@glimmer/reference'; -import { normalizeProperty } from '@glimmer/runtime'; -import type { DirtyableTag } from '@glimmer/validator'; -import { createTag, dirtyTag } from '@glimmer/validator'; -import type { SimpleElement } from '@simple-dom/interface'; -import { - ARGS, - BOUNDS, - CURLY_COMPONENT_MANAGER, - DIRTY_TAG, - IS_DISPATCHING_ATTRS, -} from './component-managers/curly'; -import { hasDOM } from '@ember/-internals/browser-environment'; - -// Keep track of which component classes have already been processed for lazy event setup. -let lazyEventsProcessed = new WeakMap>(); - -const EMPTY_ARRAY = Object.freeze([]); - -/** - Determines if the element matches the specified selector. - - @private - @method matches - @param {DOMElement} el - @param {String} selector -*/ -const elMatches: typeof Element.prototype.matches | undefined = - typeof Element !== 'undefined' ? Element.prototype.matches : undefined; - -function matches(el: Element, selector: string): boolean { - assert('cannot call `matches` in fastboot mode', elMatches !== undefined); - return elMatches.call(el, selector); -} - -/** -@module @ember/component -*/ - -interface ComponentMethods { - // Overrideable methods are defined here since you can't `declare` a method in a class - - /** - Called when the attributes passed into the component have been updated. - Called both during the initial render of a container and during a rerender. - Can be used in place of an observer; code placed here will be executed - every time any attribute updates. - @method didReceiveAttrs - @public - @since 1.13.0 - */ - didReceiveAttrs(): void; - - /** - Called when the attributes passed into the component have been updated. - Called both during the initial render of a container and during a rerender. - Can be used in place of an observer; code placed here will be executed - every time any attribute updates. - @event didReceiveAttrs - @public - @since 1.13.0 - */ - - /** - Called after a component has been rendered, both on initial render and - in subsequent rerenders. - @method didRender - @public - @since 1.13.0 - */ - didRender(): void; - - /** - Called after a component has been rendered, both on initial render and - in subsequent rerenders. - @event didRender - @public - @since 1.13.0 - */ - - /** - Called before a component has been rendered, both on initial render and - in subsequent rerenders. - @method willRender - @public - @since 1.13.0 - */ - willRender(): void; - - /** - Called before a component has been rendered, both on initial render and - in subsequent rerenders. - @event willRender - @public - @since 1.13.0 - */ - - /** - Called when the attributes passed into the component have been changed. - Called only during a rerender, not during an initial render. - @method didUpdateAttrs - @public - @since 1.13.0 - */ - didUpdateAttrs(): void; - - /** - Called when the attributes passed into the component have been changed. - Called only during a rerender, not during an initial render. - @event didUpdateAttrs - @public - @since 1.13.0 - */ - - /** - Called when the component is about to update and rerender itself. - Called only during a rerender, not during an initial render. - @method willUpdate - @public - @since 1.13.0 - */ - willUpdate(): void; - - /** - Called when the component is about to update and rerender itself. - Called only during a rerender, not during an initial render. - @event willUpdate - @public - @since 1.13.0 - */ - - /** - Called when the component has updated and rerendered itself. - Called only during a rerender, not during an initial render. - @method didUpdate - @public - @since 1.13.0 - */ - didUpdate(): void; - - /** - Called when the component has updated and rerendered itself. - Called only during a rerender, not during an initial render. - @event didUpdate - @public - @since 1.13.0 - */ - - /** - The HTML `id` of the component's element in the DOM. You can provide this - value yourself but it must be unique (just as in HTML): - - ```handlebars - {{my-component elementId="a-really-cool-id"}} - ``` - - ```handlebars - - ``` - If not manually set a default value will be provided by the framework. - Once rendered an element's `elementId` is considered immutable and you - should never change it. If you need to compute a dynamic value for the - `elementId`, you should do this when the component or element is being - instantiated: - - ```javascript - export default class extends Component { - init() { - super.init(...arguments); - - var index = this.get('index'); - this.set('elementId', `component-id${index}`); - } - } - ``` - - @property elementId - @type String - @public - */ - layoutName?: string; -} - -// A zero-runtime-overhead private symbol to use in branding the component to -// preserve its type parameter. -declare const SIGNATURE: unique symbol; - -/** - A component is a reusable UI element that consists of a `.hbs` template and an - optional JavaScript class that defines its behavior. For example, someone - might make a `button` in the template and handle the click behavior in the - JavaScript file that shares the same name as the template. - - Components are broken down into two categories: - - - Components _without_ JavaScript, that are based only on a template. These - are called Template-only or TO components. - - Components _with_ JavaScript, which consist of a template and a backing - class. - - Ember ships with two types of JavaScript classes for components: - - 1. Glimmer components, imported from `@glimmer/component`, which are the - default component's for Ember Octane (3.15) and more recent editions. - 2. Classic components, imported from `@ember/component`, which were the - default for older editions of Ember (pre 3.15). - - Below is the documentation for Classic components. If you are looking for the - API documentation for Template-only or Glimmer components, it is [available - here](/ember/release/modules/@glimmer%2Fcomponent). - - ## Defining a Classic Component - - If you want to customize the component in order to handle events, transform - arguments or maintain internal state, you implement a subclass of `Component`. - - One example is to add computed properties to your component: - - ```app/components/person-profile.js - import Component from '@ember/component'; - - export default class extends Component { - @computed('person.title', 'person.firstName', 'person.lastName') - get displayName() { - let { title, firstName, lastName } = this.person; - - if (title) { - return `${title} ${lastName}`; - } else { - return `${firstName} ${lastName}`; - } - } - } - ``` - - And then use it in the component's template: - - ```app/templates/components/person-profile.hbs -

{{this.displayName}}

- {{yield}} - ``` - - ## Customizing a Classic Component's HTML Element in JavaScript - - ### HTML Tag - - The default HTML tag name used for a component's HTML representation is `div`. - This can be customized by setting the `tagName` property. - - Consider the following component class: - - ```app/components/emphasized-paragraph.js - import Component from '@ember/component'; - - export default class extends Component { - tagName = 'em'; - } - ``` - - When invoked, this component would produce output that looks something like - this: - - ```html - - ``` - - ### HTML `class` Attribute - - The HTML `class` attribute of a component's tag can be set by providing a - `classNames` property that is set to an array of strings: - - ```app/components/my-widget.js - import Component from '@ember/component'; - - export default class extends Component { - classNames = ['my-class', 'my-other-class']; - } - ``` - - Invoking this component will produce output that looks like this: - - ```html -
- ``` - - `class` attribute values can also be set by providing a `classNameBindings` - property set to an array of properties names for the component. The return - value of these properties will be added as part of the value for the - components's `class` attribute. These properties can be computed properties: - - ```app/components/my-widget.js - import Component from '@ember/component'; - import { computed } from '@ember/object'; - - export default class extends Component { - classNames = ['my-class', 'my-other-class']; - classNameBindings = ['propertyA', 'propertyB']; - - propertyA = 'from-a'; - - get propertyB { - if (someLogic) { return 'from-b'; } - } - } - ``` - - Invoking this component will produce HTML that looks like: - - ```html -
- ``` - - Note that `classNames` and `classNameBindings` is in addition to the `class` - attribute passed with the angle bracket invocation syntax. Therefore, if this - component was invoked like so: - - ```handlebars - - ``` - - The resulting HTML will look similar to this: - - ```html -
- ``` - - If the value of a class name binding returns a boolean the property name - itself will be used as the class name if the property is true. The class name - will not be added if the value is `false` or `undefined`. - - ```app/components/my-widget.js - import Component from '@ember/component'; - - export default class extends Component { - classNameBindings = ['hovered']; - - hovered = true; - } - ``` - - Invoking this component will produce HTML that looks like: - - ```html -
- ``` - - ### Custom Class Names for Boolean Values - - When using boolean class name bindings you can supply a string value other - than the property name for use as the `class` HTML attribute by appending the - preferred value after a ":" character when defining the binding: - - ```app/components/my-widget.js - import Component from '@ember/component'; - - export default class extends Component { - classNameBindings = ['awesome:so-very-cool']; - - awesome = true; - } - ``` - - Invoking this component will produce HTML that looks like: - - ```html -
- ``` - - Boolean value class name bindings whose property names are in a - camelCase-style format will be converted to a dasherized format: - - ```app/components/my-widget.js - import Component from '@ember/component'; - - export default class extends Component { - classNameBindings = ['isUrgent']; - - isUrgent = true; - } - ``` - - Invoking this component will produce HTML that looks like: - - ```html -
- ``` - - Class name bindings can also refer to object values that are found by - traversing a path relative to the component itself: - - ```app/components/my-widget.js - import Component from '@ember/component'; - import EmberObject from '@ember/object'; - - export default class extends Component { - classNameBindings = ['messages.empty']; - - messages = EmberObject.create({ - empty: true - }); - } - ``` - - Invoking this component will produce HTML that looks like: - - ```html -
- ``` - - If you want to add a class name for a property which evaluates to true and and - a different class name if it evaluates to false, you can pass a binding like - this: - - ```app/components/my-widget.js - import Component from '@ember/component'; - - export default class extends Component { - classNameBindings = ['isEnabled:enabled:disabled']; - - isEnabled = true; - } - ``` - - Invoking this component will produce HTML that looks like: - - ```html -
- ``` - - When isEnabled is `false`, the resulting HTML representation looks like this: - - ```html -
- ``` - - This syntax offers the convenience to add a class if a property is `false`: - - ```app/components/my-widget.js - import Component from '@ember/component'; - - // Applies no class when isEnabled is true and class 'disabled' when isEnabled is false - export default class extends Component { - classNameBindings = ['isEnabled::disabled']; - - isEnabled = true; - } - ``` - - Invoking this component when the `isEnabled` property is true will produce - HTML that looks like: - - ```html -
- ``` - - Invoking it when the `isEnabled` property on the component is `false` will - produce HTML that looks like: - - ```html -
- ``` - - Updates to the value of a class name binding will result in automatic update - of the HTML `class` attribute in the component's rendered HTML - representation. If the value becomes `false` or `undefined` the class name - will be removed. - - Both `classNames` and `classNameBindings` are concatenated properties. See - [EmberObject](/ember/release/classes/EmberObject) documentation for more - information about concatenated properties. - - ### Other HTML Attributes - - The HTML attribute section of a component's tag can be set by providing an - `attributeBindings` property set to an array of property names on the - component. The return value of these properties will be used as the value of - the component's HTML associated attribute: - - ```app/components/my-anchor.js - import Component from '@ember/component'; - - export default class extends Component { - tagName = 'a'; - attributeBindings = ['href']; - - href = 'http://google.com'; - }; - ``` - - Invoking this component will produce HTML that looks like: - - ```html - - ``` - - One property can be mapped on to another by placing a ":" between the source - property and the destination property: - - ```app/components/my-anchor.js - import Component from '@ember/component'; - - export default class extends Component { - tagName = 'a'; - attributeBindings = ['url:href']; - - url = 'http://google.com'; - }; - ``` - - Invoking this component will produce HTML that looks like: - - ```html - - ``` - - HTML attributes passed with angle bracket invocations will take precedence - over those specified in `attributeBindings`. Therefore, if this component was - invoked like so: - - ```handlebars - - ``` - - The resulting HTML will looks like this: - - ```html - - ``` - - Note that the `href` attribute is ultimately set to `http://bing.com`, despite - it having attribute binidng to the `url` property, which was set to - `http://google.com`. - - Namespaced attributes (e.g. `xlink:href`) are supported, but have to be - mapped, since `:` is not a valid character for properties in Javascript: - - ```app/components/my-use.js - import Component from '@ember/component'; - - export default class extends Component { - tagName = 'use'; - attributeBindings = ['xlinkHref:xlink:href']; - - xlinkHref = '#triangle'; - }; - ``` - - Invoking this component will produce HTML that looks like: - - ```html - - ``` - - If the value of a property monitored by `attributeBindings` is a boolean, the - attribute will be present or absent depending on the value: - - ```app/components/my-text-input.js - import Component from '@ember/component'; - - export default class extends Component { - tagName = 'input'; - attributeBindings = ['disabled']; - - disabled = false; - }; - ``` - - Invoking this component will produce HTML that looks like: - - ```html - - ``` - - `attributeBindings` can refer to computed properties: - - ```app/components/my-text-input.js - import Component from '@ember/component'; - import { computed } from '@ember/object'; - - export default class extends Component { - tagName = 'input'; - attributeBindings = ['disabled']; - - get disabled() { - if (someLogic) { - return true; - } else { - return false; - } - } - }; - ``` - - To prevent setting an attribute altogether, use `null` or `undefined` as the - value of the property used in `attributeBindings`: - - ```app/components/my-text-input.js - import Component from '@ember/component'; - - export default class extends Component { - tagName = 'form'; - attributeBindings = ['novalidate']; - novalidate = null; - }; - ``` - - Updates to the property of an attribute binding will result in automatic - update of the HTML attribute in the component's HTML output. - - `attributeBindings` is a concatenated property. See - [EmberObject](/ember/release/classes/EmberObject) documentation for more - information about concatenated properties. - - ## Layouts - - The `layout` property can be used to dynamically specify a template associated - with a component class, instead of relying on Ember to link together a - component class and a template based on file names. - - In general, applications should not use this feature, but it's commonly used - in addons for historical reasons. - - The `layout` property should be set to the default export of a template - module, which is the name of a template file without the `.hbs` extension. - - ```app/templates/components/person-profile.hbs -

Person's Title

-
{{yield}}
- ``` - - ```app/components/person-profile.js - import Component from '@ember/component'; - import layout from '../templates/components/person-profile'; - - export default class extends Component { - layout = layout; - } - ``` - - If you invoke the component: - - ```handlebars - -

Chief Basket Weaver

-

Fisherman Industries

-
- ``` - - or - - ```handlebars - {{#person-profile}} -

Chief Basket Weaver

-

Fisherman Industries

- {{/person-profile}} - ``` - - It will result in the following HTML output: - - ```html -

Person's Title

-
-

Chief Basket Weaver

-

Fisherman Industries

-
- ``` - - ## Handling Browser Events - - There are two ways to handle user-initiated events: - - ### Using the `on` modifier to capture browser events - - In a component's template, you can attach an event handler to any element with the `on` modifier: - - ```handlebars -