I am trying to have handle multiple controllers at one route in Ember. This response seems like the way to go, but am having difficult getting this to work. Here is a simple example that fails to work:
App.IndexRoute = Ember.Route.extend({
model: function() {
myData = [1,2,3];
return Em.RSVP.hash({
docs1: myData,
docs2: myData
});
},
setupController: function(controller, model) {
controller.set('model', model.docs1);
this.controllerFor('documents').set('model', model.docs2);
}
});
App.DocumentsController = Ember.ArrayController.extend({
count: function() {
return this.get('length');
}.property('#each'),
});
App.IndexController = Em.ArrayController.extend({
count: function() {
return this.get('length');
}.property('#each'),
});
And a JSBin showing the results:
http://jsbin.com/wavigada/1/edit
You can see that the IndexController reports the correct count of 3, but the DocumentsController doesn't get set.
Can anyone help me out?
You will need to include the documents controller whose content you populate in setupController in your IndexController via needs:
App.IndexController = Em.ArrayController.extend({
needs: ['documents'],
count: function() {
return this.get('length');
}.property('#each'),
});
Then you need to change your template to:
<script type="text/x-handlebars" data-template-name="index">
{{count}}
{{render 'documents' controllers.documents}}
</script>
Note that just putting {{render 'documents' controllers.documents}} (as you did in your question) refers to a documents property of your current model, which doesn't exists.
See: http://jsbin.com/wavigada/6/
Related
According to official documentation, way to create itemcontroller is:
App.PostsController = Ember.ArrayController.extend({
itemController: 'post'
});
App.PostController = Ember.ObjectController.extend({
// the `title` property will be proxied to the underlying post.
titleLength: function() {
return this.get('title').length;
}.property('title')
});
But I'm not setting my ArrayController to App. It is set to a local variable behind a function scope. And the itemController property can only be string (according to documentation). So how do I set the itemController property?
My code looks like this:
var Channels=Ember.Object.extend({
list:Ember.ArrayController.create(
{
"model":[
{
"id":"display",
"label":"Display",
},{
"id":"social",
"label":"Social",
},{
"id":"email",
"label":"Email",
}
]
}
)
});
App.ChannelController=Ember.Controller.extend({
channels:Channels,
}));
<script type="text/x-handlebars" data-template-name='channel'>
<div>
{{#each channel in channels.list}}
{{channel.label}}
{{/each}}
</div>
</script>
I don't want to pollute App namespace with itemControllers that is to be used locally.
Update
Suppose my channels is like this:
var Channels=Ember.Object.extend({
list:Ember.ArrayController.create(
{
"model":[
{
"id":"display",
"label":"Display",
},{
"id":"social",
"label":"Social",
},{
"id":"email",
"label":"Email",
}
]
}
),
selected:"display"
});
and I want to something like this in template:
<script type="text/x-handlebars" data-template-name='channel'>
<h1>{{channels.selected}}</h1>
<div>
{{#each channel in channels.list}}
<div {{bind-attr class="channel.isselected:active:inactive"}}>{{channel.label}}</div>
{{/each}}
</div>
</script>
so that it outputs:
<h1>display</h1>
<div>
<div class="active">Display</div>
<div class="inactive">Social</div>
<div class="inactive">Email</div>
</div>
How do I do it with components?
You'll likely want to read the guide of components to get the full picture, but the gist of it is that you want to replace all item controllers with components. However, components will also replace the template inside of the each block as well. I don't entirely understand what's going on in your code, but here's an example roughly based on your code.
// Component
App.ChannelDisplayComponent = Ember.Component.extend({
channel: null,
isSelected: function() {
// Compute this however you want
// Maybe you need to pass in another property
}.property('channel')
});
{{! Component Template }}
<div {{bind-attr class="channel.isSelected:active:inactive"}}>
{{channel.label}}
</div>
{{!Channels Template}}
{{#each channel in channels.list}}
{{channel-component channel=channel}}
{{/each}}
The component is essentially your item controller, only it gets its own template as well.
You really shouldn't be worried about polluting the app namespace (unless you're having naming collisions, but that's a different issue). And as Kitler said, you should move to components instead of item controllers. But if you want to do this, the best way I can think of is overridding the (private) controllerAt hook.
var ItemController = Ember.Controller.extend({});
App.PostsController = Ember.ArrayController.extend({
controllerAt: function(idx, object, controllerClass) {
var subControllers = this._subControllers;
if (subControllers.length > idx) {
if (subControllers[idx]) {
return subControllers[idx];
}
}
var parentController = (this._isVirtual ? this.get('parentController') : this);
var controller = ItemController.create({
target: parentController,
parentController: parentController,
model: object
});
subControllers[idx] = controller;
return controller;
}
})
Apologies if this has already been asked and I couldn't figure that out.
I am attempting to link an Ember dynamic Route with a Template. It's not working. The error for the below code is Error while processing route: favorite undefined is not a function
The idea is that the main page should show a list of favorites that are returned via Ajax. Each favorite should have a link. The user clicks a link and the favorite is injected into the relevant template on the same page.
The main page is working correctly. With the code below, the links are currently showing index.html/#/12345ab where 12345ab is the product_id.
HTML Template:
<script type="text/x-handlebars" id="favorites">
{{#each favorite in arrangedContent}}
<div class="productBox">
{{#link-to 'favorite' favorite.product_id}}
<img {{bind-attr src=favorite.product_image}} />
{{/link-to}}
</div>
{{/each}}
{{outlet}}
</script>
<script type="text/x-handlebars" id="favorite">
<h2>{{product_name}}</h2>
<img {{bind-attr src=product_image}} />
</script>
Router code:
App.Router.map(function() {
this.resource('favorites', { path: '/'});
this.resource('favorite', { path: ':product_id' });
});
App.FavoritesRoute = Ember.Route.extend({
model: function() {
return Ember.$.ajax({
//this returns correctly
}).then(function(data) {
return data.favorites;
});
}
});
App.FavoriteRoute = Ember.Route.extend({
model: function(params) {
return App.Favorites.findBy('product_id', params.product_id);
}
});
Update:
The answer below gives the following code, but if the user goes directly to the page via the URL or a straight refresh, it fails due to the fact that the favorites model is not resolved yet. Exact error is: Cannot read property 'findBy' of undefined
App.FavoriteRoute = Ember.Route.extend({
model: function(params) {
return this.modelFor('favorites').findBy('product_id', params.product_id);
}
});
Update 2:
Entire Router code:
App.Router.map(function() {
this.resource('favorites', { path: '/'});
this.resource('favorite', { path: ':product_id' });
});
App.ApplicationRoute = Ember.Route.extend({
model: function() {
return new Promise(function(resolve, reject) {
Ember.$.ajax({
url: 'MY_URL',
dataType: 'jsonp',
jsonp: 'callback'
}).then(function(data) {
resolve(data.favorites);
});
});
},
setupController: function(controller, model) {
return this.controllerFor('favorites').set('model', model);
}
});
App.FavoriteRoute = Ember.Route.extend({
model: function(
return this.controllerFor('favorites').get('model').findBy('product_id', params.product_id);
}
});
By the looks of it, you want to find the model from the parent route. You can do it likes so:
App.FavoriteRoute = Ember.Route.extend({
model: function(params) {
this.modelFor('favorites').arrangedContent.findBy('product_id', params.product_id);
}
});
UPDATE:
The problem is that your promise from the parent route isn't getting resolved correctly. You're returning a promise but the result of that promise isn't getting resolved i.e. (return data.favorites) is not resolving the promise.
Update it to:
App.FavoritesRoute = Ember.Route.extend({
model: function() {
return new Promise(function(resolve, reject) {
Ember.$.ajax('yourURL').then(
function(data){
resolve(data.favorites);
});
});
}
});
Also incorporate the initial feedback from this answer. I have a working JSBin going against an actual endpoint to show it works: http://jsbin.com/boloya/3/edit
P.S. Look out for params.product_id, that comes in as a string. You made need to cast it to the required type in order for the findBy to work correctly.
P.S.S. I should also add, Ember.$.ajax returns a promise, but you want the model to be data.favorites which is the need for the outer promise. If you just returned Ember.$.ajax and accessed everything via model.favorites you wouldn't need it.
Your routes need to be nested for a child resource to have access to a parent via #modelFor. However, if your UI isn't nested, your routes probably shouldn't be, since nesting routes also wires up a corresponding view hierarchy.
You could always define the model for the favorite route in terms of a subset of the data returned by your ajax call:
//routes/favorite.js
model: function() {
return Ember.$.getJSON('/favorites').then(function(favorites) {
return favorites.findBy('id', params.product_id');
});
}
but then, the top-level .getJSON('/favorites)call would be made multiple times, every time the user enters/favorites, and every time he enters/favorites/:id`.
Instead, you can have the application set up the FavoritesController once upon entry. That way you can share data, but favorite doesn't have to be a child route of favorites. It might look something this:
//routes/application.js
model: function() {
return Ember.$.getJSON('/favorites');
},
setupController: function(controller, model) {
this.controllerFor('favorites').set('model', model);
}
and
//routes/favorite.js
model: function(params) {
return this.controllerFor('favorites').find('id', params.id);
}
That way, the JSON is only loaded once, ApplicationRouter is wiring up your FavoritesController for you, and the data is shared with the favorite resource.
With some help from the Ember IRC channel, this is the working code. In essence, it creates an Index template for both the favorites and the favorite template to render into. Then the favorite route can access it's parent route favorites and yet still render into the same template area.
HTML
<script type="text/x-handlebars">
<h2>Welcome to Ember.js</h2>
{{outlet}}
</script>
<script type="text/x-handlebars" id="favorites">
{{outlet}}
</script>
<script type="text/x-handlebars" id="favorites/index">
{{#each favorite in arrangedContent}}
<div class="productBox">
{{#link-to 'favorite' favorite.product_id}}
<img {{bind-attr src=favorite.product_image}} />
{{/link-to}}
</div>
{{/each}}
{{outlet}}
</script>
<script type="text/x-handlebars" id="favorite">
<h2>{{product_name}}</h2>
<img {{bind-attr src=product_image}} />
</script>
Router.js
App.Router.map(function() {
this.resource('favorites', { path: '/'}, function () {
this.resource('favorite', { path: ':product_id' });
});
});
App.IndexRoute = Ember.Route.extend({
redirect: function () {
this.replace('favorites');
}
});
App.FavoritesRoute = Ember.Route.extend({
model: function() {
return new Promise(function(resolve) {
Ember.$.ajax({
url: 'MY_URL',
dataType: 'jsonp',
jsonp: 'callback'
}).then(function(data) {
resolve(data.favorites);
});
});
}
});
App.FavoriteRoute = Ember.Route.extend({
model: function (params) {
return this.modelFor('favorites').findBy('product_id', params.product_id);
}
});
I am trying get access to a controller needed by my ApplicationController (Application needs Practice), however the PracticeController is only available after the resource has been loaded through it's respective route-url visit.
How, can I make sure the PracticeController and it's content/model is available at all times?
To be specific, I need my flashcardArray to be available throughout my application and at all times, even when the Practice-route hasn't been visited, thus loaded yet.
Thanks for any help!
Here is my code:
App.Router.map(function() {
this.resource('signup');
this.resource('login');
this.resource('profile');
this.resource('practice');
this.resource('overview');
});
App.ApplicationRoute = Ember.Route.extend({
model: function () {
return this.store.find('user');
}
});
App.LoginRoute = Ember.Route.extend({
controllerName: 'application',
model: function () {}
});
App.PracticeRoute = Ember.Route.extend({
model: function () {
return this.store.find('flashcards');
}
});
//ApplicationController
App.ApplicationController = Ember.ObjectController.extend({
needs: ['practice'],
flashcardArray: Ember.computed.alias('controllers.practice.flashcardArray'),
currentUser: Ember.computed.alias('model'),
isLoggedIn: false
}
});
//PracticeController
App.PracticeController = Ember.ObjectController.extend({
needs: ['application'],
flashcardArray: Ember.computed.alias('model'),
currentUser: Ember.computed.alias('controllers.application.currentUser')
}
});
// Practice template feeded into an outlet of the application template
<script type="text/x-handlebars" id="practice">
<div class="content-padded">
{{#each object in flashcardArray}}
<div class="card_wrapper">
<p><h1>{{{object.num_reference}}}</h1><p>
<p>PinYin: {{{object.mandarin}}}</p>
<p>def: {{object.definition}}</p>
{{/each}}
</div>
</script>
I think if you have data you need throughout the application, regardless of the user's active route, you probably need to return it as the ApplicationRoute:
App.ApplicationRoute = Ember.Route.extend({
model: function () {
return {
user: this.store.find('user'),
flashcards: this.store.find('flashcards')
};
}
});
I have an ember application with a model called users.js with associated controllers and routing. In my usersController.js, I have a function which counts the number of users in the system. I can then display this figure in my users template. However, I want to display that figure in my index template instead, is this possible? How would I go about it- right now the figure doesn't seem to be available for use outside of my users model.
Here's my usersController-
App.UsersController = Ember.ArrayController.extend({
sortProperties: ['name'],
sortAscending: true,
numUsers: function() {
return this.get('model.length');
}.property('model.[]')
});
And my html-
<script type = "text/x-handlebars" id = "index">
<h2>Homepage</h2>
//This is where I would like the figure to be
<h3>There are {{numUsers}} users </h3>
</script>
<script type = "text/x-handlebars" id = "users">
<div class="col-md-2">
{{#link-to "users.create"}}<button type="button" class="btn btn-default btn-lg"><span class="glyphicon glyphicon-plus"></button> {{/link-to}}
//This works fine
<div>Users: {{numUsers}}</div>
</div>
<div class="col-md-10">
<ul class="list-group">
{{#each user in controller}}
<li class="list-group-item">
{{#link-to "user" user}}
{{user.name}}
{{/link-to}}
</li>
{{/each}}
</ul>
{{outlet}}
</div>
</script>
You can just load all users in the IndexRoute, something like this:
App.IndexRoute = Ember.Route.extend({
model: function() {
return this.store.find('user');
}
});
And extract the shared logic, in that case user count, to a mixin, and use where needed:
App.UsersCountMixin = Ember.Mixin.create({
numUsers: function() {
return this.get('model.length');
}.property('model.[]')
});
App.IndexController = Ember.ArrayController.extend(App.UsersCountMixin, {
});
App.UsersController = Ember.ArrayController.extend(App.UsersCountMixin, {
sortProperties: ['name'],
sortAscending: true
});
So {{numUsers}} will be avaliable in your index template.
To share logic with more than one model, you will need to create some alias for model property to avoid ambiguity:
App.IndexRoute = Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
users: this.store.find('user'),
subjects: this.store.find('subject'),
})
}
});
App.UsersCountMixin = Ember.Mixin.create({
users: Ember.required(),
numUsers: function() {
return this.get('users.length');
}.property('users.[]')
});
App.SubjectsCountMixin = Ember.Mixin.create({
subjects: Ember.required(),
numSubjects: function() {
return this.get('subjects.length');
}.property('subjects.[]')
});
App.UsersController = Ember.ArrayController.extend(App.UsersCountMixin, {
users: Ember.computed.alias('model'),
sortProperties: ['name'],
sortAscending: true
});
App.SubjectsController = Ember.ArrayController.extend(App.SubjectsCountMixin, {
subjects: Ember.computed.alias('model'),
sortProperties: ['name'],
sortAscending: true
});
App.IndexController = Ember.ArrayController.extend(App.UsersCountMixin, App.SubjectsCountMixin, {});
Of course this is a lot of code to just show the data length, since you can just use:
<h3>There are {{users.length}} users </h3>
<h3>There are {{subjects.length}} subjecst </h3>
But I think you will have more complex computed properties to share. In that cases, mixins is a good way to achieve it.
I have the following code which calls an transitionToRoute('search') when a search-query is entered and the enter button is pressed or submit button is clicked.
However, my Router still won't show the searchQuery in the template where it says:
<p>You searched for: "{{searchQuery}}"</p>
and the URL looks like http://www.example.com/#/search/[object Object] when searching for something (which doesn't seem right to me).
(full code can be viewed over at: http://jsfiddle.net/Mn2yy/1/)
This is the relevant code:
Templates:
<script type="text/x-handlebars" data-template-name="container">
<button {{action "doSearch"}} rel="tooltip-bottom" title="search" class="icon"><i class="icofont-search"></i></button>
{{view Ember.TextField valueBinding="search" action="doSearch"}}
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="searchpage">
<h1>Search</h1>
{{#linkTo "home"}}Homepage{{/linkTo}}
<p>You searched for: "{{searchQuery}}"</p>
</script>
Application controller:
MyApp.ApplicationController = Ember.Controller.extend({
// the initial value of the `search` property
search: '',
doSearch: function() {
// the current value of the text field
var query = this.get('search');
this.transitionToRoute('search');
}
});
and the Searchpage route:
MyApp.SearchRoute = Ember.Route.extend({
setupController: function(controller) {
controller.set('searchQuery', this.get('query'));
},
renderTemplate: function() {
this.render('searchpage', { into: 'container' });
}
});
First, you need to define the dynamic segment in the router for the search route:
MyApp.Router.map(function() {
this.route("home", { path: "/" });
this.route("search", { path: "/search/:query" })
});
Then you set the searchQuery property on the application in the doSearch action. You also pass the query variable to the transitionToRoute method, since it'll fill in the dynamic segment.
MyApp.ApplicationController = Ember.Controller.extend({
// the initial value of the `search` property
search: '',
doSearch: function() {
// the current value of the text field
var query = this.get('search');
this.set('searchQuery', query);
this.transitionToRoute('search', query);
}
});
Since you need to access this property from the App.SearchController instance, you need to wire the 2 controllers together by using the needs API:
MyApp.SearchController = Ember.Controller.extend({
needs: ['application'],
application: Ember.computed.alias('controllers.application')
});
Aliased the controllers.application property to just application, to avoid too much typing eg. in the template.
Then you bind to this property in the search template:
<script type="text/x-handlebars" data-template-name="searchpage">
<h1>Search</h1>
{{#linkTo "home"}}Homepage{{/linkTo}}
<p>You searched for: "{{application.searchQuery}}"</p>
</script>
Last step: if you refresh the page at this point, searchQuery won't be automatically populated from the URL. Let's just fix that with the deserialize hook:
MyApp.SearchRoute = Ember.Route.extend({
deserialize: function(params) {
this.controllerFor('application').setProperties({
searchQuery: params.query,
search: params.query
});
},
renderTemplate: function() {
this.render('searchpage', { into: 'container' });
}
});
This will get the params from the URL and set up the application controller with the value of the query key.
That's pretty much it, hope I didn't miss anything!