UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

1,497 lines (1,345 loc) 178 kB
import { templateFactory, programCompilationContext } from '../@glimmer/opcode-compiler/index.js'; import { g as getFactoryFor, p as privatize } from './registry-B8WARvkP.js'; import { warn, debugFreeze, deprecate } from '../@ember/debug/index.js'; import { reifyPositional, normalizeProperty, EMPTY_ARGS, createCapturedArgs, EMPTY_POSITIONAL, curry, hash, array, concat, fn, get as get$1, templateOnlyComponent, TEMPLATE_ONLY_COMPONENT_MANAGER, on as on$1, runtimeContext, DOMTreeConstruction, DOMChanges, clientBuilder, inTransaction, renderMain } from '../@glimmer/runtime/index.js'; import { join, _backburner, schedule, _getCurrentRunLoop } from '../@ember/runloop/index.js'; import { valueForRef, isConstRef, createConstRef, isUpdatableRef, updateRef, createPrimitiveRef, childRefFor, createComputeRef, childRefFromParts, isInvokableRef, createUnboundRef, createInvokableRef, createReadOnlyRef, createDebugAliasRef, UNDEFINED_REFERENCE } from '../@glimmer/reference/index.js'; import { untrack, consumeTag, tagFor, createCache, getValue, valueForTag, beginUntrackFrame, endUntrackFrame, beginTrackFrame, endTrackFrame, validateTag, createTag, dirtyTag as DIRTY_TAG$1, CONSTANT_TAG, isTracking, debug, createUpdatableTag, CURRENT_TAG } from '../@glimmer/validator/index.js'; import { isDevelopingApp } from '@embroider/macros'; import { setInternalComponentManager, setComponentTemplate, setInternalHelperManager, setHelperManager, getInternalHelperManager, helperCapabilities, capabilityFlagsFrom, setInternalModifierManager, getInternalComponentManager, getComponentTemplate } from '../@glimmer/manager/index.js'; import { h as hasDOM } from './index-BGP1rw3B.js'; import { action as action$1 } from '../@ember/object/index.js'; import { on } from '../@ember/modifier/on.js'; import '../@ember/-internals/meta/lib/meta.js'; import { g as guidFor, a as getDebugName, e as isObject, u as uuid } from './mandatory-setter-BiXq-dpN.js'; import { registerDestructor, associateDestroyableChild, destroy } from '../@glimmer/destroyable/index.js'; import { S as tracked, g as get, Q as PROPERTY_DID_CHANGE, t as tagForObject, o as objectAt, a as tagForProperty, V as _getProp } from './cache-DORQczuy.js'; import { E as ENV } from './env-mInZ1DuF.js'; import { setOwner, getOwner, isFactory } from '../@ember/-internals/owner/index.js'; import { assert } from '../@ember/debug/lib/assert.js'; import { d as decorateFieldV2, i as initializeDeferredDecorator, a as decorateMethodV2 } from './chunk-3SQBS3Y5-Cj4eryg1.js'; import { isSimpleClick, getViewElement, clearElementView, clearViewElement, addChildView, setViewElement, setElementView, constructStyleDeprecationMessage, getViewId } from '../@ember/-internals/views/lib/system/utils.js'; import ActionManager from '../@ember/-internals/views/lib/system/action_manager.js'; import '../@ember/-internals/views/lib/component_lookup.js'; import CoreView from '../@ember/-internals/views/lib/views/core_view.js'; import ClassNamesSupport from '../@ember/-internals/views/lib/mixins/class_names_support.js'; import ChildViewsSupport from '../@ember/-internals/views/lib/mixins/child_views_support.js'; import ViewStateSupport from '../@ember/-internals/views/lib/mixins/view_state_support.js'; import ViewMixin from '../@ember/-internals/views/lib/mixins/view_support.js'; import ActionSupport from '../@ember/-internals/views/lib/mixins/action_support.js'; import { getEngineParent } from '../@ember/engine/lib/engine-parent.js'; import { flaggedInstrument, _instrumentStart } from '../@ember/instrumentation/index.js'; import { service } from '../@ember/service/index.js'; import inspect from '../@ember/debug/lib/inspect.js'; import '../@ember/-internals/runtime/lib/mixins/registry_proxy.js'; import '../@ember/-internals/runtime/lib/mixins/container_proxy.js'; import '../@ember/-internals/runtime/lib/mixins/comparable.js'; import '../@ember/-internals/runtime/lib/mixins/action_handler.js'; import { contentFor } from '../@ember/-internals/runtime/lib/mixins/-proxy.js'; import '../@ember/enumerable/mutable.js'; import TargetActionSupport from '../@ember/-internals/runtime/lib/mixins/target_action_support.js'; import '../@ember/-internals/runtime/lib/ext/rsvp.js'; import EventDispatcher from '../@ember/-internals/views/lib/system/event_dispatcher.js'; import { e as enumerableSymbol } from './to-string-B1BmwUkt.js'; import { unwrapTemplate, EMPTY_ARRAY as EMPTY_ARRAY$1, dict } from '../@glimmer/util/index.js'; import { dasherize } from '../@ember/-internals/string/index.js'; import { MUTABLE_CELL } from '../@ember/-internals/views/lib/compat/attrs.js'; import { deprecateUntil, DEPRECATIONS } from '../@ember/-internals/deprecations/index.js'; import { FrameworkObject } from '../@ember/object/-internals.js'; import { CurriedType as CurriedTypes } from '../@glimmer/vm/index.js'; import { RuntimeOpImpl, artifacts } from '../@glimmer/program/index.js'; import { a as RSVP } from './rsvp-DaQAFb0W.js'; import EngineInstance from '../@ember/engine/instance.js'; import { NodeDOMTreeConstruction } from '../@glimmer/node/index.js'; import { _ as _setProp, s as set } from './property_set-4etrFh8A.js'; import setGlobalContext from '../@glimmer/global-context/index.js'; import { isEmberArray } from '../@ember/array/-internals.js'; import { i as isProxy } from './is_proxy-DjvCKvd5.js'; import { isArray } from '../@ember/array/index.js'; import '../route-recognizer/index.js'; import './unrecognized-url-error-zpz-JEoG.js'; import '../@ember/routing/lib/routing-service.js'; import { generateControllerFactory } from '../@ember/routing/lib/generate_controller.js'; const RootTemplate = templateFactory( /* {{component this}} */ { "id": "tjANIXCV", "block": "[[[46,[30,0],null,null,null]],[],false,[\"component\"]]", "moduleName": "packages/@ember/-internals/glimmer/lib/templates/root.hbs", "isStrictMode": true }); const InputTemplate = templateFactory( /* <input {{!-- for compatibility --}} id={{this.id}} class={{this.class}} ...attributes type={{this.type}} checked={{this.checked}} value={{this.value}} {{on "change" this.change}} {{on "input" this.input}} {{on "keyup" this.keyUp}} {{on "paste" this.valueDidChange}} {{on "cut" this.valueDidChange}} /> */ { "id": "4z3DuGQ3", "block": "[[[11,\"input\"],[16,1,[30,0,[\"id\"]]],[16,0,[30,0,[\"class\"]]],[17,1],[16,4,[30,0,[\"type\"]]],[16,\"checked\",[30,0,[\"checked\"]]],[16,2,[30,0,[\"value\"]]],[4,[32,0],[\"change\",[30,0,[\"change\"]]],null],[4,[32,0],[\"input\",[30,0,[\"input\"]]],null],[4,[32,0],[\"keyup\",[30,0,[\"keyUp\"]]],null],[4,[32,0],[\"paste\",[30,0,[\"valueDidChange\"]]],null],[4,[32,0],[\"cut\",[30,0,[\"valueDidChange\"]]],null],[12],[13]],[\"&attrs\"],false,[]]", "moduleName": "packages/@ember/-internals/glimmer/lib/templates/input.hbs", "scope": () => [on], "isStrictMode": true }); function NOOP$2() {} class InternalComponent { // Override this static toString() { return 'internal component'; } constructor(owner, args, caller) { this.owner = owner; this.args = args; this.caller = caller; setOwner(this, owner); } /** * The default HTML id attribute. We don't really _need_ one, this is just * added for compatibility as it's hard to tell if people rely on it being * present, and it doens't really hurt. * * However, don't rely on this internally, like passing it to `getElementId`. * This can be (and often is) overriden by passing an `id` attribute on the * invocation, which shadows this default id via `...attributes`. */ get id() { return guidFor(this); } /** * The default HTML class attribute. Similar to the above, we don't _need_ * them, they are just added for compatibility as it's similarly hard to tell * if people rely on it in their CSS etc, and it doens't really hurt. */ get class() { return 'ember-view'; } validateArguments() { for (let name of Object.keys(this.args.named)) { if (!this.isSupportedArgument(name)) { this.onUnsupportedArgument(name); } } } named(name) { let ref = this.args.named[name]; return ref ? valueForRef(ref) : undefined; } positional(index) { let ref = this.args.positional[index]; return ref ? valueForRef(ref) : undefined; } listenerFor(name) { let listener = this.named(name); if (listener) { (isDevelopingApp() && !(typeof listener === 'function') && assert(`The \`@${name}\` argument to the <${this.constructor}> component must be a function`, typeof listener === 'function')); return listener; } else { return NOOP$2; } } isSupportedArgument(_name) { return false; } onUnsupportedArgument(_name) {} toString() { return `<${this.constructor}:${guidFor(this)}>`; } } const OPAQUE_CONSTRUCTOR_MAP = new WeakMap(); function opaquify(constructor, template) { let _opaque = { // Factory interface create() { throw assert('Use constructor instead of create'); }, toString() { return constructor.toString(); } }; let opaque = _opaque; OPAQUE_CONSTRUCTOR_MAP.set(opaque, constructor); setInternalComponentManager(INTERNAL_COMPONENT_MANAGER, opaque); setComponentTemplate(template, opaque); return opaque; } function deopaquify(opaque) { let constructor = OPAQUE_CONSTRUCTOR_MAP.get(opaque); (isDevelopingApp() && !(constructor) && assert(`[BUG] Invalid internal component constructor: ${opaque}`, constructor)); return constructor; } const CAPABILITIES$2 = { dynamicLayout: false, dynamicTag: false, prepareArgs: false, createArgs: true, attributeHook: false, elementHook: false, createCaller: true, dynamicScope: false, updateHook: false, createInstance: true, wrapped: false, willDestroy: false, hasSubOwner: false }; class InternalManager { getCapabilities() { return CAPABILITIES$2; } create(owner, definition, args, _env, _dynamicScope, caller) { (isDevelopingApp() && !(isConstRef(caller)) && assert('caller must be const', isConstRef(caller))); let ComponentClass = deopaquify(definition); let instance = new ComponentClass(owner, args.capture(), valueForRef(caller)); untrack(instance['validateArguments'].bind(instance)); return instance; } didCreate() {} didUpdate() {} didRenderLayout() {} didUpdateLayout() {} getDebugName(definition) { return definition.toString(); } getSelf(instance) { return createConstRef(instance, 'this'); } getDestroyable(instance) { return instance; } } const INTERNAL_COMPONENT_MANAGER = new InternalManager(); const UNINITIALIZED = Object.freeze({}); function elementForEvent(event) { (isDevelopingApp() && !(event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) && assert('[BUG] event target must be an <input> or <textarea> element', event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement)); return event.target; } function valueForEvent(event) { return elementForEvent(event).value; } function devirtualize(callback) { return event => callback(valueForEvent(event), event); } function valueFrom(reference) { if (reference === undefined) { return new LocalValue(undefined); } else if (isConstRef(reference)) { return new LocalValue(valueForRef(reference)); } else if (isUpdatableRef(reference)) { return new UpstreamValue(reference); } else { return new ForkedValue(reference); } } class LocalValue { static { decorateFieldV2(this.prototype, "value", [tracked]); } #value = (initializeDeferredDecorator(this, "value"), void 0); constructor(value) { this.value = value; } get() { return this.value; } set(value) { this.value = value; } } class UpstreamValue { constructor(reference) { this.reference = reference; } get() { return valueForRef(this.reference); } set(value) { updateRef(this.reference, value); } } class ForkedValue { local; upstream; lastUpstreamValue = UNINITIALIZED; constructor(reference) { this.upstream = new UpstreamValue(reference); } get() { let upstreamValue = this.upstream.get(); if (upstreamValue !== this.lastUpstreamValue) { this.lastUpstreamValue = upstreamValue; this.local = new LocalValue(upstreamValue); } (isDevelopingApp() && !(this.local) && assert('[BUG] this.local must have been initialized at this point', this.local)); return this.local.get(); } set(value) { (isDevelopingApp() && !(this.local) && assert('[BUG] this.local must have been initialized at this point', this.local)); this.local.set(value); } } class AbstractInput extends InternalComponent { validateArguments() { (isDevelopingApp() && !(this.args.positional.length === 0) && assert(`The ${this.constructor} component does not take any positional arguments`, this.args.positional.length === 0)); super.validateArguments(); } _value = valueFrom(this.args.named['value']); get value() { return this._value.get(); } set value(value) { this._value.set(value); } valueDidChange(event) { this.value = valueForEvent(event); } /** * The `change` and `input` actions need to be overridden in the `Input` * subclass. Unfortunately, some ember-source builds currently uses babel * loose mode to transpile its classes. Having the `@action` decorator on the * super class creates a getter on the prototype, and when the subclass * overrides the method, the loose mode transpilation would emit something * like `Subclass.prototype['change'] = function change() { ... }`, which * fails because `prototype['change']` is getter-only/readonly. The correct * solution is to use `Object.defineProperty(prototype, 'change', ...)` but * that requires disabling loose mode. For now, the workaround is to add the * decorator only on the subclass. This is more of a configuration issue on * our own builds and doesn't really affect apps. */ /* @action */ static { decorateMethodV2(this.prototype, "valueDidChange", [action$1]); } change(event) { this.valueDidChange(event); } /* @action */ input(event) { this.valueDidChange(event); } keyUp(event) { switch (event.key) { case 'Enter': this.listenerFor('enter')(event); this.listenerFor('insert-newline')(event); break; case 'Escape': this.listenerFor('escape-press')(event); break; } } static { decorateMethodV2(this.prototype, "keyUp", [action$1]); } listenerFor(name) { let listener = super.listenerFor(name); if (this.isVirtualEventListener(name, listener)) { return devirtualize(listener); } else { return listener; } } isVirtualEventListener(name, _listener) { let virtualEvents = ['enter', 'insert-newline', 'escape-press']; return virtualEvents.indexOf(name) !== -1; } } /** @module @ember/component */ let isValidInputType; if (hasDOM) { const INPUT_TYPES = Object.create(null); const INPUT_ELEMENT = document.createElement('input'); INPUT_TYPES[''] = false; INPUT_TYPES['text'] = true; INPUT_TYPES['checkbox'] = true; isValidInputType = type => { let isValid = INPUT_TYPES[type]; if (isValid === undefined) { try { INPUT_ELEMENT.type = type; isValid = INPUT_ELEMENT.type === type; } catch (_e) { isValid = false; } finally { INPUT_ELEMENT.type = 'text'; } INPUT_TYPES[type] = isValid; } return isValid; }; } else { isValidInputType = type => type !== ''; } /** See [Ember.Templates.components.Input](/ember/release/classes/Ember.Templates.components/methods/Input?anchor=Input). @method input @for Ember.Templates.helpers @param {Hash} options @public */ /** An opaque interface which can be imported and used in strict-mode templates to call <Input>. See [Ember.Templates.components.Input](/ember/release/classes/Ember.Templates.components/methods/Input?anchor=Input). @for @ember/component @method Input @see {Ember.Templates.components.Input} @public **/ /** The `Input` component lets you create an HTML `<input>` element. ```handlebars <Input @value="987" /> ``` creates an `<input>` element with `type="text"` and value set to 987. ### Text field If no `type` argument is specified, a default of type 'text' is used. ```handlebars Search: <Input @value={{this.searchWord}} /> ``` In this example, the initial value in the `<input>` will be set to the value of `this.searchWord`. If the user changes the text, the value of `this.searchWord` will also be updated. ### Actions The `Input` component takes a number of arguments with callbacks that are invoked in response to user events. * `enter` * `insert-newline` * `escape-press` * `focus-in` * `focus-out` * `key-down` * `key-press` * `key-up` These callbacks are passed to `Input` like this: ```handlebars <Input @value={{this.searchWord}} @enter={{this.query}} /> ``` Starting with Ember Octane, we recommend using the `{{on}}` modifier to call actions on specific events, such as the input event. ```handlebars <label for="input-name">Name:</label> <Input @id="input-name" @value={{this.name}} {{on "input" this.validateName}} /> ``` The event name (e.g. `focusout`, `input`, `keydown`) always follows the casing that the HTML standard uses. ### `<input>` HTML Attributes to Avoid In most cases, if you want to pass an attribute to the underlying HTML `<input>` element, you can pass the attribute directly, just like any other Ember component. ```handlebars <Input @type="text" size="10" /> ``` In this example, the `size` attribute will be applied to the underlying `<input>` element in the outputted HTML. However, there are a few attributes where you **must** use the `@` version. * `@type`: This argument is used to control which Ember component is used under the hood * `@value`: The `@value` argument installs a two-way binding onto the element. If you wanted a one-way binding, use `<input>` with the `value` property and the `input` event instead. * `@checked` (for checkboxes): like `@value`, the `@checked` argument installs a two-way binding onto the element. If you wanted a one-way binding, use `<input type="checkbox">` with `checked` and the `input` event instead. ### Checkbox To create an `<input type="checkbox">`: ```handlebars Emberize Everything: <Input @type="checkbox" @checked={{this.isEmberized}} name="isEmberized" /> ``` This will bind the checked state of this checkbox to the value of `isEmberized` -- if either one changes, it will be reflected in the other. @method Input @for Ember.Templates.components @param {Hash} options @public */ class _Input extends AbstractInput { static toString() { return 'Input'; } /** * The HTML class attribute. */ get class() { if (this.isCheckbox) { return 'ember-checkbox ember-view'; } else { return 'ember-text-field ember-view'; } } /** * The HTML type attribute. */ get type() { let type = this.named('type'); if (type === null || type === undefined) { return 'text'; } (isDevelopingApp() && !(typeof type === 'string') && assert('The `@type` argument to the <Input> component must be a string', typeof type === 'string')); return isValidInputType(type) ? type : 'text'; } get isCheckbox() { return this.named('type') === 'checkbox'; } _checked = valueFrom(this.args.named['checked']); get checked() { if (this.isCheckbox) { (isDevelopingApp() && warn('`<Input @type="checkbox" />` reflects its checked state via the `@checked` argument. ' + 'You wrote `<Input @type="checkbox" @value={{...}} />` which is likely not what you intended. ' + 'Did you mean `<Input @type="checkbox" @checked={{...}} />`?', untrack(() => this.args.named['checked'] !== undefined || this.args.named['value'] === undefined || typeof valueForRef(this.args.named['value']) === 'string'), { id: 'ember.built-in-components.input-checkbox-value' })); return this._checked.get(); } else { return undefined; } } set checked(checked) { (isDevelopingApp() && warn('`<Input @type="checkbox" />` reflects its checked state via the `@checked` argument. ' + 'You wrote `<Input @type="checkbox" @value={{...}} />` which is likely not what you intended. ' + 'Did you mean `<Input @type="checkbox" @checked={{...}} />`?', untrack(() => this.args.named['checked'] !== undefined || this.args.named['value'] === undefined || typeof valueForRef(this.args.named['value']) === 'string'), { id: 'ember.built-in-components.input-checkbox-value' })); this._checked.set(checked); } change(event) { if (this.isCheckbox) { this.checkedDidChange(event); } else { super.change(event); } } static { decorateMethodV2(this.prototype, "change", [action$1]); } input(event) { if (!this.isCheckbox) { super.input(event); } } static { decorateMethodV2(this.prototype, "input", [action$1]); } checkedDidChange(event) { let element = event.target; (isDevelopingApp() && !(element instanceof HTMLInputElement) && assert('[BUG] element must be an <input>', element instanceof HTMLInputElement)); this.checked = element.checked; } static { decorateMethodV2(this.prototype, "checkedDidChange", [action$1]); } isSupportedArgument(name) { let supportedArguments = ['type', 'value', 'checked', 'enter', 'insert-newline', 'escape-press']; return supportedArguments.indexOf(name) !== -1 || super.isSupportedArgument(name); } } const Input = opaquify(_Input, InputTemplate); const LinkToTemplate = templateFactory( /* <a {{!-- for compatibility --}} id={{this.id}} class={{this.class}} {{!-- deprecated attribute bindings --}} role={{this.role}} title={{this.title}} rel={{this.rel}} tabindex={{this.tabindex}} target={{this.target}} ...attributes href={{this.href}} {{on 'click' this.click}} >{{yield}}</a> */ { "id": "Ub0nir+H", "block": "[[[11,3],[16,1,[30,0,[\"id\"]]],[16,0,[30,0,[\"class\"]]],[16,\"role\",[30,0,[\"role\"]]],[16,\"title\",[30,0,[\"title\"]]],[16,\"rel\",[30,0,[\"rel\"]]],[16,\"tabindex\",[30,0,[\"tabindex\"]]],[16,\"target\",[30,0,[\"target\"]]],[17,1],[16,6,[30,0,[\"href\"]]],[4,[32,0],[\"click\",[30,0,[\"click\"]]],null],[12],[18,2,null],[13]],[\"&attrs\",\"&default\"],false,[\"yield\"]]", "moduleName": "packages/@ember/-internals/glimmer/lib/templates/link-to.hbs", "scope": () => [on], "isStrictMode": true }); const EMPTY_ARRAY = []; const EMPTY_QUERY_PARAMS = {}; debugFreeze(EMPTY_ARRAY); debugFreeze(EMPTY_QUERY_PARAMS); function isMissing(value) { return value === null || value === undefined; } function isPresent(value) { return !isMissing(value); } function isQueryParams(value) { return typeof value === 'object' && value !== null && value['isQueryParams'] === true; } /** The `LinkTo` component renders a link to the supplied `routeName` passing an optionally supplied model to the route as its `model` context of the route. The block for `LinkTo` becomes the contents of the rendered element: ```handlebars <LinkTo @route='photoGallery'> Great Hamster Photos </LinkTo> ``` This will result in: ```html <a href="/hamster-photos"> Great Hamster Photos </a> ``` ### Disabling the `LinkTo` component The `LinkTo` component can be disabled by using the `disabled` argument. A disabled link doesn't result in a transition when activated, and adds the `disabled` class to the `<a>` element. (The class name to apply to the element can be overridden by using the `disabledClass` argument) ```handlebars <LinkTo @route='photoGallery' @disabled={{true}}> Great Hamster Photos </LinkTo> ``` ### Handling `href` `<LinkTo>` will use your application's Router to fill the element's `href` property with a URL that matches the path to the supplied `routeName`. ### Handling current route The `LinkTo` component will apply a CSS class name of 'active' when the application's current route matches the supplied routeName. For example, if the application's current route is 'photoGallery.recent', then the following invocation of `LinkTo`: ```handlebars <LinkTo @route='photoGallery.recent'> Great Hamster Photos </LinkTo> ``` will result in ```html <a href="/hamster-photos/this-week" class="active"> Great Hamster Photos </a> ``` The CSS class used for active classes can be customized by passing an `activeClass` argument: ```handlebars <LinkTo @route='photoGallery.recent' @activeClass="current-url"> Great Hamster Photos </LinkTo> ``` ```html <a href="/hamster-photos/this-week" class="current-url"> Great Hamster Photos </a> ``` ### Keeping a link active for other routes If you need a link to be 'active' even when it doesn't match the current route, you can use the `current-when` argument. ```handlebars <LinkTo @route='photoGallery' @current-when='photos'> Photo Gallery </LinkTo> ``` This may be helpful for keeping links active for: * non-nested routes that are logically related * some secondary menu approaches * 'top navigation' with 'sub navigation' scenarios A link will be active if `current-when` is `true` or the current route is the route this link would transition to. To match multiple routes 'space-separate' the routes: ```handlebars <LinkTo @route='gallery' @current-when='photos drawings paintings'> Art Gallery </LinkTo> ``` ### Supplying a model An optional `model` argument can be used for routes whose paths contain dynamic segments. This argument will become the model context of the linked route: ```javascript Router.map(function() { this.route("photoGallery", {path: "hamster-photos/:photo_id"}); }); ``` ```handlebars <LinkTo @route='photoGallery' @model={{this.aPhoto}}> {{aPhoto.title}} </LinkTo> ``` ```html <a href="/hamster-photos/42"> Tomster </a> ``` ### Supplying multiple models For deep-linking to route paths that contain multiple dynamic segments, the `models` argument can be used. As the router transitions through the route path, each supplied model argument will become the context for the route with the dynamic segments: ```javascript Router.map(function() { this.route("photoGallery", { path: "hamster-photos/:photo_id" }, function() { this.route("comment", {path: "comments/:comment_id"}); }); }); ``` This argument will become the model context of the linked route: ```handlebars <LinkTo @route='photoGallery.comment' @models={{array this.aPhoto this.comment}}> {{comment.body}} </LinkTo> ``` ```html <a href="/hamster-photos/42/comments/718"> A+++ would snuggle again. </a> ``` ### Supplying an explicit dynamic segment value If you don't have a model object available to pass to `LinkTo`, an optional string or integer argument can be passed for routes whose paths contain dynamic segments. This argument will become the value of the dynamic segment: ```javascript Router.map(function() { this.route("photoGallery", { path: "hamster-photos/:photo_id" }); }); ``` ```handlebars <LinkTo @route='photoGallery' @model={{aPhotoId}}> {{this.aPhoto.title}} </LinkTo> ``` ```html <a href="/hamster-photos/42"> Tomster </a> ``` When transitioning into the linked route, the `model` hook will be triggered with parameters including this passed identifier. ### Supplying query parameters If you need to add optional key-value pairs that appear to the right of the ? in a URL, you can use the `query` argument. ```handlebars <LinkTo @route='photoGallery' @query={{hash page=1 per_page=20}}> Great Hamster Photos </LinkTo> ``` This will result in: ```html <a href="/hamster-photos?page=1&per_page=20"> Great Hamster Photos </a> ``` @for Ember.Templates.components @method LinkTo @public */ /** @module @ember/routing */ /** See [Ember.Templates.components.LinkTo](/ember/release/classes/Ember.Templates.components/methods/input?anchor=LinkTo). @for Ember.Templates.helpers @method link-to @see {Ember.Templates.components.LinkTo} @public **/ /** An opaque interface which can be imported and used in strict-mode templates to call <LinkTo>. See [Ember.Templates.components.LinkTo](/ember/release/classes/Ember.Templates.components/methods/input?anchor=LinkTo). @for @ember/routing @method LinkTo @see {Ember.Templates.components.LinkTo} @public **/ class _LinkTo extends InternalComponent { static toString() { return 'LinkTo'; } static { decorateFieldV2(this.prototype, "routing", [service('-routing')]); } #routing = (initializeDeferredDecorator(this, "routing"), void 0); validateArguments() { (isDevelopingApp() && !(!this.isEngine || this.engineMountPoint !== undefined) && assert('You attempted to use the <LinkTo> component within a routeless engine, this is not supported. ' + 'If you are using the ember-engines addon, use the <LinkToExternal> component instead. ' + 'See https://ember-engines.com/docs/links for more info.', !this.isEngine || this.engineMountPoint !== undefined)); (isDevelopingApp() && !('route' in this.args.named || 'model' in this.args.named || 'models' in this.args.named || 'query' in this.args.named) && assert('You must provide at least one of the `@route`, `@model`, `@models` or `@query` arguments to `<LinkTo>`.', 'route' in this.args.named || 'model' in this.args.named || 'models' in this.args.named || 'query' in this.args.named)); (isDevelopingApp() && !(!('model' in this.args.named && 'models' in this.args.named)) && assert('You cannot provide both the `@model` and `@models` arguments to the <LinkTo> component.', !('model' in this.args.named && 'models' in this.args.named))); super.validateArguments(); } get class() { let classes = 'ember-view'; if (this.isActive) { classes += this.classFor('active'); if (this.willBeActive === false) { classes += ' ember-transitioning-out'; } } else if (this.willBeActive) { classes += ' ember-transitioning-in'; } if (this.isLoading) { classes += this.classFor('loading'); } if (this.isDisabled) { classes += this.classFor('disabled'); } return classes; } get href() { if (this.isLoading) { return '#'; } let { routing, route, models, query } = this; (isDevelopingApp() && !(isPresent(route)) && assert('[BUG] route can only be missing if isLoading is true', isPresent(route))); // consume the current router state so we invalidate when QP changes // TODO: can we narrow this down to QP changes only? consumeTag(tagFor(routing, 'currentState')); if (isDevelopingApp()) { try { return routing.generateURL(route, models, query); } catch (e) { let details = e instanceof Error ? e.message : inspect(e); let message = `While generating link to route "${route}": ${details}`; if (e instanceof Error) { e.message = message; throw e; } else { throw message; } } } else { return routing.generateURL(route, models, query); } } click(event) { if (!isSimpleClick(event)) { return; } let element = event.currentTarget; (isDevelopingApp() && !(element instanceof HTMLAnchorElement) && assert('[BUG] must be an <a> element', element instanceof HTMLAnchorElement)); let isSelf = element.target === '' || element.target === '_self'; if (isSelf) { this.preventDefault(event); } else { return; } if (this.isDisabled) { return; } if (this.isLoading) { (isDevelopingApp() && warn('This link is in an inactive loading state because at least one of its models ' + 'currently has a null/undefined value, or the provided route name is invalid.', false, { id: 'ember-glimmer.link-to.inactive-loading-state' })); return; } let { routing, route, models, query, replace } = this; let payload = { routeName: route, queryParams: query, transition: undefined }; flaggedInstrument('interaction.link-to', payload, () => { (isDevelopingApp() && !(isPresent(route)) && assert('[BUG] route can only be missing if isLoading is true', isPresent(route))); payload.transition = routing.transitionTo(route, models, query, replace); }); } static { decorateMethodV2(this.prototype, "click", [action$1]); } get route() { if ('route' in this.args.named) { let route = this.named('route'); (isDevelopingApp() && !(isMissing(route) || typeof route === 'string') && assert('The `@route` argument to the <LinkTo> component must be a string', isMissing(route) || typeof route === 'string')); return route && this.namespaceRoute(route); } else { return this.currentRoute; } } // GH #17963 currentRouteCache = createCache(() => { consumeTag(tagFor(this.routing, 'currentState')); return untrack(() => this.routing.currentRouteName); }); get currentRoute() { return getValue(this.currentRouteCache); } // TODO: not sure why generateURL takes {}[] instead of unknown[] get models() { if ('models' in this.args.named) { let models = this.named('models'); (isDevelopingApp() && !(Array.isArray(models)) && assert('The `@models` argument to the <LinkTo> component must be an array.', Array.isArray(models))); return models; } else if ('model' in this.args.named) { return [this.named('model')]; } else { return EMPTY_ARRAY; } } get query() { if ('query' in this.args.named) { let query = this.named('query'); (isDevelopingApp() && !(query !== null && typeof query === 'object') && assert('The `@query` argument to the <LinkTo> component must be an object.', query !== null && typeof query === 'object')); return { ...query }; } else { return EMPTY_QUERY_PARAMS; } } get replace() { return this.named('replace') === true; } get isActive() { return this.isActiveForState(this.routing.currentState); } get willBeActive() { let current = this.routing.currentState; let target = this.routing.targetState; if (current === target) { return null; } else { return this.isActiveForState(target); } } get isLoading() { return isMissing(this.route) || this.models.some(model => isMissing(model)); } get isDisabled() { return Boolean(this.named('disabled')); } get isEngine() { let owner = this.owner; return getEngineParent(owner) !== undefined; } get engineMountPoint() { let owner = this.owner; return owner.mountPoint; } classFor(state) { let className = this.named(`${state}Class`); (isDevelopingApp() && !(isMissing(className) || typeof className === 'string' || typeof className === 'boolean') && assert(`The \`@${state}Class\` argument to the <LinkTo> component must be a string or boolean`, isMissing(className) || typeof className === 'string' || typeof className === 'boolean')); if (className === true || isMissing(className)) { return ` ${state}`; } else if (className) { return ` ${className}`; } else { return ''; } } namespaceRoute(route) { let { engineMountPoint } = this; if (engineMountPoint === undefined) { return route; } else if (route === 'application') { return engineMountPoint; } else { return `${engineMountPoint}.${route}`; } } isActiveForState(state) { if (!isPresent(state)) { return false; } if (this.isLoading) { return false; } let currentWhen = this.named('current-when'); if (typeof currentWhen === 'boolean') { return currentWhen; } else if (typeof currentWhen === 'string') { let { models, routing } = this; return currentWhen.split(' ').some(route => routing.isActiveForRoute(models, undefined, this.namespaceRoute(route), state)); } else { let { route, models, query, routing } = this; (isDevelopingApp() && !(isPresent(route)) && assert('[BUG] route can only be missing if isLoading is true', isPresent(route))); return routing.isActiveForRoute(models, query, route, state); } } preventDefault(event) { event.preventDefault(); } isSupportedArgument(name) { let supportedArguments = ['route', 'model', 'models', 'query', 'replace', 'disabled', 'current-when', 'activeClass', 'loadingClass', 'disabledClass']; return supportedArguments.indexOf(name) !== -1 || super.isSupportedArgument(name); } } let { prototype } = _LinkTo; let descriptorFor = (target, property) => { if (target) { return Object.getOwnPropertyDescriptor(target, property) || descriptorFor(Object.getPrototypeOf(target), property); } else { return null; } }; // @href { let superOnUnsupportedArgument = prototype['onUnsupportedArgument']; Object.defineProperty(prototype, 'onUnsupportedArgument', { configurable: true, enumerable: false, value: function onUnsupportedArgument(name) { if (name === 'href') { (isDevelopingApp() && !(false) && assert(`Passing the \`@href\` argument to <LinkTo> is not supported.`)); } else { superOnUnsupportedArgument.call(this, name); } } }); } // QP { let superModelsDescriptor = descriptorFor(prototype, 'models'); (isDevelopingApp() && !(superModelsDescriptor && typeof superModelsDescriptor.get === 'function') && assert(`[BUG] expecting models to be a getter on <LinkTo>`, superModelsDescriptor && typeof superModelsDescriptor.get === 'function')); let superModelsGetter = superModelsDescriptor.get; Object.defineProperty(prototype, 'models', { configurable: true, enumerable: false, get: function models() { let models = superModelsGetter.call(this); if (models.length > 0 && !('query' in this.args.named)) { if (isQueryParams(models[models.length - 1])) { models = models.slice(0, -1); } } return models; } }); let superQueryDescriptor = descriptorFor(prototype, 'query'); (isDevelopingApp() && !(superQueryDescriptor && typeof superQueryDescriptor.get === 'function') && assert(`[BUG] expecting query to be a getter on <LinkTo>`, superQueryDescriptor && typeof superQueryDescriptor.get === 'function')); let superQueryGetter = superQueryDescriptor.get; Object.defineProperty(prototype, 'query', { configurable: true, enumerable: false, get: function query() { if ('query' in this.args.named) { let qp = superQueryGetter.call(this); if (isQueryParams(qp)) { return qp.values ?? EMPTY_QUERY_PARAMS; } else { return qp; } } else { let models = superModelsGetter.call(this); if (models.length > 0) { let qp = models[models.length - 1]; if (isQueryParams(qp) && qp.values !== null) { return qp.values; } } return EMPTY_QUERY_PARAMS; } } }); } // Positional Arguments { let superOnUnsupportedArgument = prototype['onUnsupportedArgument']; Object.defineProperty(prototype, 'onUnsupportedArgument', { configurable: true, enumerable: false, value: function onUnsupportedArgument(name) { if (name !== 'params') { superOnUnsupportedArgument.call(this, name); } } }); } const LinkTo = opaquify(_LinkTo, LinkToTemplate); const TextareaTemplate = templateFactory( /* <textarea {{!-- for compatibility --}} id={{this.id}} class={{this.class}} ...attributes value={{this.value}} {{on "change" this.change}} {{on "input" this.input}} {{on "keyup" this.keyUp}} {{on "paste" this.valueDidChange}} {{on "cut" this.valueDidChange}} /> */ { "id": "112WKCh2", "block": "[[[11,\"textarea\"],[16,1,[30,0,[\"id\"]]],[16,0,[30,0,[\"class\"]]],[17,1],[16,2,[30,0,[\"value\"]]],[4,[32,0],[\"change\",[30,0,[\"change\"]]],null],[4,[32,0],[\"input\",[30,0,[\"input\"]]],null],[4,[32,0],[\"keyup\",[30,0,[\"keyUp\"]]],null],[4,[32,0],[\"paste\",[30,0,[\"valueDidChange\"]]],null],[4,[32,0],[\"cut\",[30,0,[\"valueDidChange\"]]],null],[12],[13]],[\"&attrs\"],false,[]]", "moduleName": "packages/@ember/-internals/glimmer/lib/templates/textarea.hbs", "scope": () => [on], "isStrictMode": true }); /** @module @ember/component */ class _Textarea extends AbstractInput { static toString() { return 'Textarea'; } get class() { return 'ember-text-area ember-view'; } // See abstract-input.ts for why these are needed change(event) { super.change(event); } static { decorateMethodV2(this.prototype, "change", [action$1]); } input(event) { super.input(event); } static { decorateMethodV2(this.prototype, "input", [action$1]); } isSupportedArgument(name) { let supportedArguments = ['type', 'value', 'enter', 'insert-newline', 'escape-press']; return supportedArguments.indexOf(name) !== -1 || super.isSupportedArgument(name); } } const Textarea = opaquify(_Textarea, TextareaTemplate); 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) { (isDevelopingApp() && !(microsyntax !== 'class') && assert('You cannot use class as an attributeBinding, use classNameBindings instead.', microsyntax !== 'class')); return [microsyntax, microsyntax, true]; } else { let prop = microsyntax.substring(0, colonIndex); let attribute = microsyntax.substring(colonIndex + 1); (isDevelopingApp() && !(attribute !== 'class') && assert('You cannot use class as an attributeBinding, use classNameBindings instead.', attribute !== 'class')); 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); (isDevelopingApp() && !(!(isSimple && isPath)) && assert(`Illegal attributeBinding: '${prop}' is not a valid attribute name.`, !(isSimple && isPath))); operations.setAttribute(attribute, reference, false, null); } function createClassNameBindingRef(rootRef, microsyntax, operations) { let parts = microsyntax.split(':'); let [prop, truthy, falsy] = parts; // NOTE: This could be an empty string (isDevelopingApp() && !(prop !== undefined) && assert('has prop', prop !== undefined)); // Will always have at least one part 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) { (isDevelopingApp() && !(path !== undefined) && assert('You must pass a path when binding a to a class name using classNameBindings', path !== undefined)); 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$1() {} /** @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, 'this'); 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$1; } } function internalHelper(helper) { return setInternalHelperManager(helper, {}); } /** @module ember */ const ACTIONS = new WeakSet(); /** The `{{action}}` helper provides a way to pass triggers for behavior (usually just a function) between components, and into components from controllers. ### Passing functions with the action helper There are three contexts an action helper can be used in. The first two contexts to discuss are attribute context, and Handlebars value context. ```handlebars {{! An example of attribute context }} <div onclick={{action "save"}}></div> {{! Examples of Handlebars value context }} {{input on-input=(action "save")}} {{yield (action "refreshData") andAnotherParam}} ``` In these contexts, the helper is called a "closure action" helper. Its behavior is simple: If passed a function name, read that function off the `actions` property of the current context. Once that function is read, or immediately if a function was passed, create a closure over that function and any arguments. The resulting value of an action helper used this way is simply a function. For example, in the attribute context: ```handlebars {{! An example of attribute context }} <div onclick={{action "save"}}></div> ``` The resulting template render logic would be: ```js var div = document.createElement('div'); var actionFunction = (function(context){ return function() { return context.actions.save.apply(context, arguments); }; })(context); div.onclick = actionFunction; ``` Thus when the div is clicked, the action on that context is called. Because the `actionFunction` is just a function, closure actions can be passed between components and still execute in the correct context. Here is an example action handler on a component: ```app/components/my-component.js import Component from '@glimmer/component'; import { action } from '@ember/object'; export default class extends Component { @action save() { this.model.save(); } } ``` Actions are always looked up on the `actions` property of the current context. This avoids collisions in the naming of common actions, such as `destroy`. Two options can be passed to the `action` helper when it is used in this way. * `target=someProperty` will look to `someProperty` instead of the current context for the `actions` hash. This can be useful when targeting a service for actions. * `value="target.value"` will read the path `target.value` off the first argument to the action when it is called and rewrite the first argument to be that value. This is useful when attaching actions to event listeners. ### Invoking an action Closure actions curry both their scope and any arguments. When invoked, any additional arguments are added to the already curried list. Actions are presented in JavaScript as callbacks, and are invoked like any other JavaScript function. For example ```app/components/update-name.js import Component from '@glimmer/component'; import { action } from '@ember/object'; export default class extends Component { @action setName(model, name) { model.set('name', name); } } ``` ```app/components/update-name.hbs