backbone general controller structure - javascript

I'm new to backbone and js. I have a general question regarding code structure. I'm currently building a webpage with a layout view and two regions. The left region is populated and shown on pageload. The right region is only shown and populated on a click in the left region. My controller looks like the following:
mainView.leftRegion.show(treeview);
...
treeItemView.on("childview:item:click", function(childview, item){
// Fetch collection of subitems from datasource
var fetchingSubitems = App.request("subitems:entity", item.get("id"));
...
// when done update item model with fetched collection
item.set({subitems : subitems});
var subitemsView = new app.subitemsList({
model : item
});
mainView.rightRegion.show(subitemsView);
}
My issue is that I want to show the right view outside of the click event and then update it, when the item model is being updated. But how should I structure it then?

you can create a general state Backbone model which contains subItems :
var State = Backbone.Model.extend({
defaults : {
subItems: []
}
});
send this model to the left and the right views on initialize :
state = new State();
var treeView = new TreeView({state:state});
var subitemsView = new app.subitemsList({state:state});
mainView.leftRegion.show(treeview);
mainView.rightRegion.show(subitemsView);
when you click on item you modify the state model :
treeItemView.on("childview:item:click", function(childview, item){
// Fetch collection of subitems from datasource
var fetchingSubitems = App.request("subitems:entity", item.get("id"));
...
// when done update state model
state.set("subItems",fetchingSubItems);
}
the right view must listen to change on subItems on state model :
this.listenTo(state, "change:subitems", this.changeCollectionAndRender);
the method "changeCollectionAndRender" must get the list of subItems from state model , update its collection and rerender :
changeCollectionAndRender:function(){
this.collection = this.state.get("subItems");
this.render();
}

Related

Backbone model property getting updated on update of another property

Here is my model:
var SendAlertsModel = Backbone.Model.extend({
defaults: {
customSubject: "",
customNote: "",
userList:[],
alertUserList:[]
}
});
Inside view:
initialize: function(options) {
var self= this;
if(_.isUndefined(options)===false){
self.model= new SendAlertsModel();
self.loggedInUser = app.user;
self.model.set("userList",options.previousTabData.get("userList"));
self.model.set("alertUserList",options.previousTabData.get("userList"));
self.model.get("alertUserList").push(self.loggedInUser);
}
},
The issue which i am facing here is when i push the loggedInUser to alertUserList array, it automatically pushes the same loggedInUser to userList array.
Please give your suggestions on this.
Thanks
//create new alerts model
model = new SendAlertsModel();
//assign logged in user to local variable
loggedInUser = app.user;
//set the user list to be the user list from the previous tab data
model.set("userList",options.previousTabData.get("userList"));
//set the alertUserList to be the previous tab user list by reference
model.set("alertUserList",options.previousTabData.get("userList"));
//push the logged in user into the alert user list
model.get("alertUserList").push(self.loggedInUser);
I think the issue occurs when you set the alertUserList to be the userList. As the user list is an object the alertUserList now contains a reference to the userList. It's not a copy. When you update the alertUserList you are actually updating the userList too.
Think of it like this:
var alertUserList = userList = {some object in memory};
In this line here you will want to create a copy rather:
model.set("alertUserList",options.previousTabData.get("userList"));
I'm not sure of what data type userList is, so it will depend on that. If you only need a shallow copy then you could do this using your underscore/lodash library (I assume that is what the "_" is):
model.set("alertUserList",_.clone(options.previousTabData.get("userList")));

calling render() after teardown() does not display list data

I have list of menu options, and each menu item has it's own Ractive instance with different template but same shared data. When each selection is changed I am calling teardown() on rendered view instance and render(domElement) on current selection's Ractive instance.
An example Instance is like below, and all follow the same structure.
var View = new Ractive({
template: '#contacts',
data: {
name: 'Contacts',
contacts : dummyData // array data
}
});
And I render them like below
var isRendered = false;
channel.subscribe("menu", function(msg) {
if(msg === "contacts") {
contentHolder.innerHTML = "";
View.render(contentHolder);
isRendered = true;
} else {
if(isRendered) {
View.teardown();
isRendered = false;
console.log(View.get('contacts')); // Here I can see the data.
}
}
});
In first render() call view is rendered as expected, but after calling teardown(), again if I call render() it does not render contacts list data and only displays name property, but was rendered on initial call.
Please help me to fix this.
Just for reference, the question was answered on GitHub
teardown() is a non-reversible call that completely destroys the ractive instance. What you want is detach() function, which will remove the ractive instance from the DOM but not destroy it. You can use it later by calling insert().

ember.js .find() only works when called 2nd time

Main goal: Using .find() to access a model other than the one available in the current controller -in order to compare data from the current controller's model with a piece of data from a 'foreign' controller's model.
What triggers the comparison:
I have a button inside a template with {{ action "isResponse"}}. This template's controller has an isResponse : function() {...}
The problem I have: The action is fired every time I click the button, but App.Answer.find() only returns content after the 2nd click. I'm wondering if this is because the Answer model hasn't loaded, but am unsure how to properly set up an observer for isLoaded in my example (if that is even the issue)
So how come App.Answer.find() returns empty the first time it's called??
App.ChoiceController = Ember.ObjectController.extend({
chosen: false,
isResponse: function() {
// successfully returns what I want from this controller's model
var questionId = this.get('question.id')
// gets DS.RecordArray of the model i'd like to compare with
var answers = App.Answer.find()
// filter to get a result that matches this.get('question.id')
var answer = answers.filter(function(ans) {
// returns all entries that match
if(ans.get('question.id') == questionId) { return true }
}, 'answers.isLoaded'); // this observer doesn't seem to hurt or help
// get the final value I need
var choice = answer.mapProperty('choice.id')
// if choice array is not empty, (should only have 1 element anyways)
if(!choice) {
this.set('chosen', choice[0]);
} else {
this.set('chosen', false);
}
}
})
Here are the models involved. Both include DS.belongsTo attributes
App.Choice = DS.Model.extend({
"question" : DS.belongsTo('App.Question')
})
App.Answer = DS.Model.extend({
"question" : DS.belongsTo('App.Question')
"choice" : DS.belongsTo('App.Choice')
})
App.Question = DS.Model.extend({
})
EDIT
Here is jsfiddle showing the behavior. Make sure to open your browser console to notice that each button requires 2 clicks for action isResponse to function properly. http://jsfiddle.net/iceking1624/QMBwe/
After reading your comment I've retought a solution to your problem and one possible way might be that you can define a AnswerController of type ArrayController (since it's for a collection of answers) and then setup this controller in your ApplicationRoute's setupController hook.
Main goal: Using .find() to access a model other than the one available in the current controller -in order to compare data from the current controller's model with a piece of data from a 'foreign' controller's model.
Later on you can then require access to the AnswerController's data using the needs API with needs:['answers'] from inside whatever controller that needs access to the answers collection, and finally have access to the data with this.get('controllers.answer'). You can find here more info on the needs API.
See here a possible solution that works correctly, displaying the right choice already on the 1st click:
App.AnswerController = Ember.ArrayController.extend({});
App.ApplicationRoute = Ember.Route.extend({
setupController: function(controller, model) {
this.controllerFor('answer').set('content', App.Answer.find());
}
});
App.ChoiceController = Ember.ObjectController.extend({
needs: ['answer'],
chosen: false,
isResponse: function() {
var questionId = this.get('question.id');
var answers = this.get('controllers.answer');
var answer = answers.content.filter(function(ans) {
if(ans.get('question.id') == questionId) { return true }
}
var choice = answer.mapProperty('choice.id');
if(!choice) {
this.set('chosen', choice[0]);
} else {
this.set('chosen', false);
}
}
});
And here a working fiddle.
Hope it helps.

How to set the id of clicked item of one collection as attribute for another collection using backbone js

I am new to backbone and I am stuck with a problem
problem in short
I have two lists to be shown.
1.Courses
2.Students for selected course
which I am able to show now.
Now in the StudentList view(it uses "Students" collection) I have an add button,which adds the student for the selected course item(which is in separate view). For that I need to know which course item I have clicked inside the StudentList view. For that I have stored the courseId in a hidden field when a course is clicked and later in StudentList view I have fetched that hidden field value to add the new student.
What I want to do is instead of storing the courseId in a hidden field,can I add the courseId to the "Students" collection as an attribute when a course is clicked.
Tried Approach
Inside StudentList view I have written something like this
var StudentList = Backbone.View.extend({
initialize: function () {
this._meta = {};
},
put: function (prop, value) {
this._meta[prop] = value;
},
get: function (prop) {
return this._meta[prop];
},
events: {
"click #btnAddStudent": "createNewStudent"
},
createNewStudent: function () {
var some = this.get("someProp");
this.collection.create({ Name: this.$el.find('#txtNewStudent').val(),
CourseId: some });
}
});
And in "courseClicked" function I have done like this(the "courseClicked" function is inside the CourseItem view)
var CourseItem = Backbone.View.extend({
events: {
'click': 'courseClicked'
},
initialize: function () {
this.students = this.options.students;
},
courseClicked: function () {
var courseId = this.model.id;
this.students.put('someProp',courseId);
this.students.fetch({ data: { courseId: courseId} });
}
});
The above doesn't work because put and get functions are not available in the courseitem view context,they are defined in StudentList view.Can anyone guide me how to accomplish my needs.
This question is continuation to the one I posted earlier on SO here
Using Event Aggregator to load a view with different model in backbone js
This link might help to know further details of my question.
Thanks for your patience
this should be a partial answer which might lead you to right way or into the trenches. Here it goes.
It is best that you have 2 views one for Students list and one for course list. Providing a checkbox list should be good for the course list as Students -> Course is one to many relationship.
Course(model) should have a selected attribute(only on client, no significance to server side) which you toggle during checkbox click. Courses (collection) should redraw the changed course with highlights etc
The Courses(Collection) should be accessible through window object. something like window.App.Students.Courses where Courses is an instance of Courses collection.
ex:window.App.Students.Courses = new window.App.Students.Collection.Courses;
Now when the course is selected and students are selected. You have global access to courses that have been selected ( which can be retrieved using underscore.js methods) then set the course id into a array for the selected students. something like for each selectedstudent ( foreach selectedCourse selectedStudent.course.push(selectedCourse)). json for student will then look like below
{
id: 1,
name: "Jack Keane",
courses: [1, 2, 5, 6, 7]
}
Code
Inside the Student view, do the following to get the selected courses for the student.
window.courses.filter(function(course){
return course.get("selected");
});
use the filter method for obtaining the selected courses. Then assign the course to the student as in #4.
var selectedCourses = window.courses.filter(function(course){
return course.get("selected");
});
this.collection.forEach(function(student){
student.set("courses") = selectedCourses;
});
by doing the above, every student now has courses attribute that gives you the selected courses for this student.

Inserting an item into a backbone.js collection

Is there a straightforward way to insert a new model item into the middle of a backbone.js Collection and then update the collection's View to include the new item in the correct position?
I'm working on a control to add/delete items from a list. Each list item has its own Model and View, and I have a View for the entire collection as well.
Each item view has a Duplicate button that clones the item's model and inserts it into the collection at the index position below the item that was clicked.
Inserting the item into the collection was straightforward, but I'm having trouble figuring out how to update the collection view. I've been trying something like this:
ListView = Backbone.View.extend({
el: '#list-rows',
initialize: function () {
_.bindAll(this);
this.collection = new Items();
this.collection.bind('add', this.addItem);
this.render();
},
render: function () {
this.collection.each(this.addItems);
return this;
},
addItem: function (item) {
var itemView = new ItemView({ model: item }),
rendered = itemView.render().el,
index = this.collection.indexOf(item),
rows = $('.item-row');
if (rows.length > 1) {
$(rows[index - 1]).after(rendered);
} else {
this.$el.append(rendered);
}
}
}
This implementation is sort of working, but I'm getting strange bugs when I add a new item. I'm sure I can sort those out, but ...
There's a voice in my head keeps telling me that there's a better way to do this. Having to manually figure out where to insert a new ItemView seems really hacky--shouldn't the collection view know how to rerender the collection already?
Any suggestions?
I don't think re-rendering the whole collection when adding a new element is the best solution. It's slower than inserting the new item at the right place, especially if the list is long.
Also, consider the following scenario. You load a few items in your collections, and then you add n more items (say the user clicks a "load more" button). To do this you would call the fetch() method passing add: true as one of the options. As the data is received back from the server, the 'add' event is fired n times and you'd end up re-rendering your list n times.
I'm actually using a variant of the code in your question, here's my callback to the 'add' event:
var view, prev, prev_index;
view = new ItemView({ model: new_item }).render().el;
prev_index = self.model.indexOf(new_item) - 1;
prev = self.$el.find('li:eq(' + prev_index + ')');
if (prev.length > 0) {
prev.after(view);
} else {
self.$el.prepend(view);
}
So, essentially I'm just using the :eq() jQuery selector instead of getting all the elements as you do, should use less memory.
The usual way I'm doing is let the ListView render each ItemView in its render function. Then I just bind the add event to the render function, like this:
ListView = Backbone.View.extend({
el: '#list-rows'
, initialize: function () {
_.bindAll(this);
this.collection = new Items();
this.collection.bind('add', this.render);
this.render();
}
, render: function () {
this.$el.empty();
var self = this;
this.collection.each(function(item) {
self.$el.append(new ItemView({ model: item }).render().el);
});
return this;
}
}
Everytime you call this.collection.add(someModel, {at: index}), the view will be re-rendered accordingly.

Categories