diff --git a/packages/@ember/-internals/glimmer/lib/components/link-to.ts b/packages/@ember/-internals/glimmer/lib/components/link-to.ts
index efbb694fc51..4a13b2d266a 100644
--- a/packages/@ember/-internals/glimmer/lib/components/link-to.ts
+++ b/packages/@ember/-internals/glimmer/lib/components/link-to.ts
@@ -2,885 +2,1838 @@
@module ember
*/
-/**
- The `{{link-to}}` 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 `{{link-to}}` becomes the innerHTML of the rendered
- element:
-
- ```handlebars
- {{#link-to 'photoGallery'}}
- Great Hamster Photos
- {{/link-to}}
- ```
-
- You can also use an inline form of `{{link-to}}` component by
- passing the link text as the first argument
- to the component:
-
- ```handlebars
- {{link-to 'Great Hamster Photos' 'photoGallery'}}
- ```
-
- Both will result in:
-
- ```html
-
- Great Hamster Photos
-
- ```
-
- ### Supplying a tagName
- By default `{{link-to}}` renders an `` element. This can
- be overridden for a single use of `{{link-to}}` by supplying
- a `tagName` option:
-
- ```handlebars
- {{#link-to 'photoGallery' tagName="li"}}
- Great Hamster Photos
- {{/link-to}}
- ```
-
- ```html
-
- Great Hamster Photos
-
- ```
-
- To override this option for your entire application, see
- "Overriding Application-wide Defaults".
-
- ### Disabling the `link-to` component
- By default `{{link-to}}` is enabled.
- any passed value to the `disabled` component property will disable
- the `link-to` component.
-
- static use: the `disabled` option:
-
- ```handlebars
- {{#link-to 'photoGallery' disabled=true}}
- Great Hamster Photos
- {{/link-to}}
- ```
-
- dynamic use: the `disabledWhen` option:
-
- ```handlebars
- {{#link-to 'photoGallery' disabledWhen=controller.someProperty}}
- Great Hamster Photos
- {{/link-to}}
- ```
-
- any truthy value passed to `disabled` will disable it except `undefined`.
-
- See "Overriding Application-wide Defaults" for more.
-
- ### Handling `href`
- `{{link-to}}` will use your application's Router to
- fill the element's `href` property with a url that
- matches the path to the supplied `routeName` for your
- router's configured `Location` scheme, which defaults
- to HashLocation.
-
- ### Handling current route
- `{{link-to}}` 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' the following
- use of `{{link-to}}`:
-
- ```handlebars
- {{#link-to 'photoGallery.recent'}}
- Great Hamster Photos
- {{/link-to}}
- ```
-
- will result in
-
- ```html
-
- Great Hamster Photos
-
- ```
-
- The CSS class name used for active classes can be customized
- for a single use of `{{link-to}}` by passing an `activeClass`
- option:
-
- ```handlebars
- {{#link-to 'photoGallery.recent' activeClass="current-url"}}
- Great Hamster Photos
- {{/link-to}}
- ```
-
- ```html
-
- Great Hamster Photos
-
- ```
-
- To override this option for your entire application, see
- "Overriding Application-wide Defaults".
-
- ### 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
- {{#link-to 'photoGallery' current-when='photos'}}
- Photo Gallery
- {{/link-to}}
- ```
-
- 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
- {{#link-to 'gallery' current-when='photos drawings paintings'}}
- Art Gallery
- {{/link-to}}
- ```
-
- ### 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
- {{#link-to 'photoGallery' aPhoto}}
- {{aPhoto.title}}
- {{/link-to}}
- ```
-
- ```html
-
- Tomster
-
- ```
-
- ### Supplying multiple models
- For deep-linking to route paths that contain multiple
- dynamic segments, multiple model arguments 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
- {{#link-to 'photoGallery.comment' aPhoto comment}}
- {{comment.body}}
- {{/link-to}}
- ```
-
- ```html
-
- A+++ would snuggle again.
-
- ```
-
- ### Supplying an explicit dynamic segment value
- If you don't have a model object available to pass to `{{link-to}}`,
- 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" });
- });
- ```
+import { alias, computed, get } from '@ember/-internals/metal';
+import { isSimpleClick } from '@ember/-internals/views';
+import { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features';
+import { assert, warn } from '@ember/debug';
+import { flaggedInstrument } from '@ember/instrumentation';
+import { assign } from '@ember/polyfills';
+import { inject as injectService } from '@ember/service';
+import { DEBUG } from '@glimmer/env';
+import EmberComponent, { HAS_BLOCK } from '../component';
+import layout from '../templates/link-to';
- ```handlebars
- {{#link-to 'photoGallery' aPhotoId}}
- {{aPhoto.title}}
- {{/link-to}}
- ```
+let LinkComponent: any;
- ```html
-
- Tomster
-
- ```
+if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) {
+ /**
+ The `{{link-to}}` 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 `{{link-to}}` becomes the innerHTML of the rendered
+ element:
+
+ ```handlebars
+ {{#link-to 'photoGallery'}}
+ Great Hamster Photos
+ {{/link-to}}
+ ```
- When transitioning into the linked route, the `model` hook will
- be triggered with parameters including this passed identifier.
+ You can also use an inline form of `{{link-to}}` component by
+ passing the link text as the first argument
+ to the component:
- ### Allowing Default Action
+ ```handlebars
+ {{link-to 'Great Hamster Photos' 'photoGallery'}}
+ ```
- By default the `{{link-to}}` component prevents the default browser action
- by calling `preventDefault()` as this sort of action bubbling is normally
- handled internally and we do not want to take the browser to a new URL (for
- example).
+ Both will result in:
- If you need to override this behavior specify `preventDefault=false` in
- your template:
+ ```html
+
+ Great Hamster Photos
+
+ ```
- ```handlebars
- {{#link-to 'photoGallery' aPhotoId preventDefault=false}}
- {{aPhotoId.title}}
- {{/link-to}}
- ```
+ ### Supplying a tagName
+ By default `{{link-to}}` renders an `` element. This can
+ be overridden for a single use of `{{link-to}}` by supplying
+ a `tagName` option:
- ### Overriding attributes
- You can override any given property of the `LinkComponent`
- that is generated by the `{{link-to}}` component by passing
- key/value pairs, like so:
+ ```handlebars
+ {{#link-to 'photoGallery' tagName="li"}}
+ Great Hamster Photos
+ {{/link-to}}
+ ```
- ```handlebars
- {{#link-to aPhoto tagName='li' title='Following this link will change your life' classNames='pic sweet'}}
- Uh-mazing!
- {{/link-to}}
- ```
+ ```html
+
+ Great Hamster Photos
+
+ ```
- See [LinkComponent](/api/ember/release/classes/LinkComponent) for a
- complete list of overrideable properties. Be sure to also
- check out inherited properties of `LinkComponent`.
+ To override this option for your entire application, see
+ "Overriding Application-wide Defaults".
- ### Overriding Application-wide Defaults
+ ### Disabling the `link-to` component
+ By default `{{link-to}}` is enabled.
+ any passed value to the `disabled` component property will disable
+ the `link-to` component.
- ``{{link-to}}`` creates an instance of `LinkComponent` for rendering. To
- override options for your entire application, export your customized
- `LinkComponent` from `app/components/link-to.js` with the desired overrides:
+ static use: the `disabled` option:
- ```javascript
- // app/components/link-to.js
- import LinkComponent from '@ember/routing/link-component';
+ ```handlebars
+ {{#link-to 'photoGallery' disabled=true}}
+ Great Hamster Photos
+ {{/link-to}}
+ ```
- export default LinkComponent.extend({
- activeClass: "is-active",
- tagName: 'li'
- })
- ```
+ dynamic use: the `disabledWhen` option:
- It is also possible to override the default event in this manner:
+ ```handlebars
+ {{#link-to 'photoGallery' disabledWhen=controller.someProperty}}
+ Great Hamster Photos
+ {{/link-to}}
+ ```
- ```javascript
- import LinkComponent from '@ember/routing/link-component';
+ any truthy value passed to `disabled` will disable it except `undefined`.
- export default LinkComponent.extend({
- eventName: 'customEventName'
- });
- ```
-
- @method link-to
- @for Ember.Templates.helpers
- @param {String} routeName
- @param {Object} [context]*
- @param [options] {Object} Handlebars key/value pairs of options, you can override any property of Ember.LinkComponent
- @return {String} HTML string
- @see {LinkComponent}
- @public
-*/
+ See "Overriding Application-wide Defaults" for more.
-import { computed, get } from '@ember/-internals/metal';
-import { isSimpleClick } from '@ember/-internals/views';
-import { assert, warn } from '@ember/debug';
-import { flaggedInstrument } from '@ember/instrumentation';
-import { assign } from '@ember/polyfills';
-import { inject as injectService } from '@ember/service';
-import { DEBUG } from '@glimmer/env';
-import EmberComponent, { HAS_BLOCK } from '../component';
-import layout from '../templates/link-to';
+ ### Handling `href`
+ `{{link-to}}` will use your application's Router to
+ fill the element's `href` property with a url that
+ matches the path to the supplied `routeName` for your
+ router's configured `Location` scheme, which defaults
+ to HashLocation.
-/**
- @module @ember/routing
-*/
+ ### Handling current route
+ `{{link-to}}` 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' the following
+ use of `{{link-to}}`:
-/**
- `LinkComponent` renders an element whose `click` event triggers a
- transition of the application's instance of `Router` to
- a supplied route by name.
+ ```handlebars
+ {{#link-to 'photoGallery.recent'}}
+ Great Hamster Photos
+ {{/link-to}}
+ ```
- `LinkComponent` components are invoked with {{#link-to}}. Properties
- of this class can be overridden with `reopen` to customize application-wide
- behavior.
+ will result in
- @class LinkComponent
- @extends Component
- @see {Ember.Templates.helpers.link-to}
- @public
-**/
+ ```html
+
+ Great Hamster Photos
+
+ ```
-const EMPTY_QUERY_PARAMS = Object.freeze({ values: Object.freeze({}) });
+ The CSS class name used for active classes can be customized
+ for a single use of `{{link-to}}` by passing an `activeClass`
+ option:
-const LinkComponent = EmberComponent.extend({
- layout,
+ ```handlebars
+ {{#link-to 'photoGallery.recent' activeClass="current-url"}}
+ Great Hamster Photos
+ {{/link-to}}
+ ```
- tagName: 'a',
+ ```html
+
+ Great Hamster Photos
+
+ ```
- /**
- Used to determine when this `LinkComponent` is active.
+ To override this option for your entire application, see
+ "Overriding Application-wide Defaults".
- @property current-when
- @public
- */
- 'current-when': null,
+ ### Keeping a link active for other routes
- /**
- Sets the `title` attribute of the `LinkComponent`'s HTML element.
+ If you need a link to be 'active' even when it doesn't match
+ the current route, you can use the `current-when` argument.
- @property title
- @default null
- @public
- **/
- title: null,
+ ```handlebars
+ {{#link-to 'photoGallery' current-when='photos'}}
+ Photo Gallery
+ {{/link-to}}
+ ```
- /**
- Sets the `rel` attribute of the `LinkComponent`'s HTML element.
+ This may be helpful for keeping links active for:
- @property rel
- @default null
- @public
- **/
- rel: null,
+ * non-nested routes that are logically related
+ * some secondary menu approaches
+ * 'top navigation' with 'sub navigation' scenarios
- /**
- Sets the `tabindex` attribute of the `LinkComponent`'s HTML element.
+ A link will be active if `current-when` is `true` or the current
+ route is the route this link would transition to.
- @property tabindex
- @default null
- @public
- **/
- tabindex: null,
+ To match multiple routes 'space-separate' the routes:
- /**
- Sets the `target` attribute of the `LinkComponent`'s HTML element.
+ ```handlebars
+ {{#link-to 'gallery' current-when='photos drawings paintings'}}
+ Art Gallery
+ {{/link-to}}
+ ```
- @since 1.8.0
- @property target
- @default null
- @public
- **/
- target: null,
+ ### 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:
- /**
- The CSS class to apply to `LinkComponent`'s element when its `active`
- property is `true`.
+ ```javascript
+ Router.map(function() {
+ this.route("photoGallery", {path: "hamster-photos/:photo_id"});
+ });
+ ```
- @property activeClass
- @type String
- @default active
- @public
- **/
- activeClass: 'active',
+ ```handlebars
+ {{#link-to 'photoGallery' aPhoto}}
+ {{aPhoto.title}}
+ {{/link-to}}
+ ```
- /**
- The CSS class to apply to `LinkComponent`'s element when its `loading`
- property is `true`.
+ ```html
+
+ Tomster
+
+ ```
- @property loadingClass
- @type String
- @default loading
- @private
- **/
- loadingClass: 'loading',
+ ### Supplying multiple models
+ For deep-linking to route paths that contain multiple
+ dynamic segments, multiple model arguments 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:
- /**
- The CSS class to apply to a `LinkComponent`'s element when its `disabled`
- property is `true`.
+ ```handlebars
+ {{#link-to 'photoGallery.comment' aPhoto comment}}
+ {{comment.body}}
+ {{/link-to}}
+ ```
- @property disabledClass
- @type String
- @default disabled
- @private
- **/
- disabledClass: 'disabled',
+ ```html
+
+ A+++ would snuggle again.
+
+ ```
- /**
- Determines whether the `LinkComponent` will trigger routing via
- the `replaceWith` routing strategy.
+ ### Supplying an explicit dynamic segment value
+ If you don't have a model object available to pass to `{{link-to}}`,
+ 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:
- @property replace
- @type Boolean
- @default false
- @public
- **/
- replace: false,
+ ```javascript
+ Router.map(function() {
+ this.route("photoGallery", { path: "hamster-photos/:photo_id" });
+ });
+ ```
- /**
- By default the `{{link-to}}` component will bind to the `href` and
- `title` attributes. It's discouraged that you override these defaults,
- however you can push onto the array if needed.
+ ```handlebars
+ {{#link-to 'photoGallery' aPhotoId}}
+ {{aPhoto.title}}
+ {{/link-to}}
+ ```
- @property attributeBindings
- @type Array | String
- @default ['title', 'rel', 'tabindex', 'target']
- @public
- */
- attributeBindings: ['href', 'title', 'rel', 'tabindex', 'target'],
+ ```html
+
+ Tomster
+
+ ```
- /**
- By default the `{{link-to}}` component will bind to the `active`, `loading`,
- and `disabled` classes. It is discouraged to override these directly.
+ When transitioning into the linked route, the `model` hook will
+ be triggered with parameters including this passed identifier.
- @property classNameBindings
- @type Array
- @default ['active', 'loading', 'disabled', 'ember-transitioning-in', 'ember-transitioning-out']
- @public
- */
- classNameBindings: ['active', 'loading', 'disabled', 'transitioningIn', 'transitioningOut'],
+ ### Allowing Default Action
- /**
- By default the `{{link-to}}` component responds to the `click` event. You
- can override this globally by setting this property to your custom
- event name.
+ By default the `{{link-to}}` component prevents the default browser action
+ by calling `preventDefault()` as this sort of action bubbling is normally
+ handled internally and we do not want to take the browser to a new URL (for
+ example).
- This is particularly useful on mobile when one wants to avoid the 300ms
- click delay using some sort of custom `tap` event.
+ If you need to override this behavior specify `preventDefault=false` in
+ your template:
- @property eventName
- @type String
- @default click
- @private
- */
- eventName: 'click',
+ ```handlebars
+ {{#link-to 'photoGallery' aPhotoId preventDefault=false}}
+ {{aPhotoId.title}}
+ {{/link-to}}
+ ```
- // this is doc'ed here so it shows up in the events
- // section of the API documentation, which is where
- // people will likely go looking for it.
- /**
- Triggers the `LinkComponent`'s routing behavior. If
- `eventName` is changed to a value other than `click`
- the routing behavior will trigger on that custom event
- instead.
+ ### Overriding attributes
+ You can override any given property of the `LinkComponent`
+ that is generated by the `{{link-to}}` component by passing
+ key/value pairs, like so:
- @event click
- @private
- */
+ ```handlebars
+ {{#link-to aPhoto tagName='li' title='Following this link will change your life' classNames='pic sweet'}}
+ Uh-mazing!
+ {{/link-to}}
+ ```
- /**
- An overridable method called when `LinkComponent` objects are instantiated.
+ See [LinkComponent](/api/ember/release/classes/LinkComponent) for a
+ complete list of overrideable properties. Be sure to also
+ check out inherited properties of `LinkComponent`.
+
+ ### Overriding Application-wide Defaults
- Example:
+ ``{{link-to}}`` creates an instance of `LinkComponent` for rendering. To
+ override options for your entire application, export your customized
+ `LinkComponent` from `app/components/link-to.js` with the desired overrides:
- ```app/components/my-link.js
+ ```javascript
+ // app/components/link-to.js
import LinkComponent from '@ember/routing/link-component';
export default LinkComponent.extend({
- init() {
- this._super(...arguments);
- console.log('Event is ' + this.get('eventName'));
- }
+ activeClass: "is-active",
+ tagName: 'li'
+ })
+ ```
+
+ It is also possible to override the default event in this manner:
+
+ ```javascript
+ import LinkComponent from '@ember/routing/link-component';
+
+ export default LinkComponent.extend({
+ eventName: 'customEventName'
});
```
- NOTE: If you do override `init` for a framework class like `Component`,
- be sure to call `this._super(...arguments)` in your
- `init` declaration! If you don't, Ember may not have an opportunity to
- do important setup work, and you'll see strange behavior in your
- application.
+ @method link-to
+ @for Ember.Templates.helpers
+ @param {String} routeName
+ @param {Object} [context]*
+ @param [options] {Object} Handlebars key/value pairs of options, you can override any property of Ember.LinkComponent
+ @return {String} HTML string
+ @see {LinkComponent}
+ @public
+ */
- @method init
- @private
+ /**
+ @module @ember/routing
*/
- init() {
- this._super(...arguments);
- // Map desired event name to invoke function
- let eventName = get(this, 'eventName');
- this.on(eventName, this, this._invoke);
- },
+ /**
+ `LinkComponent` renders an element whose `click` event triggers a
+ transition of the application's instance of `Router` to
+ a supplied route by name.
- _routing: injectService('-routing'),
+ `LinkComponent` components are invoked with {{#link-to}}. Properties
+ of this class can be overridden with `reopen` to customize application-wide
+ behavior.
- /**
- Accessed as a classname binding to apply the `LinkComponent`'s `disabledClass`
- CSS `class` to the element when the link is disabled.
+ @class LinkComponent
+ @extends Component
+ @see {Ember.Templates.helpers.link-to}
+ @public
+ **/
- When `true` interactions with the element will not trigger route changes.
- @property disabled
- @private
- */
- disabled: computed({
- get(_key: string): boolean {
- // always returns false for `get` because (due to the `set` just below)
- // the cached return value from the set will prevent this getter from _ever_
- // being called after a set has occured
- return false;
+ const UNDEFINED = Object.freeze({
+ toString() {
+ return 'UNDEFINED';
},
+ });
- set(_key: string, value: any): boolean {
- (this as any)._isDisabled = value;
-
- return value ? get(this, 'disabledClass') : false;
+ const EMPTY_QUERY_PARAMS = Object.freeze({});
+
+ LinkComponent = EmberComponent.extend({
+ layout,
+
+ tagName: 'a',
+
+ /**
+ @property route
+ @category EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS
+ @public
+ */
+ route: UNDEFINED,
+
+ /**
+ @property model
+ @category EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS
+ @public
+ */
+ model: UNDEFINED,
+
+ /**
+ @property models
+ @category EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS
+ @public
+ */
+ models: UNDEFINED,
+
+ /**
+ @property query
+ @category EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS
+ @public
+ */
+ query: UNDEFINED,
+
+ /**
+ Used to determine when this `LinkComponent` is active.
+
+ @property current-when
+ @public
+ */
+ 'current-when': null,
+
+ /**
+ Sets the `title` attribute of the `LinkComponent`'s HTML element.
+
+ @property title
+ @default null
+ @public
+ **/
+ title: null,
+
+ /**
+ Sets the `rel` attribute of the `LinkComponent`'s HTML element.
+
+ @property rel
+ @default null
+ @public
+ **/
+ rel: null,
+
+ /**
+ Sets the `tabindex` attribute of the `LinkComponent`'s HTML element.
+
+ @property tabindex
+ @default null
+ @public
+ **/
+ tabindex: null,
+
+ /**
+ Sets the `target` attribute of the `LinkComponent`'s HTML element.
+
+ @since 1.8.0
+ @property target
+ @default null
+ @public
+ **/
+ target: null,
+
+ /**
+ The CSS class to apply to `LinkComponent`'s element when its `active`
+ property is `true`.
+
+ @property activeClass
+ @type String
+ @default active
+ @public
+ **/
+ activeClass: 'active',
+
+ /**
+ The CSS class to apply to `LinkComponent`'s element when its `loading`
+ property is `true`.
+
+ @property loadingClass
+ @type String
+ @default loading
+ @private
+ **/
+ loadingClass: 'loading',
+
+ /**
+ The CSS class to apply to a `LinkComponent`'s element when its `disabled`
+ property is `true`.
+
+ @property disabledClass
+ @type String
+ @default disabled
+ @private
+ **/
+ disabledClass: 'disabled',
+
+ /**
+ Determines whether the `LinkComponent` will trigger routing via
+ the `replaceWith` routing strategy.
+
+ @property replace
+ @type Boolean
+ @default false
+ @public
+ **/
+ replace: false,
+
+ /**
+ By default the `{{link-to}}` component will bind to the `href` and
+ `title` attributes. It's discouraged that you override these defaults,
+ however you can push onto the array if needed.
+
+ @property attributeBindings
+ @type Array | String
+ @default ['title', 'rel', 'tabindex', 'target']
+ @public
+ */
+ attributeBindings: ['href', 'title', 'rel', 'tabindex', 'target'],
+
+ /**
+ By default the `{{link-to}}` component will bind to the `active`, `loading`,
+ and `disabled` classes. It is discouraged to override these directly.
+
+ @property classNameBindings
+ @type Array
+ @default ['active', 'loading', 'disabled', 'ember-transitioning-in', 'ember-transitioning-out']
+ @public
+ */
+ classNameBindings: ['active', 'loading', 'disabled', 'transitioningIn', 'transitioningOut'],
+
+ /**
+ By default the `{{link-to}}` component responds to the `click` event. You
+ can override this globally by setting this property to your custom
+ event name.
+
+ This is particularly useful on mobile when one wants to avoid the 300ms
+ click delay using some sort of custom `tap` event.
+
+ @property eventName
+ @type String
+ @default click
+ @private
+ */
+ eventName: 'click',
+
+ // this is doc'ed here so it shows up in the events
+ // section of the API documentation, which is where
+ // people will likely go looking for it.
+ /**
+ Triggers the `LinkComponent`'s routing behavior. If
+ `eventName` is changed to a value other than `click`
+ the routing behavior will trigger on that custom event
+ instead.
+
+ @event click
+ @private
+ */
+
+ /**
+ An overridable method called when `LinkComponent` objects are instantiated.
+
+ Example:
+
+ ```app/components/my-link.js
+ import LinkComponent from '@ember/routing/link-component';
+
+ export default LinkComponent.extend({
+ init() {
+ this._super(...arguments);
+ console.log('Event is ' + this.get('eventName'));
+ }
+ });
+ ```
+
+ NOTE: If you do override `init` for a framework class like `Component`,
+ be sure to call `this._super(...arguments)` in your
+ `init` declaration! If you don't, Ember may not have an opportunity to
+ do important setup work, and you'll see strange behavior in your
+ application.
+
+ @method init
+ @private
+ */
+ init() {
+ this._super(...arguments);
+
+ // Map desired event name to invoke function
+ let { eventName } = this;
+ this.on(eventName, this, this._invoke);
},
- }),
- _isActive(routerState: any) {
- if (get(this, 'loading')) {
- return false;
- }
+ _routing: injectService('-routing'),
+ _currentRoute: alias('_routing.currentRouteName'),
+ _currentRouterState: alias('_routing.currentState'),
+ _targetRouterState: alias('_routing.targetState'),
+
+ _route: computed('route', '_currentRoute', function computeLinkToComponentRoute(this: any) {
+ let { route } = this;
+ return route === UNDEFINED ? this._currentRoute : route;
+ }),
- let currentWhen = get(this, 'current-when');
+ _models: computed('model', 'models', function computeLinkToComponentModels(this: any) {
+ let { model, models } = this;
- if (typeof currentWhen === 'boolean') {
- return currentWhen;
- }
+ assert(
+ 'You cannot provide both the `@model` and `@models` arguments to the component',
+ model === UNDEFINED || models === UNDEFINED
+ );
- let isCurrentWhenSpecified = Boolean(currentWhen);
- currentWhen = currentWhen || get(this, 'qualifiedRouteName');
- currentWhen = currentWhen.split(' ');
+ if (model !== UNDEFINED) {
+ return [model];
+ } else if (models !== UNDEFINED) {
+ return models;
+ } else {
+ return [];
+ }
+ }),
- let routing = this._routing;
- let models = get(this, 'models');
- let resolvedQueryParams = get(this, 'resolvedQueryParams');
+ _query: computed('query', function computeLinkToComponentQuery(this: any) {
+ let { query } = this;
- for (let i = 0; i < currentWhen.length; i++) {
- if (
- routing.isActiveForRoute(
- models,
- resolvedQueryParams,
- currentWhen[i],
- routerState,
- isCurrentWhenSpecified
- )
- ) {
+ if (query === UNDEFINED) {
+ return EMPTY_QUERY_PARAMS;
+ } else {
+ return Object.assign({}, query);
+ }
+ }),
+
+ /**
+ Accessed as a classname binding to apply the `LinkComponent`'s `disabledClass`
+ CSS `class` to the element when the link is disabled.
+
+ When `true` interactions with the element will not trigger route changes.
+ @property disabled
+ @private
+ */
+ disabled: computed({
+ get(_key: string): boolean {
+ // always returns false for `get` because (due to the `set` just below)
+ // the cached return value from the set will prevent this getter from _ever_
+ // being called after a set has occured
+ return false;
+ },
+
+ set(this: any, _key: string, value: any): boolean {
+ this._isDisabled = value;
+
+ return value ? this.disabledClass : false;
+ },
+ }),
+
+ /**
+ Accessed as a classname binding to apply the `LinkComponent`'s `activeClass`
+ CSS `class` to the element when the link is active.
+
+ A `LinkComponent` is considered active when its `currentWhen` property is `true`
+ or the application's current route is the route the `LinkComponent` would trigger
+ transitions into.
+
+ The `currentWhen` property can match against multiple routes by separating
+ route names using the ` ` (space) character.
+
+ @property active
+ @private
+ */
+ active: computed('activeClass', '_active', function computeLinkToComponentActiveClass(
+ this: any
+ ) {
+ return this._active ? this.activeClass : false;
+ }),
+
+ _active: computed(
+ '_currentRouterState',
+ '_route',
+ '_models',
+ '_query',
+ 'loading',
+ 'current-when',
+ function computeLinkToComponentActive(this: any) {
+ let { _currentRouterState: state } = this;
+
+ if (state) {
+ return this._isActive(state);
+ } else {
+ return false;
+ }
+ }
+ ),
+
+ willBeActive: computed(
+ '_currentRouterState',
+ '_targetRouterState',
+ '_route',
+ '_models',
+ '_query',
+ 'loading',
+ 'current-when',
+ function computeLinkToComponentWillBeActive(this: any) {
+ let { _currentRouterState: current, _targetRouterState: target } = this;
+
+ if (current === target) {
+ return;
+ }
+
+ return this._isActive(target);
+ }
+ ),
+
+ _isActive(routerState: any) {
+ if (this.loading) {
+ return false;
+ }
+
+ let currentWhen = this['current-when'];
+
+ if (typeof currentWhen === 'boolean') {
+ return currentWhen;
+ }
+
+ let isCurrentWhenSpecified = Boolean(currentWhen);
+
+ if (isCurrentWhenSpecified) {
+ currentWhen = currentWhen.split(' ');
+ } else {
+ currentWhen = [this._route];
+ }
+
+ let { _models: models, _query: query, _routing: routing } = this;
+
+ for (let i = 0; i < currentWhen.length; i++) {
+ if (
+ routing.isActiveForRoute(
+ models,
+ query,
+ currentWhen[i],
+ routerState,
+ isCurrentWhenSpecified
+ )
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ transitioningIn: computed(
+ '_active',
+ 'willBeActive',
+ function computeLinkToComponentTransitioningIn(this: any) {
+ if (this.willBeActive === true && !this._active) {
+ return 'ember-transitioning-in';
+ } else {
+ return false;
+ }
+ }
+ ),
+
+ transitioningOut: computed(
+ '_active',
+ 'willBeActive',
+ function computeLinkToComponentTransitioningOut(this: any) {
+ if (this.willBeActive === false && this._active) {
+ return 'ember-transitioning-out';
+ } else {
+ return false;
+ }
+ }
+ ),
+
+ /**
+ Event handler that invokes the link, activating the associated route.
+
+ @method _invoke
+ @param {Event} event
+ @private
+ */
+ _invoke(this: any, event: Event): boolean {
+ if (!isSimpleClick(event)) {
return true;
}
- }
- return false;
- },
+ let { bubbles, preventDefault } = this;
+ let target = this.element.target;
+ let isSelf = !target || target === '_self';
- /**
- Accessed as a classname binding to apply the `LinkComponent`'s `activeClass`
- CSS `class` to the element when the link is active.
+ if (preventDefault !== false && isSelf) {
+ event.preventDefault();
+ }
- A `LinkComponent` is considered active when its `currentWhen` property is `true`
- or the application's current route is the route the `LinkComponent` would trigger
- transitions into.
+ if (bubbles === false) {
+ event.stopPropagation();
+ }
- The `currentWhen` property can match against multiple routes by separating
- route names using the ` ` (space) character.
+ if (this._isDisabled) {
+ return false;
+ }
- @property active
- @private
- */
- active: computed('activeClass', '_active', function computeLinkToComponentActiveClass(this: any) {
- return this.get('_active') ? get(this, 'activeClass') : false;
- }),
-
- _active: computed('_routing.currentState', 'attrs.params', function computeLinkToComponentActive(
- this: any
- ) {
- let currentState = get(this, '_routing.currentState');
- if (!currentState) {
+ if (this.loading) {
+ // tslint:disable-next-line:max-line-length
+ 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 false;
+ }
+
+ if (!isSelf) {
+ return false;
+ }
+
+ let {
+ _route: routeName,
+ _models: models,
+ _query: queryParams,
+ replace: shouldReplace,
+ } = this;
+
+ let payload = {
+ queryParams,
+ routeName,
+ };
+
+ flaggedInstrument(
+ 'interaction.link-to',
+ payload,
+ this._generateTransition(payload, routeName, models, queryParams, shouldReplace)
+ );
return false;
- }
- return this._isActive(currentState);
- }),
-
- willBeActive: computed('_routing.targetState', function computeLinkToComponentWillBeActive(
- this: any
- ) {
- let routing = this._routing;
- let targetState = get(routing, 'targetState');
- if (get(routing, 'currentState') === targetState) {
- return;
- }
-
- return this._isActive(targetState);
- }),
-
- transitioningIn: computed(
- 'active',
- 'willBeActive',
- function computeLinkToComponentTransitioningIn(this: any) {
- if (get(this, 'willBeActive') === true && !get(this, '_active')) {
- return 'ember-transitioning-in';
+ },
+
+ _generateTransition(
+ payload: any,
+ qualifiedRouteName: string,
+ models: any[],
+ queryParams: any[],
+ shouldReplace: boolean
+ ) {
+ let { _routing: routing } = this;
+
+ return () => {
+ payload.transition = routing.transitionTo(
+ qualifiedRouteName,
+ models,
+ queryParams,
+ shouldReplace
+ );
+ };
+ },
+
+ /**
+ Sets the element's `href` attribute to the url for
+ the `LinkComponent`'s targeted route.
+
+ If the `LinkComponent`'s `tagName` is changed to a value other
+ than `a`, this property will be ignored.
+
+ @property href
+ @private
+ */
+ href: computed(
+ '_route',
+ '_models',
+ '_query',
+ 'tagName',
+ 'loading',
+ 'loadingHref',
+ function computeLinkToComponentHref(this: any) {
+ if (this.tagName !== 'a') {
+ return;
+ }
+
+ if (this.loading) {
+ return this.loadingHref;
+ }
+
+ let { _route: route, _models: models, _query: query, _routing: routing } = this;
+
+ if (DEBUG) {
+ /*
+ * Unfortunately, to get decent error messages, we need to do this.
+ * In some future state we should be able to use a "feature flag"
+ * which allows us to strip this without needing to call it twice.
+ *
+ * if (isDebugBuild()) {
+ * // Do the useful debug thing, probably including try/catch.
+ * } else {
+ * // Do the performant thing.
+ * }
+ */
+ try {
+ return routing.generateURL(route, models, query);
+ } catch (e) {
+ // tslint:disable-next-line:max-line-length
+ assert(
+ `You attempted to generate a link for the "${this.route}" route, but did not ` +
+ `pass the models required for generating its dynamic segments. ` +
+ e.message
+ );
+ }
+ } else {
+ return routing.generateURL(route, models, query);
+ }
+ }
+ ),
+
+ loading: computed(
+ '_route',
+ '_modelsAreLoaded',
+ 'loadingClass',
+ function computeLinkToComponentLoading(this: any) {
+ let { _route: route, _modelsAreLoaded: loaded } = this;
+
+ if (!loaded || route === null || route === undefined) {
+ return this.loadingClass;
+ }
+ }
+ ),
+
+ _modelsAreLoaded: computed('_models', function computeLinkToComponentModelsAreLoaded(
+ this: any
+ ) {
+ let { _models: models } = this;
+
+ for (let i = 0; i < models.length; i++) {
+ let model = models[i];
+ if (model === null || model === undefined) {
+ return false;
+ }
+ }
+
+ return true;
+ }),
+
+ /**
+ The default href value to use while a link-to is loading.
+ Only applies when tagName is 'a'
+
+ @property loadingHref
+ @type String
+ @default #
+ @private
+ */
+ loadingHref: '#',
+
+ didReceiveAttrs() {
+ let { disabledWhen } = this;
+
+ if (disabledWhen !== undefined) {
+ this.set('disabled', disabledWhen);
+ }
+
+ let { params } = this;
+
+ if (!params || params.length === 0) {
+ assert(
+ 'You must provide at least one of the `@route`, `@model`, `@models` or `@query` argument to ``.',
+ !(
+ this.route === UNDEFINED &&
+ this.model === UNDEFINED &&
+ this.models === UNDEFINED &&
+ this.query === UNDEFINED
+ )
+ );
+
+ return;
+ }
+
+ params = params.slice();
+
+ // Process the positional arguments, in order.
+ // 1. Inline link title comes first, if present.
+ if (!this[HAS_BLOCK]) {
+ this.set('linkTitle', params.shift());
+ }
+
+ // 2. The last argument is possibly the `query` object.
+ let lastParam = params[params.length - 1];
+
+ if (lastParam && lastParam.isQueryParams) {
+ this.set('query', params.pop().values);
} else {
- return false;
+ this.set('query', UNDEFINED);
}
- }
- ),
- transitioningOut: computed(
- 'active',
- 'willBeActive',
- function computeLinkToComponentTransitioningOut(this: any) {
- if (get(this, 'willBeActive') === false && get(this, '_active')) {
- return 'ember-transitioning-out';
+ // 3. If there is a `route`, it is now at index 0.
+ if (params.length === 0) {
+ this.set('route', UNDEFINED);
} else {
- return false;
+ this.set('route', params.shift());
}
- }
- ),
+ // 4. Any remaining indices (if any) are `models`.
+ this.set('model', UNDEFINED);
+ this.set('models', params);
+ },
+ });
+
+ LinkComponent.toString = () => '@ember/routing/link-component';
+
+ LinkComponent.reopenClass({
+ positionalParams: 'params',
+ });
+} else {
/**
- Event handler that invokes the link, activating the associated route.
+ The `{{link-to}}` 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 `{{link-to}}` becomes the innerHTML of the rendered
+ element:
+
+ ```handlebars
+ {{#link-to 'photoGallery'}}
+ Great Hamster Photos
+ {{/link-to}}
+ ```
+
+ You can also use an inline form of `{{link-to}}` component by
+ passing the link text as the first argument
+ to the component:
+
+ ```handlebars
+ {{link-to 'Great Hamster Photos' 'photoGallery'}}
+ ```
+
+ Both will result in:
+
+ ```html
+
+ Great Hamster Photos
+
+ ```
+
+ ### Supplying a tagName
+ By default `{{link-to}}` renders an `` element. This can
+ be overridden for a single use of `{{link-to}}` by supplying
+ a `tagName` option:
+
+ ```handlebars
+ {{#link-to 'photoGallery' tagName="li"}}
+ Great Hamster Photos
+ {{/link-to}}
+ ```
+
+ ```html
+
+ Great Hamster Photos
+
+ ```
+
+ To override this option for your entire application, see
+ "Overriding Application-wide Defaults".
+
+ ### Disabling the `link-to` component
+ By default `{{link-to}}` is enabled.
+ any passed value to the `disabled` component property will disable
+ the `link-to` component.
+
+ static use: the `disabled` option:
+
+ ```handlebars
+ {{#link-to 'photoGallery' disabled=true}}
+ Great Hamster Photos
+ {{/link-to}}
+ ```
+
+ dynamic use: the `disabledWhen` option:
+
+ ```handlebars
+ {{#link-to 'photoGallery' disabledWhen=controller.someProperty}}
+ Great Hamster Photos
+ {{/link-to}}
+ ```
+
+ any truthy value passed to `disabled` will disable it except `undefined`.
+
+ See "Overriding Application-wide Defaults" for more.
+
+ ### Handling `href`
+ `{{link-to}}` will use your application's Router to
+ fill the element's `href` property with a url that
+ matches the path to the supplied `routeName` for your
+ router's configured `Location` scheme, which defaults
+ to HashLocation.
+
+ ### Handling current route
+ `{{link-to}}` 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' the following
+ use of `{{link-to}}`:
+
+ ```handlebars
+ {{#link-to 'photoGallery.recent'}}
+ Great Hamster Photos
+ {{/link-to}}
+ ```
+
+ will result in
+
+ ```html
+
+ Great Hamster Photos
+
+ ```
+
+ The CSS class name used for active classes can be customized
+ for a single use of `{{link-to}}` by passing an `activeClass`
+ option:
+
+ ```handlebars
+ {{#link-to 'photoGallery.recent' activeClass="current-url"}}
+ Great Hamster Photos
+ {{/link-to}}
+ ```
+
+ ```html
+
+ Great Hamster Photos
+
+ ```
+
+ To override this option for your entire application, see
+ "Overriding Application-wide Defaults".
+
+ ### 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
+ {{#link-to 'photoGallery' current-when='photos'}}
+ Photo Gallery
+ {{/link-to}}
+ ```
+
+ 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
+ {{#link-to 'gallery' current-when='photos drawings paintings'}}
+ Art Gallery
+ {{/link-to}}
+ ```
- @method _invoke
- @param {Event} event
- @private
+ ### 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
+ {{#link-to 'photoGallery' aPhoto}}
+ {{aPhoto.title}}
+ {{/link-to}}
+ ```
+
+ ```html
+
+ Tomster
+
+ ```
+
+ ### Supplying multiple models
+ For deep-linking to route paths that contain multiple
+ dynamic segments, multiple model arguments 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
+ {{#link-to 'photoGallery.comment' aPhoto comment}}
+ {{comment.body}}
+ {{/link-to}}
+ ```
+
+ ```html
+
+ A+++ would snuggle again.
+
+ ```
+
+ ### Supplying an explicit dynamic segment value
+ If you don't have a model object available to pass to `{{link-to}}`,
+ 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
+ {{#link-to 'photoGallery' aPhotoId}}
+ {{aPhoto.title}}
+ {{/link-to}}
+ ```
+
+ ```html
+
+ Tomster
+
+ ```
+
+ When transitioning into the linked route, the `model` hook will
+ be triggered with parameters including this passed identifier.
+
+ ### Allowing Default Action
+
+ By default the `{{link-to}}` component prevents the default browser action
+ by calling `preventDefault()` as this sort of action bubbling is normally
+ handled internally and we do not want to take the browser to a new URL (for
+ example).
+
+ If you need to override this behavior specify `preventDefault=false` in
+ your template:
+
+ ```handlebars
+ {{#link-to 'photoGallery' aPhotoId preventDefault=false}}
+ {{aPhotoId.title}}
+ {{/link-to}}
+ ```
+
+ ### Overriding attributes
+ You can override any given property of the `LinkComponent`
+ that is generated by the `{{link-to}}` component by passing
+ key/value pairs, like so:
+
+ ```handlebars
+ {{#link-to aPhoto tagName='li' title='Following this link will change your life' classNames='pic sweet'}}
+ Uh-mazing!
+ {{/link-to}}
+ ```
+
+ See [LinkComponent](/api/ember/release/classes/LinkComponent) for a
+ complete list of overrideable properties. Be sure to also
+ check out inherited properties of `LinkComponent`.
+
+ ### Overriding Application-wide Defaults
+
+ ``{{link-to}}`` creates an instance of `LinkComponent` for rendering. To
+ override options for your entire application, export your customized
+ `LinkComponent` from `app/components/link-to.js` with the desired overrides:
+
+ ```javascript
+ // app/components/link-to.js
+ import LinkComponent from '@ember/routing/link-component';
+
+ export default LinkComponent.extend({
+ activeClass: "is-active",
+ tagName: 'li'
+ })
+ ```
+
+ It is also possible to override the default event in this manner:
+
+ ```javascript
+ import LinkComponent from '@ember/routing/link-component';
+
+ export default LinkComponent.extend({
+ eventName: 'customEventName'
+ });
+ ```
+
+ @method link-to
+ @for Ember.Templates.helpers
+ @param {String} routeName
+ @param {Object} [context]*
+ @param [options] {Object} Handlebars key/value pairs of options, you can override any property of Ember.LinkComponent
+ @return {String} HTML string
+ @see {LinkComponent}
+ @public
*/
- _invoke(this: any, event: Event): boolean {
- if (!isSimpleClick(event)) {
- return true;
- }
- let preventDefault = get(this, 'preventDefault');
- let targetAttribute = get(this, 'target');
+ /**
+ @module @ember/routing
+ */
- if (preventDefault !== false && (!targetAttribute || targetAttribute === '_self')) {
- event.preventDefault();
- }
+ /**
+ `LinkComponent` renders an element whose `click` event triggers a
+ transition of the application's instance of `Router` to
+ a supplied route by name.
- if (get(this, 'bubbles') === false) {
- event.stopPropagation();
- }
+ `LinkComponent` components are invoked with {{#link-to}}. Properties
+ of this class can be overridden with `reopen` to customize application-wide
+ behavior.
- if (this._isDisabled) {
- return false;
- }
+ @class LinkComponent
+ @extends Component
+ @see {Ember.Templates.helpers.link-to}
+ @public
+ **/
- if (get(this, 'loading')) {
- // tslint:disable-next-line:max-line-length
- warn(
- 'This link-to is in an inactive loading state because at least one of its parameters presently has a null/undefined value, or the provided route name is invalid.',
- false,
- {
- id: 'ember-glimmer.link-to.inactive-loading-state',
+ const EMPTY_QUERY_PARAMS = Object.freeze({ values: Object.freeze({}) });
+
+ LinkComponent = EmberComponent.extend({
+ layout,
+
+ tagName: 'a',
+
+ /**
+ Used to determine when this `LinkComponent` is active.
+
+ @property current-when
+ @public
+ */
+ 'current-when': null,
+
+ /**
+ Sets the `title` attribute of the `LinkComponent`'s HTML element.
+
+ @property title
+ @default null
+ @public
+ **/
+ title: null,
+
+ /**
+ Sets the `rel` attribute of the `LinkComponent`'s HTML element.
+
+ @property rel
+ @default null
+ @public
+ **/
+ rel: null,
+
+ /**
+ Sets the `tabindex` attribute of the `LinkComponent`'s HTML element.
+
+ @property tabindex
+ @default null
+ @public
+ **/
+ tabindex: null,
+
+ /**
+ Sets the `target` attribute of the `LinkComponent`'s HTML element.
+
+ @since 1.8.0
+ @property target
+ @default null
+ @public
+ **/
+ target: null,
+
+ /**
+ The CSS class to apply to `LinkComponent`'s element when its `active`
+ property is `true`.
+
+ @property activeClass
+ @type String
+ @default active
+ @public
+ **/
+ activeClass: 'active',
+
+ /**
+ The CSS class to apply to `LinkComponent`'s element when its `loading`
+ property is `true`.
+
+ @property loadingClass
+ @type String
+ @default loading
+ @private
+ **/
+ loadingClass: 'loading',
+
+ /**
+ The CSS class to apply to a `LinkComponent`'s element when its `disabled`
+ property is `true`.
+
+ @property disabledClass
+ @type String
+ @default disabled
+ @private
+ **/
+ disabledClass: 'disabled',
+
+ /**
+ Determines whether the `LinkComponent` will trigger routing via
+ the `replaceWith` routing strategy.
+
+ @property replace
+ @type Boolean
+ @default false
+ @public
+ **/
+ replace: false,
+
+ /**
+ By default the `{{link-to}}` component will bind to the `href` and
+ `title` attributes. It's discouraged that you override these defaults,
+ however you can push onto the array if needed.
+
+ @property attributeBindings
+ @type Array | String
+ @default ['title', 'rel', 'tabindex', 'target']
+ @public
+ */
+ attributeBindings: ['href', 'title', 'rel', 'tabindex', 'target'],
+
+ /**
+ By default the `{{link-to}}` component will bind to the `active`, `loading`,
+ and `disabled` classes. It is discouraged to override these directly.
+
+ @property classNameBindings
+ @type Array
+ @default ['active', 'loading', 'disabled', 'ember-transitioning-in', 'ember-transitioning-out']
+ @public
+ */
+ classNameBindings: ['active', 'loading', 'disabled', 'transitioningIn', 'transitioningOut'],
+
+ /**
+ By default the `{{link-to}}` component responds to the `click` event. You
+ can override this globally by setting this property to your custom
+ event name.
+
+ This is particularly useful on mobile when one wants to avoid the 300ms
+ click delay using some sort of custom `tap` event.
+
+ @property eventName
+ @type String
+ @default click
+ @private
+ */
+ eventName: 'click',
+
+ // this is doc'ed here so it shows up in the events
+ // section of the API documentation, which is where
+ // people will likely go looking for it.
+ /**
+ Triggers the `LinkComponent`'s routing behavior. If
+ `eventName` is changed to a value other than `click`
+ the routing behavior will trigger on that custom event
+ instead.
+
+ @event click
+ @private
+ */
+
+ /**
+ An overridable method called when `LinkComponent` objects are instantiated.
+
+ Example:
+
+ ```app/components/my-link.js
+ import LinkComponent from '@ember/routing/link-component';
+
+ export default LinkComponent.extend({
+ init() {
+ this._super(...arguments);
+ console.log('Event is ' + this.get('eventName'));
}
- );
- return false;
- }
+ });
+ ```
+
+ NOTE: If you do override `init` for a framework class like `Component`,
+ be sure to call `this._super(...arguments)` in your
+ `init` declaration! If you don't, Ember may not have an opportunity to
+ do important setup work, and you'll see strange behavior in your
+ application.
+
+ @method init
+ @private
+ */
+ init() {
+ this._super(...arguments);
+
+ // Map desired event name to invoke function
+ let eventName = get(this, 'eventName');
+ this.on(eventName, this, this._invoke);
+ },
+
+ _routing: injectService('-routing'),
+
+ /**
+ Accessed as a classname binding to apply the `LinkComponent`'s `disabledClass`
+ CSS `class` to the element when the link is disabled.
+
+ When `true` interactions with the element will not trigger route changes.
+ @property disabled
+ @private
+ */
+ disabled: computed({
+ get(_key: string): boolean {
+ // always returns false for `get` because (due to the `set` just below)
+ // the cached return value from the set will prevent this getter from _ever_
+ // being called after a set has occured
+ return false;
+ },
+
+ set(_key: string, value: any): boolean {
+ (this as any)._isDisabled = value;
+
+ return value ? get(this, 'disabledClass') : false;
+ },
+ }),
+
+ _isActive(routerState: any) {
+ if (get(this, 'loading')) {
+ return false;
+ }
+
+ let currentWhen = get(this, 'current-when');
+
+ if (typeof currentWhen === 'boolean') {
+ return currentWhen;
+ }
+
+ let isCurrentWhenSpecified = Boolean(currentWhen);
+ currentWhen = currentWhen || get(this, 'qualifiedRouteName');
+ currentWhen = currentWhen.split(' ');
+
+ let routing = this._routing;
+ let models = get(this, 'models');
+ let resolvedQueryParams = get(this, 'resolvedQueryParams');
+
+ for (let i = 0; i < currentWhen.length; i++) {
+ if (
+ routing.isActiveForRoute(
+ models,
+ resolvedQueryParams,
+ currentWhen[i],
+ routerState,
+ isCurrentWhenSpecified
+ )
+ ) {
+ return true;
+ }
+ }
- if (targetAttribute && targetAttribute !== '_self') {
return false;
- }
-
- let qualifiedRouteName = get(this, 'qualifiedRouteName');
- let models = get(this, 'models');
- let queryParams = get(this, 'queryParams.values');
- let shouldReplace = get(this, 'replace');
-
- let payload = {
- queryParams,
- routeName: qualifiedRouteName,
- };
-
- // tslint:disable-next-line:max-line-length
- flaggedInstrument(
- 'interaction.link-to',
- payload,
- this._generateTransition(payload, qualifiedRouteName, models, queryParams, shouldReplace)
- );
- return false;
- },
-
- _generateTransition(
- payload: any,
- qualifiedRouteName: string,
- models: any[],
- queryParams: any[],
- shouldReplace: boolean
- ) {
- let routing = this._routing;
- return () => {
- payload.transition = routing.transitionTo(
- qualifiedRouteName,
- models,
- queryParams,
- shouldReplace
- );
- };
- },
+ },
- queryParams: EMPTY_QUERY_PARAMS,
+ /**
+ Accessed as a classname binding to apply the `LinkComponent`'s `activeClass`
+ CSS `class` to the element when the link is active.
+
+ A `LinkComponent` is considered active when its `currentWhen` property is `true`
+ or the application's current route is the route the `LinkComponent` would trigger
+ transitions into.
+
+ The `currentWhen` property can match against multiple routes by separating
+ route names using the ` ` (space) character.
+
+ @property active
+ @private
+ */
+ active: computed('activeClass', '_active', function computeLinkToComponentActiveClass(
+ this: any
+ ) {
+ return this.get('_active') ? get(this, 'activeClass') : false;
+ }),
+
+ _active: computed(
+ '_routing.currentState',
+ 'attrs.params',
+ function computeLinkToComponentActive(this: any) {
+ let currentState = get(this, '_routing.currentState');
+ if (!currentState) {
+ return false;
+ }
+ return this._isActive(currentState);
+ }
+ ),
+
+ willBeActive: computed('_routing.targetState', function computeLinkToComponentWillBeActive(
+ this: any
+ ) {
+ let routing = this._routing;
+ let targetState = get(routing, 'targetState');
+ if (get(routing, 'currentState') === targetState) {
+ return;
+ }
- qualifiedRouteName: computed(
- 'targetRouteName',
- '_routing.currentState',
- function computeLinkToComponentQualifiedRouteName(this: any) {
- let params = get(this, 'params');
- let paramsLength = params.length;
- let lastParam = params[paramsLength - 1];
- if (lastParam && lastParam.isQueryParams) {
- paramsLength--;
+ return this._isActive(targetState);
+ }),
+
+ transitioningIn: computed(
+ 'active',
+ 'willBeActive',
+ function computeLinkToComponentTransitioningIn(this: any) {
+ if (get(this, 'willBeActive') === true && !get(this, '_active')) {
+ return 'ember-transitioning-in';
+ } else {
+ return false;
+ }
}
- let onlyQueryParamsSupplied = this[HAS_BLOCK] ? paramsLength === 0 : paramsLength === 1;
- if (onlyQueryParamsSupplied) {
- return get(this, '_routing.currentRouteName');
+ ),
+
+ transitioningOut: computed(
+ 'active',
+ 'willBeActive',
+ function computeLinkToComponentTransitioningOut(this: any) {
+ if (get(this, 'willBeActive') === false && get(this, '_active')) {
+ return 'ember-transitioning-out';
+ } else {
+ return false;
+ }
}
- return get(this, 'targetRouteName');
- }
- ),
+ ),
- resolvedQueryParams: computed('queryParams', function computeLinkToComponentResolvedQueryParams(
- this: any
- ) {
- let resolvedQueryParams = {};
- let queryParams = get(this, 'queryParams');
+ /**
+ Event handler that invokes the link, activating the associated route.
- if (queryParams !== EMPTY_QUERY_PARAMS) {
- let { values } = queryParams;
- assign(resolvedQueryParams, values);
- }
+ @method _invoke
+ @param {Event} event
+ @private
+ */
+ _invoke(this: any, event: Event): boolean {
+ if (!isSimpleClick(event)) {
+ return true;
+ }
- return resolvedQueryParams;
- }),
+ let preventDefault = get(this, 'preventDefault');
+ let targetAttribute = get(this, 'target');
- /**
- Sets the element's `href` attribute to the url for
- the `LinkComponent`'s targeted route.
+ if (preventDefault !== false && (!targetAttribute || targetAttribute === '_self')) {
+ event.preventDefault();
+ }
- If the `LinkComponent`'s `tagName` is changed to a value other
- than `a`, this property will be ignored.
+ if (get(this, 'bubbles') === false) {
+ event.stopPropagation();
+ }
- @property href
- @private
- */
- href: computed('models', 'qualifiedRouteName', function computeLinkToComponentHref(this: any) {
- if (get(this, 'tagName') !== 'a') {
- return;
- }
-
- let qualifiedRouteName = get(this, 'qualifiedRouteName');
- let models = get(this, 'models');
-
- if (get(this, 'loading')) {
- return get(this, 'loadingHref');
- }
-
- let routing = this._routing;
- let queryParams = get(this, 'queryParams.values');
-
- if (DEBUG) {
- /*
- * Unfortunately, to get decent error messages, we need to do this.
- * In some future state we should be able to use a "feature flag"
- * which allows us to strip this without needing to call it twice.
- *
- * if (isDebugBuild()) {
- * // Do the useful debug thing, probably including try/catch.
- * } else {
- * // Do the performant thing.
- * }
- */
- try {
- routing.generateURL(qualifiedRouteName, models, queryParams);
- } catch (e) {
+ if (this._isDisabled) {
+ return false;
+ }
+
+ if (get(this, 'loading')) {
// tslint:disable-next-line:max-line-length
- assert(
- 'You attempted to define a `{{link-to "' +
- qualifiedRouteName +
- '"}}` but did not pass the parameters required for generating its dynamic segments. ' +
- e.message
+ warn(
+ 'This link-to is in an inactive loading state because at least one of its parameters presently has a null/undefined value, or the provided route name is invalid.',
+ false,
+ {
+ id: 'ember-glimmer.link-to.inactive-loading-state',
+ }
+ );
+ return false;
+ }
+
+ if (targetAttribute && targetAttribute !== '_self') {
+ return false;
+ }
+
+ let qualifiedRouteName = get(this, 'qualifiedRouteName');
+ let models = get(this, 'models');
+ let queryParams = get(this, 'queryParams.values');
+ let shouldReplace = get(this, 'replace');
+
+ let payload = {
+ queryParams,
+ routeName: qualifiedRouteName,
+ };
+
+ // tslint:disable-next-line:max-line-length
+ flaggedInstrument(
+ 'interaction.link-to',
+ payload,
+ this._generateTransition(payload, qualifiedRouteName, models, queryParams, shouldReplace)
+ );
+ return false;
+ },
+
+ _generateTransition(
+ payload: any,
+ qualifiedRouteName: string,
+ models: any[],
+ queryParams: any[],
+ shouldReplace: boolean
+ ) {
+ let routing = this._routing;
+ return () => {
+ payload.transition = routing.transitionTo(
+ qualifiedRouteName,
+ models,
+ queryParams,
+ shouldReplace
);
+ };
+ },
+
+ queryParams: EMPTY_QUERY_PARAMS,
+
+ qualifiedRouteName: computed(
+ 'targetRouteName',
+ '_routing.currentState',
+ function computeLinkToComponentQualifiedRouteName(this: any) {
+ let params = get(this, 'params');
+ let paramsLength = params.length;
+ let lastParam = params[paramsLength - 1];
+ if (lastParam && lastParam.isQueryParams) {
+ paramsLength--;
+ }
+ let onlyQueryParamsSupplied = this[HAS_BLOCK] ? paramsLength === 0 : paramsLength === 1;
+ if (onlyQueryParamsSupplied) {
+ return get(this, '_routing.currentRouteName');
+ }
+ return get(this, 'targetRouteName');
+ }
+ ),
+
+ resolvedQueryParams: computed('queryParams', function computeLinkToComponentResolvedQueryParams(
+ this: any
+ ) {
+ let resolvedQueryParams = {};
+ let queryParams = get(this, 'queryParams');
+
+ if (queryParams !== EMPTY_QUERY_PARAMS) {
+ let { values } = queryParams;
+ assign(resolvedQueryParams, values);
}
- }
- return routing.generateURL(qualifiedRouteName, models, queryParams);
- }),
+ return resolvedQueryParams;
+ }),
+
+ /**
+ Sets the element's `href` attribute to the url for
+ the `LinkComponent`'s targeted route.
+
+ If the `LinkComponent`'s `tagName` is changed to a value other
+ than `a`, this property will be ignored.
+
+ @property href
+ @private
+ */
+ href: computed('models', 'qualifiedRouteName', function computeLinkToComponentHref(this: any) {
+ if (get(this, 'tagName') !== 'a') {
+ return;
+ }
- loading: computed(
- '_modelsAreLoaded',
- 'qualifiedRouteName',
- function computeLinkToComponentLoading(this: any) {
let qualifiedRouteName = get(this, 'qualifiedRouteName');
- let modelsAreLoaded = get(this, '_modelsAreLoaded');
+ let models = get(this, 'models');
- if (!modelsAreLoaded || qualifiedRouteName === null || qualifiedRouteName === undefined) {
- return get(this, 'loadingClass');
+ if (get(this, 'loading')) {
+ return get(this, 'loadingHref');
}
- }
- ),
- _modelsAreLoaded: computed('models', function computeLinkToComponentModelsAreLoaded(this: any) {
- let models = get(this, 'models');
- for (let i = 0; i < models.length; i++) {
- let model = models[i];
- if (model === null || model === undefined) {
- return false;
+ let routing = this._routing;
+ let queryParams = get(this, 'queryParams.values');
+
+ if (DEBUG) {
+ /*
+ * Unfortunately, to get decent error messages, we need to do this.
+ * In some future state we should be able to use a "feature flag"
+ * which allows us to strip this without needing to call it twice.
+ *
+ * if (isDebugBuild()) {
+ * // Do the useful debug thing, probably including try/catch.
+ * } else {
+ * // Do the performant thing.
+ * }
+ */
+ try {
+ routing.generateURL(qualifiedRouteName, models, queryParams);
+ } catch (e) {
+ // tslint:disable-next-line:max-line-length
+ assert(
+ 'You attempted to define a `{{link-to "' +
+ qualifiedRouteName +
+ '"}}` but did not pass the parameters required for generating its dynamic segments. ' +
+ e.message
+ );
+ }
}
- }
- return true;
- }),
+ return routing.generateURL(qualifiedRouteName, models, queryParams);
+ }),
- /**
- The default href value to use while a link-to is loading.
- Only applies when tagName is 'a'
+ loading: computed(
+ '_modelsAreLoaded',
+ 'qualifiedRouteName',
+ function computeLinkToComponentLoading(this: any) {
+ let qualifiedRouteName = get(this, 'qualifiedRouteName');
+ let modelsAreLoaded = get(this, '_modelsAreLoaded');
- @property loadingHref
- @type String
- @default #
- @private
- */
- loadingHref: '#',
+ if (!modelsAreLoaded || qualifiedRouteName === null || qualifiedRouteName === undefined) {
+ return get(this, 'loadingClass');
+ }
+ }
+ ),
+
+ _modelsAreLoaded: computed('models', function computeLinkToComponentModelsAreLoaded(this: any) {
+ let models = get(this, 'models');
+ for (let i = 0; i < models.length; i++) {
+ let model = models[i];
+ if (model === null || model === undefined) {
+ return false;
+ }
+ }
- didReceiveAttrs() {
- let queryParams;
- let params = get(this, 'params');
+ return true;
+ }),
- if (params) {
- // Do not mutate params in place
- params = params.slice();
- }
-
- assert(
- 'You must provide one or more parameters to the link-to component.',
- params && params.length
- );
-
- let disabledWhen = get(this, 'disabledWhen');
- if (disabledWhen !== undefined) {
- this.set('disabled', disabledWhen);
- }
-
- // Process the positional arguments, in order.
- // 1. Inline link title comes first, if present.
- if (!this[HAS_BLOCK]) {
- this.set('linkTitle', params.shift());
- }
-
- // 2. `targetRouteName` is now always at index 0.
- this.set('targetRouteName', params[0]);
-
- // 3. The last argument (if still remaining) is the `queryParams` object.
- let lastParam = params[params.length - 1];
-
- if (lastParam && lastParam.isQueryParams) {
- queryParams = params.pop();
- } else {
- queryParams = EMPTY_QUERY_PARAMS;
- }
- this.set('queryParams', queryParams);
-
- // 4. Any remaining indices (excepting `targetRouteName` at 0) are `models`.
- params.shift();
- this.set('models', params);
- },
-});
-
-LinkComponent.toString = () => '@ember/routing/link-component';
-
-LinkComponent.reopenClass({
- positionalParams: 'params',
-});
+ /**
+ The default href value to use while a link-to is loading.
+ Only applies when tagName is 'a'
+
+ @property loadingHref
+ @type String
+ @default #
+ @private
+ */
+ loadingHref: '#',
+
+ didReceiveAttrs() {
+ let queryParams;
+ let params = get(this, 'params');
+
+ if (params) {
+ // Do not mutate params in place
+ params = params.slice();
+ }
+
+ assert(
+ 'You must provide one or more parameters to the link-to component.',
+ params && params.length
+ );
+
+ let disabledWhen = get(this, 'disabledWhen');
+ if (disabledWhen !== undefined) {
+ this.set('disabled', disabledWhen);
+ }
+
+ // Process the positional arguments, in order.
+ // 1. Inline link title comes first, if present.
+ if (!this[HAS_BLOCK]) {
+ this.set('linkTitle', params.shift());
+ }
+
+ // 2. `targetRouteName` is now always at index 0.
+ this.set('targetRouteName', params[0]);
+
+ // 3. The last argument (if still remaining) is the `queryParams` object.
+ let lastParam = params[params.length - 1];
+
+ if (lastParam && lastParam.isQueryParams) {
+ queryParams = params.pop();
+ } else {
+ queryParams = EMPTY_QUERY_PARAMS;
+ }
+ this.set('queryParams', queryParams);
+
+ // 4. Any remaining indices (excepting `targetRouteName` at 0) are `models`.
+ params.shift();
+ this.set('models', params);
+ },
+ });
+
+ LinkComponent.toString = () => '@ember/routing/link-component';
+
+ LinkComponent.reopenClass({
+ positionalParams: 'params',
+ });
+}
export default LinkComponent;
diff --git a/packages/@ember/-internals/glimmer/lib/templates/link-to.hbs b/packages/@ember/-internals/glimmer/lib/templates/link-to.hbs
index 5f543159a3f..e67d811c27e 100644
--- a/packages/@ember/-internals/glimmer/lib/templates/link-to.hbs
+++ b/packages/@ember/-internals/glimmer/lib/templates/link-to.hbs
@@ -1 +1,5 @@
-{{#if linkTitle}}{{linkTitle}}{{else}}{{yield}}{{/if}}
\ No newline at end of file
+{{~#if (has-block)~}}
+ {{yield}}
+{{~else~}}
+ {{this.linkTitle}}
+{{~/if~}}
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-angle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-angle-test.js
new file mode 100644
index 00000000000..c89d83b6f58
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-angle-test.js
@@ -0,0 +1,746 @@
+import { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features';
+import Controller from '@ember/controller';
+import { RSVP } from '@ember/-internals/runtime';
+import { Route } from '@ember/-internals/routing';
+import {
+ ApplicationTestCase,
+ classes as classMatcher,
+ moduleFor,
+ runTask,
+} from 'internal-test-helpers';
+
+if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) {
+ moduleFor(
+ ' component with query-params (rendering)',
+ class extends ApplicationTestCase {
+ constructor() {
+ super(...arguments);
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ queryParams: ['foo'],
+ foo: '123',
+ bar: 'yes',
+ })
+ );
+ }
+
+ ['@test populates href with fully supplied query param values']() {
+ this.addTemplate(
+ 'index',
+ `Index`
+ );
+
+ return this.visit('/').then(() => {
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'a',
+ attrs: { href: '/?bar=NAW&foo=456' },
+ content: 'Index',
+ });
+ });
+ }
+
+ ['@test populates href with partially supplied query param values, but omits if value is default value']() {
+ this.addTemplate(
+ 'index',
+ `Index`
+ );
+
+ return this.visit('/').then(() => {
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'a',
+ attrs: { href: '/', class: classMatcher('ember-view active') },
+ content: 'Index',
+ });
+ });
+ }
+ }
+ );
+
+ moduleFor(
+ ' component with query params (routing)',
+ class extends ApplicationTestCase {
+ constructor() {
+ super();
+ let indexProperties = {
+ foo: '123',
+ bar: 'abc',
+ };
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ queryParams: ['foo', 'bar', 'abool'],
+ foo: indexProperties.foo,
+ bar: indexProperties.bar,
+ boundThing: 'OMG',
+ abool: true,
+ })
+ );
+ this.add(
+ 'controller:about',
+ Controller.extend({
+ queryParams: ['baz', 'bat'],
+ baz: 'alex',
+ bat: 'borf',
+ })
+ );
+ this.indexProperties = indexProperties;
+ }
+
+ shouldNotBeActive(assert, selector) {
+ this.checkActive(assert, selector, false);
+ }
+
+ shouldBeActive(assert, selector) {
+ this.checkActive(assert, selector, true);
+ }
+
+ getController(name) {
+ return this.applicationInstance.lookup(`controller:${name}`);
+ }
+
+ checkActive(assert, selector, active) {
+ let classList = this.$(selector)[0].className;
+ assert.equal(
+ classList.indexOf('active') > -1,
+ active,
+ selector + ' active should be ' + active.toString()
+ );
+ }
+
+ [`@test doesn't update controller QP properties on current route when invoked`](assert) {
+ this.addTemplate('index', `Index`);
+
+ return this.visit('/').then(() => {
+ this.click('#the-link');
+ let indexController = this.getController('index');
+
+ assert.deepEqual(
+ indexController.getProperties('foo', 'bar'),
+ this.indexProperties,
+ 'controller QP properties do not update'
+ );
+ });
+ }
+
+ [`@test doesn't update controller QP properties on current route when invoked (empty query-params obj)`](
+ assert
+ ) {
+ this.addTemplate(
+ 'index',
+ `Index`
+ );
+
+ return this.visit('/').then(() => {
+ this.click('#the-link');
+ let indexController = this.getController('index');
+
+ assert.deepEqual(
+ indexController.getProperties('foo', 'bar'),
+ this.indexProperties,
+ 'controller QP properties do not update'
+ );
+ });
+ }
+
+ [`@test doesn't update controller QP properties on current route when invoked (empty query-params obj, inferred route)`](
+ assert
+ ) {
+ this.addTemplate('index', `Index`);
+
+ return this.visit('/').then(() => {
+ this.click('#the-link');
+ let indexController = this.getController('index');
+
+ assert.deepEqual(
+ indexController.getProperties('foo', 'bar'),
+ this.indexProperties,
+ 'controller QP properties do not update'
+ );
+ });
+ }
+
+ ['@test updates controller QP properties on current route when invoked'](assert) {
+ this.addTemplate(
+ 'index',
+ `
+
+ Index
+
+ `
+ );
+
+ return this.visit('/').then(() => {
+ this.click('#the-link');
+ let indexController = this.getController('index');
+
+ assert.deepEqual(
+ indexController.getProperties('foo', 'bar'),
+ { foo: '456', bar: 'abc' },
+ 'controller QP properties updated'
+ );
+ });
+ }
+
+ ['@test updates controller QP properties on current route when invoked (inferred route)'](
+ assert
+ ) {
+ this.addTemplate(
+ 'index',
+ `
+
+ Index
+
+ `
+ );
+
+ return this.visit('/').then(() => {
+ this.click('#the-link');
+ let indexController = this.getController('index');
+
+ assert.deepEqual(
+ indexController.getProperties('foo', 'bar'),
+ { foo: '456', bar: 'abc' },
+ 'controller QP properties updated'
+ );
+ });
+ }
+
+ ['@test updates controller QP properties on other route after transitioning to that route'](
+ assert
+ ) {
+ this.router.map(function() {
+ this.route('about');
+ });
+
+ this.addTemplate(
+ 'index',
+ `
+
+ About
+
+ `
+ );
+
+ return this.visit('/').then(() => {
+ let theLink = this.$('#the-link');
+ assert.equal(theLink.attr('href'), '/about?baz=lol');
+
+ runTask(() => this.click('#the-link'));
+
+ let aboutController = this.getController('about');
+
+ assert.deepEqual(
+ aboutController.getProperties('baz', 'bat'),
+ { baz: 'lol', bat: 'borf' },
+ 'about controller QP properties updated'
+ );
+ });
+ }
+
+ ['@test supplied QP properties can be bound'](assert) {
+ this.addTemplate(
+ 'index',
+ `
+
+ Index
+
+ `
+ );
+
+ return this.visit('/').then(() => {
+ let indexController = this.getController('index');
+ let theLink = this.$('#the-link');
+
+ assert.equal(theLink.attr('href'), '/?foo=OMG');
+
+ runTask(() => indexController.set('boundThing', 'ASL'));
+
+ assert.equal(theLink.attr('href'), '/?foo=ASL');
+ });
+ }
+
+ ['@test supplied QP properties can be bound (booleans)'](assert) {
+ this.addTemplate(
+ 'index',
+ `
+
+ Index
+
+ `
+ );
+
+ return this.visit('/').then(() => {
+ let indexController = this.getController('index');
+ let theLink = this.$('#the-link');
+
+ assert.equal(theLink.attr('href'), '/?abool=OMG');
+
+ runTask(() => indexController.set('boundThing', false));
+
+ assert.equal(theLink.attr('href'), '/?abool=false');
+
+ this.click('#the-link');
+
+ assert.deepEqual(
+ indexController.getProperties('foo', 'bar', 'abool'),
+ { foo: '123', bar: 'abc', abool: false },
+ 'bound bool QP properties update'
+ );
+ });
+ }
+ ['@test href updates when unsupplied controller QP props change'](assert) {
+ this.addTemplate(
+ 'index',
+ `
+
+ Index
+
+ `
+ );
+
+ return this.visit('/').then(() => {
+ let indexController = this.getController('index');
+ let theLink = this.$('#the-link');
+
+ assert.equal(theLink.attr('href'), '/?foo=lol');
+
+ runTask(() => indexController.set('bar', 'BORF'));
+
+ assert.equal(theLink.attr('href'), '/?bar=BORF&foo=lol');
+
+ runTask(() => indexController.set('foo', 'YEAH'));
+
+ assert.equal(theLink.attr('href'), '/?bar=BORF&foo=lol');
+ });
+ }
+
+ ['@test The component with only query params always transitions to the current route with the query params applied'](
+ assert
+ ) {
+ // Test harness for bug #12033
+ this.addTemplate(
+ 'cars',
+ `
+ Create new car
+ Page 2
+ {{outlet}}
+ `
+ );
+
+ this.addTemplate(
+ 'cars.create',
+ `Close create form`
+ );
+
+ this.router.map(function() {
+ this.route('cars', function() {
+ this.route('create');
+ });
+ });
+
+ this.add(
+ 'controller:cars',
+ Controller.extend({
+ queryParams: ['page'],
+ page: 1,
+ })
+ );
+
+ return this.visit('/cars/create').then(() => {
+ let router = this.appRouter;
+ let carsController = this.getController('cars');
+
+ assert.equal(router.currentRouteName, 'cars.create');
+
+ runTask(() => this.click('#close-link'));
+
+ assert.equal(router.currentRouteName, 'cars.index');
+ assert.equal(router.get('url'), '/cars');
+ assert.equal(carsController.get('page'), 1, 'The page query-param is 1');
+
+ runTask(() => this.click('#page2-link'));
+
+ assert.equal(router.currentRouteName, 'cars.index', 'The active route is still cars');
+ assert.equal(router.get('url'), '/cars?page=2', 'The url has been updated');
+ assert.equal(carsController.get('page'), 2, 'The query params have been updated');
+ });
+ }
+
+ ['@test the component applies activeClass when query params are not changed'](
+ assert
+ ) {
+ this.addTemplate(
+ 'index',
+ `
+ Index
+ Index
+ Index
+ `
+ );
+
+ this.addTemplate(
+ 'search',
+ `
+ Index
+ Index
+ Index
+ Index
+ Index
+ Index
+ Index
+ {{outlet}}
+ `
+ );
+
+ this.addTemplate(
+ 'search.results',
+ `
+ Index
+ Index
+ Index
+ Index
+ Index
+ Index
+ Index
+ `
+ );
+
+ this.router.map(function() {
+ this.route('search', function() {
+ this.route('results');
+ });
+ });
+
+ this.add(
+ 'controller:search',
+ Controller.extend({
+ queryParams: ['search', 'archive'],
+ search: '',
+ archive: false,
+ })
+ );
+
+ this.add(
+ 'controller:search.results',
+ Controller.extend({
+ queryParams: ['sort', 'showDetails'],
+ sort: 'title',
+ showDetails: true,
+ })
+ );
+
+ return this.visit('/')
+ .then(() => {
+ this.shouldNotBeActive(assert, '#cat-link');
+ this.shouldNotBeActive(assert, '#dog-link');
+
+ return this.visit('/?foo=cat');
+ })
+ .then(() => {
+ this.shouldBeActive(assert, '#cat-link');
+ this.shouldNotBeActive(assert, '#dog-link');
+
+ return this.visit('/?foo=dog');
+ })
+ .then(() => {
+ this.shouldBeActive(assert, '#dog-link');
+ this.shouldNotBeActive(assert, '#cat-link');
+ this.shouldBeActive(assert, '#change-nothing');
+
+ return this.visit('/search?search=same');
+ })
+ .then(() => {
+ this.shouldBeActive(assert, '#same-search');
+ this.shouldNotBeActive(assert, '#change-search');
+ this.shouldNotBeActive(assert, '#same-search-add-archive');
+ this.shouldNotBeActive(assert, '#only-add-archive');
+ this.shouldNotBeActive(assert, '#remove-one');
+
+ return this.visit('/search?search=same&archive=true');
+ })
+ .then(() => {
+ this.shouldBeActive(assert, '#both-same');
+ this.shouldNotBeActive(assert, '#change-one');
+
+ return this.visit('/search/results?search=same&sort=title&showDetails=true');
+ })
+ .then(() => {
+ this.shouldBeActive(assert, '#same-sort-child-only');
+ this.shouldBeActive(assert, '#same-search-parent-only');
+ this.shouldNotBeActive(assert, '#change-search-parent-only');
+ this.shouldBeActive(assert, '#same-search-same-sort-child-and-parent');
+ this.shouldNotBeActive(assert, '#same-search-different-sort-child-and-parent');
+ this.shouldNotBeActive(assert, '#change-search-same-sort-child-and-parent');
+ });
+ }
+
+ ['@test the component applies active class when query-param is a number'](assert) {
+ this.addTemplate(
+ 'index',
+ `
+
+ Index
+
+ `
+ );
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ queryParams: ['page'],
+ page: 1,
+ pageNumber: 5,
+ })
+ );
+
+ return this.visit('/')
+ .then(() => {
+ this.shouldNotBeActive(assert, '#page-link');
+ return this.visit('/?page=5');
+ })
+ .then(() => {
+ this.shouldBeActive(assert, '#page-link');
+ });
+ }
+
+ ['@test the component applies active class when query-param is an array'](assert) {
+ this.addTemplate(
+ 'index',
+ `
+ Index
+ Index
+ Index
+ `
+ );
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ queryParams: ['pages'],
+ pages: [],
+ pagesArray: [1, 2],
+ biggerArray: [1, 2, 3],
+ emptyArray: [],
+ })
+ );
+
+ return this.visit('/')
+ .then(() => {
+ this.shouldNotBeActive(assert, '#array-link');
+
+ return this.visit('/?pages=%5B1%2C2%5D');
+ })
+ .then(() => {
+ this.shouldBeActive(assert, '#array-link');
+ this.shouldNotBeActive(assert, '#bigger-link');
+ this.shouldNotBeActive(assert, '#empty-link');
+
+ return this.visit('/?pages=%5B2%2C1%5D');
+ })
+ .then(() => {
+ this.shouldNotBeActive(assert, '#array-link');
+ this.shouldNotBeActive(assert, '#bigger-link');
+ this.shouldNotBeActive(assert, '#empty-link');
+
+ return this.visit('/?pages=%5B1%2C2%2C3%5D');
+ })
+ .then(() => {
+ this.shouldBeActive(assert, '#bigger-link');
+ this.shouldNotBeActive(assert, '#array-link');
+ this.shouldNotBeActive(assert, '#empty-link');
+ });
+ }
+ ['@test the component applies active class to the parent route'](assert) {
+ this.router.map(function() {
+ this.route('parent', function() {
+ this.route('child');
+ });
+ });
+
+ this.addTemplate(
+ 'application',
+ `
+ Parent
+ Child
+ Parent
+ {{outlet}}
+ `
+ );
+
+ this.add(
+ 'controller:parent.child',
+ Controller.extend({
+ queryParams: ['foo'],
+ foo: 'bar',
+ })
+ );
+
+ return this.visit('/')
+ .then(() => {
+ this.shouldNotBeActive(assert, '#parent-link');
+ this.shouldNotBeActive(assert, '#parent-child-link');
+ this.shouldNotBeActive(assert, '#parent-link-qp');
+ return this.visit('/parent/child?foo=dog');
+ })
+ .then(() => {
+ this.shouldBeActive(assert, '#parent-link');
+ this.shouldNotBeActive(assert, '#parent-link-qp');
+ });
+ }
+
+ ['@test The component disregards query-params in activeness computation when current-when is specified'](
+ assert
+ ) {
+ let appLink;
+
+ this.router.map(function() {
+ this.route('parent');
+ });
+
+ this.addTemplate(
+ 'application',
+ `
+
+ Parent
+
+ {{outlet}}
+ `
+ );
+
+ this.addTemplate(
+ 'parent',
+ `
+
+ Parent
+
+ {{outlet}}
+ `
+ );
+
+ this.add(
+ 'controller:parent',
+ Controller.extend({
+ queryParams: ['page'],
+ page: 1,
+ })
+ );
+
+ return this.visit('/')
+ .then(() => {
+ appLink = this.$('#app-link');
+
+ assert.equal(appLink.attr('href'), '/parent');
+ this.shouldNotBeActive(assert, '#app-link');
+
+ return this.visit('/parent?page=2');
+ })
+ .then(() => {
+ appLink = this.$('#app-link');
+ let router = this.appRouter;
+
+ assert.equal(appLink.attr('href'), '/parent');
+ this.shouldBeActive(assert, '#app-link');
+ assert.equal(this.$('#parent-link').attr('href'), '/parent');
+ this.shouldBeActive(assert, '#parent-link');
+
+ let parentController = this.getController('parent');
+
+ assert.equal(parentController.get('page'), 2);
+
+ runTask(() => parentController.set('page', 3));
+
+ assert.equal(router.get('location.path'), '/parent?page=3');
+ this.shouldBeActive(assert, '#app-link');
+ this.shouldBeActive(assert, '#parent-link');
+
+ runTask(() => this.click('#app-link'));
+
+ assert.equal(router.get('location.path'), '/parent');
+ });
+ }
+
+ ['@test the component default query params while in active transition regression test'](
+ assert
+ ) {
+ this.router.map(function() {
+ this.route('foos');
+ this.route('bars');
+ });
+
+ let foos = RSVP.defer();
+ let bars = RSVP.defer();
+
+ this.addTemplate(
+ 'application',
+ `
+ Foos
+ Baz Foos
+ Quux Bars
+ `
+ );
+
+ this.add(
+ 'controller:foos',
+ Controller.extend({
+ queryParams: ['status'],
+ baz: false,
+ })
+ );
+
+ this.add(
+ 'route:foos',
+ Route.extend({
+ model() {
+ return foos.promise;
+ },
+ })
+ );
+
+ this.add(
+ 'controller:bars',
+ Controller.extend({
+ queryParams: ['status'],
+ quux: false,
+ })
+ );
+
+ this.add(
+ 'route:bars',
+ Route.extend({
+ model() {
+ return bars.promise;
+ },
+ })
+ );
+
+ return this.visit('/').then(() => {
+ let router = this.appRouter;
+ let foosLink = this.$('#foos-link');
+ let barsLink = this.$('#bars-link');
+ let bazLink = this.$('#baz-foos-link');
+
+ assert.equal(foosLink.attr('href'), '/foos');
+ assert.equal(bazLink.attr('href'), '/foos?baz=true');
+ assert.equal(barsLink.attr('href'), '/bars?quux=true');
+ assert.equal(router.get('location.path'), '/');
+ this.shouldNotBeActive(assert, '#foos-link');
+ this.shouldNotBeActive(assert, '#baz-foos-link');
+ this.shouldNotBeActive(assert, '#bars-link');
+
+ runTask(() => barsLink.click());
+ this.shouldNotBeActive(assert, '#bars-link');
+
+ runTask(() => foosLink.click());
+ this.shouldNotBeActive(assert, '#foos-link');
+
+ runTask(() => foos.resolve());
+
+ assert.equal(router.get('location.path'), '/foos');
+ this.shouldBeActive(assert, '#foos-link');
+ });
+ }
+ }
+ );
+}
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-curly-test.js
similarity index 98%
rename from packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-test.js
rename to packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-curly-test.js
index b1f5f6e4314..c7842f5927a 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-curly-test.js
@@ -5,7 +5,6 @@ import {
ApplicationTestCase,
classes as classMatcher,
moduleFor,
- runLoopSettled,
runTask,
} from 'internal-test-helpers';
@@ -725,17 +724,5 @@ moduleFor(
this.shouldBeActive(assert, '#foos-link');
});
}
-
- [`@test the {{link-to}} component throws a useful error if you invoke it wrong`](assert) {
- assert.expect(1);
-
- this.addTemplate('application', `{{#link-to id='the-link'}}Index{{/link-to}}`);
-
- expectAssertion(() => {
- this.visit('/');
- }, /You must provide one or more parameters to the link-to component/);
-
- return runLoopSettled();
- }
}
);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/rendering-angle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/rendering-angle-test.js
new file mode 100644
index 00000000000..48b9ab05227
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/rendering-angle-test.js
@@ -0,0 +1,111 @@
+import { moduleFor, ApplicationTestCase, RenderingTestCase, runTask } from 'internal-test-helpers';
+
+import { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features';
+import Controller from '@ember/controller';
+import { set } from '@ember/-internals/metal';
+import { LinkComponent } from '@ember/-internals/glimmer';
+
+if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) {
+ moduleFor(
+ ' component (rendering tests)',
+ class extends ApplicationTestCase {
+ [`@test throws a useful error if you invoke it wrong`](assert) {
+ assert.expect(1);
+
+ this.addTemplate('application', `Index`);
+
+ expectAssertion(() => {
+ this.visit('/');
+ }, /You must provide at least one of the `@route`, `@model`, `@models` or `@query` argument to ``/);
+ }
+
+ ['@test should be able to be inserted in DOM when the router is not present']() {
+ this.addTemplate('application', `Go to Index`);
+
+ return this.visit('/').then(() => {
+ this.assertText('Go to Index');
+ });
+ }
+
+ ['@test re-renders when title changes']() {
+ let controller;
+
+ this.addTemplate('application', `{{title}}`);
+
+ this.add(
+ 'controller:application',
+ Controller.extend({
+ init() {
+ this._super(...arguments);
+ controller = this;
+ },
+ title: 'foo',
+ })
+ );
+
+ return this.visit('/').then(() => {
+ this.assertText('foo');
+ runTask(() => set(controller, 'title', 'bar'));
+ this.assertText('bar');
+ });
+ }
+
+ ['@test re-computes active class when params change'](assert) {
+ let controller;
+
+ this.addTemplate('application', 'foo');
+
+ this.add(
+ 'controller:application',
+ Controller.extend({
+ init() {
+ this._super(...arguments);
+ controller = this;
+ },
+ routeName: 'index',
+ })
+ );
+
+ this.router.map(function() {
+ this.route('bar', { path: '/bar' });
+ });
+
+ return this.visit('/bar').then(() => {
+ assert.equal(this.firstChild.classList.contains('active'), false);
+ runTask(() => set(controller, 'routeName', 'bar'));
+ assert.equal(this.firstChild.classList.contains('active'), true);
+ });
+ }
+
+ ['@test able to safely extend the built-in component and use the normal path']() {
+ this.addComponent('custom-link-to', {
+ ComponentClass: LinkComponent.extend(),
+ });
+
+ this.addTemplate('application', `{{title}}`);
+
+ this.add(
+ 'controller:application',
+ Controller.extend({
+ title: 'Hello',
+ })
+ );
+
+ return this.visit('/').then(() => {
+ this.assertText('Hello');
+ });
+ }
+ }
+ );
+
+ moduleFor(
+ ' component (rendering tests, without router)',
+ class extends RenderingTestCase {
+ ['@test should be able to be inserted in DOM when the router is not present - block']() {
+ this.render(`Go to Index`);
+
+ this.assertText('Go to Index');
+ }
+ }
+ );
+}
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/rendering-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/rendering-curly-test.js
similarity index 84%
rename from packages/@ember/-internals/glimmer/tests/integration/components/link-to/rendering-test.js
rename to packages/@ember/-internals/glimmer/tests/integration/components/link-to/rendering-curly-test.js
index 3be9729f188..6cdf926401e 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/rendering-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/rendering-curly-test.js
@@ -7,6 +7,30 @@ import { LinkComponent } from '@ember/-internals/glimmer';
moduleFor(
'{{link-to}} component (rendering tests)',
class extends ApplicationTestCase {
+ [`@feature(ember-glimmer-angle-bracket-built-ins) throws a useful error if you invoke it wrong`](
+ assert
+ ) {
+ assert.expect(1);
+
+ this.addTemplate('application', `{{#link-to id='the-link'}}Index{{/link-to}}`);
+
+ expectAssertion(() => {
+ this.visit('/');
+ }, /You must provide at least one of the `@route`, `@model`, `@models` or `@query` argument to ``/);
+ }
+
+ [`@feature(!ember-glimmer-angle-bracket-built-ins) throws a useful error if you invoke it wrong`](
+ assert
+ ) {
+ assert.expect(1);
+
+ this.addTemplate('application', `{{#link-to id='the-link'}}Index{{/link-to}}`);
+
+ expectAssertion(() => {
+ this.visit('/');
+ }, /You must provide one or more parameters to the link-to component/);
+ }
+
['@test should be able to be inserted in DOM when the router is not present']() {
this.addTemplate('application', `{{#link-to 'index'}}Go to Index{{/link-to}}`);
@@ -18,7 +42,8 @@ moduleFor(
['@test re-renders when title changes']() {
let controller;
- this.addTemplate('application', '{{link-to title routeName}}');
+ this.addTemplate('application', `{{link-to title 'index'}}`);
+
this.add(
'controller:application',
Controller.extend({
@@ -27,7 +52,6 @@ moduleFor(
controller = this;
},
title: 'foo',
- routeName: 'index',
})
);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js
new file mode 100644
index 00000000000..2b1fe78393b
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js
@@ -0,0 +1,1853 @@
+/* eslint-disable no-inner-declarations */
+// ^^^ remove after unflagging EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS
+import { moduleFor, ApplicationTestCase, runLoopSettled, runTask } from 'internal-test-helpers';
+import { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features';
+import Controller, { inject as injectController } from '@ember/controller';
+import { A as emberA, RSVP } from '@ember/-internals/runtime';
+import { alias } from '@ember/-internals/metal';
+import { subscribe, reset } from '@ember/instrumentation';
+import { Route, NoneLocation } from '@ember/-internals/routing';
+import { EMBER_IMPROVED_INSTRUMENTATION } from '@ember/canary-features';
+
+if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) {
+ // IE includes the host name
+ function normalizeUrl(url) {
+ return url.replace(/https?:\/\/[^\/]+/, '');
+ }
+
+ function shouldNotBeActive(assert, element) {
+ checkActive(assert, element, false);
+ }
+
+ function shouldBeActive(assert, element) {
+ checkActive(assert, element, true);
+ }
+
+ function checkActive(assert, element, active) {
+ let classList = element.attr('class');
+ assert.equal(classList.indexOf('active') > -1, active, `${element} active should be ${active}`);
+ }
+
+ moduleFor(
+ ' component (routing tests)',
+ class extends ApplicationTestCase {
+ constructor() {
+ super();
+
+ this.router.map(function() {
+ this.route('about');
+ });
+
+ this.addTemplate(
+ 'index',
+ `
+
Home
+ About
+ Self
+ `
+ );
+ this.addTemplate(
+ 'about',
+ `
+ About
+ Home
+ Self
+ `
+ );
+ }
+
+ ['@test The component navigates into the named route'](assert) {
+ return this.visit('/')
+ .then(() => {
+ assert.equal(this.$('h3.home').length, 1, 'The home template was rendered');
+ assert.equal(
+ this.$('#self-link.active').length,
+ 1,
+ 'The self-link was rendered with active class'
+ );
+ assert.equal(
+ this.$('#about-link:not(.active)').length,
+ 1,
+ 'The other link was rendered without active class'
+ );
+
+ return this.click('#about-link');
+ })
+ .then(() => {
+ assert.equal(this.$('h3.about').length, 1, 'The about template was rendered');
+ assert.equal(
+ this.$('#self-link.active').length,
+ 1,
+ 'The self-link was rendered with active class'
+ );
+ assert.equal(
+ this.$('#home-link:not(.active)').length,
+ 1,
+ 'The other link was rendered without active class'
+ );
+ });
+ }
+
+ [`@test the component doesn't add an href when the tagName isn't 'a'`](assert) {
+ this.addTemplate(
+ 'index',
+ `About`
+ );
+
+ return this.visit('/').then(() => {
+ assert.equal(this.$('#about-link').attr('href'), undefined, 'there is no href attribute');
+ });
+ }
+
+ [`@test the component applies a 'disabled' class when disabled`](assert) {
+ this.addTemplate(
+ 'index',
+ `
+ About
+ About
+ `
+ );
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ shouldDisable: true,
+ dynamicDisabledWhen: 'shouldDisable',
+ })
+ );
+
+ return this.visit('/').then(() => {
+ assert.equal(
+ this.$('#about-link-static.disabled').length,
+ 1,
+ 'The static link is disabled when its disabledWhen is true'
+ );
+ assert.equal(
+ this.$('#about-link-dynamic.disabled').length,
+ 1,
+ 'The dynamic link is disabled when its disabledWhen is true'
+ );
+
+ let controller = this.applicationInstance.lookup('controller:index');
+ runTask(() => controller.set('dynamicDisabledWhen', false));
+
+ assert.equal(
+ this.$('#about-link-dynamic.disabled').length,
+ 0,
+ 'The dynamic link is re-enabled when its disabledWhen becomes false'
+ );
+ });
+ }
+
+ [`@test the component doesn't apply a 'disabled' class if disabledWhen is not provided`](
+ assert
+ ) {
+ this.addTemplate('index', `About`);
+
+ return this.visit('/').then(() => {
+ assert.ok(
+ !this.$('#about-link').hasClass('disabled'),
+ 'The link is not disabled if disabledWhen not provided'
+ );
+ });
+ }
+
+ [`@test the component supports a custom disabledClass`](assert) {
+ this.addTemplate(
+ 'index',
+ `About`
+ );
+
+ return this.visit('/').then(() => {
+ assert.equal(
+ this.$('#about-link.do-not-want').length,
+ 1,
+ 'The link can apply a custom disabled class'
+ );
+ });
+ }
+
+ [`@test the component supports a custom disabledClass set via bound param`](
+ assert
+ ) {
+ this.addTemplate(
+ 'index',
+ `About`
+ );
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ disabledClass: 'do-not-want',
+ })
+ );
+
+ return this.visit('/').then(() => {
+ assert.equal(
+ this.$('#about-link.do-not-want').length,
+ 1,
+ 'The link can apply a custom disabled class via bound param'
+ );
+ });
+ }
+
+ [`@test the component does not respond to clicks when disabledWhen`](assert) {
+ this.addTemplate(
+ 'index',
+ `About`
+ );
+
+ return this.visit('/')
+ .then(() => {
+ return this.click('#about-link');
+ })
+ .then(() => {
+ assert.equal(this.$('h3.about').length, 0, 'Transitioning did not occur');
+ });
+ }
+
+ [`@test the component does not respond to clicks when disabled`](assert) {
+ this.addTemplate(
+ 'index',
+ `About`
+ );
+
+ return this.visit('/')
+ .then(() => {
+ return this.click('#about-link');
+ })
+ .then(() => {
+ assert.equal(this.$('h3.about').length, 0, 'Transitioning did not occur');
+ });
+ }
+
+ [`@test the component responds to clicks according to its disabledWhen bound param`](
+ assert
+ ) {
+ this.addTemplate(
+ 'index',
+ `About`
+ );
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ disabledWhen: true,
+ })
+ );
+
+ return this.visit('/')
+ .then(() => {
+ return this.click('#about-link');
+ })
+ .then(() => {
+ assert.equal(this.$('h3.about').length, 0, 'Transitioning did not occur');
+
+ let controller = this.applicationInstance.lookup('controller:index');
+ controller.set('disabledWhen', false);
+
+ return runLoopSettled();
+ })
+ .then(() => {
+ return this.click('#about-link');
+ })
+ .then(() => {
+ assert.equal(
+ this.$('h3.about').length,
+ 1,
+ 'Transitioning did occur when disabledWhen became false'
+ );
+ });
+ }
+
+ [`@test The component supports a custom activeClass`](assert) {
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ About
+ Self
+ `
+ );
+
+ return this.visit('/').then(() => {
+ assert.equal(this.$('h3.home').length, 1, 'The home template was rendered');
+ assert.equal(
+ this.$('#self-link.zomg-active').length,
+ 1,
+ 'The self-link was rendered with active class'
+ );
+ assert.equal(
+ this.$('#about-link:not(.active)').length,
+ 1,
+ 'The other link was rendered without active class'
+ );
+ });
+ }
+
+ [`@test The component supports a custom activeClass from a bound param`](assert) {
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ About
+ Self
+ `
+ );
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ activeClass: 'zomg-active',
+ })
+ );
+
+ return this.visit('/').then(() => {
+ assert.equal(this.$('h3.home').length, 1, 'The home template was rendered');
+ assert.equal(
+ this.$('#self-link.zomg-active').length,
+ 1,
+ 'The self-link was rendered with active class'
+ );
+ assert.equal(
+ this.$('#about-link:not(.active)').length,
+ 1,
+ 'The other link was rendered without active class'
+ );
+ });
+ }
+
+ // See https://github.com/emberjs/ember.js/issues/17771
+ [`@skip The component supports 'classNameBindings' with custom values [GH #11699]`](
+ assert
+ ) {
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ About
+ `
+ );
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ foo: false,
+ })
+ );
+
+ return this.visit('/').then(() => {
+ assert.equal(
+ this.$('#about-link.foo-is-false').length,
+ 1,
+ 'The about-link was rendered with the falsy class'
+ );
+
+ let controller = this.applicationInstance.lookup('controller:index');
+ runTask(() => controller.set('foo', true));
+
+ assert.equal(
+ this.$('#about-link.foo-is-true').length,
+ 1,
+ 'The about-link was rendered with the truthy class after toggling the property'
+ );
+ });
+ }
+ }
+ );
+
+ moduleFor(
+ ' component (routing tests - location hooks)',
+ class extends ApplicationTestCase {
+ constructor() {
+ super();
+
+ this.updateCount = 0;
+ this.replaceCount = 0;
+
+ let testContext = this;
+ this.add(
+ 'location:none',
+ NoneLocation.extend({
+ setURL() {
+ testContext.updateCount++;
+ return this._super(...arguments);
+ },
+ replaceURL() {
+ testContext.replaceCount++;
+ return this._super(...arguments);
+ },
+ })
+ );
+
+ this.router.map(function() {
+ this.route('about');
+ });
+
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ About
+ Self
+ `
+ );
+ this.addTemplate(
+ 'about',
+ `
+ About
+ Home
+ Self
+ `
+ );
+ }
+
+ visit() {
+ return super.visit(...arguments).then(() => {
+ this.updateCountAfterVisit = this.updateCount;
+ this.replaceCountAfterVisit = this.replaceCount;
+ });
+ }
+
+ ['@test The component supports URL replacement'](assert) {
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ About
+ `
+ );
+
+ return this.visit('/')
+ .then(() => {
+ return this.click('#about-link');
+ })
+ .then(() => {
+ assert.equal(
+ this.updateCount,
+ this.updateCountAfterVisit,
+ 'setURL should not be called'
+ );
+ assert.equal(
+ this.replaceCount,
+ this.replaceCountAfterVisit + 1,
+ 'replaceURL should be called once'
+ );
+ });
+ }
+
+ ['@test The component supports URL replacement via replace=boundTruthyThing'](
+ assert
+ ) {
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ About
+ `
+ );
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ boundTruthyThing: true,
+ })
+ );
+
+ return this.visit('/')
+ .then(() => {
+ return this.click('#about-link');
+ })
+ .then(() => {
+ assert.equal(
+ this.updateCount,
+ this.updateCountAfterVisit,
+ 'setURL should not be called'
+ );
+ assert.equal(
+ this.replaceCount,
+ this.replaceCountAfterVisit + 1,
+ 'replaceURL should be called once'
+ );
+ });
+ }
+
+ ['@test The component supports setting replace=boundFalseyThing'](assert) {
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ About
+ `
+ );
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ boundFalseyThing: false,
+ })
+ );
+
+ return this.visit('/')
+ .then(() => {
+ return this.click('#about-link');
+ })
+ .then(() => {
+ assert.equal(
+ this.updateCount,
+ this.updateCountAfterVisit + 1,
+ 'setURL should be called'
+ );
+ assert.equal(
+ this.replaceCount,
+ this.replaceCountAfterVisit,
+ 'replaceURL should not be called'
+ );
+ });
+ }
+ }
+ );
+
+ if (EMBER_IMPROVED_INSTRUMENTATION) {
+ moduleFor(
+ 'The component with EMBER_IMPROVED_INSTRUMENTATION',
+ class extends ApplicationTestCase {
+ constructor() {
+ super();
+
+ this.router.map(function() {
+ this.route('about');
+ });
+
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ About
+ Self
+ `
+ );
+ this.addTemplate(
+ 'about',
+ `
+ About
+ Home
+ Self
+ `
+ );
+ }
+
+ beforeEach() {
+ return this.visit('/');
+ }
+
+ afterEach() {
+ reset();
+
+ return super.afterEach();
+ }
+
+ ['@test The component fires an interaction event'](assert) {
+ assert.expect(2);
+
+ subscribe('interaction.link-to', {
+ before() {
+ assert.ok(true, 'instrumentation subscriber was called');
+ },
+ after() {
+ assert.ok(true, 'instrumentation subscriber was called');
+ },
+ });
+
+ return this.click('#about-link');
+ }
+
+ ['@test The component interaction event includes the route name'](assert) {
+ assert.expect(2);
+
+ subscribe('interaction.link-to', {
+ before(name, timestamp, { routeName }) {
+ assert.equal(routeName, 'about', 'instrumentation subscriber was passed route name');
+ },
+ after(name, timestamp, { routeName }) {
+ assert.equal(routeName, 'about', 'instrumentation subscriber was passed route name');
+ },
+ });
+
+ return this.click('#about-link');
+ }
+
+ ['@test The component interaction event includes the transition in the after hook'](
+ assert
+ ) {
+ assert.expect(1);
+
+ subscribe('interaction.link-to', {
+ before() {},
+ after(name, timestamp, { transition }) {
+ assert.equal(
+ transition.targetName,
+ 'about',
+ 'instrumentation subscriber was passed route name'
+ );
+ },
+ });
+
+ return this.click('#about-link');
+ }
+ }
+ );
+ }
+
+ moduleFor(
+ 'The component - nested routes and link-to arguments',
+ class extends ApplicationTestCase {
+ ['@test The component supports leaving off .index for nested routes'](assert) {
+ this.router.map(function() {
+ this.route('about', function() {
+ this.route('item');
+ });
+ });
+
+ this.addTemplate('about', `About
{{outlet}}`);
+ this.addTemplate('about.index', `Index
`);
+ this.addTemplate(
+ 'about.item',
+ `About
`
+ );
+
+ return this.visit('/about/item').then(() => {
+ assert.equal(normalizeUrl(this.$('#item a').attr('href')), '/about');
+ });
+ }
+
+ [`@test The component supports custom, nested, current-when`](assert) {
+ this.router.map(function() {
+ this.route('index', { path: '/' }, function() {
+ this.route('about');
+ });
+
+ this.route('item');
+ });
+
+ this.addTemplate('index', `Home
{{outlet}}`);
+ this.addTemplate(
+ 'index.about',
+ `ITEM`
+ );
+
+ return this.visit('/about').then(() => {
+ assert.equal(
+ this.$('#other-link.active').length,
+ 1,
+ 'The link is active since current-when is a parent route'
+ );
+ });
+ }
+
+ [`@test The component does not disregard current-when when it is given explicitly for a route`](
+ assert
+ ) {
+ this.router.map(function() {
+ this.route('index', { path: '/' }, function() {
+ this.route('about');
+ });
+
+ this.route('items', function() {
+ this.route('item');
+ });
+ });
+
+ this.addTemplate('index', `Home
{{outlet}}`);
+ this.addTemplate(
+ 'index.about',
+ `ITEM`
+ );
+
+ return this.visit('/about').then(() => {
+ assert.equal(
+ this.$('#other-link.active').length,
+ 1,
+ 'The link is active when current-when is given for explicitly for a route'
+ );
+ });
+ }
+
+ ['@test The component does not disregard current-when when it is set via a bound param'](
+ assert
+ ) {
+ this.router.map(function() {
+ this.route('index', { path: '/' }, function() {
+ this.route('about');
+ });
+
+ this.route('items', function() {
+ this.route('item');
+ });
+ });
+
+ this.add(
+ 'controller:index.about',
+ Controller.extend({
+ currentWhen: 'index',
+ })
+ );
+
+ this.addTemplate('index', `Home
{{outlet}}`);
+ this.addTemplate(
+ 'index.about',
+ `ITEM`
+ );
+
+ return this.visit('/about').then(() => {
+ assert.equal(
+ this.$('#other-link.active').length,
+ 1,
+ 'The link is active when current-when is given for explicitly for a route'
+ );
+ });
+ }
+
+ ['@test The component supports multiple current-when routes'](assert) {
+ this.router.map(function() {
+ this.route('index', { path: '/' }, function() {
+ this.route('about');
+ });
+ this.route('item');
+ this.route('foo');
+ });
+
+ this.addTemplate('index', `Home
{{outlet}}`);
+ this.addTemplate(
+ 'index.about',
+ `ITEM`
+ );
+ this.addTemplate(
+ 'item',
+ `ITEM`
+ );
+ this.addTemplate(
+ 'foo',
+ `ITEM`
+ );
+
+ return this.visit('/about')
+ .then(() => {
+ assert.equal(
+ this.$('#link1.active').length,
+ 1,
+ 'The link is active since current-when contains the parent route'
+ );
+
+ return this.visit('/item');
+ })
+ .then(() => {
+ assert.equal(
+ this.$('#link2.active').length,
+ 1,
+ 'The link is active since you are on the active route'
+ );
+
+ return this.visit('/foo');
+ })
+ .then(() => {
+ assert.equal(
+ this.$('#link3.active').length,
+ 0,
+ 'The link is not active since current-when does not contain the active route'
+ );
+ });
+ }
+
+ ['@test The component supports boolean values for current-when'](assert) {
+ this.router.map(function() {
+ this.route('index', { path: '/' }, function() {
+ this.route('about');
+ });
+ this.route('item');
+ });
+
+ this.addTemplate(
+ 'index.about',
+ `
+ ITEM
+ ITEM
+ `
+ );
+
+ this.add('controller:index.about', Controller.extend({ isCurrent: false }));
+
+ return this.visit('/about').then(() => {
+ assert.ok(
+ this.$('#about-link').hasClass('active'),
+ 'The link is active since current-when is true'
+ );
+ assert.notOk(
+ this.$('#index-link').hasClass('active'),
+ 'The link is not active since current-when is false'
+ );
+
+ let controller = this.applicationInstance.lookup('controller:index.about');
+ runTask(() => controller.set('isCurrent', true));
+
+ assert.ok(
+ this.$('#index-link').hasClass('active'),
+ 'The link is active since current-when is true'
+ );
+ });
+ }
+
+ ['@test The component defaults to bubbling'](assert) {
+ this.addTemplate(
+ 'about',
+ `
+
+ About
+
+ {{outlet}}
+ `
+ );
+
+ this.addTemplate('about.contact', ``);
+
+ this.router.map(function() {
+ this.route('about', function() {
+ this.route('contact');
+ });
+ });
+
+ let hidden = 0;
+
+ this.add(
+ 'route:about',
+ Route.extend({
+ actions: {
+ hide() {
+ hidden++;
+ },
+ },
+ })
+ );
+
+ return this.visit('/about')
+ .then(() => {
+ return this.click('#about-contact');
+ })
+ .then(() => {
+ assert.equal(this.$('#contact').text(), 'Contact', 'precond - the link worked');
+
+ assert.equal(hidden, 1, 'The link bubbles');
+ });
+ }
+
+ [`@test The component supports bubbles=false`](assert) {
+ this.addTemplate(
+ 'about',
+ `
+
+
+ About
+
+
+ {{outlet}}
+ `
+ );
+
+ this.addTemplate('about.contact', ``);
+
+ this.router.map(function() {
+ this.route('about', function() {
+ this.route('contact');
+ });
+ });
+
+ let hidden = 0;
+
+ this.add(
+ 'route:about',
+ Route.extend({
+ actions: {
+ hide() {
+ hidden++;
+ },
+ },
+ })
+ );
+
+ return this.visit('/about')
+ .then(() => {
+ return this.click('#about-contact');
+ })
+ .then(() => {
+ assert.equal(this.$('#contact').text(), 'Contact', 'precond - the link worked');
+
+ assert.equal(hidden, 0, "The link didn't bubble");
+ });
+ }
+
+ [`@test The component supports bubbles=boundFalseyThing`](assert) {
+ this.addTemplate(
+ 'about',
+ `
+
+
+ About
+
+
+ {{outlet}}
+ `
+ );
+
+ this.addTemplate('about.contact', ``);
+
+ this.add(
+ 'controller:about',
+ Controller.extend({
+ boundFalseyThing: false,
+ })
+ );
+
+ this.router.map(function() {
+ this.route('about', function() {
+ this.route('contact');
+ });
+ });
+
+ let hidden = 0;
+
+ this.add(
+ 'route:about',
+ Route.extend({
+ actions: {
+ hide() {
+ hidden++;
+ },
+ },
+ })
+ );
+
+ return this.visit('/about')
+ .then(() => {
+ return this.click('#about-contact');
+ })
+ .then(() => {
+ assert.equal(this.$('#contact').text(), 'Contact', 'precond - the link worked');
+ assert.equal(hidden, 0, "The link didn't bubble");
+ });
+ }
+
+ [`@test The component moves into the named route with context`](assert) {
+ this.router.map(function() {
+ this.route('about');
+ this.route('item', { path: '/item/:id' });
+ });
+
+ this.addTemplate(
+ 'about',
+ `
+ List
+
+ {{#each model as |person|}}
+ -
+
+ {{person.name}}
+
+
+ {{/each}}
+
+ Home
+ `
+ );
+
+ this.addTemplate(
+ 'item',
+ `
+ Item
+ {{model.name}}
+ Home
+ `
+ );
+
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ About
+ `
+ );
+
+ this.add(
+ 'route:about',
+ Route.extend({
+ model() {
+ return [
+ { id: 'yehuda', name: 'Yehuda Katz' },
+ { id: 'tom', name: 'Tom Dale' },
+ { id: 'erik', name: 'Erik Brynroflsson' },
+ ];
+ },
+ })
+ );
+
+ return this.visit('/about')
+ .then(() => {
+ assert.equal(this.$('h3.list').length, 1, 'The home template was rendered');
+ assert.equal(
+ normalizeUrl(this.$('#home-link').attr('href')),
+ '/',
+ 'The home link points back at /'
+ );
+
+ return this.click('#yehuda');
+ })
+ .then(() => {
+ assert.equal(this.$('h3.item').length, 1, 'The item template was rendered');
+ assert.equal(this.$('p').text(), 'Yehuda Katz', 'The name is correct');
+
+ return this.click('#home-link');
+ })
+ .then(() => {
+ return this.click('#about-link');
+ })
+ .then(() => {
+ assert.equal(normalizeUrl(this.$('li a#yehuda').attr('href')), '/item/yehuda');
+ assert.equal(normalizeUrl(this.$('li a#tom').attr('href')), '/item/tom');
+ assert.equal(normalizeUrl(this.$('li a#erik').attr('href')), '/item/erik');
+
+ return this.click('#erik');
+ })
+ .then(() => {
+ assert.equal(this.$('h3.item').length, 1, 'The item template was rendered');
+ assert.equal(this.$('p').text(), 'Erik Brynroflsson', 'The name is correct');
+ });
+ }
+
+ [`@test The component binds some anchor html tag common attributes`](assert) {
+ this.addTemplate(
+ 'index',
+ `
+ Home
+
+ Self
+
+ `
+ );
+
+ return this.visit('/').then(() => {
+ let link = this.$('#self-link');
+ assert.equal(link.attr('title'), 'title-attr', 'The self-link contains title attribute');
+ assert.equal(link.attr('rel'), 'rel-attr', 'The self-link contains rel attribute');
+ assert.equal(link.attr('tabindex'), '-1', 'The self-link contains tabindex attribute');
+ });
+ }
+
+ [`@test The component supports 'target' attribute`](assert) {
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ Self
+ `
+ );
+
+ return this.visit('/').then(() => {
+ let link = this.$('#self-link');
+ assert.equal(link.attr('target'), '_blank', 'The self-link contains `target` attribute');
+ });
+ }
+
+ [`@test The component supports 'target' attribute specified as a bound param`](
+ assert
+ ) {
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ Self
+ `
+ );
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ boundLinkTarget: '_blank',
+ })
+ );
+
+ return this.visit('/').then(() => {
+ let link = this.$('#self-link');
+ assert.equal(link.attr('target'), '_blank', 'The self-link contains `target` attribute');
+ });
+ }
+
+ [`@test the component calls preventDefault`](assert) {
+ this.router.map(function() {
+ this.route('about');
+ });
+
+ this.addTemplate('index', `About`);
+
+ return this.visit('/').then(() => {
+ assertNav({ prevented: true }, () => this.$('#about-link').click(), assert);
+ });
+ }
+
+ [`@test the component does not call preventDefault if '@preventDefault={{false}}' is passed as an option`](
+ assert
+ ) {
+ this.router.map(function() {
+ this.route('about');
+ });
+
+ this.addTemplate(
+ 'index',
+ `About`
+ );
+
+ return this.visit('/').then(() => {
+ assertNav({ prevented: false }, () => this.$('#about-link').trigger('click'), assert);
+ });
+ }
+
+ [`@test the component does not call preventDefault if '@preventDefault={{boundFalseyThing}}' is passed as an option`](
+ assert
+ ) {
+ this.router.map(function() {
+ this.route('about');
+ });
+
+ this.addTemplate(
+ 'index',
+ `About`
+ );
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ boundFalseyThing: false,
+ })
+ );
+
+ return this.visit('/').then(() => {
+ assertNav({ prevented: false }, () => this.$('#about-link').trigger('click'), assert);
+ });
+ }
+
+ [`@test The component does not call preventDefault if 'target' attribute is provided`](
+ assert
+ ) {
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ Self
+ `
+ );
+
+ return this.visit('/').then(() => {
+ assertNav({ prevented: false }, () => this.$('#self-link').click(), assert);
+ });
+ }
+
+ [`@test The component should preventDefault when 'target = _self'`](assert) {
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ Self
+ `
+ );
+
+ return this.visit('/').then(() => {
+ assertNav({ prevented: true }, () => this.$('#self-link').click(), assert);
+ });
+ }
+
+ [`@test The component should not transition if target is not equal to _self or empty`](
+ assert
+ ) {
+ this.addTemplate(
+ 'index',
+ `
+
+ About
+
+ `
+ );
+
+ this.router.map(function() {
+ this.route('about');
+ });
+
+ return this.visit('/')
+ .then(() => this.click('#about-link'))
+ .then(() => {
+ expectDeprecation(() => {
+ let currentRouteName = this.applicationInstance
+ .lookup('controller:application')
+ .get('currentRouteName');
+ assert.notEqual(
+ currentRouteName,
+ 'about',
+ 'link-to should not transition if target is not equal to _self or empty'
+ );
+ }, 'Accessing `currentRouteName` on `controller:application` is deprecated, use the `currentRouteName` property on `service:router` instead.');
+ });
+ }
+
+ [`@test The component accepts string/numeric arguments`](assert) {
+ this.router.map(function() {
+ this.route('filter', { path: '/filters/:filter' });
+ this.route('post', { path: '/post/:post_id' });
+ this.route('repo', { path: '/repo/:owner/:name' });
+ });
+
+ this.add(
+ 'controller:filter',
+ Controller.extend({
+ filter: 'unpopular',
+ repo: { owner: 'ember', name: 'ember.js' },
+ post_id: 123,
+ })
+ );
+
+ this.addTemplate(
+ 'filter',
+ `
+ {{filter}}
+ Unpopular
+ Unpopular
+ Post
+ Post
+ Repo
+ `
+ );
+
+ return this.visit('/filters/popular').then(() => {
+ assert.equal(normalizeUrl(this.$('#link').attr('href')), '/filters/unpopular');
+ assert.equal(normalizeUrl(this.$('#path-link').attr('href')), '/filters/unpopular');
+ assert.equal(normalizeUrl(this.$('#post-path-link').attr('href')), '/post/123');
+ assert.equal(normalizeUrl(this.$('#post-number-link').attr('href')), '/post/123');
+ assert.equal(
+ normalizeUrl(this.$('#repo-object-link').attr('href')),
+ '/repo/ember/ember.js'
+ );
+ });
+ }
+
+ [`@test Issue 4201 - Shorthand for route.index shouldn't throw errors about context arguments`](
+ assert
+ ) {
+ assert.expect(2);
+ this.router.map(function() {
+ this.route('lobby', function() {
+ this.route('index', { path: ':lobby_id' });
+ this.route('list');
+ });
+ });
+
+ this.add(
+ 'route:lobby.index',
+ Route.extend({
+ model(params) {
+ assert.equal(params.lobby_id, 'foobar');
+ return params.lobby_id;
+ },
+ })
+ );
+
+ this.addTemplate(
+ 'lobby.index',
+ `Lobby`
+ );
+
+ this.addTemplate(
+ 'lobby.list',
+ `Lobby`
+ );
+
+ return this.visit('/lobby/list')
+ .then(() => this.click('#lobby-link'))
+ .then(() => shouldBeActive(assert, this.$('#lobby-link')));
+ }
+
+ [`@test Quoteless route param performs property lookup`](assert) {
+ this.router.map(function() {
+ this.route('about');
+ });
+
+ this.addTemplate(
+ 'index',
+ `
+ string
+ path
+ `
+ );
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ foo: 'index',
+ })
+ );
+
+ let assertEquality = href => {
+ assert.equal(normalizeUrl(this.$('#string-link').attr('href')), '/');
+ assert.equal(normalizeUrl(this.$('#path-link').attr('href')), href);
+ };
+
+ return this.visit('/').then(() => {
+ assertEquality('/');
+
+ let controller = this.applicationInstance.lookup('controller:index');
+ runTask(() => controller.set('foo', 'about'));
+
+ assertEquality('/about');
+ });
+ }
+
+ [`@test The component refreshes href element when one of params changes`](assert) {
+ this.router.map(function() {
+ this.route('post', { path: '/posts/:post_id' });
+ });
+
+ let post = { id: '1' };
+ let secondPost = { id: '2' };
+
+ this.addTemplate('index', `post`);
+
+ this.add('controller:index', Controller.extend());
+
+ return this.visit('/').then(() => {
+ let indexController = this.applicationInstance.lookup('controller:index');
+ runTask(() => indexController.set('post', post));
+
+ assert.equal(
+ normalizeUrl(this.$('#post').attr('href')),
+ '/posts/1',
+ 'precond - Link has rendered href attr properly'
+ );
+
+ runTask(() => indexController.set('post', secondPost));
+
+ assert.equal(
+ this.$('#post').attr('href'),
+ '/posts/2',
+ 'href attr was updated after one of the params had been changed'
+ );
+
+ runTask(() => indexController.set('post', null));
+
+ assert.equal(
+ this.$('#post').attr('href'),
+ '#',
+ 'href attr becomes # when one of the arguments in nullified'
+ );
+ });
+ }
+
+ [`@test The component is active when a route is active`](assert) {
+ this.router.map(function() {
+ this.route('about', function() {
+ this.route('item');
+ });
+ });
+
+ this.addTemplate(
+ 'about',
+ `
+
+ About
+ Item
+ {{outlet}}
+
+ `
+ );
+
+ return this.visit('/about')
+ .then(() => {
+ assert.equal(this.$('#about-link.active').length, 1, 'The about route link is active');
+ assert.equal(this.$('#item-link.active').length, 0, 'The item route link is inactive');
+
+ return this.visit('/about/item');
+ })
+ .then(() => {
+ assert.equal(this.$('#about-link.active').length, 1, 'The about route link is active');
+ assert.equal(this.$('#item-link.active').length, 1, 'The item route link is active');
+ });
+ }
+
+ [`@test The component works in an #each'd array of string route names`](assert) {
+ this.router.map(function() {
+ this.route('foo');
+ this.route('bar');
+ this.route('rar');
+ });
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ routeNames: emberA(['foo', 'bar', 'rar']),
+ route1: 'bar',
+ route2: 'foo',
+ })
+ );
+
+ this.addTemplate(
+ 'index',
+ `
+ {{#each routeNames as |routeName|}}
+ {{routeName}}
+ {{/each}}
+ {{#each routeNames as |r|}}
+ {{r}}
+ {{/each}}
+ a
+ b
+ `
+ );
+
+ let linksEqual = (links, expected) => {
+ assert.equal(links.length, expected.length, 'Has correct number of links');
+
+ let idx;
+ for (idx = 0; idx < links.length; idx++) {
+ let href = this.$(links[idx]).attr('href');
+ // Old IE includes the whole hostname as well
+ assert.equal(
+ href.slice(-expected[idx].length),
+ expected[idx],
+ `Expected link to be '${expected[idx]}', but was '${href}'`
+ );
+ }
+ };
+
+ return this.visit('/').then(() => {
+ linksEqual(this.$('a'), ['/foo', '/bar', '/rar', '/foo', '/bar', '/rar', '/bar', '/foo']);
+
+ let indexController = this.applicationInstance.lookup('controller:index');
+ runTask(() => indexController.set('route1', 'rar'));
+
+ linksEqual(this.$('a'), ['/foo', '/bar', '/rar', '/foo', '/bar', '/rar', '/rar', '/foo']);
+
+ runTask(() => indexController.routeNames.shiftObject());
+
+ linksEqual(this.$('a'), ['/bar', '/rar', '/bar', '/rar', '/rar', '/foo']);
+ });
+ }
+
+ [`@test the component throws a useful error if you invoke it wrong`](assert) {
+ assert.expect(1);
+
+ this.router.map(function() {
+ this.route('post', { path: 'post/:post_id' });
+ });
+
+ this.addTemplate('application', `Post`);
+
+ assert.throws(() => {
+ this.visit('/');
+ }, /(You attempted to generate a link for the "post" route, but did not pass the models required for generating its dynamic segments.|You must provide param `post_id` to `generate`)/);
+
+ return runLoopSettled();
+ }
+
+ [`@test the component does not throw an error if its route has exited`](assert) {
+ assert.expect(0);
+
+ this.router.map(function() {
+ this.route('post', { path: 'post/:post_id' });
+ });
+
+ this.addTemplate(
+ 'application',
+ `
+ Home
+ Default Post
+ {{#if currentPost}}
+ Current Post
+ {{/if}}
+ `
+ );
+
+ this.add(
+ 'controller:application',
+ Controller.extend({
+ defaultPost: { id: 1 },
+ postController: injectController('post'),
+ currentPost: alias('postController.model'),
+ })
+ );
+
+ this.add('controller:post', Controller.extend());
+
+ this.add(
+ 'route:post',
+ Route.extend({
+ model() {
+ return { id: 2 };
+ },
+ serialize(model) {
+ return { post_id: model.id };
+ },
+ })
+ );
+
+ return this.visit('/')
+ .then(() => this.click('#default-post-link'))
+ .then(() => this.click('#home-link'))
+ .then(() => this.click('#current-post-link'))
+ .then(() => this.click('#home-link'));
+ }
+
+ [`@test the component's active property respects changing parent route context`](
+ assert
+ ) {
+ this.router.map(function() {
+ this.route('things', { path: '/things/:name' }, function() {
+ this.route('other');
+ });
+ });
+
+ this.addTemplate(
+ 'application',
+ `
+ OMG
+ LOL
+ `
+ );
+
+ return this.visit('/things/omg')
+ .then(() => {
+ shouldBeActive(assert, this.$('#omg-link'));
+ shouldNotBeActive(assert, this.$('#lol-link'));
+
+ return this.visit('/things/omg/other');
+ })
+ .then(() => {
+ shouldBeActive(assert, this.$('#omg-link'));
+ shouldNotBeActive(assert, this.$('#lol-link'));
+ });
+ }
+
+ [`@test the component populates href with default query param values even without query-params object`](
+ assert
+ ) {
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ queryParams: ['foo'],
+ foo: '123',
+ })
+ );
+
+ this.addTemplate('index', `Index`);
+
+ return this.visit('/').then(() => {
+ assert.equal(this.$('#the-link').attr('href'), '/', 'link has right href');
+ });
+ }
+
+ [`@test the component populates href with default query param values with empty query-params object`](
+ assert
+ ) {
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ queryParams: ['foo'],
+ foo: '123',
+ })
+ );
+
+ this.addTemplate(
+ 'index',
+ `Index`
+ );
+
+ return this.visit('/').then(() => {
+ assert.equal(this.$('#the-link').attr('href'), '/', 'link has right href');
+ });
+ }
+
+ [`@test the component with only query-params and a block updates when route changes`](
+ assert
+ ) {
+ this.router.map(function() {
+ this.route('about');
+ });
+
+ this.add(
+ 'controller:application',
+ Controller.extend({
+ queryParams: ['foo', 'bar'],
+ foo: '123',
+ bar: 'yes',
+ })
+ );
+
+ this.addTemplate(
+ 'application',
+ `Index`
+ );
+
+ return this.visit('/')
+ .then(() => {
+ assert.equal(
+ this.$('#the-link').attr('href'),
+ '/?bar=NAW&foo=456',
+ 'link has right href'
+ );
+
+ return this.visit('/about');
+ })
+ .then(() => {
+ assert.equal(
+ this.$('#the-link').attr('href'),
+ '/about?bar=NAW&foo=456',
+ 'link has right href'
+ );
+ });
+ }
+
+ ['@test [GH#17018] passing model to with `hash` helper works']() {
+ this.router.map(function() {
+ this.route('post', { path: '/posts/:post_id' });
+ });
+
+ this.add(
+ 'route:index',
+ Route.extend({
+ model() {
+ return RSVP.hash({
+ user: { name: 'Papa Smurf' },
+ });
+ },
+ })
+ );
+
+ this.addTemplate(
+ 'index',
+ `Post`
+ );
+
+ this.addTemplate('post', 'Post: {{this.model.user.name}}');
+
+ return this.visit('/')
+ .then(() => {
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'a',
+ attrs: { href: '/posts/someId' },
+ content: 'Post',
+ });
+
+ return this.click('a');
+ })
+ .then(() => {
+ this.assertText('Post: Papa Smurf');
+ });
+ }
+
+ [`@test The component can use dynamic params`](assert) {
+ this.router.map(function() {
+ this.route('foo', { path: 'foo/:some/:thing' });
+ this.route('bar', { path: 'bar/:some/:thing/:else' });
+ });
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ init() {
+ this._super(...arguments);
+ this.dynamicLinkParams = ['foo', 'one', 'two'];
+ },
+ })
+ );
+
+ this.addTemplate(
+ 'index',
+ `
+ Home
+ Dynamic
+ `
+ );
+
+ return this.visit('/').then(() => {
+ let link = this.$('#dynamic-link');
+
+ assert.equal(link.attr('href'), '/foo/one/two');
+
+ let controller = this.applicationInstance.lookup('controller:index');
+ runTask(() => {
+ controller.set('dynamicLinkParams', ['bar', 'one', 'two', 'three']);
+ });
+
+ assert.equal(link.attr('href'), '/bar/one/two/three');
+ });
+ }
+
+ [`@test GJ: to a parent root model hook which performs a 'transitionTo' has correct active class #13256`](
+ assert
+ ) {
+ assert.expect(1);
+
+ this.router.map(function() {
+ this.route('parent', function() {
+ this.route('child');
+ });
+ });
+
+ this.add(
+ 'route:parent',
+ Route.extend({
+ afterModel() {
+ this.transitionTo('parent.child');
+ },
+ })
+ );
+
+ this.addTemplate(
+ 'application',
+ `Parent`
+ );
+
+ return this.visit('/')
+ .then(() => {
+ return this.click('#parent-link');
+ })
+ .then(() => {
+ shouldBeActive(assert, this.$('#parent-link'));
+ });
+ }
+ }
+ );
+
+ moduleFor(
+ 'The component - loading states and warnings',
+ class extends ApplicationTestCase {
+ [`@test with null/undefined dynamic parameters are put in a loading state`](
+ assert
+ ) {
+ assert.expect(19);
+ let warningMessage =
+ '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.';
+
+ this.router.map(function() {
+ this.route('thing', { path: '/thing/:thing_id' });
+ this.route('about');
+ });
+
+ this.addTemplate(
+ 'index',
+ `
+
+ string
+
+
+ string
+
+ `
+ );
+
+ this.add(
+ 'controller:index',
+ Controller.extend({
+ destinationRoute: null,
+ routeContext: null,
+ loadingClass: 'i-am-loading',
+ })
+ );
+
+ this.add(
+ 'route:about',
+ Route.extend({
+ activate() {
+ assert.ok(true, 'About was entered');
+ },
+ })
+ );
+
+ function assertLinkStatus(link, url) {
+ if (url) {
+ assert.equal(normalizeUrl(link.attr('href')), url, 'loaded link-to has expected href');
+ assert.ok(!link.hasClass('i-am-loading'), 'loaded linkComponent has no loadingClass');
+ } else {
+ assert.equal(normalizeUrl(link.attr('href')), '#', "unloaded link-to has href='#'");
+ assert.ok(link.hasClass('i-am-loading'), 'loading linkComponent has loadingClass');
+ }
+ }
+
+ let contextLink, staticLink, controller;
+
+ return this.visit('/')
+ .then(() => {
+ contextLink = this.$('#context-link');
+ staticLink = this.$('#static-link');
+ controller = this.applicationInstance.lookup('controller:index');
+
+ assertLinkStatus(contextLink);
+ assertLinkStatus(staticLink);
+
+ return expectWarning(() => {
+ return this.click(contextLink[0]);
+ }, warningMessage);
+ })
+ .then(() => {
+ // Set the destinationRoute (context is still null).
+ runTask(() => controller.set('destinationRoute', 'thing'));
+ assertLinkStatus(contextLink);
+
+ // Set the routeContext to an id
+ runTask(() => controller.set('routeContext', '456'));
+ assertLinkStatus(contextLink, '/thing/456');
+
+ // Test that 0 isn't interpreted as falsy.
+ runTask(() => controller.set('routeContext', 0));
+ assertLinkStatus(contextLink, '/thing/0');
+
+ // Set the routeContext to an object
+ runTask(() => {
+ controller.set('routeContext', { id: 123 });
+ });
+ assertLinkStatus(contextLink, '/thing/123');
+
+ // Set the destinationRoute back to null.
+ runTask(() => controller.set('destinationRoute', null));
+ assertLinkStatus(contextLink);
+
+ return expectWarning(() => {
+ return this.click(staticLink[0]);
+ }, warningMessage);
+ })
+ .then(() => {
+ runTask(() => controller.set('secondRoute', 'about'));
+ assertLinkStatus(staticLink, '/about');
+
+ // Click the now-active link
+ return this.click(staticLink[0]);
+ });
+ }
+ }
+ );
+
+ function assertNav(options, callback, assert) {
+ let nav = false;
+
+ function check(event) {
+ assert.equal(
+ event.defaultPrevented,
+ options.prevented,
+ `expected defaultPrevented=${options.prevented}`
+ );
+ nav = true;
+ event.preventDefault();
+ }
+
+ try {
+ document.addEventListener('click', check);
+ callback();
+ } finally {
+ document.removeEventListener('click', check);
+ assert.ok(nav, 'Expected a link to be clicked');
+ }
+ }
+}
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js
similarity index 96%
rename from packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-test.js
rename to packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js
index f6ae8f5e538..218d27aee29 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js
@@ -1,4 +1,5 @@
import { moduleFor, ApplicationTestCase, runLoopSettled, runTask } from 'internal-test-helpers';
+import { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features';
import Controller, { inject as injectController } from '@ember/controller';
import { A as emberA, RSVP } from '@ember/-internals/runtime';
import { alias } from '@ember/-internals/metal';
@@ -560,7 +561,7 @@ if (EMBER_IMPROVED_INSTRUMENTATION) {
return this.click('#about-link');
}
- ['@test The {{link-to}} helper interaction event includes the transition in the after hook'](
+ ['@test The {{link-to}} component interaction event includes the transition in the after hook'](
assert
) {
assert.expect(1);
@@ -585,7 +586,7 @@ if (EMBER_IMPROVED_INSTRUMENTATION) {
moduleFor(
'The {{link-to}} component - nested routes and link-to arguments',
class extends ApplicationTestCase {
- ['@test The {{link-to}} helper supports leaving off .index for nested routes'](assert) {
+ ['@test The {{link-to}} component supports leaving off .index for nested routes'](assert) {
this.router.map(function() {
this.route('about', function() {
this.route('item');
@@ -1001,7 +1002,7 @@ moduleFor(
});
}
- [`@test The {{link-to}} helper binds some anchor html tag common attributes`](assert) {
+ [`@test The {{link-to}} component binds some anchor html tag common attributes`](assert) {
this.addTemplate(
'index',
`
@@ -1020,7 +1021,7 @@ moduleFor(
});
}
- [`@test The {{link-to}} helper supports 'target' attribute`](assert) {
+ [`@test The {{link-to}} component supports 'target' attribute`](assert) {
this.addTemplate(
'index',
`
@@ -1035,7 +1036,7 @@ moduleFor(
});
}
- [`@test The {{link-to}} helper supports 'target' attribute specified as a bound param`](
+ [`@test The {{link-to}} component supports 'target' attribute specified as a bound param`](
assert
) {
this.addTemplate(
@@ -1056,7 +1057,7 @@ moduleFor(
});
}
- [`@test the {{link-to}} helper calls preventDefault`](assert) {
+ [`@test the {{link-to}} component calls preventDefault`](assert) {
this.router.map(function() {
this.route('about');
});
@@ -1068,7 +1069,7 @@ moduleFor(
});
}
- [`@test the {{link-to}} helper does not call preventDefault if 'preventDefault=false' is passed as an option`](
+ [`@test the {{link-to}} component does not call preventDefault if 'preventDefault=false' is passed as an option`](
assert
) {
this.router.map(function() {
@@ -1085,7 +1086,7 @@ moduleFor(
});
}
- [`@test the {{link-to}} helper does not call preventDefault if 'preventDefault=boundFalseyThing' is passed as an option`](
+ [`@test the {{link-to}} component does not call preventDefault if 'preventDefault=boundFalseyThing' is passed as an option`](
assert
) {
this.router.map(function() {
@@ -1109,7 +1110,7 @@ moduleFor(
});
}
- [`@test The {{link-to}} helper does not call preventDefault if 'target' attribute is provided`](
+ [`@test The {{link-to}} component does not call preventDefault if 'target' attribute is provided`](
assert
) {
this.addTemplate(
@@ -1125,7 +1126,7 @@ moduleFor(
});
}
- [`@test The {{link-to}} helper should preventDefault when 'target = _self'`](assert) {
+ [`@test The {{link-to}} component should preventDefault when 'target = _self'`](assert) {
this.addTemplate(
'index',
`
@@ -1139,7 +1140,7 @@ moduleFor(
});
}
- [`@test The {{link-to}} helper should not transition if target is not equal to _self or empty`](
+ [`@test The {{link-to}} component should not transition if target is not equal to _self or empty`](
assert
) {
this.addTemplate(
@@ -1171,7 +1172,7 @@ moduleFor(
});
}
- [`@test The {{link-to}} helper accepts string/numeric arguments`](assert) {
+ [`@test The {{link-to}} component accepts string/numeric arguments`](assert) {
this.router.map(function() {
this.route('filter', { path: '/filters/:filter' });
this.route('post', { path: '/post/:post_id' });
@@ -1282,7 +1283,7 @@ moduleFor(
});
}
- [`@test The {{link-to}} helper refreshes href element when one of params changes`](assert) {
+ [`@test The {{link-to}} component refreshes href element when one of params changes`](assert) {
this.router.map(function() {
this.route('post', { path: '/posts/:post_id' });
});
@@ -1322,7 +1323,7 @@ moduleFor(
});
}
- [`@test The {{link-to}} helper is active when a route is active`](assert) {
+ [`@test The {{link-to}} component is active when a route is active`](assert) {
this.router.map(function() {
this.route('about', function() {
this.route('item');
@@ -1353,7 +1354,7 @@ moduleFor(
});
}
- [`@test The {{link-to}} helper works in an #each'd array of string route names`](assert) {
+ [`@test The {{link-to}} component works in an #each'd array of string route names`](assert) {
this.router.map(function() {
this.route('foo');
this.route('bar');
@@ -1412,7 +1413,7 @@ moduleFor(
});
}
- [`@test The non-block form {{link-to}} helper moves into the named route`](assert) {
+ [`@test The non-block form {{link-to}} component moves into the named route`](assert) {
assert.expect(3);
this.router.map(function() {
this.route('contact');
@@ -1454,7 +1455,7 @@ moduleFor(
});
}
- [`@test The non-block form {{link-to}} helper updates the link text when it is a binding`](
+ [`@test The non-block form {{link-to}} component updates the link text when it is a binding`](
assert
) {
assert.expect(8);
@@ -1538,7 +1539,7 @@ moduleFor(
});
}
- [`@test The non-block form {{link-to}} helper moves into the named route with context`](
+ [`@test The non-block form {{link-to}} component moves into the named route with context`](
assert
) {
assert.expect(5);
@@ -1971,14 +1972,21 @@ moduleFor(
);
moduleFor(
- 'The {{link-to}} helper - loading states and warnings',
+ 'The {{link-to}} component - loading states and warnings',
class extends ApplicationTestCase {
[`@test {{link-to}} with null/undefined dynamic parameters are put in a loading state`](
assert
) {
assert.expect(19);
- let warningMessage =
- 'This link-to is in an inactive loading state because at least one of its parameters presently has a null/undefined value, or the provided route name is invalid.';
+ let warningMessage;
+
+ if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) {
+ warningMessage =
+ '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.';
+ } else {
+ warningMessage =
+ 'This link-to is in an inactive loading state because at least one of its parameters presently has a null/undefined value, or the provided route name is invalid.';
+ }
this.router.map(function() {
this.route('thing', { path: '/thing/:thing_id' });
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/transitioning-classes-angle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/transitioning-classes-angle-test.js
new file mode 100644
index 00000000000..dc1306c5f67
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/transitioning-classes-angle-test.js
@@ -0,0 +1,347 @@
+/* eslint-disable no-inner-declarations */
+// ^^^ remove after unflagging EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS
+import { RSVP } from '@ember/-internals/runtime';
+import { Route } from '@ember/-internals/routing';
+import { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features';
+import { moduleFor, ApplicationTestCase, runTask } from 'internal-test-helpers';
+
+if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) {
+ function assertHasClass(assert, selector, label) {
+ let testLabel = `${selector.attr('id')} should have class ${label}`;
+
+ assert.equal(selector.hasClass(label), true, testLabel);
+ }
+
+ function assertHasNoClass(assert, selector, label) {
+ let testLabel = `${selector.attr('id')} should not have class ${label}`;
+
+ assert.equal(selector.hasClass(label), false, testLabel);
+ }
+
+ moduleFor(
+ ' component: .transitioning-in .transitioning-out CSS classes',
+ class extends ApplicationTestCase {
+ constructor() {
+ super();
+
+ this.aboutDefer = RSVP.defer();
+ this.otherDefer = RSVP.defer();
+ this.newsDefer = RSVP.defer();
+ let _this = this;
+
+ this.router.map(function() {
+ this.route('about');
+ this.route('other');
+ this.route('news');
+ });
+
+ this.add(
+ 'route:about',
+ Route.extend({
+ model() {
+ return _this.aboutDefer.promise;
+ },
+ })
+ );
+
+ this.add(
+ 'route:other',
+ Route.extend({
+ model() {
+ return _this.otherDefer.promise;
+ },
+ })
+ );
+
+ this.add(
+ 'route:news',
+ Route.extend({
+ model() {
+ return _this.newsDefer.promise;
+ },
+ })
+ );
+
+ this.addTemplate(
+ 'application',
+ `
+ {{outlet}}
+ Index
+ About
+ Other
+ News
+ `
+ );
+ }
+
+ beforeEach() {
+ return this.visit('/');
+ }
+
+ afterEach() {
+ super.afterEach();
+ this.aboutDefer = null;
+ this.otherDefer = null;
+ this.newsDefer = null;
+ }
+
+ ['@test while a transition is underway'](assert) {
+ let $index = this.$('#index-link');
+ let $about = this.$('#about-link');
+ let $other = this.$('#other-link');
+
+ $about.click();
+
+ assertHasClass(assert, $index, 'active');
+ assertHasNoClass(assert, $about, 'active');
+ assertHasNoClass(assert, $other, 'active');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-in');
+ assertHasClass(assert, $about, 'ember-transitioning-in');
+ assertHasNoClass(assert, $other, 'ember-transitioning-in');
+
+ assertHasClass(assert, $index, 'ember-transitioning-out');
+ assertHasNoClass(assert, $about, 'ember-transitioning-out');
+ assertHasNoClass(assert, $other, 'ember-transitioning-out');
+
+ runTask(() => this.aboutDefer.resolve());
+
+ assertHasNoClass(assert, $index, 'active');
+ assertHasClass(assert, $about, 'active');
+ assertHasNoClass(assert, $other, 'active');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-in');
+ assertHasNoClass(assert, $about, 'ember-transitioning-in');
+ assertHasNoClass(assert, $other, 'ember-transitioning-in');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-out');
+ assertHasNoClass(assert, $about, 'ember-transitioning-out');
+ assertHasNoClass(assert, $other, 'ember-transitioning-out');
+ }
+
+ ['@test while a transition is underway with activeClass is false'](assert) {
+ let $index = this.$('#index-link');
+ let $news = this.$('#news-link');
+ let $other = this.$('#other-link');
+
+ $news.click();
+
+ assertHasClass(assert, $index, 'active');
+ assertHasNoClass(assert, $news, 'active');
+ assertHasNoClass(assert, $other, 'active');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-in');
+ assertHasClass(assert, $news, 'ember-transitioning-in');
+ assertHasNoClass(assert, $other, 'ember-transitioning-in');
+
+ assertHasClass(assert, $index, 'ember-transitioning-out');
+ assertHasNoClass(assert, $news, 'ember-transitioning-out');
+ assertHasNoClass(assert, $other, 'ember-transitioning-out');
+
+ runTask(() => this.newsDefer.resolve());
+
+ assertHasNoClass(assert, $index, 'active');
+ assertHasNoClass(assert, $news, 'active');
+ assertHasNoClass(assert, $other, 'active');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-in');
+ assertHasNoClass(assert, $news, 'ember-transitioning-in');
+ assertHasNoClass(assert, $other, 'ember-transitioning-in');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-out');
+ assertHasNoClass(assert, $news, 'ember-transitioning-out');
+ assertHasNoClass(assert, $other, 'ember-transitioning-out');
+ }
+ }
+ );
+
+ moduleFor(
+ ` component: .transitioning-in .transitioning-out CSS classes - nested link-to's`,
+ class extends ApplicationTestCase {
+ constructor() {
+ super();
+ this.aboutDefer = RSVP.defer();
+ this.otherDefer = RSVP.defer();
+ let _this = this;
+
+ this.router.map(function() {
+ this.route('parent-route', function() {
+ this.route('about');
+ this.route('other');
+ });
+ });
+ this.add(
+ 'route:parent-route.about',
+ Route.extend({
+ model() {
+ return _this.aboutDefer.promise;
+ },
+ })
+ );
+
+ this.add(
+ 'route:parent-route.other',
+ Route.extend({
+ model() {
+ return _this.otherDefer.promise;
+ },
+ })
+ );
+
+ this.addTemplate(
+ 'application',
+ `
+ {{outlet}}
+
+ Index
+
+
+ About
+
+
+ Other
+
+ `
+ );
+ }
+
+ beforeEach() {
+ return this.visit('/');
+ }
+
+ resolveAbout() {
+ return runTask(() => {
+ this.aboutDefer.resolve();
+ this.aboutDefer = RSVP.defer();
+ });
+ }
+
+ resolveOther() {
+ return runTask(() => {
+ this.otherDefer.resolve();
+ this.otherDefer = RSVP.defer();
+ });
+ }
+
+ teardown() {
+ super.teardown();
+ this.aboutDefer = null;
+ this.otherDefer = null;
+ }
+
+ [`@test while a transition is underway with nested link-to's`](assert) {
+ // TODO undo changes to this test but currently this test navigates away if navigation
+ // outlet is not stable and the second $about.click() is triggered.
+ let $about = this.$('#about-link');
+
+ $about.click();
+
+ let $index = this.$('#index-link');
+ $about = this.$('#about-link');
+ let $other = this.$('#other-link');
+
+ assertHasClass(assert, $index, 'active');
+ assertHasNoClass(assert, $about, 'active');
+ assertHasNoClass(assert, $about, 'active');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-in');
+ assertHasClass(assert, $about, 'ember-transitioning-in');
+ assertHasNoClass(assert, $other, 'ember-transitioning-in');
+
+ assertHasClass(assert, $index, 'ember-transitioning-out');
+ assertHasNoClass(assert, $about, 'ember-transitioning-out');
+ assertHasNoClass(assert, $other, 'ember-transitioning-out');
+
+ this.resolveAbout();
+
+ $index = this.$('#index-link');
+ $about = this.$('#about-link');
+ $other = this.$('#other-link');
+
+ assertHasNoClass(assert, $index, 'active');
+ assertHasClass(assert, $about, 'active');
+ assertHasNoClass(assert, $other, 'active');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-in');
+ assertHasNoClass(assert, $about, 'ember-transitioning-in');
+ assertHasNoClass(assert, $other, 'ember-transitioning-in');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-out');
+ assertHasNoClass(assert, $about, 'ember-transitioning-out');
+ assertHasNoClass(assert, $other, 'ember-transitioning-out');
+
+ $other.click();
+
+ $index = this.$('#index-link');
+ $about = this.$('#about-link');
+ $other = this.$('#other-link');
+
+ assertHasNoClass(assert, $index, 'active');
+ assertHasClass(assert, $about, 'active');
+ assertHasNoClass(assert, $other, 'active');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-in');
+ assertHasNoClass(assert, $about, 'ember-transitioning-in');
+ assertHasClass(assert, $other, 'ember-transitioning-in');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-out');
+ assertHasClass(assert, $about, 'ember-transitioning-out');
+ assertHasNoClass(assert, $other, 'ember-transitioning-out');
+
+ this.resolveOther();
+
+ $index = this.$('#index-link');
+ $about = this.$('#about-link');
+ $other = this.$('#other-link');
+
+ assertHasNoClass(assert, $index, 'active');
+ assertHasNoClass(assert, $about, 'active');
+ assertHasClass(assert, $other, 'active');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-in');
+ assertHasNoClass(assert, $about, 'ember-transitioning-in');
+ assertHasNoClass(assert, $other, 'ember-transitioning-in');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-out');
+ assertHasNoClass(assert, $about, 'ember-transitioning-out');
+ assertHasNoClass(assert, $other, 'ember-transitioning-out');
+
+ $about.click();
+
+ $index = this.$('#index-link');
+ $about = this.$('#about-link');
+ $other = this.$('#other-link');
+
+ assertHasNoClass(assert, $index, 'active');
+ assertHasNoClass(assert, $about, 'active');
+ assertHasClass(assert, $other, 'active');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-in');
+ assertHasClass(assert, $about, 'ember-transitioning-in');
+ assertHasNoClass(assert, $other, 'ember-transitioning-in');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-out');
+ assertHasNoClass(assert, $about, 'ember-transitioning-out');
+ assertHasClass(assert, $other, 'ember-transitioning-out');
+
+ this.resolveAbout();
+
+ $index = this.$('#index-link');
+ $about = this.$('#about-link');
+ $other = this.$('#other-link');
+
+ assertHasNoClass(assert, $index, 'active');
+ assertHasClass(assert, $about, 'active');
+ assertHasNoClass(assert, $other, 'active');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-in');
+ assertHasNoClass(assert, $about, 'ember-transitioning-in');
+ assertHasNoClass(assert, $other, 'ember-transitioning-in');
+
+ assertHasNoClass(assert, $index, 'ember-transitioning-out');
+ assertHasNoClass(assert, $about, 'ember-transitioning-out');
+ assertHasNoClass(assert, $other, 'ember-transitioning-out');
+ }
+ }
+ );
+}
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/transitioning-classes-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/transitioning-classes-curly-test.js
similarity index 100%
rename from packages/@ember/-internals/glimmer/tests/integration/components/link-to/transitioning-classes-test.js
rename to packages/@ember/-internals/glimmer/tests/integration/components/link-to/transitioning-classes-curly-test.js
diff --git a/tests/docs/expected.js b/tests/docs/expected.js
index ffe1aa8bdc0..0910470de8a 100644
--- a/tests/docs/expected.js
+++ b/tests/docs/expected.js
@@ -354,6 +354,7 @@ module.exports = {
'mixin',
'model',
'modelFor',
+ 'models',
'mount',
'mut',
'name',
@@ -406,6 +407,7 @@ module.exports = {
'pushObjects',
'pushState',
'query-params',
+ 'query',
'queryParams',
'queryParamsDidChange',
'queues',
@@ -477,6 +479,7 @@ module.exports = {
'reverseObjects',
'rootElement',
'rootURL',
+ 'route',
'routeDidChange',
'routeName',
'routeWillChange',