Ember updating array does not update template - ember.js

I'm going mad with this problem:
I have a component with a template that uses a {{#each}} loop to iterate an array of comments;
comments: Ember.computed('commentsModel', function() {
return this.get('commentsModel').toArray();
}),
actions: {
addOne: function() {
this.set('comments', [{content: 'agent k'}]);
}
}
The component is called like this:
{{photo-comments commentsModel=model.comments}}
Where model.comments is returned by the model hook of the route;
When I click the button that triggers "addOne" event, the template updates correctly, all the comments are removed and only the new comment "agent k" is shown;
but if I want to ADD a comment into the comments array, instead of overwriting the whole array, it doesn't work;
I get no errors but the template simply ignores it and does not updates!
I tried with push:
actions: {
addOne: function() {
this.set('comments', this.get('comments').push({content: 'agent k'});
}
}
with pushObject:
actions: {
addOne: function() {
this.set('comments', this.get('comments').pushObject({content: 'agent k'});
}
}
nothing! Since this is driving me crazy, can someone tell what's wrong with this?
Template:
{{log comments}}
{{#each comments as |comment|}}
<div class="card">
<div class="card-content">
{{comment.username}}<br>
{{comment.content}}
</div>
<div class="card-action">
{{comment.createdAt}}
</div>
</div>
{{/each}}

I think the underlying issue here is that you're misusing the Ember.computed helper.
Your component should define a default value for the comments property, if there is one:
comments: null,
actions: {
addOne: function() {
this.get('comments').pushObject({content: 'agent k'});
}
}
Your template should call the component by passing the model value:
{{photo-comments comments=model.comments}}
Assuming of course, that model.comments is available because it was retrieved by the route's model hook.
In your example code, you would have been attempting to push a new object to a computed property wrapping an array representation of a model, rather than pushing a new value to the model itself.

Related

Filtering an array in ember

Ok so I'm fairly new to programing, I know how to run a filter on a JSON Array but I cant seem to figure it out when I'm pulling the data from firebase and viewing it in an Ember app.
this is my route.js code:
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.findAll('user');
}
});
This is my template.hbs code the href="#!" is the generic from materialize for the dropdown buttons:
<div class="list-wrapper col-xs-10 col-xs-offset-1">
<div class="col-xs-12 button-div">
{{#md-btn-dropdown text='Filter' class="col-xs-2" belowOrigin=true}}
<li>Female</li>
<li>Male</li>
<li>All</li>
{{/md-btn-dropdown}}
</div>
{{#each model as |info|}}
<div class="col-xs-3 user-card">
<div class="card-info">
<ul>
<li>Last Name- {{info.lastName}}</li>
<li>First Name- {{info.firstName}}</li>
<li>Gender- {{info.gender}}</li>
<li>{{info.email}} </li>
</ul>
</div>
</div>
{{/each}}
</div>
{{outlet}}
This is my controller.js code which I no is all wrong:
import Ember from 'ember';
export default Ember.Controller.extend({
customFilter: function(gender) {
return function(el) {
var r = el.user;
return r.gender === gender;
};
}
});
and this is my model:
import DS from 'ember-data';
import Ember from 'ember';
export default DS.Model.extend({
lastName: DS.attr('string'),
firstName: DS.attr('string'),
gender: DS.attr('string'),
email: DS.attr('string')
});
I've searched high and low and I'm sure I'm just missing something basic and stupid. What I want is for the dropdown menu to be able to filter and display only female, male or all. Again I'm new to this stuff so I apologize if this is a pretty basic thing. Thank You
What your missing is an action that updates your controller when an item in the dropdown is actually selected.
Some helpful reading:
Triggering changes with actions
Computed Properties
Here's how to put actions in your dropdown component
{{#md-btn-dropdown text='Filter' class="col-xs-2" belowOrigin=true}}
<li><a {{action "filterUpdated" "female"}}>Female</a></li>
<li><a {{action "filterUpdated" "male"}}>Male</a></li>
<li><a {{action "filterUpdated"}}>All</a></li>
{{/md-btn-dropdown}}
In your controller you then need to handle this action like so:
import Ember from 'ember';
export default Ember.Controller.extend({
// the people property is an alias of the model object
// which essentially makes people a synonym for model
// read more http://emberjs.com/api/classes/Ember.computed.html#method_alias
people: Ember.computed.alias('model'),
// holds the currently selected gender, e.g., "female". A null value indicates there is no filter.
currentFilter: null,
/*
filteredPeople is a computed array containing people models.
The array is recomputed every time the model changes or the currentFilter changes,
see the .property() bit at the end.
read more: http://emberjs.com/api/classes/Ember.computed.html#method_filter
*/
filteredPeople: Ember.computed.filter('people', function(person/*, index, array*/) {
// this function is passed each item in the model array, i.e., the person argument
// there's no need to use the index nor array arguments, so I've commented them out
if(this.get('currentFilter') === null) {
// when the filter is null, we don't filter any items from the array
return true;
} else {
// otherwise only return true if the gender matches the current filter
return person.gender === this.get('currentFilter');
}
}).property('people', 'currentFilter'),
actions: {
filterUpdated: function (value) {
if (Ember.isEmpty(value)) {
// an empty value means we should clear the filter
this.set('currentFilter', null);
}
else {
this.set('currentFilter', value);
}
}
}
});
Finally, edit your template to change
{{#each model as |info|}}
to
{{#each filteredPeople as |info|}}
Also at a meta level, don't apologize for asking questions! Everyone is new at something at somepoint, and often asking is the best way to learn. That's what stackoverflow is all about :)
Something like this would work:
gender: 'All',
filteredModel: Ember.computed.filter('model', function(person) {
return person.gender === this.get('gender');
}).property('gender'),
this assumes that it starts on all, and then when the dropdown changes the value of gender, then the filteredModel will get updated. You can then in your hbs file change the result to:
{{#each filteredModel as |info|}}

Ember action : set property of only target of #each

I have a few actions that I'm placing on each item in a loop. Currently the action reveals all of the book-covers, instead of just one I want to target.
http://guides.emberjs.com/v2.0.0/templates/actions
Looks like I can pass a parameter, but I'm not sure of the syntax.
I've done this before in earlier version and remember using this or should it be
{{action 'showCover' book}} ... ?
Controller
import Ember from 'ember';
export default Ember.Controller.extend( {
actions: {
showCover(book) { // ?
this.set('coverVisible', true); // or
this.toggleProperty('coverVisible');
},
...
}
});
other thoughts...
actions: {
showCover(book) {
// currently this is just setting the *route in general* to coverVisible:true - which is not what I want
this.set('coverVisible', true);
// I can see this class - the route...
console.log(this);
// I can see the model of this route...
console.log(this.model);
// and I can see the book object...
console.log(book);
// but how do I set just the book object???
// I would expect book.set('property', true) etc.
console.log(book.coverVisible);
console.log(this.coverVisible);
}
}
Template
{{#each model as |book|}}
<li class='book'>
<article>
{{#if book.coverVisible}}
<figure class='image-w book-cover'>
<img src='{{book.cover}}' alt='Cover for {{book.title}}'>
</figure>
{{/if}}
...
{{#if book.cover}}
{{#unless book.coverVisible}}
<div {{action 'showCover'}} class='switch show-cover'>
<span>Show cover</span>
</div>
{{/unless}}
{{/if}}
{{/each}}
ALSO - please suggest a title for this if you can think of a more succinct one.
http://ember-twiddle.com/f44a48607738a0b9af81
#sheriffderek, You have already provided the solution in your question itself. You can pass the additional parameters after the action name. Something like:
<button {{action "showCover" book}}>Show Cover </button>
Working example using ember-twiddle: http://ember-twiddle.com/e7141e41bd5845c7a75c
You should be calling book.set('coverVisible', true); as you are wanting to set the property on the book itself.
actions: {
showCover: function(book) {
book.set('coverVisible', true);
}

In Ember, is there a way to update a component without a full re-render/route transition

I have a map application. When clicking on a pin, I want to update a "teaser" component in the template by supplying e.g. a query parameter previewId without triggering a full re-render of the route, because that would re-initialize the map and center it on the init position, take a lot of time, in short: is ugly and bad user experience.
So what I have is a map route:
export default Ember.Route.extend({
model: function () {
return this.store.findAll('map-object-proxy');
}
});
a map controller, where I handle the query params:
export default Ember.Controller.extend({
queryParams: ['previewId'],
previewId: null,
previewObject: Ember.computed('previewId', function () {
return this.store.findRecord('map-object', 1);
})
});
and a map-panel component which gets handed the previewObject from the map.hbs template:
<div id="map"></div>
<!-- ... -->
<div class="row" id="teaser-header">
<div class="col-xs-12">{{previewObject.someProperty}}</div>
</div>
map.hbs has this Handlebars markup:
{{map-panel elementId="map-panel" objectProxies=model previewObject=previewObject}}
Sorry, I've not yet quite come to terms with ember's component architecture, even more so as controllers will be deprecated soon and somehow seem to act like a fifth wheel.
Thanks!
You can "refresh" your component by wrapping it like this:
{{#if refresh}}
//component template
{{/if}}
and then an Action to hide and show the component, forcing it to render again.
const _this = this;
this.set('refresh', false);
Ember.run.next(function () {
_this.set('refresh', true);
});

Ember: Update ObjectController property from ArrayController action?

Disclaimer: I'm quite new to Ember. Very open to any advice anyone may have.
I have a action in a ArrayController that should set an ObjectController property. How I can access the right context to set that property when creating a new Object?
Here is abbreviated app code show my most recent attempt:
ChatApp.ConversationsController = Ember.ArrayController.extend({
itemController: 'conversation',
actions: {
openChat: function(user_id, profile_id){
if(this.existingChat(profile_id)){
new_chat = this.findBy('profile_id', profile_id).get('firstObject');
}else{
new_chat = this.store.createRecord('conversation', {
profile_id: profile_id,
});
new_chat.save();
}
var flashTargets = this.filterBy('profile_id', profile_id);
flashTargets.setEach('isFlashed', true);
}
},
existingChat: function(profile_id){
return this.filterBy('profile_id', profile_id).get('length') > 0;
}
});
ChatApp.ConversationController = Ember.ObjectController.extend({
isFlashed: false
});
The relevant template code:
{{#each conversation in controller itemController="conversation"}}
<li {{bind-attr class="conversation.isFlashed:flashed "}}>
<h3>Profile: {{conversation.profile}} Conversation: {{conversation.id}}</h3>
other stuff
</li>
{{/each}}
I don't see why you need an object that handles setting a property for all the elements in your list. Have each item take care of itself, this means components time.
Controllers and Views will be deprecated anyway, so you would do something like:
App.IndexRoute = Ember.Route.extend({
model: function() {
return [...];
}
});
App.ConversationComponent = Ember.Component.extend({
isFlashed: false,
actions: {
// handle my own events and properties
}
});
and in your template
{{#each item in model}}
{{conversation content=item}}
{{/each}}
So, whenever you add an item to the model a new component is created and you avoid having to perform the existingChat logic.
ArrayController and ItemController are going to be depreciated. As you are new to Ember I think that it would be better for you not to use them and focus on applying to coming changes.
What I can advice you is to create some kind of proxy object that will handle your additional properties (as isFlashed, but also like isChecked or isActive, etc.). This proxy object (actually an array of proxy objects) can look like this (and be a computed property):
proxiedCollection: Ember.computed.map("yourModelArray", function(item) {
return Object.create({
content: item,
isFlashed: false
});
});
And now, your template can look like:
{{#each conversation in yourModelArray}}
<li {{bind-attr class="conversation.isFlashed:flashed "}}>
<h3>Profile: {{conversation.content.profile}} Conversation: {{conversation.content.id}}</h3>
other stuff
</li>
{{/each}}
Last, but not least you get rid of ArrayController. However, you would not use filterBy method (as it allows only one-level deep, and you would have the array of proxy objects, that each of them handles some properties you filtered by - e.g. id). You can still use explicit forEach and provide a function that handles setting:
this.get("proxiedCollection").forEach((function(_this) {
return function(proxiedItem) {
if (proxiedItem.get("content.profile_id") === profile_id) {
return proxiedItem.set("isFlashed", true);
}
};
})(this));

Creating a map property and enumerating its properties in a template?

I have poured through a ton of documentation and I can't seem to find an answer to a very basic question. I have a component that needs to store a map (key/value) as a property:
App.SimpleTestComponent = Ember.Component.extend({
data: Ember.A(),
actions: {
add: function() {
this.get('data').set('test', 'value');
}
}
});
The template for the component looks like this:
<script type="text/x-handlebars" data-template-name="components/simple-test">
{{#each item in data}}
<p>
<strong>{{item.key}}:</strong>
{{ item.value}}
</p>
{{/each}}
<button {{action 'add'}}>Add</button>
</script>
However, this doesn't work. No items are displayed after clicking the button and the problem seems to be with the {{#each}} block. How do I correctly enumerate over the data property?
Ember.A() is shorthand for Ember.NativeArray. This is why your
code isn't working, you're also calling .set which is a method
inherited from Ember.Observable. So what you're really doing is
just setting an object property on the array rather than its
content.
What you probably want is Ember.Map which is an internal class
but many developers use it anyways. You will still need to return an
array of objects as the following example:
App.SimpleTestComponent = Ember.Component.extend({
// Shared map among all SimpleTestComponents
map: Ember.Map.create(),
// Or per component map
init: function() {
this.set('map', Ember.Map.create());
},
data: function() {
// this doesn't have to be locally scoped...
var arr = Ember.A();
this.get('map').forEach(function(key, value) {
arr.addObject({key: key, value: value});
});
return arr;
}.property('map'),
actions: {
add: function() {
this.get('map').set('test', 'value');
}
}
});
This doesn't really work when you have keys that have multiples
values simply becauses Ember.Map always overwrites on .set
If performance is of concern and you would like to have multiple
values per key then you will need to implement your own
map class with and a handlebars helper to display it.