Using Ember, I have an object in my controller that looks like this:
ItemDetails: {
Retrieved: '2016-07-09',
User: 'someuser',
Items: [
{
Name: 'Item',
Price: 'value'
},
// snip
]
}
This is set in the controller init using this.set('ItemDetails', jsonObject).
The template is rendered as
{{#each ItemDetails.Items as |Item|}}
{{Item.Name}}
{{Item.Price}}
<input type="button" {{action "Remove" Item.Name }} value="Remove" class="btn btn-primary" />
{{/each}}
The action for Remove is defined thusly:
Remove: function(itemName) {
var details = this.get('ItemDetails');
var index = -1;
for(var i = 0; i < details.Facets.length; i++) {
if(details.Items[i].Name == itemName) {
index = i;
break;
}
}
if(index > -1) {
details.Items.splice(index, 1)
this.set('Search', details);
}
}
Now using the Chrome debugger I can confirm that the array is correct and the correct item is being removed. Subsequent calls in also show the array internally looks like it should.
However, the template does not reflect this change, i.e. the Item is still shown along with it's remove button. What am I doing wrong that the template is not being updated?
There are certain public methods which should be used to modify the array so that changes are observable. Here is the link:
http://emberjs.com/api/classes/Ember.MutableArray.html
In your case, looks like the logic is correct. However, instead of using .splice() you should use .removeAt(index). Or, there are other simpler ways to solve it like using .removeObject().
For example:
http://emberjs.jsbin.com/zezera/edit?html,css,js,output
Hope this helps. Thanks
Related
I have 2 collections: TestPlans and Users
TestPlans has an array called "assigned" which contains user _ids.
Users has an avatarUrl string
I need to iterate over each testPlan and furthermore each assignedUser and grab an avatarUrl for that user:
Something like {{#each testPlan}} {{#each assignedUser}} {{avatarUrl}}
the helper for testPlans I have:
testPlan: function() {
var testPlans = TestPlans.find({}, { sort: {createdAt: -1}}).fetch();
return testPlans;
}
the helper for assignedUser I have:
assignedUser: function(){
var assignedUsers = TestPlans.findOne({_id: this._id}).assigned;
return assignedUsers;
}
I'm not sure how call the avatarUrl for each assignedUser though.
The query would be
Users.findOne({_id: thisUserId}).profile.avatarUrl;
Just not sure how to pass the userid exactly.
I would change your assignedUser template helper to accept a user ID parameter and then return the Avatar URL
assignedUser: function(userId) {
return Users.findOne({_id: userId}).profile.avatarUrl
};
Then your template code would look something like this.
{{#each tp in testPlan}}
{{#each au in tp.assignedUser}}
{{avatarUrl au._id}}
{{/each}}
{{/each}}
Also, I like to use each...in instead of just each because it makes it more explicit what is actually happening
if assignedUsers is an array of userIds, which might be the best implementation in this case..
//html
{{#each testPlan}}
{{#each assignedUser}}
{{profile.avatarUrl}}
//you can do this because {{this}} is the user object
//{{profile.avatarUrl}} is the same as {{this.profile.avatarUrl}}
{{/each}}
{{/each}}
//js
assignedUser: function(){
var assignedUsers = TestPlans.findOne({_id: this._id}).assigned;
var usersCursor = Users.find({_id:{$in:assignedUsers}}});
return usersCursor;
}
I'm having a problem with an Ember computed property: It seems as though once the template gets updated, it stops listening to changes in the dependency property. But I don't understand why that would be the case.
Here's my template:
{{input type="text" value=searchText placeholder="Search for users..."}}
<br>
<ul>
{{#each user in searchResults}}
<li>{{user.Handle}}</li>
{{else}}
<p>No users found.</p>
{{/each}}
</ul>
And below is my controller:
App.AutocompleteController = Ember.Controller.extend({
searchText: null,
searchResults: function () {
var searchText = this.get('searchText');
var data = { 'searchTerm' : searchText };
var self = this;
alert("Calling searchResults");
if (!searchText) { return; }
if (searchText.length < 2) { return; }
$.get('/searchUsers', data).then(function (response) {
self.set("searchResults", JSON.parse(response));
}); //end then
}.property('searchText')
});
The first time searchResults actually makes an AJAX call and returns data, the autocomplete results get populated, but after that, searchResults doesn't get called again until I refresh the client.
NEVER MIND. It's right there in the code. On a successful ajax code, I'm reassigning searchResults to a static array, no longer a function.
Returning won't work out of a .then, however, so I still need a workaround for returning the data. For that, I will add a more traditional Ember event listener to call my 'search' function which will reset the property of 'searchResults'
New template:
{{input type="text" value=searchText placeholder="Search for users..." action=search on='change'}}
<ul>
{{#each user in searchResults}}
<li>{{user.Handle}}</li>
{{else}}
<p>No users found.</p>
{{/each}}
</ul>
New controller:
App.AutocompleteController = Ember.Controller.extend({
searchText: null,
search: function () {
var searchText = this.get('searchText');
var data = { 'searchTerm' : searchText };
var self = this;
if (!searchText) { return; }
if (searchText.length < 2) { return; }
else {
$.get('/searchUsers', data).then(function (response) {
self.set("searchResults", JSON.parse(response));
}); //end then
}
}.property('searchText')
});
The problem stems from the inability to do comparisons in the template:
{{#if model.input.type == 'select'}}
The option around this, that I'm aware of so far, is to use a custom Handlebars helper. This would allow me to loop through the model and build out the HTML, but this doesn't allow me to bind attributes, plus just it seems like too much work.
1. Can't bind attributes, and I don't want to build out HTML with a JS string.
Handlebars.registerHelper('formBuilder', function()
{
var html_string = '';
for(var q=0; q<this.content.questions.length; q++)
{
if(this.content.questions[q].type.match(/select/i))
{ /* build out html with model propeties */ }
if(this.content.questions[q].type.match(/checkbox|radio/i))
{ /* build out html with model propeties */ }
}
return new HandleBars.SafeString(html_string);
}
A better option would be if I could track the index in my helper, and return a Handlebars expression, all without breaking the template's loop:
2. Can't pass in loop index or return expressions.
{{#each question in model.questions}}
{{#formBuilder parent.index this}}
/* uses #index to iterate through its children and return a Handlebars
expressions for either select or checkbox html */
{{/formBuilder}}
{{/each}}
...and the formBuilder helper would look something like this:
Handlebars.registerHelper('formBuilder', function(q_index, model)
{
if(model[q_index].type == 'select')
{
return Handlebars.compile({{select action 'myAction' type='model[q_index].type'}});
}
if(model[q_index].type.match(/checkbox|radio/i))
{
return Handlebars.compile({{input action 'myAction' type='model[q_index].type'}});
}
});
wich would return controll to the outer loop.
How is this problem solved in Ember?
Looking at what you are trying to achieve more closely, I would suggest you use a view instead of a handlebars helper to create either a checkbox or a select field. The way you would do that would be
{{#each question in questions}}
{{view QuestionView contentBinding=this}}
{{/each}}
and in your questionView,
App.QuestionView = Em.View.extend({
defaultTemplate: (function() {
if(this.get('type') === 'select') {
return Em.Handlebars.compile(//select box string goes here);
}
else {
return Em.Handlebars.compile(//checkbox html);
}
})()
});
I have a list of items I'm trying to display with Ember. For each of these items, I'd like to be able to dynamically select the view type to use to display it based on a "message_type" field in each model.
I currently have something like this, which totally sucks and is not scalable:
{{#each message in controller}}
{{#if message.isImage}}
{{view App.ImageMessageView}}
{{/if}}
....
{{#if message.isVideo}}
{{view App.VideoMessageView}}
{{/if}}
{{/each}}
How can you dynamically select a view based on a model's field in Ember?
Here is a similar question that showed 2 ways to do this: Collection of objects of multiple models as the iterable content in a template in Ember.js
rendering items based on a property or their type
I know of two ways to do this:
add a boolean property to each object and use a handlebars {{#if}} to check that property and render the correct view
extend Ember.View and use a computed property to switch which template is rendered based on which type of object is being rendered (based on Select view template by model type/object value using Ember.js)
Method 1
JS:
App.Post = Ember.Object.extend({
isPost: true
});
App.Bookmark = Ember.Object.extend({
isBookmark: true
});
App.Photo = Ember.Object.extend({
isPhoto: true
});
template:
<ul>
{{#each item in controller.stream}}
{{#if item.isPost}}
<li>post: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isBookmark}}
<li>bookmark: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isPhoto}}
<li>photo: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{/each}}
</ul>
Method 2
JS:
App.StreamItemView = Ember.View.extend({
tagName: "li",
templateName: function() {
var content = this.get('content');
if (content instanceof App.Post) {
return "StreamItemPost";
} else if (content instanceof App.Bookmark) {
return "StreamItemBookmark";
} else if (content instanceof App.Photo) {
return "StreamItemPhoto";
}
}.property(),
_templateChanged: function() {
this.rerender();
}.observes('templateName')
})
template:
<ul>
{{#each item in controller.streamSorted}}
{{view App.StreamItemView contentBinding=item}}
{{/each}}
</ul>
JSBin example - the unsorted list is rendered with method 1, and the sorted list is rendered with method 2
It would probably need some more thought, but here is what I have come up with quickly:
var get = Ember.get,
isGlobalPath = Ember.isGlobalPath,
normalizePath = Ember.Handlebars.normalizePath;
var getProp = function (context, property, options) {
if (isGlobalPath(property)) {
return get(property);
} else {
var path = normalizePath(context, property, options.data);
return get(path.root, path.path);
}
};
Ember.Handlebars.registerHelper('detect', function (definition, instance, options) {
Ember.assert("You must pass exactly two argument to the detect helper", arguments.length === 3);
Ember.assert("You must pass a block to the detect helper", options.fn && options.fn !== Handlebars.VM.noop);
var path = '_detect_' + definition.replace('.', '_').toLowerCase();
context = (options.contexts && options.contexts[0]) || this;
definition = getProp(context, definition, options);
instance = getProp(context, instance, options);
context.set(path, definition.detectInstance(instance));
return Ember.Handlebars.helpers.boundIf.call(options.contexts[0], path, options);
});
Then you can use a helper like this:
{{#detect App.Definition instance}}
DETECTED
{{else}}
NOT DETECTED
{{/detect}}
How to get index item of array in an emberjs view.
This returns an error:
{{#view}}
{{oneArray[0]}}
{{/view}}
I don't want to use {{#each}} to show all items, just want to show the special index item. How?
Or can I get the index of {{#each}}?
{{#each oneArray}}
{{#if index>0}}
don't show the first item {{oneArray[index]}}
{{/if}}
{{/each}}
As shown in this article, you can define a new array for including the index in each row:
App.MyView = Ember.View.extend({
oneArrayWithIndices: function() {
return this.get('oneArray').map(function(item, index) {
return {item: i, index: idx};
});
}.property('oneArray.#each')
});
and then in your template you can access the index like so:
{{#each view.oneArrayWithIndices}}
index: {{this.index}} <br />
item: {{this.item}}
{{/#each}}
However, since you just want to show specific items from the array, it is better to do it in your view (or even better in your controller). Try to keep your templates logic-less.
Therefore, create a new array in your view/controller that includes only the items you want to show:
myFilteredArray: function() {
return this.get('oneArray').filter( function(item, index) {
// return true if you want to include this item
// for example, with the code below we include all but the first item
if (index > 0) {
return true;
}
});
}.property('oneArray.#each')