I have the View containing some button. When that View becomes activated it should take the some URL parameter (in my case -- site id) and set it to the button attribute "data-site_id". There is a router too in the app. But I don't know how to implement it with the best way. Till now I see 3 supposed solutions:
Extract site id from URL hash. URL is build by such a pattern:
"sites/edit/:id(/:tab)": "editSite",
the question is -- may I use here a router itself (and, if yes then how?) or can not, and should parse it with common JS way? Of course, router & view are two different objects and located in different files/scopes.
Save the site_id in model. But I'm not sure how to store it from router. I think I can create an instance of model, set it as variable under router scope and then treat it as usual, something like this:
(function(app, $, config, _) {
var Model = new app.modelName();
var Router = app.Router = Backbone.Router.extend({
routes: {
"": "start",
//....
"sites/edit/:id(/:tab)": "editSite",
//...
},
//....
editSite: function(id, tab){
Model.set('site_id', id);
}
//....
})(window.Application, jQuery, window.chatConfig, _);
after that I can extract the site id from model in any time I want
Assign a data-site_id attribute to the button just from the router. But this doesn't look as a best practice, right?
So what is your advice?
Your second suggested solution makes the most sense, you could then instantiate your view passing that model to the constructor.
See http://backbonejs.org/#View-constructor
Related
Inside an application we allow users to create new records, related to an existing record. To achieve this, we use actions something like this:
createUser() {
var route = this;
var model = this.store.createRecord('user', {
client: route.modelFor('client'),
});
route.transitionTo('user.update', model);
},
The user.update route renders a user-form component, using the model that was passed in the transition. The same route is also used to update existing users.
The issue with this approach is as follows; when refreshing the page, the page errors because the route fails to find the respective record when querying the store (at this point, the URL is /users/null/update). Ideally I'd pass the client (or client.id) argument in the URL so that:
The page can be reloaded without issue.
The client associated with the user is set correctly.
How can I achieve this in Ember.js? I know that this can easily be done using nested routes (by nesting the user.update route inside a client route), but this doesn't make sense visually.
The relevant parts of the router are as follows:
this.route('clients');
this.route('client', {path: 'clients/:id'}, function() {
this.route('users');
});
this.route('user', {path: 'users/:id'}, function() {
this.route('update');
});
All I do in the user/update.hbs template is {{user-form user=model}}
The problem is that the model you just created has no id at that point because it is not saved, ember can´t route to a model without an id, if possible save the model before you try to transition to the route, if you don´t want to save the model because the user can cancel the action check this thread where a user had the same problem (if I understand you problem correctly), I provided a solution for that problem that I´m using in my own project
https://stackoverflow.com/a/33107273/2214998
This is an architectural question about Backbone JS:
AppView that contains a DOM element placeholder that is loaded with LeadsView.
LeadsView contains a DOM placeholder to present LeadView
My current route is #app/leads/1 which means that all 3 Views are loaded. AppView->LeadsView-> LeadView of lead #1.
Now suddenly the user hit the refresh button of the browser. The router would try to take it to #app/leads/:1 which routes to "app/leads/:lead_id" : "showLeadView", but AppView and LeadsView has not been rendered this time, hence the rendering of LeadView will fail.
It's looking for the DOM element to render itself into, but cannot find it.
How is that handled with Backbone?
TIA,
Nimrod.
You can programmatically initialize and render the other views when the "app/leads/:lead_id" : "showLeadView" route is hit.
The problem now becomes: How to find if a view was rendered or not ?
I solved this by adding a new property to my Models and Collections that would tell me if they were populated (either if they were fetched or if they have attributes set). An implementation for this can be found in the Thorax library repo.
And it looks like this:
isPopulated: function() {
/*jshint -W089 */
// We are populated if we have attributes set
var attributes = _.clone(this.attributes),
defaults = getValue(this, 'defaults') || {};
for (var default_key in defaults) {
if (attributes[default_key] != defaults[default_key]) {
return true;
}
delete attributes[default_key];
}
var keys = _.keys(attributes);
return keys.length > 1 || (keys.length === 1 && keys[0] !== this.idAttribute);
}
With this method i can verify if the collection needed to render the parent views has been populated. If it was't than call the methods that would be called if you navigated to app and then to leads (you practically access the routes programmatically).
It could look something like this:
if(!appModel.isPopulated())
this.showAppView();
if(!leadsCollection.isPopulated())
this.showLeadsView();
As you can see this means keeping references to appModel and leadsCollection in your router.
Update (clarification)
In the first line of my answer I was mentioning something like this:
AppView has a reference to a sub-view LeadsView that has a reference to a sub-view LeadView.
in your router you have an instance of AppView.
you pass appModel to AppView and call .render() on it when you hit app router.
you pass leadsCollection to AppView.getLeadsView() and call .render() on it when you hit leads route.
you pass the right lead model from leadsCollection to AppView.getLeadsView().getLeadView() and call .render() on it when you hit the route.
Or you can use something like Thorax
It already adds handling for subviews (takes care of event delegation, event destruction, DOM cleaning, etc.) and offers template handlers for it so you can point in your template where you want the sub-view to be rendered.
I hope now, it is clearer what I meant and as you can see it answers your question.
I cannot run the code below because router is undefined in my view. However I'm struggling to understand where I actually defined var router = new MyRouter(); my view, controller app start etc?
I'm using marionette and my router seems to automatically match my controllers.
success: function (page) {
id = page.get('id')
router.navigate('page/' + id, {trigger: true});
}
If you didn't declare a router attribute, you don't have one available (it doesn't get created automatically or anything), hence the undefined value.
To navigate to a given page, you can also use Backbone.history.navigate (see for example https://github.com/davidsulc/marionette-gentle-introduction/blob/master/assets/js/app.js).
In addition, you might want to reconsider using the trigger: true option, per the reasons explained here (Routing chapter) in the free sample to my book on Marionette.
so I need to be able to support an (finitely) unlimited number of views in ember.
Basically, I'm creating a survey app. The survey can have an undefined number of steps, hence an undefined number of views/controllers. The number of steps is got from the server, along with the content and the handlebars template (there are only a finite number of possible content types for a step), and I need some way of being able to extend the survey for the different surveys. So basically one ember app handles the entire thing, I've managed to abstract my code for rendering the content types out enough that it doesn't matter for the ember step: i now just need to make it support an unlimited number of steps
Is it possible to have ember views and controllers, but instead of referencing them by name, reference them by array indice? Like normally you'd go App.TestView = …, and router.get('applicationController').connectOutlet('test'), but could I use App.AllView[0] = Ember.View.extend(); and router.get('applicationController').connectOutlet('test')[0] or something? Then I can have a for loop at the start, which grabs the survey data which contains the content types and number of steps, plug it in a view array, and we're away
There might be other ways to achieve this, so any solutions would be great. Creating a new ember app is not an option, the same app needs to service whatever is passed in as a survey object.
You could create an array of view classes that contains the different steps. You can add new views to the array using the handlebars template name.
App.surveyViews = [];
App.surveyControllers = [];
App.surveyViews['firstStep'] = Em.View.extend({ templateName: 'my-handlebars-template' });
App.surveyControllers['firstStep'] = Em.Controller.extend();
Your route can take in the step from the route and use that to look up the appropriate view class and controller to use for the connectOutlet.
step: Em.Route.extend({
route: '/:step',
connectOutlets: function(router, context) {
router.get('applicationController').connectOutlet({
viewClass: App.surveyViews[context.step],
controller: App.surveyControllers[context.step].create(),
context: {}
});
}
})
You can add any extra logic you need for the context, etc., but that would be the basic idea.
I have an action:
{{action create target="controller"}}
which I have targeted to the bound controller (rather than the router) like this:
App.AddBoardController = Ember.Controller.extend
create: ->
App.store.createRecord App.Board, {title: #get "boardName"}
App.store.commit()
//TODO: Redirect to route
How do I redirect back to a route from the controller action?
Use transitionToRoute('route') to redirect inside an Ember controller action:
App.AddBoardController = Ember.Controller.extend({
create: function(){
...
//TODO: Redirect to route
this.transitionToRoute('route_name');
}
...
In fact, this is not Ember idiomatic. From what I know, and what I have learnt from Tom Dale himself, here are some remarks about that code:
First, you should not transitionTo from elsewhere than inside the router: by doing so, you are exposing yourself to serious issues as you don't know in which state is the router, so to keep stuff running, you will quickly have to degrade your design, and by the way the overall quality of you code, and finally the stability of your app,
Second, the action content you are showing should be located inside the router to avoid undesired context execution. The router is indeed a way to enforce a coherent behavior for the whole app, with actions being processed only in certain states. While you are putting the actions implementation into Controllers, those actions can be called at anytime, any including wrong...
Finally, Ember's controllers are not aimed to contain behavior as they rather are value-added wrappers, holding mainly computed properties. If you nevertheless want to factorize primitives, maybe the model can be a good place, or a third party context, but certainly not the Controller.
You should definitely put the action inside the router, and transitionTo accordingly.
Hope this will help.
UPDATE
First example (close to your sample)
In the appropriated route:
saveAndReturnSomewhere: function (router, event) {
var store = router.get('store'),
boardName = event.context; // you pass the (data|data container) here. In the view: {{action saveAndReturnSomewhere context="..."}}
store.createRecord(App.Board, {
title: boardName
});
store.commit();
router.transitionTo('somewhere');
}
Refactored example
I would recommend having the following routes:
show: displays an existing item,
edit: proposes to input item's fields
Into the enclosing route, following event handlers:
createItem: create a new record and transitionTo edit route, e.g
editItem: transitionTo edit route
Into the edit route, following event handlers:
saveItem: which will commit store and transitionTo show route, e.g
EDIT: Keep reading, Mike's answer discusses some of the problems with this approach.
You can just call transitionTo directly on the router. If you are using defaults this looks like App.router.transitionTo('route', context).