UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

1,490 lines (1,340 loc) 185 kB
import { templateFactory, EvaluationContextImpl } from '../@glimmer/opcode-compiler/index.js'; import { g as getFactoryFor, p as privatize } from './registry-BJpQx6hv.js'; import { warn, debugFreeze, deprecate } from '../@ember/debug/index.js'; import { reifyPositional, normalizeProperty, EMPTY_ARGS, createCapturedArgs, EMPTY_POSITIONAL, curry, hash, get as get$1, fn, concat, array, on as on$1, TEMPLATE_ONLY_COMPONENT_MANAGER, templateOnlyComponent, clientBuilder, runtimeOptions, inTransaction, renderMain } from '../@glimmer/runtime/index.js'; import { join, schedule, _backburner, _getCurrentRunLoop } from '../@ember/runloop/index.js'; import { valueForRef, isConstRef, createConstRef, isUpdatableRef, updateRef, createComputeRef, createPrimitiveRef, childRefFor, childRefFromParts, isInvokableRef, createUnboundRef, UNDEFINED_REFERENCE, createInvokableRef, createReadOnlyRef, createDebugAliasRef } 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, hasInternalComponentManager, 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-CXNsxygN.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-fCezwMOy.js'; import { E as ENV } from './env-CwR5CFCu.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-DaIn5NlY.js'; import { 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 { artifacts, RuntimeOpImpl } from '../@glimmer/program/index.js'; import { a as RSVP } from './rsvp-ziM3qQyS.js'; import EngineInstance from '../@ember/engine/instance.js'; import { _ as _setProp, s as set } from './property_set-2JtwI-ab.js'; import setGlobalContext from '../@glimmer/global-context/index.js'; import { isEmberArray } from '../@ember/array/-internals.js'; import { i as isProxy } from './is_proxy-DxRbm8wn.js'; import { isArray } from '../@ember/array/index.js'; import '../route-recognizer/index.js'; import './unrecognized-url-error-Csk7hcJF.js'; import '../@ember/routing/lib/routing-service.js'; import { generateControllerFactory } from '../@ember/routing/lib/generate_controller.js'; const RootTemplate = templateFactory( /* {{component this}} */ { "id": "yTlmws8O", "block": "[[[46,[30,0],null,null,null]],[],[\"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": "Cc/BCoQJ", "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\"],[]]", "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$3 = { 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$3; } 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": "7Z3LFeO/", "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\"],[\"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 = { 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[] // eslint-disable-next-line @typescript-eslint/no-empty-object-type 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) { // eslint-disable-next-line @typescript-eslint/no-empty-object-type 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() && true && 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')); // eslint-disable-next-line @typescript-eslint/no-empty-object-type let superModelsGetter = superModelsDescriptor.get; Object.defineProperty(prototype, 'models', { configurable: true, enumerable: false, // eslint-disable-next-line @typescript-eslint/no-empty-object-type 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')); // eslint-disable-next-line @typescript-eslint/no-empty-object-type let superQueryGetter = superQueryDescriptor.get; Object.defineProperty(prototype, 'query', { configurable: true, enumerable: false, // eslint-disable-next-line @typescript-eslint/no-empty-object-type 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": "KVdeMchh", "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\"],[]]", "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); /** * @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) { (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.