Skip to content

Commit

Permalink
{{linkTo}} bound contexts, loading class
Browse files Browse the repository at this point in the history
Merge of work between @drogus and @machty.

Replaces and Closes #2181
Replaces and Closes #2252

{{linkTo}} contexts are now bound. e.g.:

    {{linkTo 'foo' pathToObj}}

will generate a linkTo that updates its
href if the resolved value of `pathToObj`
changes.

Also, if ANY of the linkTo params yield
null/undefined, including the first route name
param (if it is unquoted), href will be '#',
a new `loadingClass` class will be applied to
the linkTo element, and clicking/invoking
the loading linkTo will no-op and produce
an Ember.Logger.warn message that the linkTo
isn't loaded yet.

Note: due to overlap in loading logic and the recently
added stringified linkTo route behavior, the loading
logic is hidden behind the ENV.HELPER_PARAM_LOOKUPS
flag for now.
  • Loading branch information
machty committed Jul 2, 2013
1 parent b314eb6 commit 3327586
Show file tree
Hide file tree
Showing 2 changed files with 243 additions and 19 deletions.
128 changes: 111 additions & 17 deletions packages/ember-routing/lib/helpers/link_to.js
Expand Up @@ -27,15 +27,12 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
return resolveParams(options.context, options.params, { types: types, data: data });
}

function args(linkView, router, route) {
var passedRouteName = route || get(linkView, 'namedRoute'), routeName;

routeName = fullRouteName(router, passedRouteName);

Ember.assert(fmt("The attempt to linkTo route '%@' failed. The router did not find '%@' in its possible routes: '%@'", [passedRouteName, passedRouteName, Ember.keys(router.router.recognizer.names).join("', '")]), router.hasRoute(routeName));

var ret = [ routeName ];
return ret.concat(resolvedPaths(linkView.parameters));
function createPath(path) {
var fullPath = 'paramsContext';
if(path !== '') {
fullPath += '.' + path;
}
return fullPath;
}

/**
Expand Down Expand Up @@ -75,6 +72,16 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
**/
activeClass: 'active',

/**
The CSS class to apply to `LinkView`'s element when its `loading`
property is `true`.
@property loadingClass
@type String
@default loading
**/
loadingClass: 'loading',

/**
The CSS class to apply to a `LinkView`'s element when its `disabled`
property is `true`.
Expand All @@ -95,7 +102,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
**/
replace: false,
attributeBindings: ['href', 'title'],
classNameBindings: ['active', 'disabled'],
classNameBindings: ['active', 'loading', 'disabled'],

/**
By default the `{{linkTo}}` helper responds to the `click` event. You
Expand Down Expand Up @@ -124,10 +131,39 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
**/

init: function() {
this._super();
this._super.apply(this, arguments);

// Map desired event name to invoke function
var eventName = get(this, 'eventName');
this.on(eventName, this, this._invoke);

var params = this.parameters.params,
length = params.length,
context = this.parameters.context,
self = this,
path, paths = Ember.A([]), i;

set(this, 'paramsContext', context);

for(i=0; i < length; i++) {
paths.pushObject(createPath(params[i]));
}

var observer = function(object, path) {
var notify = true, i;
for(i=0; i < paths.length; i++) {
if(!get(this, paths[i])) {
notify = false;
}
}
if(notify) {
this.notifyPropertyChange('routeArgs');
}
};

for(i=0; i < length; i++) {
this.registerObserver(this, paths[i], this, observer);
}
},

/**
Expand Down Expand Up @@ -167,7 +203,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
@property active
**/
active: Ember.computed(function() {
var router = this.get('router'),
var router = get(this, 'router'),
params = resolvedPaths(this.parameters),
currentWhen = this.currentWhen || get(this, 'namedRoute'),
currentWithIndex = currentWhen + '.index',
Expand All @@ -177,6 +213,21 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
if (isActive) { return get(this, 'activeClass'); }
}).property('namedRoute', 'router.url'),

loading: Ember.computed(function() {
if (!get(this, 'routeArgs')) { return get(this, 'loadingClass'); }
}).property('routeArgs'),

/**
Accessed as a classname binding to apply the `LinkView`'s `activeClass`
CSS `class` to the element when the link is active.
A `LinkView` is considered active when its `currentWhen` property is `true`
or the application's current route is the route the `LinkView` would trigger
transitions into.
@property active
**/

router: Ember.computed(function() {
return this.get('controller').container.lookup('router:main');
}),
Expand All @@ -197,8 +248,13 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {

if (get(this, '_isDisabled')) { return false; }

var router = this.get('router'),
routeArgs = args(this, router);
if (get(this, 'loading')) {
Ember.Logger.warn("This linkTo's parameters are either not yet loaded or point to an invalid route.");
return false;
}

var router = get(this, 'router'),
routeArgs = get(this, 'routeArgs');

if (this.get('replace')) {
router.replaceWith.apply(router, routeArgs);
Expand All @@ -207,6 +263,31 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
}
},

routeArgs: Ember.computed(function() {

var router = get(this, 'router'),
namedRoute = get(this, 'namedRoute'), routeName;

if (!namedRoute && this.namedRouteBinding) {
// The present value of namedRoute is falsy, but since it's a binding
// and could be valid later, don't treat as error.
return;
}
namedRoute = fullRouteName(router, namedRoute);

Ember.assert(fmt("The attempt to linkTo route '%@' failed. The router did not find '%@' in its possible routes: '%@'", [namedRoute, namedRoute, Ember.keys(router.router.recognizer.names).join("', '")]), router.hasRoute(namedRoute));

var resolvedContexts = resolvedPaths(this.parameters), paramsPresent = true;
for (var i = 0, l = resolvedContexts.length; i < l; ++i) {
var context = resolvedContexts[i];

// If contexts aren't present, consider the linkView unloaded.
if (context === null || typeof context === 'undefined') { return; }
}

return [ namedRoute ].concat(resolvedContexts);
}).property('namedRoute'),

/**
Sets the element's `href` attribute to the url for
the `LinkView`'s targeted route.
Expand All @@ -219,9 +300,21 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
href: Ember.computed(function() {
if (this.get('tagName') !== 'a') { return false; }

var router = this.get('router');
return router.generate.apply(router, args(this, router));
}).property('namedRoute')
var router = get(this, 'router'),
routeArgs = get(this, 'routeArgs');

return routeArgs ? router.generate.apply(router, routeArgs) : get(this, 'loadingHref');
}).property('routeArgs'),

/**
The default href value to use while a linkTo is loading.
Only applies when tagName is 'a'
@property loadingHref
@type String
@default #
*/
loadingHref: '#'
});

LinkView.toString = function() { return "LinkView"; };
Expand Down Expand Up @@ -420,3 +513,4 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
});
});


134 changes: 132 additions & 2 deletions packages/ember/tests/helpers/link_to_test.js
@@ -1,5 +1,6 @@
var Router, App, AppView, templates, router, eventDispatcher, container, originalTemplates;
var get = Ember.get, set = Ember.set;
var oldParamFlagValue;

function bootApplication() {
router = container.lookup('router:main');
Expand All @@ -17,6 +18,9 @@ function compile(template) {

module("The {{linkTo}} helper", {
setup: function() {
oldParamFlagValue = Ember.ENV.HELPER_PARAM_LOOKUPS;
Ember.ENV.HELPER_PARAM_LOOKUPS = true;

Ember.run(function() {
App = Ember.Application.create({
name: "App",
Expand Down Expand Up @@ -51,6 +55,7 @@ module("The {{linkTo}} helper", {
teardown: function() {
Ember.run(function() { App.destroy(); });
Ember.TEMPLATES = originalTemplates;
Ember.ENV.HELPER_PARAM_LOOKUPS = oldParamFlagValue;
}
});

Expand Down Expand Up @@ -502,7 +507,6 @@ test("The {{linkTo}} helper doesn't change view context", function() {
test("Quoteless route param performs property lookup", function() {
Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#linkTo 'index' id='string-link'}}string{{/linkTo}}{{#linkTo foo id='path-link'}}path{{/linkTo}}{{#linkTo view.foo id='view-link'}}{{view.foo}}{{/linkTo}}");

var oldFlagValue = Ember.ENV.HELPER_PARAM_LOOKUPS;
Ember.ENV.HELPER_PARAM_LOOKUPS = true;

function assertEquality(href) {
Expand Down Expand Up @@ -540,7 +544,133 @@ test("Quoteless route param performs property lookup", function() {
});

assertEquality('/about');
});

test("linkTo with null/undefined dynamic parameters are put in a loading state", function() {

expect(17);

var oldWarn = Ember.Logger.warn, warnCalled = false;
Ember.Logger.warn = function() { warnCalled = true; };
Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#linkTo destinationRoute routeContext loadingClass='i-am-loading' id='context-link'}}string{{/linkTo}}{{#linkTo secondRoute loadingClass='i-am-loading' id='static-link'}}string{{/linkTo}}");

Ember.ENV.HELPER_PARAM_LOOKUPS = true;

var thing = Ember.Object.create({ id: 123 });

App.IndexController = Ember.Controller.extend({
destinationRoute: null,
routeContext: null
});

App.AboutRoute = Ember.Route.extend({
activate: function() {
ok(true, "About was entered");
}
});

App.Router.map(function() {
this.route('thing', { path: '/thing/:thing_id' });
this.route('about');
});

bootApplication();

Ember.run(function() {
router.handleURL("/");
});

function assertLinkStatus($link, url) {
if (url) {
equal(normalizeUrl($link.attr('href')), url, "loaded linkTo has expected href");
ok(!$link.hasClass('i-am-loading'), "loaded linkView has no loadingClass");
} else {
equal(normalizeUrl($link.attr('href')), '#', "unloaded linkTo has href='#'");
ok($link.hasClass('i-am-loading'), "loading linkView has loadingClass");
}
}

var $contextLink = Ember.$('#context-link', '#qunit-fixture'),
$staticLink = Ember.$('#static-link', '#qunit-fixture'),
controller = container.lookup('controller:index');

assertLinkStatus($contextLink);
assertLinkStatus($staticLink);

Ember.run(function() {
warnCalled = false;
$contextLink.click();
ok(warnCalled, "Logger.warn was called from clicking loading link");
});

// Set the destinationRoute (context is still null).
Ember.run(function() { controller.set('destinationRoute', 'thing'); });
assertLinkStatus($contextLink);

// Set the routeContext to an id
Ember.run(function() { controller.set('routeContext', '456'); });
assertLinkStatus($contextLink, '/thing/456');

// Set the routeContext to an object
Ember.run(function() { controller.set('routeContext', thing); });
assertLinkStatus($contextLink, '/thing/123');

// Set the destinationRoute back to null.
Ember.run(function() { controller.set('destinationRoute', null); });
assertLinkStatus($contextLink);

Ember.run(function() {
warnCalled = false;
$staticLink.click();
ok(warnCalled, "Logger.warn was called from clicking loading link");
});

Ember.run(function() { controller.set('secondRoute', 'about'); });
assertLinkStatus($staticLink, '/about');

// Click the now-active link
Ember.run(function() { $staticLink.click(); });

Ember.Logger.warn = oldWarn;
});

test("The {{linkTo}} helper refreshes href element when one of params changes", function() {
Router.map(function() {
this.route('post', { path: '/posts/:post_id' });
});

var post = Ember.Object.create({id: '1'}),
secondPost = Ember.Object.create({id: '2'});

App.PostRoute = Ember.Route.extend({
model: function(params) {
return post;
},

serialize: function(post) {
return { post_id: post.get('id') };
}
});

Ember.TEMPLATES.index = compile('{{#linkTo "post" post id="post"}}post{{/linkTo}}');

App.IndexController = Ember.Controller.extend();
var indexController = container.lookup('controller:index');

Ember.run(function() { indexController.set('post', post); });

bootApplication();

Ember.run(function() { router.handleURL("/"); });

equal(Ember.$('#post', '#qunit-fixture').attr('href'), '/posts/1', 'precond - Link has rendered href attr properly');

Ember.run(function() { indexController.set('post', secondPost); });

equal(Ember.$('#post', '#qunit-fixture').attr('href'), '/posts/2', 'href attr was updated after one of the params had been changed');

Ember.run(function() { indexController.set('post', null); });

Ember.ENV.HELPER_PARAM_LOOKUPS = oldFlagValue;
equal(Ember.$('#post', '#qunit-fixture').attr('href'), '/posts/2', 'href attr does not change when one of the arguments in nullified');
});

0 comments on commit 3327586

Please sign in to comment.