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.
Related
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.
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.
I have component that I want to provide data too. I am using Ember-CLI if that helps.
The component is a map that I am loading onto the page that I than want to place markers on. I used a component so I could use the didInsertElement method to get access to the element once it is ready.
export default Ember.Component.extend({
componentMap: '',
didInsertElement: function() {
navigator.geolocation.getCurrentPosition(position => {
//Initialize map...
this.populateMap();
});
},
populateMap: function() {
//Get store
var store = this.get('parentView.targetObject.store');
console.log(store);
//Search Store
var data = store.find('restaurant');
//Where is the data?!
data.map(item => {
console.log(item.get('name'));
});
}
});
I am having an issues getting the data from a store. I have seen a couple methods, here shows two different methods. First being the this.get('parentView.targetObject.store') or this.get('targetObject.store'). I have also tried the {{component store=store}} method, but that was not working for me either. This might have to do with a fundamental lack of understanding of data flow in an ember app.
I am using Ember CLI and I am wondering if it has anything to do with the context of this inside modules?
If I am way off base as to how I should do this, please let em know!
UPDATE: Adding route, controller and template for context.
Route
export default Ember.Route.extend({
model: function() {
return this.store.find('restaurant');
}
});
Controller
export default Ember.Controller.extend({
actions: {
add: function() {
var $addForm = $('.add-form');
$addForm.show();
}
}
});
Template (index.hbs, which is output in application.hbs {{outlet}})
{{main-map store=store}}
Thanks.
What is happening is as follows:
The model associated with your control is populated as an array of restaurants, not a single map or anything of that sort.
return this.store.find('restaurant'); returns an array of restaurants from the store which ultimately populates the model of your controller.
If you want access to the data contained within your model in your component, you should pass the model as an argument into your component.
So, you can pass the array of restaurants as follows (rename the property as appropriate):
{{main-map data=model}}
Or, if in theory you wanted to display a component for each restaurant:
{{#each restaurant in model}}
{{your-component name=restuarant.name}}
{{/each}}
In my Ember application, I wanted to have a controller wrapping a collection of models, that I could inject into other controllers.
I've set it up like this:
app/controllers/zones.js:
export default Ember.Controller.extend({
model: function () {
return this.store.find('zone');
}
});
app/controllers/zones/index.js:
export default Ember.Controller.extend({
needs: ['zones'],
zones: Ember.computed.alias('controllers.zones.model')
});
This seems like it ought to work, but unfortunately, it doesn't. I get this error in my JavaScript console (in the browser):
Error: Assertion Failed: The value that #each loops over must be an Array. You passed function () {
"use strict";
return this.store.find('zone');
}
I've tried moving stuff around, or using ArrayController rather than just Controller, but I still get this error.
This makes very little sense to me, any ideas?
Here is the thing, model is the function need to resolve the model for route not controller. That model then automatically injected to controllers model property.
Ember way
In ember way I would suggest move this model definition to the route for controller. Something like this.
export default Ember.Route.extend({
model: function (param) {
return this.store.find('zone');
}
});
This is the Ember way of doing thing. Resolve model in route then have controller to filter / decorate it.
I would also suggest using ArrayController instead of Controller since you are handling number of models.
The other way
Again if you want to have model resolved in controller. I warn you its not the Ember way but you can do it something like this -
export default Ember.ArrayController.extend({
//dont override the model property
mydata: function () {
return this.store.find('zone');
}.property('model'),
});
I figured out the problem – I just needed to set my overridden model implementation to be a property, like this:
app/controllers/zones.js (injected controller):
export default Ember.Controller.extend({
model: function () {
return this.store.find('zone');
}.property() // `.property()` turns the function into an iterable object for use in templates and the like.
});
The main controller is still the same.
app/controllers/zones/index.js (active route controller):
export default Ember.Controller.extend({
needs: ['zones'],
zones: Ember.computed.alias('controllers.zones.model')
});
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');
}