How to use mixin inside custom helper? - ember.js

I have defined some helper functions inside a mixin; however, I am not sure how to uses these functions inside a custom helper.
I have referred to question below but seems ember no longer have Mixin.apply method.
Accessing an Ember.Mixin within a custom handlebars helper

If you got class based helper, then you can use Mixin as usual.
export default Ember.Helper.extend(YourMixinName,{
session: Ember.inject.service(),
onNewUser: Ember.observer('session.currentUser', function() {
this.recompute();
}),
compute() {
return this.get('session.currentUser.email');
}
});

The default helper cannot use mixin because it is stateless.
However, if you need a helper that interacts with your application. You can do so by making a class-based helper.
https://guides.emberjs.com/v2.11.0/templates/writing-helpers/#toc_class-based-helpers
Code snippet below is a class-based helper,
import Ember from 'ember';
export default Ember.Helper.extend({
compute([value, ...rest], hash) {
let dollars = Math.floor(value / 100);
let cents = value % 100;
let sign = hash.sign === undefined ? '$' : hash.sign;
if (cents.toString().length === 1) { cents = '0' + cents; }
return `${sign}${dollars}.${cents}`;
}
});

Related

Recompute Ember custom helper on change of a controller property that is passed to the helper

I'm changing the value of a property in my controller and the helper fails to recompute them.
Sample code here:
My template looks like,
{{#if (my-helper info)}}
<span>Warning</span>
{{/if}}
In my controller,
changeAction: function() {
let that = this,
info = that.get("info");
set(info, "showWarning", true);
}
my helper,
import { helper as buildHelper } from '#ember/component/helper';
export default buildHelper(function(params) {
let that = this,
info = that.get("info");
if(info.showWarning ) {
return true;
} else {
return false
}
});
I see several issues with your code:
The template helper seems to get an object as it's first and only position param: {{my-helper info}} while info is { showWarning: true }. A template helper does recompute if the value passed it changes but not if a property of that value changes. A quick fix would be {{my-helper info.showWarning}}.
In your template helper your are trying to access the property on it's this context. As far as I know that's not supported. As you are using a positional param and it's the first one, it's available as first entry inparams array. So your template helper should look like:
export default buildHelper(function([info]) {
if(info.showWarning ) {
return true;
} else {
return false
}
});
What version of Ember are you using? If it's >= 3.1 you don't need to use this.get() in your controller. If you are using Ember < 3.1 you need to use info.get() also in template helper.
But as described before I would not recommend passing an object to a template helper as it's only updated if the object itself is replaced. Changing a property of it is not enough. You might be able to do so using Class-based Helpers but I wouldn't recommend to do so as it's error prune.

How to refactor {{link-to}} helper into a function that can be used in transitionTo?

I currently use a {{link-to}} helper that was written by someone else to explicitly state the query params to pass to the next route and strip out others that are not stated. It looks like this:
//link-to example
{{#link-to 'route' (explicit-query-params fromDate=thisDate toDate=thisDate)} Link Text {{/link-to}}
//the helper
import {helper} from '#ember/component/helper';
import Object from '#ember/object';
import {assign} from '#ember/polyfills';
export function explicitQueryParams(params, hash) {
let values = assign({}, hash);
values._explicitQueryParams = true;
return Object.create({
isQueryParams: true,
values,
});
}
export default helper(explicitQueryParams);
// supporting method in router.js
const Router = EmberRouter.extend({
_hydrateUnsuppliedQueryParams(state, queryParams) {
if (queryParams._explicitQueryParams) {
delete queryParams._explicitQueryParams;
return queryParams;
}
return this._super(state, queryParams);
},
});
I've recently had a use case where I need to apply the same logic to a transitionTo() that is being used to redirect users from a route based on their access:
beforeModel() {
if (auth) {
this.transitionTo('route')
} else {
this.transitionTo('access-denied-route')
}
},
I am really struggling to see how I can refactor what I have in the handlebars helper to a re-usable function in the transitionTo() segment. I'm even unsure if transitionTo() forwards the same arguments as {{link-to}} or if I will have to fetch the queryParams somehow from a different location.
Any insight would be greatly appreciated.
Well first off, tapping into private methods like _hydrateUnsuppliedQueryParams is risky. It will make upgrading more difficult. Most people use resetController to clear stick query params. You could also explicitly clear the default values by passing empty values on the transition.
But, ill bite because this can be fun to figure out :) Check this ember-twiddle that does what you're wanting.
If you work from the beginning in the transitionTo case, we can see that in the router.js implementation:
transitionTo(...args) {
let queryParams;
let arg = args[0];
if (resemblesURL(arg)) {
return this._doURLTransition('transitionTo', arg);
}
let possibleQueryParams = args[args.length - 1];
if (possibleQueryParams && possibleQueryParams.hasOwnProperty('queryParams')) {
queryParams = args.pop().queryParams;
} else {
queryParams = {};
}
let targetRouteName = args.shift();
return this._doTransition(targetRouteName, args, queryParams);
}
So, if the last argument is an object with a query params obj, that's going directly into _doTransition, which ultimately calls:
_prepareQueryParams(targetRouteName, models, queryParams, _fromRouterService) {
let state = calculatePostTransitionState(this, targetRouteName, models);
this._hydrateUnsuppliedQueryParams(state, queryParams, _fromRouterService);
this._serializeQueryParams(state.handlerInfos, queryParams);
if (!_fromRouterService) {
this._pruneDefaultQueryParamValues(state.handlerInfos, queryParams);
}
}
which has the _hydrateUnsuppliedQueryParams function. So, to make this all work, you can't share the function directly from the helper you've created. Rather, just add _explicitQueryParams: true to your query params. Job done :)
The link-to version is different. The query params use
let queryParams = get(this, 'queryParams.values');
since the link-to component can take a variable number of dynamic segments and there needs to be some way to distinguish between the passed dynamic segments, a passed model, and query params.

EmberJS - Get controller created in {{render}} helper

I have a {{render 'B' model}} helper in template A, so B/BView/BController are essentially children of A/AView/AController. BController even has AController as its parentController.
Is there a way to (easily) reference BController from AController? I'd prefer not to set something to B's parentController because it's not always A.
Ember lets you use a needs property for this purpose: http://emberjs.com/guides/controllers/dependencies-between-controllers/
App.AController = Ember.ObjectController.extend({
needs: ['b']
actions: {
somethingWithA: function() {
var bController= this.get('controllers.b');
// ...
}
}
});

EmberJS Inheritance Conundrum

Consider this situation. I have a common logic which I want to reuse across Ember.ArrayController and Ember.ObjectController instances.
Both Ember.ArrayController and Ember.ObjectController are derived from a basic Ember.Object so I am trying something like:
AbstractController = Ember.Object.extend({
// some methods with common logic
});
AbstractArrayController = AbstractController.extend({});
AbstractObjectController = AbstractController.extend({});
The problem is I also need AbstractArrayController and AbstractObjectController to extend from their parents (Ember.ArrayController and Ember.ObjectController).
How would I achieve this sort of inheritance?
I am looking at reopen and reopenClass methods right now, maybe they could be useful: http://emberjs.com/api/classes/Ember.Object.html#method_reopen
I tried just doing something like:
Ember.Object.reopenClass({
foo: function () {
return "foo";
}.property("foo")
});
But that doesn't seem to work.
Another way to put the problem:
App.HelloController = Ember.ObjectController.extend({
foo: function () {
return "foo";
}.property("foo")
});
App.WorldController = Ember.ObjectController.extend({
foo: function () {
return "foo";
}.property("foo")
});
How to abstract the foo computed property?
reopenClass adds methods on the class object, not the instance objects. When you do:
Ember.Object.reopenClass({
foo: function () {
return "foo";
}.property("foo")
});
You are creating Ember.Object.foo().
You need to use reopen if you want to methods at an instance level, for example Ember.Object.create().foo().
To answer you question, the best way to abstract a function that many types of objects can use is with a mixin. To create a mixin you use.
var mixin = Ember.Mixin.create({
foo: function() {
return 'foo';
}
});
And to have your objects take advantage of that mixin you can use.
var MyController = Ember.ObjectController.extend(mixin, {
// ...
});
More about mixins: http://codingvalue.com/blog/emberjs-mixins/ and http://emberjs.com/api/classes/Ember.Mixin.html

Is it possible to share behavior with controllers in ember through inheritance instead of using 'needs'

Below I have provided an example of what I 'think' should work, but does not. My expectation is that if a controller inherits from another controller, than it should be able to access provided behaviors in the parent class. I know that the same behavior can be achieved with 'needs', but I think it would be much cleaner if you could inherit behavior.
https://gist.github.com/4589210
You can alternatively use mixin. What it is? It is kind of multi inheritance.
App.Important = Ember.Mixin.create({
sharedBehavior: function() {
return "A winner is you!!!";
}.property()
});
App.OtherImportant = Ember.Mixin.create({
otherSharedBehavior: function() {
return "A winner is not you!!!";
}.property()
});
App.AnotherController = Ember.controller.extend(App.Important, App.OtherImportant, {
importantStuff: function() {
var ohBeehave = this.get("sharedBehavior");
if(ohBeehave) {
return ohBeehave;
} else {
return "FML";
}
}.property("sharedBehavior")
});
See http://emberjs.com/api/classes/Ember.Mixin.html
You are correct that controllers should be able to inherit properties from other controllers. Here is a working example based on your gist:
http://jsbin.com/uzeyum/1/edit
App = Ember.Application.create();
App.ApplicationController = Ember.Controller.extend({
sharedBehavior: function() {
return "A winner is you!!!";
}.property()
});
App.AnotherController = App.ApplicationController.extend({
importantStuff: function() {
var ohBeehave = this.get("sharedBehavior");
if(ohBeehave) {
return ohBeehave;
} else {
return "FML";
}
}.property("sharedBehavior")
});
That said, from experience I can tell you that inheritance is very rarely what you want. There are many reasons for this, see Prefer composition over inheritance? for detail.
By declaring an array of dependencies via the needs property rather than inheritance you will find over time that your application is less brittle and easier to test/change.