Direction-aware transition with Ember Liquid Fire's {{liquid-with}} helper - ember.js

In my Ember app, I would like to make nice Liquid Fire transitions within the same route when the model of a dynamic route changes. Here's my router:
// app/router.js
Router.map(function() {
this.route("campaigns", { path: "/" }, function() {
this.route("campaign", { path: "/campaign/:election_year" });
});
});
I would like the view to leave the screen to the left when switching to an election_year that is in the future (e.g., from campaign/2008 to campaign/2012) and to the right the other way around.
My first thought was to use use the {{liquid-outlet}} and the toModel function in the app/transitions.js file, but Edward Faulkner (creator of Liquid Fire) says in this thread that
liquid-outlet does not handle model-to-model transitions on the same
route
and that I should use {{liquid-with}} instead. However I'm at a loss as to how to handle the directionality, i.e. make the view go left when election_year increases, and go right when it decreases. Any clues?

When using a liquid-with (or liquid-bind) helper, there is not necessarily a route or model involved in the animation, and so constraints like toRoute or toModel do not apply. Instead, you can use toValue which will match against whatever you're passing into the helper.
Assuming you're passing a Campaign model to liquid-with, you could use a rule like this in transitions.js:
this.transition(
this.toValue(function(newValue, oldValue) {
return newValue instanceof Campaign &&
oldValue instanceof Campaign &&
newValue.electionYear > oldValue.electionYear
}),
this.use('toLeft'),
this.reverse('toRight')
)
Explanation:
We have a constraint, an animation to use when it matches (toLeft), and an animation to use when it matches with "to" and "from" reversed (toRight),
All the rule constraints (including toValue, fromRoute, etc) can accept:
simple values like toValue(42) or toRoute('posts') that will be compared with ===
regular expressions like toRoute(/unicorn/)
functions that test the value, like toValue(n => n > 1).
functions that compare the value and the "other value", like
toValue((newThing, oldThing) => newThing.betterThan(oldThing))
The final case is what we're using in the solution above.
(We are continuing to refine these APIs. In this particular case, I think we should add an explicit name for the comparison rule, like this.compareValues(function(oldValue, newValue){...}), instead of just overloading toValue to also do comparison. But this should not affect your solution, as I'm not going to break the existing behavior in the foreseeable future.)

Related

How to maintain unknown/wildcard queryParams through a transition?

I have a route (route-a) that transitions to another route (route-b) and I am trying to find a way for the destination URL to maintain the all query parameters, even if route-b does not know about them in advance.
For example, if a user visits https://example.com/route-a/?var1=x&var2=y, and the transition to route-b happens like this:
afterModel(model, transition) {
this.transitionTo('route-b', model, {queryParams: transition.to.queryParams}) // transition route-a to route-b
}
...the ultimate URL will be https://example.com/route-b/ — without the query params.
Now, I realize the "Ember way" is to define the queryParams on route-b's controller in advance, but in this particular use-case, I do not know the queryParams in advance. Route B consumes any and all query params provided to it, which means they would be impossible to enumerate in advance.
How can I transition to a new route without dropping query parameters that are not specifically enumerated on the destination route's controller?
Is there a way to handle unknown queryParams, or is there the notion of a wildcard for queryParams (similar to *path routes)?
Update: I'm not marking this as the answer, because as jelhan notes below, using a computed property for this key is explicitly identified as a no-no in the docs. But it worked for our use-case, and it might for others, though I'm guessing it may break down if you have additional queryParams in other routes that might conflict when Ember attempts to combine them.
Previous answer:
My solution here ended up using Ember's computed method to auto-generate the Array of query params by parsing the URL.
queryParams: computed("router.location", function () {
let qp = this.get("router.location").getURL().split("?")[1];
if (qp) {
let qpAsObj = JSON.parse(
'{"' +
decodeURI(qp)
.replace(/"/g, '\\"')
.replace(/&/g, '","')
.replace(/=/g, '":"') +
'"}'
);
return Object.keys(qpAsObj)
}
})
If you don't want to subsequently maintain those query params on the page/model the next time a user re-visits that page ("sticky query params"), you will also need to remove the queryParams on the route:
resetController(controller) {
// unset all queryParams when leaving the route
controller.queryParams.forEach(v => {
controller.set(v, null)
})
}
This solution is... not ideal, but it works and we have tests written to ensure that we will catch any errors if it breaks going forward.

Ember's volatile and templates

I have a property mood which is part of the interface for a component. Behind the scenes I have a computed property called _mood:
const { computed, typeOf } = Ember;
_mood: computed('mood','A','B','C' function() {
let mood = this.get('mood');
if (typeOf(mood) === 'function') {
mood = mood(this);
}
return !mood || mood === 'default' ? '' : `mood-${mood}`;
}).volatile(),
I have a situation where with the volatile() that hangs of of Ember's computed object resolves all non DOM unit tests successfully but for some reason it is not triggering the template to update the DOM under any circumstance. It should, at the very least, update if any of properties being watched change (in this case ['mood','A', 'B', 'C']) change. Because it is volatile (aka, doesn't cache results) the new results would show up in a template if the template knew to re-render the component but for some reason it doesn't.
If I remove the volatile() tag it works fine for static/scalar values but if mood is a function then the result is cached and therefore it only works once (not a working solution for this problem).
How do I get the best of both worlds?
I'm still not sure why the volatile() method is turning off updates to templates. This might be a real bug but in terms of solving my problem the important thing to recognise was that the volatile approach was never the best approach.
Instead the important thing to ensure is that when mood comes in as a function that the function's dependencies are included in the CP's dependencies. So, for instance, if mood is passed the following function:
mood: function(context) {
return context.title === 'Monkeys' ? 'happy' : 'sad';
}
For this function to evaluate effectively -- and more importantly to trigger a re-evaluation at the right time -- the title property must be part of the computed property. Hopefully that's straight forward as to why but here's how I thought I might accommodated this:
_moodDependencies: ['title','subHeading','style'],
_mood: computed('mood','size','_moodDependencies', function() {
let mood = this.get('mood');
console.log('mood is: %o', mood);
if (typeOf(mood) === 'function') {
run( ()=> {
mood = mood(this);
});
}
return !mood || mood === 'default' ? '' : `mood-${mood}`;
}),
That's better, as it allows at build time for a static set of properties to be defined per component (the _mood CP for me is part of a mixin used by a few components). Unfortunately this doesn't yet work completely as the it apparently doesn't unpack/destructure the _moodDependencies.
I'll get this sorted and update this unless someone else beats me to the punch.

Ember.js: How do I determine if a property has really changed in an Observer?

Let's say I have Articles which are backed by Sources. Each article has one source.
Sources have associated HTML which will be rendered on screen.
I want this HTML to be rendered only if the source changed.
App.ArticleView = Ember.View.extend({
didInsertElement: function() {
this.addObserver('controller.source.id', function() {
console.log(arguments);
renderHTML();
});
});
});
This behaves exactly as stated in the addObserver documentation, "Note that the observers are triggered any time the value is set, regardless of whether it has actually changed. Your observer should be prepared to handle that."
If setting a controller.model of Article A with source 1 is followed by setting a controller.model of Article B with source 1, the observer will call the method but I want to prevent renderHTML() from happening.
The documentation mentions "Observer Methods" which I'm not sure how to put to use in this case. Its signature (function(sender, key, value, rev) { };) looks exactly like what I need, but in my tests the arguments to the observer method are always 0: (current view), 1: "controller.source.id".
How can I get the previous value of controller.source.id, so as to determine whether to renderHTML() or not?
Ember.set won't set the value if it's the same as the current value, so your observer won't fire unless the value changes.
Here's an example:
http://emberjs.jsbin.com/jiyesuzi/2/edit
And here's the code in Ember.set that does it (https://github.com/emberjs/ember.js/blob/master/packages_es6/ember-metal/lib/property_set.js#L73)
// only trigger a change if the value has changed
if (value !== currentValue) {
Ember.propertyWillChange(obj, keyName);
if (MANDATORY_SETTER) {
if ((currentValue === undefined && !(keyName in obj)) || !obj.propertyIsEnumerable(keyName)) {
Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter
} else {
meta.values[keyName] = value;
}
} else {
obj[keyName] = value;
}
Ember.propertyDidChange(obj, keyName);
}
Unfortunately for you're case, Ember considers changing a portion of the chain as changing the item, aka if controller.source changes, then the observer will fire. You'll need to track your id differently to avoid you're observer from firing. You can create a different observer that always sets the current id on a local property, and then it won't fire the update when the chain is broken.
Inside the controller
currentArticleId: null,
watchArticle: function(){
this.set('currentArticleId', this.get('article.id'));
}.observes('article.id')
And then you would watch controller.currentArticleId

Ember.js: Is it possible to inject a dependency on a specific Route/Controller Mixin?

Let's say I have a SessionManager instance which I want to be accessible in every Route extending my ProtectedRoute Mixin, is it possible to inject this dependency into a "group of routes" as I can reference a single Route instance?
So instead of:
App.inject('route:protected1', 'sessionManager', 'session_manager:main');
App.inject('route:protected2', 'sessionManager', 'session_manager:main');
....
I could do something like
App.inject('route:protectedmixin', 'sessionManager', session_manager:main);
You certainly can, but it might involve a bit of juggling. You could define any logic to decide what to inject and where if you want to rely on the default conventions you could manually find this objects and then use the fullname when injecting.
Another option would be to do it for each route, regardless of whether they include the Mixin or not. Inject doesn't need the full name, if you call `App.inject('route', ...) it would work by default.
If going with option one, it would look something like this. You basically need to find those routes implementing their mixins and then inject into all of those.
var guidForMixin = Ember.guidFor(App.YourMixin);
var routesToInjectInto = Ember.keys(App).filter(function (key) {
var route, mixins;
if (key.match(/Route$/))
route = App[key];
mixins = Ember.meta(route).mixins;
if (mixins) {
!!mixins[guidForMixin];
}
return false;
);
routesToInjectInto.each( function (key) {
var keyForInjection = Ember.decamelize(key);
App.inject('route:' + keyForInjection, 'sessionManager', 'session_manager:main');
});
Also I would suggest doing all of this inside an initializer, but that might be a minor consideration.
Ember.onload('Ember.Application', function(Application) {
Application.initializer {
name: "sessionManager"
initialize: function (container, application) {
// do the above here. Refer to app as the namespace instead of App.
// use the container instead of App.__container__ to register.
};
});

Force a controller to always act as a proxy to a model in Ember

I'm looping through a content of an ArrayController whose content is set to a RecordArray. Each record is DS.Model, say Client
{{# each item in controller}}
{{item.balance}}
{{/each}}
balance is a property of the Client model and a call to item.balance will fetch the property from the model directly. I want to apply some formatting to balance to display in a money format. The easy way to do this is to add a computed property, balanceMoney, to the Client object and do the formatting there:
App.Client = DS.Model({
balance: DS.attr('balance'),
balanceMoney: function() {
// format the balance property
return Money.format(this.get('balance');
}.property('balance')
});
This serves well the purpose, the right place for balanceMoney computed property though, is the client controller rather than the client model. I was under the impression that Ember lookup properties in the controller first and then tries to retrieve them in the model if nothing has been found. None of this happen here though, a call to item.balanceMoney will just be ignored and will never reach the controller.
Is it possible to configure somehow a controller to act always as a proxy to the model in all circumstances.
UPDATE - Using the latest version from emberjs master repository you can configure the array controller to resolve records' methods through a controller proxy by overriding the lookupItemController method in the ArrayController. The method should return the name of the controller without the 'controller' suffix i.e. client instead of clientController. Merely setting the itemControllerClass property in the array controller doesn't seem to work for the moment.
lookupItemController: function( object ) {
return 'client';
},
This was recently added to master: https://github.com/emberjs/ember.js/commit/2a75cacc30c8d02acc83094b47ae8a6900c0975b
As of this writing it is not in any released versions. It will mostly likely be part of 1.0.0.pre.3.
If you're only after formatting, another possibility is to make a handlebars helper. You could implement your own {{formatMoney item.balance}} helper, for instance.
For something more general, I made this one to wrap an sprintf implementation (pick one of several out there):
Ember.Handlebars.registerHelper('sprintf', function (/*arbitrary number of arguments*/) {
var options = arguments[arguments.length - 1],
fmtStr = arguments[0],
params = Array.prototype.slice.call(arguments, 1, -1);
for (var i = 0; i < params.length; i++) {
params[i] = this.get(params[i]);
}
return vsprintf(fmtStr, params);
});
And then you can do {{sprintf "$%.2f" item.balance}}.
However, the solution #luke-melia gave will be far more flexible--for example letting you calculate a balance in the controller, as opposed to simply formatting a single value.
EDIT:
A caveat I should have mentioned because it's not obvious: the above solution does not create a bound handlebars helper, so changes to the underlying model value won't be reflected. There's supposed to be a registerBoundHelper already committed to Ember.js which would fix this, but that too is not released yet.