Getting the name of a function provided to an Ember `action` helper - ember.js

I'm assuming this isn't possible, but wanted to see if anyone knew any better.
With ES6, you can get the name of a function. For instance:
function foo() { return true; }
function bar() { return true; }
const functionContainer = foo;
foo.name; // 'foo'
bar.name; // 'bar'
functionContainer.name; // 'foo'
In Ember, you can pass an action into an action helper. For instance:
export default Ember.Component.extend({
actions: {
bar() {
return true;
}
}
});
And the template:
{{foo-component foo=(action "bar")}}
Within foo-component, is there some way to do this:
export default Ember.Component.extend({
doFoo: Ember.on('didRecieveAttrs', function() {
console.log(this.attrs.foo.name); // 'bar'
})
});
When I try, I just get a blank string. This makes sense, since it looks like the bar action is getting wrapped in a nameless function by ember-metal.
Anyone know of a way to grab that name? It would make a huge difference for a project I'm working on.

Nope, you can't do exactly what you want < insert technical discussion about closures here >, but you can kinda fake it by just adding another param to your component, like so:
{{foo-component foo=(action "bar") actionName="bar"}}
then in your component.js you can access
this.attrs.actionName // "bar"

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.

How to unit test an ember controller

I have a single action defined in an ember controller that calls 2 separate functions that are part of the controller. I'd like to mock out those functions in a unit test in order to confirm if the action method called the correct function.
My controller looks like this:
export default Ember.Controller.extend({
functionA() {
return;
},
functionB() {
return;
},
actions: {
actionMethod(param) {
if(param) {
return this.functionA();
}
else {
return this.functionB();
}
}
}
});
In practice, the controller works, however in the unit test, functionA and functionB are both undefined. I tried to log this to the console, but can't find where functionA and functionB are, so I'm unable to properly mock them. I expected them to be in the top level of the object next to actions, but instead I only found _actions with actionMethod properly defined.
My unit test looks like below
const functionA = function() { return; }
const functionB = function() { return; }
test('it can do something', function(assert) {
let controller = this.subject();
// I don't want the real functions to run
controller.set('functionA', functionA);
controller.set('functionB', functionB);
controller.send('actionMethod', '');
// raises TypeError: this.functionA is not a function
// this doesn't work etiher
// controller.functionB = functionB;
// controller.functionA = functionA;
// controller.actions.actionMethod();
}
Does anyone have any ideas on how I can replace those functions in the testing environment? Or perhaps, is there a better way to test this functionality or set up my controller?
edit
typo: this.subject to this.subject()
To replace the functions of the controller in the unit test, you can pass parameter to the this.subject() function:
let controller = this.subject({
functionA(){
//this function overriddes functionA
},
functionB(){
//this function overriddes functionB
},
});
Look at the sample twiddle.
This method is especially useful for replacing the injected service of the controllers.
Introduce corresponding property you are dealing with, let us say name property,
So your controllers would be looking like this,
import Ember from 'ember';
export default Ember.Controller.extend({
name:'',
functionA() {
this.set('name','A');
},
functionB() {
this.set('name','B');
},
actions: {
actionMethod(param) {
if(param) {
return this.functionA();
}
else {
return this.functionB();
}
}
}
});
And you can test for the name property value after calling actionMethod.
test(" testing functionA has been called or not", function(assert){
let controller = this.subject();
controller.send('actionMethod',true);
//If you would like to call functionA just say controller.functionA()
assert.strictEqual(controller.get('name'),'A',' name property has A if actionMethod arguments true');
controller.send('actionMethod',false);
assert.strictEqual(controller.get('name'),'B',' name property has B actionMethod arguments false');
});

this.transitionToRoute not working in my controller Ember

I am using a controller to read the value selected on a drop down menu, take in parameters of some input fields and then save the record. It creates the record and takes in the information just fine. My problem lies when I try to transition to another page at the end of the action. I keep getting the error: Cannot read property 'transitionToRoute' of undefined
I am completely stumped. Any ideas?
Here is my controller code:
var teamId;
export default Ember.Controller.extend({
auth: Ember.inject.service(),
actions: {
onSelectEntityType: function(value) {
console.log(value);
teamId = value;
return value;
},
createProcess: function(processName, processDescription) {
var currentID = this.get('auth').getCurrentUser();
let team = this.get('store').peekRecord('team', teamId);
let user = this.get('store').peekRecord('user', currentID);
let process = this.get('store').createRecord('process', {
team: team,
user: user,
name: processName,
description: processDescription
});
process.save().then(function () {
this.transitionToRoute('teams', teamId);
});
}
}
});
Here is the corresponding route:
export default Ember.Route.extend({
auth: Ember.inject.service(),
model: function() {
var currentID = this.get('auth').getCurrentUser();
return this.store.find('user', currentID);
}
});
You should have clear understanding about this keyword in Javascript. The keyword this only depends on how the function was called, not how/when/where it was defined.
function foo() {
console.log(this);
}
// normal function call
foo(); // `this` will refer to `window`
// as object method
var obj = {bar: foo};
obj.bar(); // `this` will refer to `obj`
// as constructor function
new foo(); // `this` will refer to an object that inherits from `foo.prototype`
Have a look at the MDN documentation to learn more.
You can cache the this in normal variable this and then access inside the call back.
var self = this;
process.save().then(function () {
self.transitionToRoute('teams', teamId);
});
ECMASCript 6 introduced arrow functions whose this is lexically scoped. Here, this is looked up in scope just like a normal variable.
process.save().then(() => {
this.transitionToRoute('teams', teamId);
});

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