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')
Related
I have a component that accepts an array of values and array of the same length with a validation error string for each value. I'd like to display a list of form fields with inputs for each value and error pair. I've tried creating a computed property like this:
var myComponent = Ember.Component.extend({
//values: <provided array of input values>
//errors: <provided array of error strings>
valuesAndErrors: function() {
var combined = [];
for (var i = 0; i < values.length; i++) {
combined.pushObject({
value: this.get('values')[i],
error: this.get('errors')[i]
});
}
return combined;
}.property('values.#each', 'errors.#each')
});
But unfortunately the changes made to values in valuesAndErrors (e.g. via {{input value=valuesAndErrors.value}}) are not pushed back to the source values array. What is the correct way to iterate over the values and errors arrays simultaneously without breaking bindings like this?
I'm currently using Ember 1.9.
Instead of passing in a separate array for values and errors, why not have a computed property in the controller that combines the two and then pass that into the component?
So, your controller might look something like this:
App.ApplicationController = Ember.Controller.extend({
values: function(){
return ["one", "two", "three"];
}.property(),
errors: function(){
return ["oneError", "twoError", "threeError"];
}.property(),
valuesAndErrors: function() {
var combined = [];
var values = this.get('values');
var errors = this.get('errors');
values.forEach(function(value, index){
combined.pushObject({
value: value,
error: errors[index]
});
});
return combined;
}.property('values.#each', 'errors.#each')
});
And your component template (you don't even need any component JS for this to work):
<script type="text/x-handlebars" id='components/value-error'>
<h2>Inside of Component</h2>
{{#each item in valuesAndErrors }}
{{ input value=item.value }} - {{ input value=item.error }}<p/>
{{/each}}
</script>
Working example here
UPDATE
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}}
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}}
I am trying to build a blog application with Ember. I have models for different types of post - article, bookmark, photo. I want to display a stream of the content created by the user for which I would need a collection of objects of all these models arranged in descending order of common attribute that they all have 'publishtime'. How to do this?
I tried something like
App.StreamRoute = Ember.Route.extend({
model: function() {
stream = App.Post.find();
stream.addObjects(App.Bookmark.find());
stream.addObjects(App.Photo.find());
return stream;
}
}
where the resource name is stream
But it doesn't work. I am using the latest released Ember 1.0.0 rc 2 and handlebars 1.0.0 rc 3 with jQuery 1.9.1 and ember-data.
Probably the way I am trying to achieve this whole thing is wrong. The problem is even if I am able to use the collection of objects of multiple models to iterate in the template, I would still need to distinguish between the type of each object to display its properties apart from the common property of 'publishtime'.
You can use a computed property to combine the various arrays and then use Javascript's built in sorting to sort the combined result.
Combining the arrays and sorting them
computed property to combine the multiple arrays:
stream: function() {
var post = this.get('post'),
bookmark = this.get('bookmark'),
photo = this.get('photo');
var stream = [];
stream.pushObjects(post);
stream.pushObjects(bookmark);
stream.pushObjects(photo);
return stream;
}.property('post.#each', 'bookmark.#each', 'photo.#each'),
example of sorting the resulting computed property containing all items:
//https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort
streamSorted: function() {
var streamCopy = this.get('stream').slice(); // copy so the original doesn't change when sorting
return streamCopy.sort(function(a,b){
return a.get('publishtime') - b.get('publishtime');
});
}.property('stream.#each.publishtime')
});
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's a little complicated than that, but #twinturbo's example shows nicely how to aggregate separate models into a single array.
Code showing the aggregate array proxy:
App.AggregateArrayProxy = Ember.ArrayProxy.extend({
init: function() {
this.set('content', Ember.A());
this.set('map', Ember.Map.create());
},
destroy: function() {
this.get('map').forEach(function(array, proxy) {
proxy.destroy();
});
this.super.apply(this, arguments);
},
add: function(array) {
var aggregate = this;
var proxy = Ember.ArrayProxy.create({
content: array,
contentArrayDidChange: function(array, idx, removedCount, addedCount) {
var addedObjects = array.slice(idx, idx + addedCount);
addedObjects.forEach(function(item) {
aggregate.pushObject(item);
});
},
contentArrayWillChange: function(array, idx, removedCount, addedCount) {
var removedObjects = array.slice(idx, idx + removedCount);
removedObjects.forEach(function(item) {
aggregate.removeObject(item);
});
}
});
this.get('map').set(array, proxy);
},
remove: function(array) {
var aggregate = this;
array.forEach(function(item) {
aggregate.removeObject(item);
});
this.get('map').remove(array);
}
});