From f0e2b5bc44e770322a4c76ad1dd8e477a32d1103 Mon Sep 17 00:00:00 2001 From: Nathan Hammond Date: Tue, 25 Aug 2015 00:40:49 -0700 Subject: [PATCH] [BUGFIX beta] Further cleanup of the link-to component. - Make it easier to extend by pulling more functionality into scope. - Removed unnecessarily imperative code. - Combine the loops for the params model deprecation search. - Make what's happening with params more explicit with comments. - Name anonymous functions. - Reword and polish documentation. - `var` => `let` --- .../lib/components/link-to.js | 278 +++++++++--------- 1 file changed, 136 insertions(+), 142 deletions(-) diff --git a/packages/ember-routing-views/lib/components/link-to.js b/packages/ember-routing-views/lib/components/link-to.js index de91c0ff68f..9fc439968ae 100644 --- a/packages/ember-routing-views/lib/components/link-to.js +++ b/packages/ember-routing-views/lib/components/link-to.js @@ -4,7 +4,7 @@ */ /** - The `{{link-to}}` helper renders a link to the supplied + 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 @@ -16,9 +16,9 @@ {{/link-to}} ``` - You can also use an inline form of `{{link-to}}` helper by + You can also use an inline form of `{{link-to}}` component by passing the link text as the first argument - to the helper: + to the component: ```handlebars {{link-to 'Great Hamster Photos' 'photoGallery'}} @@ -52,9 +52,10 @@ To override this option for your entire application, see "Overriding Application-wide Defaults". - ### Disabling the `link-to` helper + ### Disabling the `link-to` component By default `{{link-to}}` is enabled. - any passed value to `disabled` helper property will disable the `link-to` helper. + any passed value to the `disabled` component property will disable + the `link-to` component. static use: the `disabled` option: @@ -73,7 +74,7 @@ ``` any passed value to `disabled` will disable it except `undefined`. - to ensure that only `true` disable the `link-to` helper you can + to ensure that only `true` disable the `link-to` component you can override the global behaviour of `Ember.LinkComponent`. ```javascript @@ -245,7 +246,7 @@ ### Allowing Default Action - By default the `{{link-to}}` helper prevents the default browser 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). @@ -260,8 +261,8 @@ ``` ### Overriding attributes - You can override any given property of the Ember.LinkComponent - that is generated by the `{{link-to}}` helper by passing + You can override any given property of the `Ember.LinkComponent` + that is generated by the `{{link-to}}` component by passing key/value pairs, like so: ```handlebars @@ -275,9 +276,9 @@ check out inherited properties of `LinkComponent`. ### Overriding Application-wide Defaults - ``{{link-to}}`` creates an instance of Ember.LinkComponent + ``{{link-to}}`` creates an instance of `Ember.LinkComponent` for rendering. To override options for your entire - application, reopen Ember.LinkComponent and supply the + application, reopen `Ember.LinkComponent` and supply the desired values: ``` javascript @@ -314,7 +315,6 @@ import Ember from 'ember-metal/core'; import { assert, deprecate } from 'ember-metal/debug'; import { get } from 'ember-metal/property_get'; -import { set } from 'ember-metal/property_set'; import { computed } from 'ember-metal/computed'; import { deprecatingAlias } from 'ember-metal/computed_macros'; import { isSimpleClick } from 'ember-views/system/utils'; @@ -333,17 +333,17 @@ linkToTemplate.meta.revision = 'Ember@VERSION_STRING_PLACEHOLDER'; transition of the application's instance of `Ember.Router` to a supplied route by name. - Instances of `LinkComponent` will most likely be created through - the `link-to` Handlebars helper, but properties of this class - can be overridden to customize application-wide behavior. + `Ember.LinkComponent` components are invoked with {{#link-to}}. Properties + of this class can be overridden with `reopen` to customize application-wide + behavior. @class LinkComponent @namespace Ember @extends Ember.Component - @see {Handlebars.helpers.link-to} + @see {Ember.Templates.helpers.link-to} @private **/ -var LinkComponent = EmberComponent.extend({ +let LinkComponent = EmberComponent.extend({ defaultLayout: linkToTemplate, tagName: 'a', @@ -356,7 +356,7 @@ var LinkComponent = EmberComponent.extend({ currentWhen: deprecatingAlias('current-when', { id: 'ember-routing-view.deprecated-current-when', until: '3.0.0' }), /** - Used to determine when this LinkComponent is active. + Used to determine when this `LinkComponent` is active. @property currentWhen @public @@ -446,7 +446,7 @@ var LinkComponent = EmberComponent.extend({ replace: false, /** - By default the `{{link-to}}` helper will bind to the `href` and + 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. @@ -458,8 +458,8 @@ var LinkComponent = EmberComponent.extend({ attributeBindings: ['href', 'title', 'rel', 'tabindex', 'target'], /** - By default the `{{link-to}}` helper will bind to the `active`, `loading`, and - `disabled` classes. It is discouraged to override these directly. + 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 @@ -469,7 +469,7 @@ var LinkComponent = EmberComponent.extend({ classNameBindings: ['active', 'loading', 'disabled', 'transitioningIn', 'transitioningOut'], /** - By default the `{{link-to}}` helper responds to the `click` event. You + 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. @@ -497,7 +497,7 @@ var LinkComponent = EmberComponent.extend({ */ /** - An overridable method called when LinkComponent objects are instantiated. + An overridable method called when `LinkComponent` objects are instantiated. Example: @@ -523,7 +523,7 @@ var LinkComponent = EmberComponent.extend({ this._super(...arguments); // Map desired event name to invoke function - var eventName = get(this, 'eventName'); + let eventName = get(this, 'eventName'); this.on(eventName, this, this._invoke); }, @@ -548,6 +548,27 @@ var LinkComponent = EmberComponent.extend({ } }), + _computeActive(routerState) { + if (get(this, 'loading')) { return false; } + + let routing = get(this, '_routing'); + let models = get(this, 'models'); + let resolvedQueryParams = get(this, 'resolvedQueryParams'); + + let currentWhen = get(this, 'current-when'); + let isCurrentWhenSpecified = !!currentWhen; + currentWhen = currentWhen || get(this, 'qualifiedRouteName'); + currentWhen = currentWhen.split(' '); + + for (let i = 0, len = currentWhen.length; i < len; i++) { + if (routing.isActiveForRoute(models, resolvedQueryParams, currentWhen[i], routerState, isCurrentWhenSpecified)) { + return get(this, 'activeClass'); + } + } + + return false; + }, + /** Accessed as a classname binding to apply the `LinkComponent`'s `activeClass` CSS `class` to the element when the link is active. @@ -562,30 +583,30 @@ var LinkComponent = EmberComponent.extend({ @property active @private */ - active: computed('attrs.params', '_routing.currentState', function computeLinkComponentActive() { - var currentState = get(this, '_routing.currentState'); + active: computed('attrs.params', '_routing.currentState', function computeLinkToComponentActive() { + let currentState = get(this, '_routing.currentState'); if (!currentState) { return false; } - return computeActive(this, currentState); + return this._computeActive(currentState); }), - willBeActive: computed('_routing.targetState', function() { - var routing = get(this, '_routing'); - var targetState = get(routing, 'targetState'); + willBeActive: computed('_routing.targetState', function computeLinkToComponentWillBeActive() { + let routing = get(this, '_routing'); + let targetState = get(routing, 'targetState'); if (get(routing, 'currentState') === targetState) { return; } - return !!computeActive(this, targetState); + return !!this._computeActive(targetState); }), - transitioningIn: computed('active', 'willBeActive', function() { - var willBeActive = get(this, 'willBeActive'); + transitioningIn: computed('active', 'willBeActive', function computeLinkToComponentTransitioningIn() { + let willBeActive = get(this, 'willBeActive'); if (typeof willBeActive === 'undefined') { return false; } return !get(this, 'active') && willBeActive && 'ember-transitioning-in'; }), - transitioningOut: computed('active', 'willBeActive', function() { - var willBeActive = get(this, 'willBeActive'); + transitioningOut: computed('active', 'willBeActive', function computeLinkToComponentTransitioningOut() { + let willBeActive = get(this, 'willBeActive'); if (typeof willBeActive === 'undefined') { return false; } return get(this, 'active') && !willBeActive && 'ember-transitioning-out'; @@ -603,7 +624,7 @@ var LinkComponent = EmberComponent.extend({ if (!isSimpleClick(event)) { return true; } if (this.attrs.preventDefault !== false) { - var targetAttribute = this.attrs.target; + let targetAttribute = this.attrs.target; if (!targetAttribute || targetAttribute === '_self') { event.preventDefault(); } @@ -618,22 +639,50 @@ var LinkComponent = EmberComponent.extend({ return false; } - var targetAttribute2 = this.attrs.target; + let targetAttribute2 = this.attrs.target; if (targetAttribute2 && targetAttribute2 !== '_self') { return false; } - var routing = get(this, '_routing'); - var targetRouteName = this._computeRouteNameWithQueryParams(get(this, 'targetRouteName')); - var models = get(this, 'models'); - var queryParamValues = get(this, 'queryParams.values'); - var shouldReplace = get(this, 'attrs.replace'); + let routing = get(this, '_routing'); + let qualifiedRouteName = get(this, 'qualifiedRouteName'); + let models = get(this, 'models'); + let queryParamValues = get(this, 'queryParams.values'); + let shouldReplace = get(this, 'attrs.replace'); - routing.transitionTo(targetRouteName, models, queryParamValues, shouldReplace); + routing.transitionTo(qualifiedRouteName, models, queryParamValues, shouldReplace); }, queryParams: null, + qualifiedRouteName: computed('targetRouteName', '_routing.currentState', function computeLinkToComponentQualifiedRouteName() { + let params = this.attrs.params.slice(); + let lastParam = params[params.length - 1]; + if (lastParam && lastParam.isQueryParams) { + params.pop(); + } + let onlyQueryParamsSupplied = (this[HAS_BLOCK] ? params.length === 0 : params.length === 1); + if (onlyQueryParamsSupplied) { + return get(this, '_routing.currentRouteName'); + } + return get(this, 'targetRouteName'); + }), + + resolvedQueryParams: computed('queryParams', function computeLinkToComponentResolvedQueryParams() { + let resolvedQueryParams = {}; + let queryParams = get(this, 'queryParams'); + + if (!queryParams) { return resolvedQueryParams; } + + let values = queryParams.values; + for (let key in values) { + if (!values.hasOwnProperty(key)) { continue; } + resolvedQueryParams[key] = values[key]; + } + + return resolvedQueryParams; + }), + /** Sets the element's `href` attribute to the url for the `LinkComponent`'s targeted route. @@ -644,45 +693,36 @@ var LinkComponent = EmberComponent.extend({ @property href @private */ - href: computed('models', 'targetRouteName', '_routing.currentState', function computeLinkComponentHref() { + href: computed('models', 'qualifiedRouteName', function computeLinkToComponentHref() { if (get(this, 'tagName') !== 'a') { return; } - var targetRouteName = get(this, 'targetRouteName'); - var models = get(this, 'models'); + let qualifiedRouteName = get(this, 'qualifiedRouteName'); + let models = get(this, 'models'); if (get(this, 'loading')) { return get(this, 'loadingHref'); } - targetRouteName = this._computeRouteNameWithQueryParams(targetRouteName); - - var routing = get(this, '_routing'); - var queryParams = get(this, 'queryParams.values'); - return routing.generateURL(targetRouteName, models, queryParams); + let routing = get(this, '_routing'); + let queryParams = get(this, 'queryParams.values'); + return routing.generateURL(qualifiedRouteName, models, queryParams); }), - loading: computed('models', 'targetRouteName', function() { - var targetRouteName = get(this, 'targetRouteName'); - var models = get(this, 'models'); + loading: computed('_modelsAreLoaded', 'qualifiedRouteName', function computeLinkToComponentLoading() { + let qualifiedRouteName = get(this, 'qualifiedRouteName'); + let modelsAreLoaded = get(this, '_modelsAreLoaded'); - if (!modelsAreLoaded(models) || targetRouteName == null) { + if (!modelsAreLoaded || qualifiedRouteName == null) { return get(this, 'loadingClass'); } }), - _computeRouteNameWithQueryParams(route) { - var params = this.attrs.params.slice(); - var lastParam = params[params.length - 1]; - if (lastParam && lastParam.isQueryParams) { - params.pop(); - } - let onlyQueryParamsSupplied = (this[HAS_BLOCK] ? params.length === 0 : params.length === 1); - if (onlyQueryParamsSupplied) { - var appController = this.container.lookup('controller:application'); - if (appController) { - return get(appController, 'currentRouteName'); - } + _modelsAreLoaded: computed('models', function computeLinkToComponentModelsAreLoaded() { + let models = get(this, 'models'); + for (let i = 0, l = models.length; i < l; i++) { + if (models[i] == null) { return false; } } - return route; - }, + + return true; + }), /** The default href value to use while a link-to is loading. @@ -696,22 +736,14 @@ var LinkComponent = EmberComponent.extend({ loadingHref: '#', willRender() { - var queryParams; + let queryParams; - var attrs = this.attrs; + let attrs = this.attrs; // Do not mutate params in place - var params = attrs.params.slice(); - - assert('You must provide one or more parameters to the link-to helper.', params.length); + let params = attrs.params.slice(); - var lastParam = params[params.length - 1]; - - if (lastParam && lastParam.isQueryParams) { - queryParams = params.pop(); - } else { - queryParams = {}; - } + assert('You must provide one or more parameters to the link-to component.', params.length); if (attrs.disabledClass) { this.set('disabledClass', attrs.disabledClass); @@ -725,16 +757,34 @@ var LinkComponent = EmberComponent.extend({ this.set('disabled', attrs.disabledWhen); } + if (attrs.loadingClass) { + this.set('loadingClass', attrs.loadingClass); + } + + // Process the positional arguments, in order. + // 1. Inline link title comes first, if present. if (!this[HAS_BLOCK]) { this.set('linkTitle', params.shift()); } - if (attrs.loadingClass) { - set(this, 'loadingClass', attrs.loadingClass); + // 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 = {}; } + this.set('queryParams', queryParams); - for (let i = 0; i < params.length; i++) { - var value = params[i]; + // 4. Any remaining indices (excepting `targetRouteName` at 0) are `models`. + let models = []; + + for (let i = 1; i < params.length; i++) { + let value = params[i]; while (ControllerMixin.detect(value)) { deprecate( @@ -746,71 +796,15 @@ var LinkComponent = EmberComponent.extend({ value = value.get('model'); } - params[i] = value; + models.push(value); } - let targetRouteName; - let models = []; - targetRouteName = this._computeRouteNameWithQueryParams(params[0]); - - for (let i = 1; i < params.length; i++) { - models.push(params[i]); - } - - let resolvedQueryParams = getResolvedQueryParams(queryParams, targetRouteName); - - this.set('targetRouteName', targetRouteName); this.set('models', models); - this.set('queryParams', queryParams); - this.set('resolvedQueryParams', resolvedQueryParams); } }); LinkComponent.toString = function() { return 'LinkComponent'; }; -function computeActive(view, routerState) { - if (get(view, 'loading')) { return false; } - - var currentWhen = get(view, 'current-when'); - var isCurrentWhenSpecified = !!currentWhen; - currentWhen = currentWhen || get(view, 'targetRouteName'); - currentWhen = currentWhen.split(' '); - for (var i = 0, len = currentWhen.length; i < len; i++) { - if (isActiveForRoute(view, currentWhen[i], isCurrentWhenSpecified, routerState)) { - return get(view, 'activeClass'); - } - } - - return false; -} - -function modelsAreLoaded(models) { - for (var i = 0, l = models.length; i < l; i++) { - if (models[i] == null) { return false; } - } - - return true; -} - -function isActiveForRoute(view, routeName, isCurrentWhenSpecified, routerState) { - var service = get(view, '_routing'); - return service.isActiveForRoute(get(view, 'models'), get(view, 'resolvedQueryParams'), routeName, routerState, isCurrentWhenSpecified); -} - -function getResolvedQueryParams(queryParamsObject, targetRouteName) { - var resolvedQueryParams = {}; - - if (!queryParamsObject) { return resolvedQueryParams; } - - var values = queryParamsObject.values; - for (var key in values) { - if (!values.hasOwnProperty(key)) { continue; } - resolvedQueryParams[key] = values[key]; - } - - return resolvedQueryParams; -} - LinkComponent.reopenClass({ positionalParams: 'params' });