Ember.js update model when view changed? - javascript

New to ember here and I thought if you bind the data between view and model then both side will sync up if one changed.
Currently I have my model setup with Emberfire which returns an array of colors:
App.ApplicationRoute = Ember.Route.extend({
model: function() {
return EmberFire.Array.create({
ref: new Firebase("https://ember-starter.firebaseio.com/shape")
});
},
setupController: function(controller, model) {
controller.set('model', model);
}
});
My template is setup as such:
<button {{action "add"}}>Draw</button>
{{#each controller}}
{{#view App.ShapeView contentBinding="content"}}
<div>{{color}}</div>
{{/view}}
{{/each}}
I have a button to add new color to the array:
App.ApplicationController = Ember.ArrayController.extend ({
actions: {
add: function() {
var newColor = {
color: "#222222"
};
this.pushObject(newColor);
}
}
});
Within the view I setup a click action to set the color property:
App.ShapeView = Ember.View.extend({
click: function() {
var self = this;
var changeColor = self.set('context.color', '#121212');
}
});
With the current setup, I'm able to fetch/display a list of colors and change the color to #121212 upon click. However the data doesn't persist to the model(firebase). I'm wondering if I did something wrong or there are better ways to save/sync changes between view and model.
thanks in advance!

May be it is because you have a typo in your add function... this.pushObject(newColore); should be this.pushObject(newColor);
add: function() {
var newColor = {
color: "#222222"
};
this.pushObject(newColor);
}
Good luck

Related

Backbone collection fetch render runs twice with router on first load

I am fairly new to Backbone and am trying to get my head around routers and calling a collection from a database.
I have the following
Collection:
var Scorecards = Backbone.Collection.extend({
model:Scorecard,
url:"http://localhost:3002/api/scorecards",
initialize:function(){
this.fetch({
success: this.fetchSuccess,
error: this.fetchError
});
},
fetchSuccess: function (collection, response) {
console.log("results");
if(collection.length>0) {
var view = new ScorecardsView({el:'#scorecards-container', model:scorecards});
view.render();
}
else{
var view = new NoScorecardsView({el:'#scorecards-container'});
view.render();
}
},
fetchError: function(collection, response) {
throw new Error("Failed to get scorecards");
}
});
Router:
var ScorecardRouter = Backbone.Router.extend ({
routes: {
'' : 'home',
'create': 'createScorecard',
'edit': 'editScorecard'
},
home: function () {
console.log("Home view");
var view = new ScorecardsView({el:'#scorecards-container', model:scorecards});
view.render();
},
createScorecard: function () {
console.log('Create view');
var view = new CreateScorecardView({el:'#scorecards-container'});
view.render();
}
});
Scorecards view:
var ScorecardsView = Backbone.View.extend({
initialize: function(){
this.model.on('destroy', this.render, this);
},
render: function() {
console.log("Scorecard render");
var self = this;
this.$el.html(ScorecardContTemp);
this.model.each(function(scorecard){
var scorecardView = new ScorecardView({model:scorecard});
self.$('.scorecards-items tbody').append(scorecardView.render().$el);
});
},
events: {
"click #scorecard-create-btn" : "createScorecardView",
},
createScorecardView: function(e) {
e.preventDefault();
scorecardRouter.navigate('create', {trigger: true});
}
});
and I start things off with this
var scorecards = new Scorecards;
var scorecardRouter = new ScorecardRouter();
Backbone.history.start();
My problem is, when I first hit the home route, I'm getting the view render function running twice. Because firstly the fetch is calling it and also the route is calling it.
I need to remove the call from either the fetch success or the route, but when I do I get no results on initial load and I have to navigate to a different route and back to.
How are you supposed to achieve this? So I can fetch the results once and then display them via the route the fetch is successful but also show them in the route when a user navigates to it.
I hope that makes sense?
Any help would be great.
First of all, your data shouldn't know how it is rendered, so new View() anywhere within a Model or a Collection is a sure sign of a problem. Your views should watch their data and update themselves.
Your other possible source of confusion is passing {trigger: true} to your router navigate method. What kind of trouble that brings is elaborately explained in this classic Backbone article: Don’t Execute A Backbone.js Route Handler From Your Code.
For now, you definitely should remove the view rendering from the collection. Instead, your view should be aware of the collection and update itself when the data changes.
Here's an example of how I would setup my view to watch the collection:
/** Scorecard model */
var Scorecard = Backbone.Model.extend({
defaults: {
name: '',
email: ''
}
});
/** Scorecard View (I know it totally doesn't look like a scorecard, just an example view) */
var ScorecardView = Backbone.View.extend({
template: _.template('<%=name%>, <em><%=email%></em>'),
render: function(){
this.$el.html( this.template( this.model.toJSON() ) );
return this;
}
});
/** Collection */
var Scorecards = Backbone.Collection.extend({
model: Scorecard,
/** using fake api for the sake of this example to work */
url: "http://jsonplaceholder.typicode.com/users",
initialize:function(){
this.fetch({
success: this.fetchSuccess,
error: this.fetchError
});
},
fetchSuccess: function (collection, response) {
console.log("results:", collection);
},
fetchError: function(collection, response) {
throw new Error("Failed to get scorecards");
}
});
/** Scorecards view: */
var ScorecardsView = Backbone.View.extend({
initialize: function(){
this.collection.on('destroy', this.render, this);
/** render one added item whenever it comes to collection */
this.collection.on('add', this.addOne, this);
},
render: function() {
console.log("Scorecard render");
/** clean the items container,
which will be useful when items get destroyed
and we'll want to re-render whole collection */
this.$el.find('.scorecards-items').empty();
/** in case collection already has data, let's render it */
this.collection.each(this.addOne, this);
},
addOne: function(scorecard){
var scorecardView = new ScorecardView({ model: scorecard });
this.$('.scorecards-items').append(scorecardView.render().$el);
}
});
/** Router: */
var ScorecardRouter = Backbone.Router.extend ({
routes: {
'' : 'home'
},
home: function () {
console.log("Home view");
var view = new ScorecardsView({
el:'#scorecards-container',
collection: scorecards
});
view.render();
}
});
/** starting things off */
var scorecards = new Scorecards();
var scorecardRouter = new ScorecardRouter();
Backbone.history.start();
<script src='http://code.jquery.com/jquery.js'></script>
<script src='http://underscorejs.org/underscore.js'></script>
<script src='http://backbonejs.org/backbone.js'></script>
<div id="scorecards-container">
<div class="scorecards-items"></div>
</div>

How do I get Backbone to render the subView properly?

I am relatively new to Backbone.js and having difficulty rendering a subView. I have subViews in other parts of the app working properly, but I cant even render simple text in this one.
View:
Feeduni.Views.UnifeedShow = Backbone.View.extend({
template: JST['unifeeds/show'],
tagName: "section",
className: "unifeed-show",
render: function() {
var content = this.template({ unifeed: this.model });
this.$el.html(content);
var subView;
var that = this;
this.model.stories().each(function(stories) {
subView = new Feeduni.Views.StoriesShow({ model: stories });
that.subViews.push(subView);
that.$el.find(".show-content").append(subView.render().$el);
});
return this;
},
});
Subview:
Feeduni.Views.StoriesShow = Backbone.View.extend({
template: JST['stories/show'],
tagName: "div",
className: 'stories-show',
render: function() {
this.$el.text("Nothing shows up here");
return this;
},
});
Model:
Feeduni.Models.Unifeed = Backbone.Model.extend({
urlRoot: "/api/uninews",
stories: function() {
this._stories = this._stories || new Feeduni.Subsets.StoriesSub([], {
parentCollection: Feeduni.all_unifeeds
});
return this._stories;
},
});
The text "Nothing shows up here" should be displaying in the "show content" element, but all I get is this:
<section class="unifeed-show">
<article class="show-content">
</article>
</section>
Below is a slight modification of your code showing a working main view managing some sub-views.
var UnifeedShow = Backbone.View.extend({
// I've hard-coded the template here just for a sample
template: _.template("Feed: <%= feedName %><br/> <ul class='show-content'></ul>"),
className: "unifeed-show",
initialize: function () {
// Create an array to store our sub-views
this.subViews = [];
},
render: function () {
var content = this.template(this.model.toJSON());
this.$el.html(content);
var subView;
var that = this;
var subViewContent = this.$el.find(".show-content");
this.model.stories().each(function (story) {
var subView = new StoryShow({
model: story
});
this.subViews.push(subView);
subViewContent.append(subView.render().$el);
}, this);
return this;
}
});
var StoryShow = Backbone.View.extend({
tagName: 'li',
// This template will show the title
template: _.template('Title: <%= title %>'),
className: 'stories-show',
render: function () {
var content = this.template(this.model.toJSON());
this.$el.html(content);
return this;
},
});
var Unifeed = Backbone.Model.extend({
stories: function () {
// I'm just returning the value set on this model as a collection;
// You may need to do something different.
return new Backbone.Collection(this.get('stories'));
}
});
// ================================
// Code below is creating the model & view, then rendering
// ================================
// Create our model
var feed = new Unifeed();
// Put some data in the model so we have something to show
feed.set('feedName', 'A Sample Feed');
feed.set('stories', [{
title: "Story #1",
id: 1
}, {
title: "Story #2",
id: 5
}]);
// Create our main view
var mainView = new UnifeedShow({
model: feed,
el: $('#main')
});
// Render it, which should render the sub-views
mainView.render();
Here's a working JSFiddle:
https://jsfiddle.net/pwagener/7o9k5d6j/7/
Note that while this manual sort of sub-view management works OK, you'll be better off using something like a Marionette LayoutView to help manage parent and sub-views. It builds good best practices for this sort of thing without you needing to do it yourself.
Have fun!
The subview is named Feeduni.Views.StoriesShow but in your main view you are instantiating new Feeduni.Views.StoryShow. Name them consistently and see if you still have problems.

Backbone: how to get and display just one model data in a view via router

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) });
}

Backbone.js model event not triggering

I've got the following view file:
var BucketTransferView = Backbone.View.extend(
{
initialize: function(args)
{
_.bindAll(this);
this.from_bucket = args.from_bucket;
this.to_bucket = args.to_bucket;
},
events:
{
'click input[type="submit"]' : 'handleSubmit',
},
render: function()
{
$(this.el).html(ich.template_transfer_bucket(this.model.toJSON()));
return this;
},
handleSubmit: function(e)
{
that = this;
this.model.save(
{
date: 1234567890,
amount: this.$('#amount').val(),
from_bucket_id: this.from_bucket.get('id'),
to_bucket_id: this.to_bucket.get('id')
},
{
success: function()
{
// recalculate all bucket balances
window.app.model.buckets.trigger(
'refresh',
[that.to_bucket.get('id'), that.from_bucket.get('id')]
);
}
}
);
$.colorbox.close();
}
});
My buckets collection has this refresh method:
refresh: function(buckets)
{
that = this;
_.each(buckets, function(bucket)
{
that.get(bucket).fetch();
});
}
My problem is that when the fetch() happens and changes the collection's models, it's not triggering change events in other view classes that has the same models in it. The view's models have the same cid, so I thought it would trigger.
What's the reason this doesn't happen?
Fetch will create new model objects. Any view that's tied to the collection should bind to the collection's reset event and re-render itself. The view's models will still have the same cid's because they're holding a reference to an older version of the model. If you look at the buckets collection it probably has different cids.
My suggestion is in the view that renders the buckets, you should render all the child views and keep a reference to those views. then on the reset event, remove all the child views and re-render them.
initialize: function()
{
this.collection.bind('reset', this.render);
this._childViews = [];
},
render: function()
{
_(this._childViews).each(function(viewToRemove){
view.remove();
}, this);
this.collection.each(function(model){
var childView = new ChildView({
model: model
});
this._childViews.push(childView);
}, this)
}
I hope this works for you, or at least gets you going in the right direction.

In a Backbone app what is work of model, views and collections by convention

I am right now developing a dead simple application using backbonejs mvc javascript library.Just to show how simple it is here is the html
Sample Html
<div class="container">
<div>
<label>
Name:
<input id="txtName" type="text" title="Type your name please" /></label>
<button id="btnSubmit" type="button" value="Submit">
Submit</button>
</div>
<div id="names">
</div>
</div>
Operation
Here's all i want the application to do.
User Types a name(or some alphanumeric string) and clicks submit.
The value(which is what they call model i suppose) after validation will be sent to restful service.
Service will return the same string along with status of database save operation.
I am now confused as to where the click event will handled(is it in the model?), after that where should the render method be called?(is it in views). Below you will find the script i had managed up until now
Model.js
//store a global reference to model
window["model"] = Backbone.Model.extend({
defaults: { name: "Type your name"
},
validate: function () {
}
});
View.js
//store a global reference to view
window["view"] = Backbone.View.extend({});
nothing in the view to say :(
Application.js
//when every thing is ready kick of the application
$(document).ready(function () {
var modelInstance = new model();
});
so do i add click event to the button in application.js or model.js.which is the convention/best practice?
Will it be possible for me to render the names collection returned from service into #names and status of the database insertion into another div somewhere up above the container? Can the view render both views?
So, here is a disclaimer: If this is all your app is doing, then Backbone is way too much ceremony. When the app gets more complex with more moving parts, Backbone becomes pretty amazing.
That being said, I have created a jsFiddle to highlight how you might use Backbone for what you want to do: http://jsfiddle.net/BrianGenisio/CZwnk/4/
Basically, you have a Person model and a People collection to hold the Person models. You then have three views: NewPersonView which is the form for entering your data, which is responsible for adding new items to the People collection. Then you have a PeopleView view which is responsible for showing People collection, which will watch the collection and show any additions. Finally, is the PersonView which is responsible for showing an individual Person model.
When it all comes together, it looks a little something like this:
HTML:
<script id="new_person_template" type="text/template">
<label>
Name:
<input id="txtName" type="text" title="Type your name please" />
</label>
<button id="btnSubmit" type="button" value="Submit">Submit</button>
</script>
<script id="person_template" type="text/template">
Hello, my name is <%= name %>
</script>
<div id="container">
</div>
JavaScript
window.app = {};
window.app.Person = Backbone.Model.extend({
defaults: { name: "Type your name" },
validate: function () { }
});
window.app.People = Backbone.Collection.extend({
model: window.app.Person
});
window.app.NewPersonView = Backbone.View.extend({
template: _.template($("#new_person_template").html()),
initialize: function () {
_.bindAll(this, 'submit');
},
events:
{
"click #btnSubmit": "submit"
},
render: function () {
$(this.el).html(this.template());
return this;
},
submit: function (x, y, z) {
// If you want this to go up to the server and sync, do this instead:
// this.model.create({name: $("#txtName").val()});
// but since we don't really have a server right now, we'll just create and add
var person = new window.app.Person({name: $("#txtName").val()});
this.model.add(person);
}
});
window.app.PersonView = Backbone.View.extend({
tagname: "li",
template: _.template($("#person_template").html()),
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
window.app.PeopleView = Backbone.View.extend({
tagName: "ul",
initialize: function() {
_.bindAll(this, "render", "renderPerson");
this.model.bind("add", this.renderPerson);
},
render: function() {
console.log("rendering");
this.model.each(this.renderPerson);
return this;
},
renderPerson: function(person) {
var personView = new window.app.PersonView({model: person});
$(this.el).append(personView.render().el);
}
});
$(document).ready(function () {
var people = new window.app.People();
var newPersonView = new window.app.NewPersonView({model: people});
var peopleView = new window.app.PeopleView({model: people});
$("#container").html(newPersonView.render().el);
$("#container").append(peopleView.render().el);
});
i would do the click in the view
you would need 2 views in my eyes, 1 is an appview that holds your entire app
the other is just a view for each usermodel you render out in your list .
here is your user model
var User = Backbone.Model.extend({
defaults: {
id: 0,
name: 'no-name'
}
});
here is your collection
var Users = Backbone.Collection.extend({
model: User,
create : function(model, options) {
var coll = this;
options || (options = {});
if (!(model instanceof Backbone.Model)) {
model = new this.model(model, {collection: coll});
} else {
model.collection = coll;
}
var success = function(savedModel, resp) {
coll.add(savedModel);
if (options.success) options.success(nextModel, resp);
// fire success event
coll.trigger("savesuccess", savedModel);
};
var error = function(resp) {
if(options.error) options.error(resp);
// fire error event
coll.trigger("saveerror");
}
return model.save(null, {success : success, error : error});
}
});
so here is a possible user view
var UserRow = Backbone.View.extend({
tagName: "li",
initialize: function(){
_.bindAll(this, 'render');
this.model.bind('change', this.render);
},
className: "user",
render: function() {
var user = this.model;
$(this.el).html(user.get('name'));
return this;
}
});
here is your appView
var AppView = Backbone.View.extend({
el: 'body',
events: {
'click button#btnSubmit': 'addUser'
},
initialize: function(){
this.collection = new Users();
this.collection.bind('add', this.appendUser);
this.collection.bind('savesuccess', this.saveSuccess);
this.collection.bind('saveerror', this.saveError);
this.counter = 0;
this.render();
},
addUser: function(){
this.counter++;
var user = new User();
user.set({
id: user.get('id') + this.counter,
name: $('#txtName', this.el).val()
});
this.collection.add(user);
},
appendUser: function(user){
var userRow = new UserRow({
model: user
});
$('ul#names', this.el).append(userRow.render().el);
},
saveSuccess: function(user){
alert('successfully saved, you can append something to the AppView DOM to show that saving was successful ...');
},
saveError: function(){
alert('failed to save, you can append something to the AppView Dom to show that saving failed, or remove the item again ...');
}
});
here you initialize it all
var App = new AppView();
as you can see, rendering the items itself does not happen after saving it in the database, it is added to the collection, the appView binds to the add event of the collection and appends the new user to your list.
the CRUD server connection works automatically, due to using the create function on the collection.
the idea here is that you don't need to user backbone events, trigger an event from your collections successful save or error save, and let any view bind to that event to make something happen on the screen when save was a success or not.

Categories

Resources