I’m doing a very basic application with Ember and Ember Data.
For some reason I always have the same problem. My application renders and displays the data correctly, but if I remove and search, it doesn't update the view.
I’ve already asked this here—the link has more code examples—but with not much luck. Here is how I’m trying to do it:
App = Ember.Application.create({
LOG_TRANSITIONS: true, LOG_VIEW_LOOKUPS: true
});
App.ApplicationAdapter = DS.FixtureAdapter.extend();
App.Sample = DS.Model.extend({ name: DS.attr('string') });
App.IndexRoute = Ember.Route.extend({
model: function() {
return this.store.find('sample');
}
});
App.IndexController = Ember.ArrayController.extend({
actions: {
remove: function(sample) {
sample.destroyRecord();
}
}
});
App.Sample.FIXTURES = [
{ id: 1, name: 'Learn Ember.js'},
{ id: 2, name: 'Record 2' },
{ id: 3, name: 'Test Delete' }
];
App.ApplicationRoute = Ember.Route.extend({
actions: {
showModal: function(name, content) {
this.controllerFor(name).set('content', content);
this.render(name, {
into: 'application',
outlet: 'modal'
});
},
removeModal: function() {
this.disconnectOutlet({
outlet: 'modal',
parentView: 'application'
});
}
}
});
App.MyModalComponent = Ember.Component.extend({
actions: {
ok: function() {
this.$('.modal').modal('hide');
this.sendAction('ok');
}
},
show: function() {
this.$('.modal').modal().on('hidden.bs.modal', function() {
this.sendAction('close');
}.bind(this));
}.on('didInsertElement')
});
From your code I have tried to come up with a reasonable solution for your problem
Before I get into the solution I think the controller should be IndexController rather than sampleDeleteModalController because ember expects controller to have same name as the route.
App.SampleDeleteModalController = Ember.ObjectController.extend({
actions: {
remove: function() {
// Two ways
this.get('model').destroyRecord();
this.transitionToRoute('index');
}
}
});
transitionToRoute from the same route will not refresh a view.This will work only when you want to redirect to another route.
Solution to refresh view
option 1 : you can capture the same action inside index route after removing the record you can do this.refesh() which refreshes the model.
option 2 : You have to explicitly update the binded model inside the controller.
actions: {
remove: function() {
// Two ways
var localCopy = this.get('model');
localCopy.destroyRecord();
this.set('model',localCopy);
}
}
option 3 : After you set your model your model and then do this.rerender().Which is almost equivalent to window.reload()
Related
I have Uncaught Type Error : UserRegisterView is not a constructor.I dont understand this error.I looked all code but i dont find it.
Sorry of my bad english.Please help me
Thanks for answer
UPDATED
UserRegisterView is here
var UserRegisterView = Backbone.View.extend({
model: User,
el: '#form',
events: {
'click input[id="infoWeek"]': 'infoWeek',
'click input[id="infoMonth"]': 'infoMonth'
},
infoWeek: function() {
this.$el.find("#dayOfMonth").hide();
this.render();
},
infoMonth: function() {
this.$el.find("#dayOfWeek").hide();
this.render();
}
});
var AddUserView = Backbone.View.extend({
el: $(".page"),
events: {
'click #saveUser': 'saveUser'
},
saveUser: function() {
var user = new User();
user.set({
username: $("#username").val(),
lastName: $("#lastName").val(),
regNumber: $("#regNumber").val(),
password: $("#password").val(),
departmentName: $("#departmentName").val(),
email: $("#email").val(),
role: $("#role").val()
});
user.save();
if (document.getElementById('isOpen').checked) {
user.set("isOpen", $("#isOpen").val("1"));
user.save();
} else {
user.set("isOpen", $("#isOpen").val("0"));
user.save();
}
if (document.getElementById('dayOfWeek').checked) {
user.set("dayOfWeek", $("#dayOfWeek").val());
user.save();
} else if (document.getElementById('dayOfMonth').checked) {
user.set("dayOfMonth", $("#dayOfMonth").val());
user.save();
}
$("#username").val("");
$("#firstName").val("");
$("#lastName").val("");
$("#regNumber").val("");
$("#password").val("");
$("#deparmentName").val("");
$("#email").val("");
$("#isOpen").val("");
$("#dayOfWeek").val("");
$("#dayOfMonth").val("");
},
render: function() {
var that = this;
var template = Handlebars.compile(UserRegister);
var myHtml = template(that.model.toJSON());
that.$el.html(myHtml);
return this;
}
});
return {
AddUserView: AddUserView,
UserRegisterView: UserRegisterView
};
});
router user func.
define([
'jquery',
'underscore',
'backbone',
'handlebars',
'spin',
'app/models/LoginModel',
'app/views/LoginView',
'app/views/UserRegisterView'
], function($,
_,
Backbone,
Handlebars,
Spinner,
Login,
LoginView,
UserRegisterView
) {
var Router = Backbone.Router.extend({
routes: {
'search': 'search',
'login': 'login',
'travels': 'travels',
'user': 'user',
'menu': 'menu',
'': 'home'
},
user: function() {
disposeView(new UserRegisterView().render());
}
dispose.view on util.js
function disposeView(view) {
Backbone.View.prototype.close = function() {
this.unbind();
this.undelegateEvents();
};
/* Şu anki viewi yok et */
if (this.currentView !== undefined) {
this.currentView.close();
}
/* Yeni view oluştur. */
this.currentView = view;
this.currentView.delegateEvents();
return this.currentView;
}
What's happening
Your UserRegisterView module returns an object which contains two constructors.
return {
AddUserView: AddUserView,
UserRegisterView: UserRegisterView
};
When using this module, what you're getting is the object above.
define([
// ...
'app/views/UserRegisterView'
], function(
// ...
UserRegisterView // value of the return in the module
) {
So you're kind of misleading yourself by calling it UserRegisterView as it's not the constructor, but the object containing the constructor.
To get a new UserRegisterView view instance with the current way your module is setup, you'd need to call it like so:
var userView = new UserRegisterView.UserRegisterView();
Or to create a AddUserView instance:
var addView = new UserRegisterView.AddUserView();
Solutions
Split up the module, one for each view constructor.
Change the name so at least it's not misleading (like UserViewsModule)
Other improvements
That being said, there are other improvements that could be made to your Backbone code.
var UserRegisterView = Backbone.View.extend({
// that's useless (if not used) and not a view property.
// model: User,
// don't use `el` like that, especially when using the view as a shared Constructor
el: '#form',
events: {
'click input[id="infoWeek"]': 'onInfoWeekClick',
'click input[id="infoMonth"]': 'onInfoMonthClick'
},
initialize: function() {
// Cache jQuery object of the view's element
this.$dayOfMonth = this.$("#dayOfMonth");
this.$dayOfMonth = this.$("#dayOfMonth");
// also use the shortcut function instead of `this.$el.find()`
}
onInfoWeekClick: function(e) {
this.$dayOfMonth.hide();
// calling render here is useless unless your using it as a parent
// view, where the child view overrides the render function.
},
onInfoMonthClick: function(e) {
this.$dayOfMonth.hide();
}
});
The disposeView function could be simplified:
function disposeView(view) {
var current = this.currentView;
if (current) current.close();
current = this.currentView = view;
current.delegateEvents();
return current;
}
Don't change the default Backbone view prototype each time the function is called. Instead, add the function once.
_.extend(Backbone.View.prototype, {
close: function() {
this.unbind();
this.undelegateEvents();
},
// any other function you want to add can go here.
});
In another answer, I go into details on how to extend Backbone's core classes with requirejs transparently.
You're already using jQuery, so don't use JavaScript DOM API document.getElementById('isOpen') interspersed with jQuery selectors $('#isOpen').
I made some improvements to the following view. Take the time to create yourself some utility functions (like reset and getValues) to simplify the flow of the code and encapsulate the complexity.
var AddUserView = Backbone.View.extend({
el: $(".page"),
events: {
'click #saveUser': 'saveUser'
},
// compile the template once while creating the view class
template: Handlebars.compile(UserRegister),
// get the selector string out of the code and place them in one place
// easy to change and maintain.
fields: {
username: "#username",
firstName: "#firstName",
lastName: "#lastName",
regNumber: "#regNumber",
password: "#password",
deparmentName: "#deparmentName",
email: "#email",
isOpen: "#isOpen",
dayOfWeek: "#dayOfWeek",
dayOfMonth: "#dayOfMonth",
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
// cache jQuery object of every field once after a render
this.field = _.reduce(this.fields, function(fields, selector, key) {
fields['$' + key] = this.$(selector);
return fields;
}, {}, this);
return this;
},
reset: function() {
// reset all the fields once without repeating code.
_.each(this.field, function($field) {
$field.val("");
});
return this;
},
getValues: function(keys) {
// get the value of multiple fields returned in a nice object
// ready to be sent to a Backbone model.
return _.reduce(keys, function(data, key) {
data[key] = this.field[key].val();
return data;
}, {}, this);
},
saveUser: function() {
var field = this.field,
user = new User(this.getValues([
'username',
'lastName',
'regNumber',
'password',
'departmentName',
'email',
'role',
]));
user.set({ isOpen: field.$isOpen.is(':checked') });
if (field.$dayOfWeek.is(':checked')) {
user.set("dayOfWeek", field.$dayOfWeek.val());
} else if (field.$dayOfMonth.is(':checked')) {
user.set("dayOfMonth", field.$dayOfMonth.val());
}
user.save();
this.reset();
},
});
In the following snippet, you're putting the context (this) into a local variable. I see that a lot and I could say that 90% of the times I see it on Stack Overflow questions, it makes no sense. It clearly screams copy-pasted.
render: function() {
var that = this;
// ...
that.$el.html(myHtml);
return this;
}
Please tell me you see that you're putting this into that, then using that throughout the function, then you still return this?!
Putting the context into a local variable is useful when the object is needed in a dynamically created callback.
render: function() {
var that = this; // this is available here
setTimeout(function() {
// here this is not available.
that.handleCallback();
}, 10);
// here we are in the same context as the first line.
return this;
}
I have created a very basic backbone app, to understand how it works.
In the router, I just wanna display just 1 model, i.e. a user, not the whole collection, by passing an id in the url, how to do that?
For example, I'd like to do someapp.com/app/#user/2, and this would display just user no2 details.
Please see my work in jsfiddle
// router
var ViewsRouter = Backbone.Router.extend({
routes: {
'': 'viewOne',
'one': 'viewOne',
'two': 'viewTwo',
'user/:id': 'user'
},
viewOne: function() {
var view = new TheViewOne({ model: new TheModel });
},
viewTwo: function() {
var view = new UserView({ model: new TheModel });
},
user: function(id) {
// how to get just 1 user with the corresponding id passed as argument
// and display it???
}
});
Many thanks.
https://github.com/coding-idiot/BackboneCRUD
I've written a complete Backbone CRUD with no backend stuff for beginners. Below is the part where we get the user from the collection and show/render it.
View
var UserEditView = Backbone.View.extend({
render: function(options) {
if (options && options.id) {
var template = _.template($("#user-edit-template").html(), {
user: userCollection.get(options.id)
});
this.$el.html(template);
} else {
var template = _.template($("#user-edit-template").html(), {
user: null
});
// console.log(template);
this.$el.html(template);
}
return this;
},
Router
router.on('route:editUser', function(id) {
console.log("Show edit user view : " + id);
var userEditView = new UserEditView({
el: '.content'
});
userEditView.render({
id: id
});
});
Update
Particularly, sticking to your code, the router will look something like this :
user: function(id) {
var view = new UserView({ model: userCollection.get(id) });
}
So, this is what my model looks like (represented by fixture data):
var posts = [{
id: 'b026324c6904b2a9',
title: "My new front door",
author: { name: "Matthieu" },
date: new Date('2013-10-28T12:19:30.789'),
status: 'new',
hidden_message: "hidden1"
}, {
id: '26ab0db90d72e28a',
title: "Best pizza in town",
author: { name: "Harry" },
date: new Date('2013-10-28T12:19:30.789'),
status: '',
hidden_message: "hidden2"
}, {
id: '6d7fce9fee471194',
title: "Skateboard dreamland",
author: { name: "Matthieu" },
date: new Date('2013-10-28T12:19:30.789'),
status: 'solved',
hidden_message: "hidden3"
}, {
id: '48a24b70a0b37653',
title: "my house looks like a pumpkin",
author: { name: "Harry" },
date: new Date('2013-10-28T12:19:30.789'),
status: '',
hidden_message: "hidden4"
}];
My route:
App.IndexRoute = Ember.Route.extend({
model: function() {
return posts;
}
});
And, I'd like to be able to display a certain piece of HTML in the template if the corresponding post is new, a different one if it's solved, and nothing if the status is blank. It seems to me as if the best way to do this is using an {{#if}} helper, but that doesn't do equality comparisons, it can only take a boolean variable. So, I'd like to do something like this:
App.IndexController = Ember.ArrayController.extend({
isNew: function(val) {
if(this.get('currentitem.status') === 'new') {
return true;
}
return false;
}.property('isNew')
});
But I can't find out how to select the item being currently accessed by {{#each}} in the template. Is this even possible, and if yes, how do I do it (or something similar)?
Thanks all!
The correct way to do this is to create an itemcontroller that helps you by providing a controller per item in your collection.
App.IndexController = Ember.ArrayController.extend({
itemController: "PostItem",
});
App.PostItemController = Ember.ObjectController.extend({
isNew: function() {
if(this.get('status') === 'new') {
return true;
}
return false;
}.property('status')
});
Then in your handlebar template you can just call {{isNew}} in the {{#each}}-context.
I've put up a working fiddle that you can test it out in.
http://jsfiddle.net/LordDaimos/v8NR3/1/
Best way would probably be to wrap each post in an object that has the isNew method, like this:
var postObject = Ember.Object.extend({
isNew: function() {
if(this.get('status') === 'new') {
return true;
}
return false;
}.property('status')
});
App.IndexRoute = Ember.Route.extend({
model: function() {
return posts.map(function(post){
return postObject.create(post);
});
}
});
this way you could query on each post.
I'm struggling to get a simple master-detail scenario working with Backbone. Here's the jsfiddle and code is below.
Problem 1: this navigation doesn't work at all if I switch "pushstate" to true. What I really want is to have no hashes/pound signs in my urls.
Problem 2: my users might rock up on a url like /accommodation/287, not always on the home page. How would you deal with that using the router?
Thanks a lot for any help!
var AccommodationItem = Backbone.Model.extend({
defaults: {
html: "",
loaded: false
},
urlRoot: "/Home/Accommodation/"
});
var AccommodationItemView = Backbone.View.extend({
tagName: "li",
template: _.template("<a href='#accommodation/<%= id %>'><%= description %></a>"),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var AccommodationList = Backbone.Collection.extend({
model: AccommodationItem
});
var DetailView = Backbone.View.extend({
initialize: function () { },
render: function () {
this.$el.html(this.model.get("html"));
},
setModel: function (model) {
this.model = model;
var $this = this;
if (!this.model.get("loaded")) {
/*
this.model.fetch({ success: function () {
$this.model.set("loaded", true);
$this.render();
}
});*/
$this.model.set("html", "<h2>Full item " + this.model.get("id") + "</h2>");
$this.model.set("loaded", true);
$this.render();
} else {
$this.render();
}
}
});
var AccommodationListView = Backbone.View.extend({
tagName: "ul",
initialize: function () {
this.collection.on("reset", this.render, this);
},
render: function () {
this.addAll();
},
addOne: function (item) {
var itemView = new AccommodationItemView({ model: item });
this.$el.append(itemView.render().el);
},
addAll: function () {
this.collection.forEach(this.addOne, this);
}
});
var App = new (Backbone.Router.extend({
routes: {
"": "index",
"accommodation/:id": "show"
},
initialize: function () {
this.detailView = new DetailView({ model: new AccommodationItem({ id: 1 }) });
$("#detail").append(this.detailView.el);
this.accommodationList = new AccommodationList();
this.accommodationListView = new AccommodationListView({ collection: this.accommodationList });
$("#app").append(this.accommodationListView.el);
},
start: function () {
Backbone.history.start({ pushState: false });
},
index: function () {
this.fetchCollections();
},
show: function (id) {
var model = this.accommodationList.get(id);
this.detailView.setModel(model);
},
fetchCollections: function () {
var items = [{ id: 1, description: "item one" }, { id: 2, description: "item two" }, { id: 3, description: "item three" }];
this.accommodationList.reset(items);
}
}));
$(function () {
App.start();
});
EDIT: In a comment below I mentioned the Codeschool backbone.js tutorial. Just want to say that I have now finished BOTH parts of the course and it DOES cover exactly the AppView pattern described in the accepted answer. It's an excellent course and I thoroughly recommend it.
you have a few of the concepts mixed up.
There is too much to explain here, so I've (very roughly) put together a patch of your code that works as you intend. I would advise that you put it side-by-side with your own and see what I have done differently.
http://jsfiddle.net/wtxK8/2
A couple of things, you should not init Backbone.history from within a router. your 'init' should look something more like this
$(function () {
window.app = new App();
window.appView = new AppView({el:document});
Backbone.history.start({ pushState: true });
});
This is setting a 'wrapper' view than encompasses the entire page. Also, you have far too much logic in your router. Try to only use the router for routes. After my quick re factor, your router only contains this:
var App = Backbone.Router.extend({
routes: {
"": "index",
"accommodation/:id": "show"
},
show: function (id) {
var model = window.appView.accommodationList.get(id);
window.appView.detailView.setModel(model);
}
});
The AppView (that I have written for you now does all of that initialize work.
var AppView = Backbone.View.extend({
initialize : function(){
this.detailView = new DetailView({ model: new AccommodationItem({ id: 1 }) });
$("#detail").append(this.detailView.el);
this.accommodationList = new AccommodationList();
this.accommodationListView = new AccommodationListView({ collection: this.accommodationList });
$("#app").append(this.accommodationListView.el);
this.fetchCollections();
},
fetchCollections: function () {
var items = [
{ id: 1, description: "item one" },
{ id: 2, description: "item two" },
{ id: 3, description: "item three" }
];
this.accommodationList.reset(items);
}
});
Even after my re factor, it's still far from optimal, but I have provided it all to help you on your journey of learning :)
I would then recommend you follow some of the on-line tutorials step-by-step so that you can set up the structure of your app in a better way.
Good Luck, and be sure to check out http://jsfiddle.net/wtxK8/2 to see it working.
EDIT: I have not address your second question. there is enough to be worked on with question 1 to keep you busy. If I have more time later, I will help further.
I've just started foundational work for a Backbone JS SPA (single page application). I'm using the basic Underscore templating support, and am having issues with unexpected routing occurring.
Basically, the sign up view is shown initially as expected, POSTs succesfully when I click a button and I have it navigate to a simple test view. However, the test view is quickly rendered and then I get re-routed to the default sign up view again.
I see the history of the test page and if I hit the back button I go back to that test view which works fine. I see there is some event being triggered in Backbone which is routing me back to the blank fragment (sign up page), but I have no idea why. I've tried messing with the replace and trigger options on the navigate call with no luck.
As a side note, the start() and stop() View functions were adapted from this article: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/ . I tried removing this and it had no effect.
$(document).ready( function(){
Backbone.View.prototype.start = function() {
console.debug('starting');
if (this.model && this.modelEvents) {
_.each(this.modelEvents,
function(functionName, event) {
this.model.bind(event, this[functionName], this);
}, this);
}
console.debug('started');
return this;
};
Backbone.View.prototype.stop = function() {
console.debug('stopping');
if (this.model && this.modelEvents) {
_.each(this.modelEvents,
function(functionName, event) {
this.model.unbind(event, this[functionName]);
}, this);
}
console.debug('stopped');
return this;
};
var myApp = {};
myApp.SignUp = Backbone.Model.extend({
urlRoot:"rest/v1/user",
defaults: {
emailAddress: "email#me.com",
firstName: "First Name",
lastName: "Last Name",
password: "",
confirmPassword: ""
}
});
myApp.SignUpView = Backbone.View.extend({
el: $('#bodyTarget'),
modelEvents: {
'change' : 'render'
},
render: function(){
document.title = "Sign Up Page";
// Compile the template using underscore
var template = _.template( $("#signUpTemplate").html(), this.model.toJSON());
// Load the compiled HTML into the Backbone "el"
this.$el.html( template );
return this;
},
events: {
"click .signUpButton": "signUp"
},
signUp: function() {
bindFormValues(this);
this.model.save(this.model.attributes, {error: this.signUpFailure});
myApp.router.navigate("test", {trigger: true});
},
signUpFailure: function(model, response) {
alert("Failure: " + response);
}
});
myApp.TestView = Backbone.View.extend({
el: $('#bodyTarget'),
modelEvents: {
'change' : 'render'
},
render: function() {
document.title = "Test Page";
this.$el.html( "<div>this is a test view</div>");
return this;
}
});
// for now, just pull values from form back into model
function bindFormValues(view) {
var mod = view.model;
var el = view.$el;
var updates = {};
for (var prop in mod.attributes) {
var found = el.find('* [name="' + prop + '"]');
if (found.length > 0) {
updates[prop] = found.val();
}
}
mod.set(updates/*, {error: errorHandler}*/);
}
// routers
myApp.Router = Backbone.Router.extend({
routes: {
'test': 'test',
'': 'home',
},
home: function() {
console.debug('home:enter');
this.signUpView = new myApp.SignUpView({model: new myApp.SignUp()});
this.showView(this.signUpView);
console.debug('home:exit');
},
test: function() {
console.debug('test:enter');
this.testView = new myApp.TestView({model: new myApp.SignUp()});
this.showView(this.testView);
console.debug('test:exit');
},
showView: function(view) {
if (this.currentView) {
this.currentView.stop();
}
this.currentView = view.start().render();
}
});
myApp.router = new myApp.Router();
Backbone.history.start();
});
My HTML page just brings in the relevant scripts and has the div element bodyTarget which is injected with the views when it loads.
Edit: Duh, I found the problem. It turns out I needed to prevent event propagation on the call to signUp() by returning false.