I am learning Ember.js 1.0.0-rc.3 and I would like to know the proper way to create a "dynamic MVC" app. What I mean by "Dynamic MVC" is that the only content I have when hitting the index page is a collection of entity names. The rest will be dynamically generated, i.e. model (data), eventually specific actions for the controllers, and specially templates.
Here is what I am trying to achieve:
I have a collection of entities (such as ["User", "Customer", "Invoice", etc]).
I expect the route for each entity to be:
.../#/user/
.../#/customer/
.../#/invoice/
for each entity name
this.resource("entity name lower case");
To make it simple for the explanation, hitting an entity route will display a view with 2 links. One to create a new entity, one to display a list of entities. The routes will then be:
.../#/user/ => will display a "Create" link and "List" link
.../#/user/create/ => sub route to display a form to create a User
.../#/user/list/ => sub route to display a collection of existing Users
.../#/customer/
.../#/customer/create/
.../#/customer/list/
...etc...
Now the templates for the sub routes (".../#/user/create/", ".../#/user/list/", etc.) don't exist but instead will be loaded/created on demand. That's part of a new UI Design Pattern named SSD (Sketch, Schema, Data) where a template is generated based on a Sketch (a skeleton of a view), a Schema (specific to that view). Same for the actions.
1) First question.
Knowing that I may end up with 100 entities+, is it a correct approach to have a Controller, a Model, a View for each route? And as well sub routes... I am a bit concerned about the performances here as all these objects are created when hitting the index page.
In the current sample, I will have the following Ember objects:
UserController, UserRoute, UserView
UserCreateController, UserCreateRoute, UserCreateView
UserListController, UserListRoute, UserListView
CustomerController, CustomerRoute, CustomerView
CustomerCreateController, CustomerCreateRoute, CustomerCreateView
CustomerListController, CustomerListRoute, CustomerListView
etc.
//it becomes easily heavy
I tried to link multiple paths to the same MVC objects but then it's hard to identify the current entity used. Analyzing the current path, if doable, seems really ugly.
For instance:
this.resource('entity', { path: '/' + user });
this.resource('entity', { path: '/' + customer });
this.resource('entity', { path: '/' + invoice });
//all attached to EntityController, EntityView and EntityRoute
//Could be working but hard to identity which entity User, Customer or Invoice is really used
this.resource('entity.create', { path: '/' + user + '/create' });
this.resource('entity.create', { path: '/' + customer + '/create' });
this.resource('entity.create', { path: '/' + invoice + '/create' });
//Same for EntityCreate
I also tried to use this.controllerFor('entity'), this.controllerFor('entity.create'), this.controllerFor('entity.list') but this doesn't reduce the number of objects and it makes the code more complicated than it should be.
2) Second Question:
To be able to work on dynamic templates, I implement setController() with an ajax call to load the necessary SSD which will be used to create the template and also the model.
I save the template to Ember.TEMPLATES[template name], change a data property, and attach an observer to the renderTemplate of the Route.
Is it the proper way of dynamically changing a template?
Here is a sample (not tested, just to see the process of loading/changing a template and model):
App["UserCreateRoute"] = Ember.Route.extend({
templateName: "loading", //display a temporary template while loading the template
renderTemplate: function() {
this.render(this.get('templateName'), {
///...
});
}.observes("templateChanged"),
setupController: function(controller, model) {
var self = this;
$.when(` ajax load `).done(function( response ) {
var templateName = xxx; // based on the entity for instance "user/create"
if (`template doesn't exist`) {
Ember.TEMPLATES[templateName] = CreateMyTemplate( response );
self.set("templateName", templateName);
}
//also change the model
self.set("data", reponse.someData );
//eventually add method to the controller?
//...
//tell renderTemplate to re-render
self.set("templateChanged", new Date());
})
},
model: function(params) {
//simplified for sample
return this.get("data")
}
})
It's a bit long, thanks for reading!
Related
i'm currently building an ember app using Yeoman Ember Generator.
this is my template folder structure looks like:
template
|---requisitions
|---draft.hbs
|---pending.hbs
|---waiting.hbs
requisitions.hbs
app.hbs
application.hbs
this is my router.js
Metabuyer.Router.map(function () {
this.route('app');
this.resource('requisitions', function(){
this.resource('draft');
this.resource('pending');
this.resource('waiting');
});
});
in my DS.Store , i have Requisition model which working just fine.
Metabuyer.RequisitionsRoute = Ember.Route.extend({
model: function () {
return this.store.findAll('requisition');
}
});
Draft, pending and waiting route share the same requisition model but filter it based on their needs, like below
Metabuyer.DraftRoute = Ember.Route.extend({
model: function(params){
var filterResult = this.store.filter('requisition', function(requisition){
return requisition.get('state') === 'draft';
});
console.log(test);
return filterResult;
});
}
});
My problem is.
When i use this.resource('draft') in my router nothing is being rendered in my page (blank page), but in my console, the filtered objects are being returned.
if i used this.route('draft') the page is rendered, but the content of the page are not filtered, or should i say, my Metabuyer.DraftRoute is not being called.
Thank you so much for your help, :'(
http://emberjs.com/guides/routing/defining-your-routes/
Routes nested under a resource take the name of the resource plus their name as their route name.
An index route is also needed on parent routes.
So navigating to /requistions loads up the RequesitionsRoute and RequisitionsIndexRoute you need to set the model on RequisitionsIndexRoute and use RequisitionsIndexControlleretc. You will need to rename requisitions.hbs to index.hbs and move it to the requisitions directory.
You also need to prefix your draft route object name with the parent so DraftRoute becomes RequisitionsDraftRoute and the same for controllers, views etc.
Hi guys i have bunch of images that i want to sort by 'Recent' or 'Popular' or 'Hot'.
For now i have a route which is defined like this:
App.Router.map(function () {
this.route("browse");
});
I wanted to do something like browse/recent to show the images by recent and browse/popular for the popular but I cant nest routes.
Shall I change my code so instead of the browse route ill have images resource?
And nest into it my filters? so ill have something like images/recent images/popular...
It seems like too many routes, maybe ill have in the future 10 filters does it mean ill have to create 10 different routes & controllers? cant i just use 1 controller and set a logic to filter(with ember-data)?
You should probably use a noun (images) as a resource name. You can then create multiple routes, each applying different filter on your data (different model hook), but each using the same controller / template. A simplified example:
First, create an images resource, with individual routes for your filters:
App.Router.map(function() {
this.resource('images', function () {
this.route('hot');
this.route('new');
});
});
Then, create a shared route, which will use hardcoded template and controller. The part with setupController is needed because the default controller will be (probably auto-generated) controller for ImagesNew or ImagesHot. You must take the given model and use it to set up shared ImagesController.
App.ImagesRoute = Ember.Route.extend({
renderTemplate: function() {
this.render('images', {
controller: 'images'
});
},
setupController: function (_, model) {
this.controllerFor('images').set('content', model);
}
});
App.ImagesController = Ember.Controller.extend({
// your shared logic here
});
Finally, you can create filtering routes. Each should inherit the base ImagesRoute and provide its own filtered data in the model hook.
App.ImagesHotRoute = App.ImagesRoute.extend({
model: function () {
return this.store.getHotImages();
}
});
App.ImagesNewRoute = App.ImagesRoute.extend({
model: function () {
return this.store.getNewImages();
}
});
Working jsbin example here.
It's a best practice to start with a resource and then nest routes within it.
App.Router.map(function() {
this.resource('images', { path: '/' }, function() {
this.route('browse');
this.route('hottest');
this.route('popular');
});
});
As far as creating ten different controllers, that is not necessary. I'd imagine that the route logic will be different (HottestRoute will load the hottest photos, PopularRoute will load the most popular), but the controller logic should be the same. It is probably best to have named controllers, but they can just extend an already defined controlled.
App.ImagesPopularController = ImagesController.extend();
This may be abusing Ember, but I want to create a computed property for the number of items in the store.
I'm trying to prototype a UI that exists entirely client-side. I'm using fixture data with the local storage adapter. That way, I can start off with canned data, but I can also add data and have it persist across reloads.
As I'm currently working on the data layer, I've built a settings route that gives me a UI to reset various models. I would like to add a Handlebars expression like {{modelCount}} so I can see how many records there are in the store. That's quicker than using the Ember Data Chrome extension, which resets to the routes tab on every page reload.
The following will show me the number of records once, but does not change when the number of records changes:
modelCount: function() {
var self = this;
this.store.find("my_model").then(function(records) {
self.set("modelCount", records.get("length"));
});
}.property()
I get that the store is supposed to proxy an API in the real world, and find returns a promise, but that's about the limit of my knowledge. I don't know how tell Ember to that I want to know how many records there are, or if this is even a valid question.
Load the result of store.find into an Ember.ArrayController's content and then bind the length of content to modelCount. An example:
App.SomeRoute = Ember.Route.extend({
model: function(){
return this.store.find('my_model');
}
});
App.SomeController = Ember.ArrayController.extend({
modelCount: Ember.computed.alias('content.length')
});
See a working example in http://jsbin.com/iCuzIJE/1/edit.
I found a workable solution by combining the answer from #panagiotis, and a similar question, How to load multiple models sequentially in Ember JS route.
In my router, I sequentially load my models:
model: function() {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
self.store.find("model1").then(function(model1) {
self.store.find("model2").then(function(model2) {
self.store.find("model3").then(function(model3) {
resolve({
model1: model1,
model2: model2,
model3: model3
});
});
});
});
});
},
Then in my controller, I define simple computed properties:
model1Count: function() {
return this.get("model1.length");
}.property("model1.length"),
...
At the moment, I try to connect my ember.js application with my webserver. The web application has a datepicker. After a date was selected I like my model to "reload". With reload I mean asking my webserver for the new data containing the specific date.
Below you see my route contacting the server for the required information.
App.PicturesRoute = Ember.Route.extend({
model: function(params) {
return $.getJSON('http://api.<server>.com/pictures?date=' + params.date).then(function(data) {
return data.pictures.map(function(picture) {
picture.body = picture.content;
return event;
});
});
}
});
In the case that I write the date manually in the string everything works fine and I receive data. Now, I have the problem that I don't figure out how to do it dynamically. How should I create the best connection between UI and model. Of course I can implement an action in my controller but how should this controller call/reload the model?
Since the date is part of your URL you should just use transitionTo or transitionToRoute. You probably have a route set up that allows you to match URLs that look something like /pictures/2013-10-09. Things get a little funky since 2013-10-09 isn't really an object id. Usually with transitionToRoute Ember expects that you'll pass in a live model that represents the content you're transitioning to. This would be the same object that Ember would look up by executing the model hook if the route is hit directly (without link-to or transitionTo). Since the date is really a query param and not an id you can use the setupController method to get around the funkiness.
So, your route might look something like this (this is simplified, you'll want to use the appropriate AJAX calls, of course) :
App.PicturesRoute = Ember.Route.extend({
model : function(params){
console.log('calling model for PicturesRoute');
return { date : params.date }; // return a fake model
},
setupController : function(controller, model){
// Make sure not to call super since we don't want to set
// a single object instead of an array
// this._super(controller,model); <-- do not use!
console.log('calling setupController for PicturesRoute');
// Instead set the `date` property directly
controller.set('date',model.date);
// Then find/build an array and set it as the model
var pictures = [
{name : "Pic 1 - " + model.date},
{name : "Pic 2 - " + model.date}
];
controller.set('model',pictures);
console.log(model);
}
});
Then within the app when you detect a change from the date picker you'd call something like this :
var dateFromPicker = ... // however you get a hold of the date string from the picker
var fakeModel = { date : dateFromPicker };
this.transitionTo('pictures',fakeModel);
Here's a JSBin showing a very simplified version of this idea : http://jsbin.com/ucanam/1396/edit
I hope that makes sense.
I have a two page Ember.js application using ember-data to wrap a simple RESTful API. The main page is a list of products, and the other page is a product details page for one product. The data is loaded via an API that only has an "index" request, /api/products.
The above works fine when navigating the site via the main page, however I'm not sure how best to handle navigating directly to the product details page. I need ember-data to request all products and keep these products client-side so that as the user navigates the simple site it doesn't make any more requests back to the API for products. However, the ProductIndexView and ProductIndexController in my application should preferably only see the one record.
Is there a good way to handle this in Ember.js? I know that I could add a computed property to the controller that filters down the full list and then pass that into the view template. However, I'd rather the view and controller not know about the full list.
You need to nest all your routes in a resources that fetches all products.
Something like this:
App.Route.map(function() {
this.resource('products', { path: '/' }, function() {
this.route('index');
this.resource('product', { path:'/:product_id'} );
});
});
App.ProductsRoute = Ember.Route.extend({
model: function() {
return App.Product.find({});
}
});
App.ProductsIndexRoute = Ember.Route.extend({
model: function() {
return this.modelFor('products');
};
});
Use the products/index template to display all products.
Use the product template to display a product detail.
Notice the {} I included in App.Product.find({}). This enforces ember-data to return a promise. This is necessary to make the product route wait for all products to arrive from the server before being called.