Computed Property Macros with Ember CLI - ember.js

I'm attempting to DRY up my application and move some functionality into macros with Ember CLI. After reading this article, I thought I could get things working but I'm getting an undefined is not a function TypeError: undefined is not a function error when trying to use the macro with any arguments. If I don't pass any arguments, ember doesn't throw the error. To generate the file I'm using the command ember generate util calc-array
// utils/calc-array.js
import Ember from 'ember';
export default function calcArray(collection, key, calculation) {
return function() {
...
}.property('collection.#each');
}
// controller/measurements.js
import Ember from 'ember';
import calculate from '../../utils/calc-array';
export default Ember.ArrayController.extend({
high: calculate(this.get('model'), 'value', 'high'),
...
});

this.get('model') causes the problem - this points to global object, not controller instance. Pass the string (i.e. model) and use this.get inside computed property.
Also collection.#each will not work, it's not a valid path.
Summing it up:
export default function calcArray(collectionPath, key, calculation) {
return function() {
var collection = this.get(collectionPath);
...
}.property(collectionPath + '.#each');
}

Related

dynamic looping on a controller property

I am using ember 2.17.
I added this property to a controller:
export default Controller.extend({
newAttachments: new Array()
...
})
I add elements in it through this controller action:
setAttachment(file) {
console.log('trying');
this.get('newAttachments').push(file);
}
When I use the action, the message is displayed in the console, and in Ember inspector I can see the array is no longer empty :
However, the following code in the view has no output :
{{#each newAttachments as |file|}}
<p>in loop</p>
{{/each}}
Why is it not displaying anything? In a component it would work, why not here ?
Ember can't observe native arrays. Therefor the framework doesn't know that a value is pushed into the array. You should use ember's own Ember.NativeArray and it's pushObject method instead. That one ensures that the framework is informed if an entry is added to or removed from array. Changed code would look like this:
import { A } from '#ember/array';
export default Controller.extend({
newAttachments: A(),
setAttachment(file){
this.get('newAttachments').pushObject(file)
}
})
You shouldn't add the array as a property of an EmberObject as this might introduce a leak between instances. That's not a production issue in that case cause controllers are singletons in ember.js. But you might see strange behavior in tests. Refactoring for native classes will resolve that issues as class fields are not leaked between instances. For old EmberObject based classes initializing the value in init hook or using a computed property are common ways to deal with that issue:
// computed property
import { computed } from '#ember/object';
import { A } from '#ember/array';
export default Controller.extend({
newAttachments: computed(() => A()),
});
// init hook
import { A } from '#ember/array';
export default Controller.extend({
init() {
this._super(...arguments);
this.set('newAttachments', A());
}
});
Please note that you don't need to use get() if running Ember >= 3.1.

Ember CLI: custom input helper

I'm trying to extend Ember's TextField with UrlField so that if someone forgets to include http://, it does it for them.
Here's my View:
views/input-url.js
import Ember from 'ember';
export default Ember.TextField.extend({
type: 'url',
didInsertElement: function() {
this._super.apply(this, arguments);
this.formatValue();
},
onValueChange: function() {
this.formatValue();
}.observes('value'),
formatValue: function() {
var pattern = /^https{0,1}:\/\/[A-Za-z0-9]+\.[A-Za-z0-9]+/g;
if (pattern.test(this.get('value')))
return;
if (!pattern.test('http://' + this.get('value')))
return;
this.set('value', 'http://' + this.get('value'));
}
});
If I use it in my template like this, it works fine:
{{view "input-url" value=url}}
I prefer to use custom view helpers, so I created this (following the guide at the bottom of this page: http://guides.emberjs.com/v1.11.0/templates/writing-helpers/):
helpers/input-url.js
import Ember from 'ember';
import InputUrl from '../views/input-url';
export default Ember.Handlebars.makeBoundHelper(InputUrl);
Now trying to render this in my template doesn't work:
{{input-url value=url}}
I've also tried different permutations of this, including what's shown in the guide Ember.Handlebars.makeBoundHelper('input-url', InputUrl); (which throws an error), but I can't seem to get my input field to show up. What am I doing wrong?
Not sure what you are doing wrong with your view helper, but there is a much simpler solution: take advantage of the fact that Ember.Textfield is a component. http://emberjs.com/api/classes/Ember.TextField.html
Simply move views/input-url.js to components/input-url.js and get rid of your view helper.
Then {{input-url value=url}} should work automatically.
If you want do to this using a helper, you cannot extend Ember.TextField because extends Ember.Component and is not a Handlebars helper.
The way to do this using a helper would actually be simpler. Since you are using Ember-CLI, you can create a helper called "input-url" with the command ember g helper input-url and the only code you would need is the code within your formatValue() function:
helpers/input-url.js
// define patter globally so it's not recreated each time the function is called
var pattern = /^https{0,1}:\/\/[A-Za-z0-9]+\.[A-Za-z0-9]+/g;
export function inputUrl(value) {
if (pattern.test(value)) {
return value;
}
if (!pattern.test('http://' + value)) {
return value;
}
return 'http://' + value;
};
export default Ember.Handlebars.makeBoundHelper(inputUrl);
And you can use it like:
{{input-url PASS_YOUR_URL_HERE}}
Where the value you pass will be the value of the value variable within the helper.
You could also create a component, as #Gaurav suggested, using the exact code you have above, just in components/input-url.js instead and delete the helper cause it is not necessary anymore. You also have to edit the corresponding template of the component if you want it to display the value with a single handlebars expression:
templates/components/input-url.hbs
{{value}}
The usage with a component would be:
{{input-url value=PASS_YOUR_URL_HERE}}

How to create a inject helper for something other than service?

https://github.com/emberjs/ember.js/blob/5fd2d035b30aa9ebfe73de824b3b283ec8e589cc/packages/ember-runtime/lib/system/service.js#L31
In the line I reference above the ember-core team imports this createInjectionHelper and uses it to add a clean/simple api for injecting services like so
App.ApplicationRoute = Ember.Route.extend({
authManager: Ember.inject.service('auth'),
model: function() {
return this.get('authManager').findCurrentUser();
}
});
How can I create something like this myself for a non service?
Your example usage will change slightly from what you have above. We will cover what the injectRepositories does in a little bit.
import injectRepositories from 'app/utils/inject';
export default Ember.Route.extend({
repository: injectRepositories('person'),
model: function() {
var repository = this.get('repository');
return repository.find();
}
});
The initializer can be improved with the following changes:
import registerWithContainer from "ember-cli-auto-register/register";
export function initialize(_, application) {
registerWithContainer("repositories", application);
application.inject("repositories", "store", "store:main");
}
export default {
name: "repositories",
after: "store",
initialize: initialize
};
Let's break down what is happening in each line.
registerWithContainer("repositories", application);
In the line above, we are deferring to the ember-addon ember-cli-auto-register. This addon will take a directory, in this situation, the repositories directory and register each object into the Ember container to able to be accessed via a lookup. They will be inserted as if doing the following:
application.register("repositories:person", PersonRepository);
Then we add a function to do the injection using the ember-addon ember-cli-injection:
// app/utils/inject.js
import inject from "ember-cli-injection/inject";
var injectRepositories = inject("repositories");
export default injectRepositories;
This then allows us the opportunity to use the newly created function above to access these objects with the code below:
import injectRepositories from 'app/utils/inject';
export default Ember.Route.extend({
repository: injectRepositories('person'),
model: function() {
var repository = this.get('repository');
return repository.find();
}
});
Since each object is now in the container, we can look it up and inject at runtime instead of during the initialization of the application. We register the repositories key in the function and this then returns a computed property (see code below from ember-cli-injection). We do this as a computed property to allow lazy loading. The object is not fetched from the container until the property is accessed.
import Ember from 'ember';
var injection = function(key) {
return function(name) {
return Ember.computed(function(propertyName) {
var objectName = name || propertyName;
return this.container.lookup(key + ':' + objectName);
});
};
};
export default injection;
We also allow for a name to passed to the repositories function, for example repository: injectRepositories('person'). This allows you to name your object whatever you would like when injecting it.
If you would like to just name the object the same as the name of the repository injected into the container you can alternatively do person: injectRepositories(). This will pass the person key to the computed property as the propertyName and since the name was left null when injecting, the objectName will instead be person. This matches the API produces similar results but is not the same as that of the Ember.inject.service and Ember.inject.controller API that is available as of Ember 1.10.
I don't think it's their intention for you to use it this way. The standard way is to use App.inject() if you're using plain ember, or do this in an initializer if you're using ember-cli.
In your case:
// Register the service
App.register('service:auth', {
findCurrentUser: function() {
// Do your magic
}
}, { instantiate: false });
App.inject('route', 'auth', 'service:auth');
Then in your model hook you can use this.auth.findCurrentUser();. You can also inject this into controllers and components if you need. Also note, to keep it clean, that you might want to include a separate module instead of defining your auth module in the service registration.
More info here:
http://emberjs.com/guides/understanding-ember/dependency-injection-and-service-lookup/#toc_dependency-injection-with-code-register-inject-code
NOTE
A service is also not a "special" thing. You can inject anything you want into pretty much anything you want using this method.

Make data from store app-wide available?

I've got a base-route encapsulating all other routes in my application.
I'd like to make the categories I retrieve from the store via this.store.find('category') available everywhere in my application.
I tried retrieving it in the base-controller with this code:
import Ember from 'ember';
export default Ember.ArrayController.extend({
// retrieve all categories as they are needed in several places
categories: function() {
return this.get('store').find('category');
}
});
and creating an alias from child-controllers via:
categories: Ember.computed.alias('controllers.base.categories')
but it gives this error-message:
Uncaught Error: Assertion Failed: The value that #each loops over must be an Array. You passed function () {
return this.get('store').find('category');
}
How can I solve my problem?
Make it a computed property.
categories: function() {
return this.get('store').find('category');
}.property()
Controllers are being deprecated in 2.0, so I'd look into using a service architecture instead of a base controller architecture.

Testing ember controllers that use mixins with DOM logic

How can I change this mixin, that I call in a controller, so that I am able to run tests.
import Ember from 'ember';
export default
Ember.Mixin.create({
scrollViewTo: function (navigateToId) {
Ember.run.scheduleOnce("afterRender", function () {
Ember.$('html,body').animate({scrollTop: Ember.$(navigateToId).offset().top}, 1000);
});
}
});
Any code in my test that results in scrollViewTo beign called result in this:
at http://localhost:7357/assets/test-loader.js:14:29: Cannot read property 'top' of undefined
I suspect I need to move all this logic to events in my view?