How can I pass dynamic arguments to the component helper.
In production, I am iterating over a json hash that will render different components with different attrs each time.
I have a scaled down example in this jsbin.
I am returning this json hash:
columns: Ember.computed('people.[]', function() {
return this.get('people').map(function(person){
return {
component: 'x-person',
attrs: {
person: person
}
};
});
})
That I then iterate over and try and pass the arguments into the component:
{{#each columns as |column|}}
{{component column.component column.attrs}}
{{/each}}
The component is getting created but the attributes are not set.
How can I properly pass the arguments to the component helper?
If you want more flexibility like not having to update the way each attr is used in the dynamic component, I suggest this:
Create an initializer to extend Ember.Component
import Ember from 'ember';
var Component = {
loadAttrsHash: Ember.on('didReceiveAttrs',function(attrs){
if(this.get('attrsHash')){
let hash = this.get('attrsHash');
Object.keys(hash).forEach(key=>{
this.set(key,hash[key]);
});
}
}),
};
export function initialize(/* application */) {
Ember.Component = Ember.Component.extend(Component);
}
export default {
name: 'component',
initialize
};
Then you can create dynamic components with an attrs hash
{#each columns as |column|}}
{{component column.component attrsHash=column.model}}
{{/each}}
Then you can access attrsHash.person, simply with {{person}} in the child component just as if it was passed directly.
This way, the component is more durable as it can be used with direct attributes as well as attrsHash, and should update whenever attrsHash changes.
You have to follow some convention to pass all data to dynamic components using model property.
Columns:
columns: Ember.computed('people.[]', function() {
return this.get('people').map(function(person){
return {
component: 'x-person',
model: {
person: person
}
};
});
})
Template:
{#each columns as |column|}}
{{component column.component model=column.model}}
{{/each}}
And in all components that are dynamically created you should access properties via model attribute passed to component. So, if something were color before it should now become model.color etc.
Related
I am having an issue with transforming my model object in an Ember application.
I have a calendar route which defines a model like so.
this.store.findAll('competency');
I am trying to transform the data returned from this in a controller method calendarItems which is defined as a property.
What I would like to do is take my model and call the map function on it, transform the object and return it back as a property.
I have changed the controller to a ArrayController to allow Enumberable methods to be called but still no luck. The function is present when I can this.get('model').map
Any ideas where I am tripping up? Maybe my approach is way out but basically I just want to transform am model and present that data to the template.
Thanks in advance!
Ryan
Edit
This is my transform logic but the array that is logged in the template is just [].
export default Ember.Controller.extend({
calendarItems: Ember.computed('model', function () {
return this.get('model').map(function(competency) {
return 1;
}, this);
})
});
console.log(this.get('model').get('length'));
returns 0 which made me assume I have no model items from the store to iterate one which would explain the blank array. However if I place an {{#each}} in the template to print out the models I am receiving 3 models.
Edit
Calendar Route
model:
return this.store.findAll('competency');
}
Calendar Controller
calendarItems: Ember.computed('model', function () {
return this.get('model').map(function(competency) {
return 1;
}, this);
})
Calendar Template
{{log calendarItems}}
You have two major options.
First, a computed property in model. Example from docs:
export default DS.Model.extend({
firstName: DS.attr(),
lastName: DS.attr(),
fullName: function() {
return this.get('firstName') + ' ' + this.get('lastName');
}.property('firstName', 'lastName')
});
In template you may use it like this:
{{#each model as |item|}}
{{item.fullName}}
{{/each}}
Second, computed property in controller, based on model. Example of a controller:
import Ember from 'ember';
export default Ember.Controller.extend({
something: Ember.computed('model', function () {
var ret = [];
this.get('model').forEach(function(item) {
ret.push(item.get('something'));
}, this);
return ret;
})
});
Example of template:
{{#each something as |item|}}
{{item}}
{{/each}}
Also, you can define a helper, pass each item to it and return what you need. More about this method here
Every google result is about an ArrayController sorting. Need a sorting mechanism without using ArrayController.
There is a model where there are sort params. Like say 'sortOrder' as one of the properties in the model (which will be from a back end).
Will be rendering this model using #each but this should do the iteration based on the sortOrder property and not the model's ID property.
In Ember 2.0 SortableMixin is deprecated and is on its way out too.
In the Controller (not the ArrayController) you may define a new computed property like SortedUsers1,2,3 below:
export default Ember.Controller.extend({
sortProps: ['lastName'],
sortedUsers1: Ember.computed.sort('model', 'sortProps'),
sortedUsers2: Ember.computed.sort('content', 'sortProps'),
sortedUsers3: Ember.computed('content', function(){
return this.get('content').sortBy('lastName');
})
});
The assumption above is that the model itself is an array of users with lastName as one of user properties. Dependency on 'model' and 'content' look equivalent to me. All three computed properties above produce the same sorted list.
Note that you cannot replace 'sortProps' argument with 'lastName' in sortedUsers1,2 - it won't work.
To change sorting order modify sortProps to
sortProps: ['lastName:desc']
Also if your template is in users/index folder then your controller must be there as well. The controller in users/ would not do, even if the route loading model is in users/.
In the template the usage is as expected:
<ul>
{{#each sortedUsers1 as |user|}}
<li>{{user.lastName}}</li>
{{/each}}
</ul>
Here is how I manually sort (using ember compare)
import Ember from "ember";
import { attr, Model } from "ember-cli-simple-store/model";
var compare = Ember.compare, get = Ember.get;
var Foo = Model.extend({
orderedThings: function() {
var things = this.get("things");
return things.toArray().sort(function(a, b) {
return compare(get(a, "something"), get(b, "something"));
});
}.property("things.#each.something")
});
You just need to include a SortableMixin to either controller or component and then specify the sortAscending and sortProperties property.
Em.Controller.extend(Em.SortableMixin, {
sortAscending: true,
sortProperties: ['val']
});
Here is a working demo.
In situations like that, I use Ember.ArrayProxy with a Ember.SortableMixin directly.
An ArrayProxy wraps any other object that implements Ember.Array
and/or Ember.MutableArray, forwarding all requests. This makes it very
useful for a number of binding use cases or other cases where being
able to swap out the underlying array is useful.
So for example, I may have a controller property as such:
sortedItems: function(){
var items = Ember.ArrayProxy.extend(Ember.SortableMixin).create({content: this.get('someCollection')});
items.set('sortProperties', ['propNameToSortOn']);
return items;
}.property()
Like so: JSBin
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}}
I would like to do something like this
App.FooComponent = Ember.Component.extend({
tagName: 'ul',
propertyPath: '',
data: []
});
And in foo-component.hbs (this obviously doesn't work):
{{#each item in data}}
<li>{{propertyPath}}</li>
{{/each}}
I would use the component as follows:
{{foo-component data=content propertyPath='name'}}
where content is a collection of objects, each with a 'name' property.
I've tried to do it within the component by using a computed property and binding to that:
itemNames: function() {
var propertyPath = 'data.#each.' + this.get('propertyPath');
return this.get(propertyPath);
}.property(???)
But that has the problem of how to set the dependent key so the property is recomputed.
I would use something like this:
items: Ember.computed.map('data', function(dataObject) {
return {
name: dataObject.name,
otherProp: dataObject.otherProp
};
}).property('data.#each.{name, otherProp}')
You declare a computed property that maps the objects in data to some other objects. In your function, you can just return an object with the properties you need (I used name and otherProp). Then, to make sure that the property watches the name and otherProp properties on every object, I override the dependent keys with the .property() call. (By default, Ember.computed.map will call .property('data.#each') for you.)
Then, in your template:
{{#each items}}
<li>{{name}}</li>
{{/each}}
EDIT:
The dynamic property name is a little weird. One way you could do it is to declare a computed property on the class at creation time.
init: function() {
var propertyPath = this.get('propertyPath');
this.reopen({
propertyPathUpdater: function() {
this.notifyPropertyChange('data');
}.observes('data.#each.' + propertyPath)
});
}
Depending on how your values update, you might have to mess with it a little, but hopefully you get the idea. The idea being that Ember doesn't like dynamic properties. :)
i have a component and when user click on component it add some value to store,i try to use this way but i get an error :
OlapApp.MeasureListItemComponent = Ember.Component.extend({
tagName: 'li',
isDisabled: false,
attributeBindings: ['isDisabled:disabled'],
classBindings: ['isDisabled:MeasureListItemDisabled'],
actions: {
add: function(measure) {
var store = this.get('store');
store.push('OlapApp.AxisModel', {
uniqueName: measure.uniqueName,
name: measure.name,
hierarchyUniqueName: measure.hierarchyUniqueName,
type: 'row',
isMeasure: true,
orderId: 1
});
}
}
});
and this is error:
Uncaught TypeError: Cannot call method 'push' of undefined MeasureListItemComponent.js:18
is it posible to push record to store from component? why i cant access to store ?
my model name is 'AxisModel' and application namespace is 'OlapApp'
Since Ember v1.10, the store can be injected to components using initializers, see: http://emberjs.com/blog/2015/02/07/ember-1-10-0-released.html#toc_injected-properties:
export default Ember.Component.extend({
store: Ember.inject.service()
});
In a component the store does not get injected automatically like in route's or controller's when your app starts. This is because components are thought to be more isolated.
What follows below is not considered a best practice. A component should use data passed into it and not know about it's environment. The best way to handle this case would be using sendAction to bubble up what you want to do, and handle the action with the store in the controller itself.
#sly7_7 suggestion is a good one, and if you have a lot of components from where you need access to the store then it might be a good way to do it.
Another approach to get to your store could be to get the store your component surrounding controller has reference to. In this case it doesn't matter which controller this is because every controller has already a reference to the store injected into it. So now to get to your store could be done by getting the component's targetObject which will be the controller surrounding the component and then get the store.
Example:
OlapApp.MeasureListItemComponent = Ember.Component.extend({
...
actions: {
add: function(measure) {
var store = this.get('targetObject.store');
...
}
}
});
See here for a working example.
Hope it helps.
Update in response to your comment having nested components
If for example you child component is only nested one level then you could still refer to parent's targetObject using parentView:
App.ChildCompComponent = Ember.Component.extend({
storeName: '',
didInsertElement: function() {
console.log(this.get('parentView.targetObject.store'));
this.set('storeName', this.get('parentView.targetObject.store'));
}
});
Updated example.
Since Ember 2.1.0
export default Ember.Component.extend({
store: Ember.inject.service('store'),
});
before Ember 2.1.0 - dependency injection way
App.MyComponent = Ember.Component.extend({
store: Ember.computed(function() {
return this.get('container').lookup('store:main');
})
});
before Ember 2.1.0 - controller way
You can pass store as property from controller:
App.MyComponent = Ember.Component.extend({
value: null,
store: null,
tagName: "input",
didInsertElement: function () {
if (!this.get('store')) {
throw 'MyComponent requires store for autocomplete feature. Inject as store=store'
}
}
});
Store is available on each controller. So in parent view you can include component as follows:
{{view App.MyComponent
store=store
class="some-class"
elementId="some-id"
valueBinding="someValue"
}}
Passing properties to component is documented here
The current ember-cli way to do this appears to be with an initializer. Very similar to the #Sly7_7 answer.
To get a basic model use:
ember g initializer component-store-injector
Then edit this to:
// app/initializers/component-store-injector.js
export function initialize(container, application) {
application.inject('component', 'store', 'store:main');
}
export default {
name: 'component-store-injector',
initialize: initialize
};
I believe this will add the store to all components.
Stolen from https://github.com/ember-cli/ember-cli-todos
I don't know if components are intended to be used such a way. But if you want, I think you can declare an initializer and inject the store into all components.
Ember.onLoad('OlaApp', function(OlaApp) {
OlapApp.initializer({
name: 'injectStoreIntoComponents',
before: 'registerComponents',
initialize: function(container, application){
container.register('store:main', App.Store);
container.injection('component', 'store', 'store:main');
}
})
});
Here is a contrived but working example: http://jsbin.com/AlIyUDo/6/edit
The store can be injected with help of dependency injection.
Example
import Ember from 'ember';
export default Ember.Component.extend({
/**
*
*/
store: Ember.inject.service(),
/**
* Initialize the component.
*/
init() {
this.initialize();
this._super();
},
/**
* Initialize the properties and prerequisites.
*/
initialize() {
// Set the component properties
this.todos().then((data) => {
this.set('todoEntries', data);
});
},
/**
* Returns the todo entries.
*
* #returns {*|Promise|Promise.<T>}
*/
todos() {
const store = this.get('store');
return store.findAll('todo');
},
});
Another way which no one has yet mentioned is to simply pass controller.store to the component e.g.
{{my-awesome-component store=controller.store}}