I have two JS arrays, one route, I can only get one model per route and regardless of trying endless ways of creating ArrayControllers and using them I can't use a 2nd model in my template.
e.g.,
{{#each}} // uses content model and default controller... works fine
<label>{{name}}</label>
{{/each}
{{#each %%whatever%%}} // trying to use another model/ JS array
<label>{{name}}</label>
{{/each}
Either I get errors or no data shows up.
I've tried specifying the array directly, using an ArrayController and setting its content and/or model to be the JS array. I've tried using in...
For example, suppose I have
var x = [{name : "Jo"}, {name : "Bob"}];
var y = [{name : "Jake"}, {name : "Ben"}];
How can I display them as
<label>Jo</label><label>Bob</label>
<label>Jake</label><label>Ben</label>
?
(No reduction here, have to use two separate arrays. My example is more complex but the page does not require all out complicating things like creating extra views, templates, and routes)
it would be nice if one could do
{{#each x}}
<label>{{name}}</label>
{{/each}
{{#each y}}
<label>{{name}}</label>
{{/each}
but this doesn't work.
Any ideas, this is driving me crazy! (I know I could add a variable to each element that specifies which collection it part of then use #if but this is too much of a hack and not very convenient)
Thanks...
You can use the setupController hook in your Route to set properties on the controller. These controller properties are then rendered by the template.
// define some arrays
App.x = [{name: "Jo"}, {name: "Bob"}];
App.y = [{name: "Jake"}, {name: "Ben"}];
App.ApplicationRoute = Ember.Route.extend({
setupController: function (controller, model) {
// make these available to the controller
controller.set('x', App.x)
controller.set('y', App.y)
}
});
See: http://jsfiddle.net/n724p/
Related
I have a function that takes in an array and randomizes the values inside of it.
shuffle([1,2,3,4]) => [2,4,1,3];
Now, I have succesfully used this function in my server side code for my ember app like so:
mirage config.js file
this.get('/games/:id', function (db, request) {
var game = games.find((game) => request.params.id === game.id);
var cardsShuffled = shuffle(game.attributes.cards);
game.attributes.cards = cardsShuffled;
return {data: game};
});
And then rendering my handlebars view like so:
play.hbs
<div>
{{#each model.cards as |instance|}}
{{game-card symbol=instance.url}}
{{/each}}
</div>
But I was wondering if it is better to create some sort of handlebars helper? I'm new to ember, but it doesn't look too clean to me to have the backend do that kind of logic. So here's what I was thinking:
shuffle-array.js helper
import Ember from 'ember';
export function shuffleArray(array) {
var m = array.length, t, i;
while (m) {
i = Math.floor(Math.random() * m--);
t = array[m];
array[m] = array[i];
array[i] = t;
}
return array;
};
export default Ember.Helper.helper(shuffleArray);
And then somehow use it like so (tried this. failed):
play.hbs (revised)
<div>
{{shuffle-array model.cards}}
{{#each model.cards as |instance|}}
{{game-card symbol=instance.url}}
{{/each}}
</div>
So I guess my question is two-fold: Is it acceptable/unnaceptable to have this kind of logic coming from the mirage backend? Also, what is the best way to have a handlebars helper implement this?
Is it acceptable/unnaceptable to have this kind of logic coming from the mirage backend?
If the logic isn't meant to be persistent, I'd say no.
Also, what is the best way to have a handlebars helper implement this?
I'd prefer a computed property on the relevant controller. Using this shuffle function, it'd be:
import Ember from 'ember';
export default Ember.Controller.extend({
shuffledCars: Ember.computed('model.cars.[]', function(){
return shuffle(this.get('model.cars'));
})
});
Acceptable this or not depends on your needs. As I can see from code, you are shuffling some cards for some game. It possible that you need to do such kind of things on backend because of some security concerns.
But doing it with helper is also good and acceptable (but user with abilities to code can read/manipulate data). A few notes about helpers:
Helper function have two parameters: array of unnamed parameters and a hash of named parameters. So in your code you will access first parameter as array[0]
You should use helpers like this:
{{#each (shuffle-array model.cards) as |instance|}}
{{game-card symbol=instance.url}}
{{/each}}
More about helpers
Computed properties, mentioned in other answer, also are good for such kind of things. Maybe even better, because helper will shuffle array on each rendering (i.e. when user navigates back and forth), while computed prop will do that only when original array changes.
I have the following pattern recurring throughout my app, and I was wondering if there is a way to clean this up:
Array Controller:
App.ThingsController = Ember.ArrayController.extend()
Template for Array Controller (using Emblem)
each thing in controller
= render 'thing' thing
Object Controller:
App.ThingController = Ember.ObjectController.extend()
I don't like having that intermediary template that has only those two lines.
I looked into itemController as a potential solution but that doesn't seem to do what I want.
Instead of rendering inside your each loop you could just include the code, and then use itemController. This would consolidate your templates into one, but still allow you to use the ObjectController.
{{each thing in controller itemController="thing"}}
{{thing.name}}.id = {{thing.id}}
{{/each}}
Part of learning Ember.js I am trying to create a Table View in Ember, based on example by Adam.
The issue I am facing is that if I create a seperate controller for the View and include an itemController for the Rows, ember gives following error: Uncaught TypeError: Cannot call method 'lookup' of null ember-1.0.0-rc.6.js:13933
When I debug this I find that in the following code :
controllerAt: function(idx, object, controllerClass) {
var container = get(this, 'container'),
subControllers = get(this, '_subControllers'),
subController = subControllers[idx];
if (!subController) {
subController = container.lookup("controller:" + controllerClass, { singleton: false });
The container is retrieved as null.
Whereas when it is run through ApplicationController, no such issue is there.
JS Fiddle Using ApplicationController for the View and another controller for itemController - Works Fine
JS Bin
in this the item Controller is specified as {{#each controller itemController="tableRow"}} and the controller is App.TableRowController
Here is a very similar JS Bin, JS Fiddle using a seperate TableViewController :
The item controller is similarly specified as {{#each controller itemController='tableRow'}.
A seperate Controller for Table View is binded using : {{view App.TableView controllerBinding="tableViewController"}} and this tableViewController is specified as a property in ApplicationController as :
App.ApplicationController = Ember.ArrayController.extend({
tableViewController: function() {
var tc = Ember.get('App.TableViewController').create();
tc.set('content',Ember.ArrayProxy.create({
content: Ember.A(tableData)})
);
return tc;
}.property()
});
But for some reason, the itemController does not work here.
Here is the JS Fiddle Using seperate Controller for View, but without any itemController - this works fine
Is there anything I am missing in the controller ?
Please help. Thanks.
With Ember most of the time you don't create objects directly, you declare the classes for things like controller, model, etc. And ember creates these objects using an IOC container. Avoid things like Controller.create. Similarly avoid directly controllerBinding instead use needs.
So, instead of providing a controllerBinding pass the content to be rendered by the App.TableView.
{{view App.TableView contentBinding=content}}
The setup of the tableData also belongs in a model() hook. It works in the sample because tableData variable is in scope.
Here's the updated jsfiddle.
I would like to know what's the best way of designing the display of different representations of the same data model in Ember.js. To ask my question, I'll use the TodoMVC of Ember.JS, which has 3 representations of todo-data:
any todo, i.e. the entire todo list (TodosIndexRoute)
todos that are still active and incomplete (TodosActiveRoute)
todos that have been completed (TodosCompletedRoute)
Currently, you can see each of the 3 by clicking on the words at the bottom of the list, directing to a different URL each time. Since currently each representation has a route, it makes sense that each representation gets its unique URL. The main page displays the entire todo list (1.).
A. What is the best ember.js design to make the main page display all 3 representations (i.e. under just one URL)?
B. How about the best design that displays all 3 on the main page as well as on separate pages?
Currently I only figured out a clumsy way and made this modified TodoMVC app that shows incomplete and completed lists at the bottom of the page.
In index.html, I added new named lists:
{{#each todosactive itemController="todo"}}
{{ title }},
{{/each}}
In the js router, I copied TodosActiveRoute and TodosCompletedRoute into TodoIndexRoute, which is code duplication, very bad.
Todos.TodosIndexRoute = Ember.Route.extend({
setupController: function () {
var todos = Todos.Todo.find();
this.controllerFor('todos').set('filteredTodos', todos);
var todos_active = Todos.Todo.filter(function (todo) {
if (!todo.get('isCompleted')) {
return true;
}
});
this.controllerFor('todos').set('todosactive', todos_active);
...
});
I feel like I'm missing an elegant way of doing this, but my current ember.js knowledge is very limited. Should I use {{partial}}, {{render}}, render, or something else?
I tried {{ partial }} and {{ render }}, but I can't get them to display any data .
Thanks for helping out.
A) Ember tries to work really closely with urls. This is a good thing since if you want to share a url, the view should be consistent. The url is a powerful tool and each unique url should link to the same unique page. Having one url that links to multiple views isn't great, and certainly not shareable. If you have some time listen to some talks by Tom Dale and Yehuda Katz for an interesting overview of ember and what they're trying to do.
B) You can include different views on one page. Have a look at the guides, most notably on rendering templates and using helpers for more information on including different views under one url.
A) To display all 3 representations in one view, we actually just need the basic model in the single route. The key is for the controller to give you flavors of that model. The other important thing is to use data binding in the handlebars template. You can see the running version here.
In the IndexRoute, add a model that gets the plain todos list:
Todos.TodosIndexRoute = Ember.Route.extend({
model: function(params) {
return Todos.Todo.find();
},
...
Make a TodosListView that doesn't need to have anything:
Todos.TodoListView = Ember.View.extend();
In the controller, add 2 computed properties that returns the desired arrays:
Todos.TodosController = Ember.ArrayController.extend({
...
todosActive: function() {
return this.filterProperty('isCompleted', false);
}.property('#each.isCompleted'),
todosCompleted: function() {
return this.filterProperty('isCompleted', true);
}.property('#each.isCompleted'),
...
Finally, in the HTML template:
<script type="text/x-handlebars" data-template-name="todos">
Active:
{{#view Todos.TodoListView lalaBinding="todosActive"}}
{{#each view.lala}}
{{title}},
{{/each}}
{{/view}}
Completed:
{{#view Todos.TodoListView dataBinding="todosCompleted"}}
{{#each view.data}}
{{title}},
{{/each}}
{{/view}}
</script>
note the dataBinding.
Thanks to the folks on #emberjs irc for helping out.
I might be using this all wrong, but:
I've got an ArrayController representing a collection of products. Each product gets rendered and there are several actions a user could take, for example edit the product title or copy the description from a different product.
Question is: how do you interact with the controller for the specific product you're working with? How would the controller know which product was being edited?
I also tried to create an Ember.Select with selectionBinding set to "controller.somevar" but that also failed.
I think the most important thing you need to do, is first move as much logic as you can away from the views, and into controllers.
Another thing that would be useful in your case, is to have an itemController for each product in the list. That way, you can handle item specific logic in this item controller.
I don't have enough information to understand your architecture, so I will make a few assumptions.
Given you have the following ProductsController:
App.ProductsController = Ember.ArrayController.extend();
You need to create a ProductController that will be created to wrap every single product on its own.
App.ProductController = Ember.ObjectController.extend();
You need to modify your template as follows:
{{#each controller itemController="product"}}
<li>name</li>
{{/each}}
Now every product in your list will have its own ProductController, which can handle one product's events and will act as the context for every list item.
Another option:
If you will only be handling one product at a time, you can use routes to describe which product you are working with:
App.Router.map(function() {
this.resource('products', { path: '/products' }, function() {
this.resource('product', { path: '/:product_id' }, function() {
this.route('edit');
});
});
});
And create a controller for editing a product:
App.ProductEditController = Ember.ObjectController.extend();
And your list items would link to that product route:
{{#each controller}}
<li>{{#linkTo "product.edit" this}}name{{/linkTo}}</li>
{{/each}}
If you define itemController on your ProductsController you don't need to specify that detail in your template:
App.ProductsController = Em.ArrayController.extend({
itemController: 'product',
needs: ['suppliers'],
actions: {
add: function() {
// do something to add an item to content collection
}
}
});
App.ProductController = Em.ObjectController.extend({
actions: {
remove: function() {
// do something to remove the item
}
}
});
Use a collection template like this:
<button {{action="add"}}>Add Item</button>
<ul>
{{#each controller}}
<li>{{name}} <button {{action="remove"}}>x</button></li>
{{/each}}
</ul>
The Ember documentation describesitemController here:
You can even define a function lookupItemController which can dynamically decide the item controller (eg based on model type perhaps).
The thing I found when rendering a collection wrapped in an ArrayController within another template/view is the way #each is used. Make sure you use {{#each controller}} as Teddy Zeeny shows otherwise you end up using the content model items and NOT the item controller wrapped items. You may not notice this until you try using actions which are intended to be handled by the item controller or other controller based content decoration.
When I need to nest an entire collection in another view I use the view helper as follows to set the context correctly so that any collection level actions (eg an add item button action) get handled by the array controller and not by the main controller setup by the route.
So in my products template I would do something like this to list the nested suppliers (assuming your route for 'product' has properly the 'suppliers' controller):
{{view controller=controllers.suppliers templateName="products/suppliers"}}
The suppliers template just follows the same pattern as the template I show above.