In this step, we are going to setup a tree view. To make room for a tree view, we will put the current list view in a tab view, then we will put our new tree view in another tab of the same tab view.
Also, so far we just have tasks with no relationships. We should probably add another model object so we can create a hierarchy of objects. Let's put tasks inside of a project model object.
Here are the steps to this step:
- Step 1 Define a new model object, and relationships
- Define the model object
- Define the relationship
- Define the fixture data
- Step 2 Define a tab view to put the tree view
- Step 3 Make the model object behave like a tree node
- Step 4 Create a tree controller
- Step 5 Add a tree view
- Step 6 Keep the existing controllers in sync with tree view selections
- Step 7 Display the current selected project
Video 1: Covers Step 1 and Step 2.
Video 2: Covers Step 2 (the rest), Step 3, and Step 5.
Video 3: Covers Step 4, Step 6, and Step 7.
Step 1 Define a new model object, and relationships
Define the model object and the relationship
Run this from the root of the project directory on the command line:
$ sc-gen model Todos.Project
Modify the apps/todos/models/project.js file to add a name, description and related tasks:
Todos.Project = SC.Record.extend(
/** @scope Todos.Project.prototype */ {
name: SC.Record.attr(String),
description: SC.Record.attr(String),
tasks: SC.Record.toMany("Todos.Task", {
inverse: "project", isMaster: YES
}),
}) ;
Let's go ahead and make this relationship bi-directional by modifying Todos.Task:
Todos.Task = SC.Record.extend(
/** @scope Todos.Task.prototype */ {
isDone: SC.Record.attr(Boolean),
description: SC.Record.attr(String),
projectCode: SC.Record.attr(String),
project: SC.Record.toOne("Todos.Project", {
inverse: "tasks", isMaster: NO
}),
}) ;
Define the fixture data
Add task fixture, notice that we add the project guids under the project key.
sc_require('models/task');
Todos.Task.FIXTURES = [
{ guid: 1,
description: "Build my first SproutCore app",
isDone: false,
project: 1
},
{ guid: 2,
description: "Build a really awesome SproutCore app",
projectCode : "abc-xyz",
isDone: false,
project: 1
},
{ guid: 3,
description: "Next, the world!",
isDone: false,
projectCode : "abc-def",
project: 2
},
{ guid: 4,
description: "Conquer the world!",
isDone: false,
project: 2
}
];
Add project fixture data, notice we add a list of task guids under the tasks key:
sc_require('models/project');
Todos.Project.FIXTURES = [
{
guid: 1,
description: "first project",
name : "first project",
tasks: [1,2]
},
{
guid: 2,
description: "second project",
name : "second project",
tasks: [3,4]
},
];
Step 2 Define a tab view to put the tree view
Add a tab view to the middle view. Then put the list and tree in the tab view.
middleView: SC.SplitView.design({
layout: { left: 0, top: 36, right: 0, bottom: 32 },
layoutDirection: SC.LAYOUT_HORIZONTAL,
autoresizeBehavior: SC.RESIZE_TOP_LEFT,
defaultThickness: 0.8,
//The list view is nested into the scrollview which is now in the splitview.
topLeftView: SC.TabView.design({
layout: { left: 15, right: 15, top: 15, bottom: 15},
nowShowing: 'Todos.mainPage.taskList',
itemTitleKey: 'title',
itemValueKey: 'value',
items: [
{title: 'Task List',
value: 'Todos.mainPage.taskList'},
]
}),
...
Later we will add the tree view to this tab.
Step 3 Make the model object behave like a tree node
Add the treeItemsIsExpanded and the treeItemChildren properties:
Todos.Project = SC.Record.extend(
/** @scope Todos.Project.prototype */ {
name: SC.Record.attr(String),
description: SC.Record.attr(String),
tasks: SC.Record.toMany("Todos.Task", {
inverse: "project", isMaster: YES
}),
// TODO: Add your own code here.
treeItemIsExpanded: NO,
treeItemChildren: function(){
return this.get("tasks");
}.property(),
isProject: function(){return YES;},
}) ;
Todos.Task = SC.Record.extend(
/** @scope Todos.Task.prototype */ {
isDone: SC.Record.attr(Boolean),
description: SC.Record.attr(String),
projectCode: SC.Record.attr(String),
project: SC.Record.toOne("Todos.Project", {
inverse: "tasks", isMaster: NO
}),
//Custom code here
treeItemIsExpanded: NO,
treeItemChildren: function(){
return null;
}.property('guid').cacheable(),
name: function(){
return this.get("description");
}.property('description').cacheable(),
isTask: function(){return YES;},
}) ;
Step 4 Create a tree controller
Create a tree controller and initialize the first root node.
Todos.projectsTreeController = SC.TreeController.create({
populate: function(){
var rootNode = SC.Object.create({
treeItemIsExpanded: YES,
name: "root",
treeItemChildren: function(){
var projectQuery = SC.Query.local(Todos.Project, { orderBy: 'name' })
var projects = Todos.store.find(projectQuery);
return projects;
}.property()
});
this.set('content', rootNode);
},
}) ;
Change the main function to call this controllers populate method (apps/todos/main.js).
Todos.main = function main() {
Todos.getPath('mainPage.mainPane').append() ;
var query = SC.Query.local(Todos.Task, { orderBy: 'isDone,description' })
var tasks = Todos.store.find(query);
Todos.tasksArrayController.set('content', tasks);
Todos.projectsTreeController.populate();
} ;
function main() { Todos.main(); }
Step 5 Add a tree view
Add the tree view to the mainPage, and then add it to the tab view.
Todos.mainPage = SC.Page.design({
...
projectTreeView: SC.ListView.design({
layout: {
top: 5,
width: 300,
bottom: 5,
left: 5
},
contentValueKey: "name",
contentBinding: "Todos.projectsTreeController.arrangedObjects",
selectionBinding : "Todos.projectsTreeController.selection"
}),
Put the tree view in the tab view and make it the default view.
...
topLeftView: SC.TabView.design({
layout: { left: 15, right: 15, top: 15, bottom: 15},
nowShowing: 'Todos.mainPage.projectTreeView',
itemTitleKey: 'title',
itemValueKey: 'value',
items: [
{title: 'Task List',
value: 'Todos.mainPage.taskList'},
{title: 'Project Tree',
value: 'Todos.mainPage.projectTreeView'},
]
}),
...
Step 6 Keep the existing controllers in sync with tree view selections
Create a controller that listens for when a task is selected and notifies the other views.
This keeps the form and list view in sync.
Todos.treeNodeController = SC.ObjectController.create({
contentBinding: SC.Binding.single('Todos.projectsTreeController.selection'),
observeContent: function() {
var record = this.get("content");
if (record.isTask) {
Todos.tasksArrayController.selectObject(record);
}
}.observes("content"),
});
Step 7 Display the current selected project
Capture the current projectName and display it in the form:
Todos.treeNodeController = SC.ObjectController.create({
contentBinding: SC.Binding.single('Todos.projectsTreeController.selection'),
projectName: null,
observeContent: function() {
var record = this.get("content");
if (record.isTask) {
var project = record.get("project");
var name = project.get("name");
this.set("projectName", name);
Todos.tasksArrayController.selectObject(record);
} else if (record.isProject) {
var name = record.get("name");
this.set("projectName", name);
}
}.observes("content"),
});
Now that we captured it, display it on the form:
bottomRightView: SC.View.design({
childViews: ['prompt', 'okButton', 'descriptionLabel', 'descriptionText',
'isDoneCheckbox', 'projectCodeLabel', 'projectCodeText', 'projectCodeMessage',
'projectInfo'],
projectInfo: SC.LabelView.design({
layout: { top: 190, left: 20, width: 400, height: 18 },
textAlign: SC.ALIGN_CENTER,
backgroundColor: "blue",
valueBinding: "Todos.treeNodeController.projectName"
}),
Now the next step is to make this tasks and projects relationships editable. This is what we cover in the next lesson (number 9).
Comments (6)
Ido Ran said
at 11:31 pm on Sep 7, 2010
Hi,
I think there is a violation of MVC here.
If you add to Todos.Project methods for handling tree view like treeItemIsExpanded and treeItemChildren then your model is not clean, it is not become a model for display in tree view.
I other UI frameworks I use (specifically WPF) they solve it by wrapping the model item with UI item like TreeViewItem which has those methods. Off course this has it own share of problems but at least it keep the separation clean.
Ido
Richard Hightower said
at 12:12 pm on Sep 8, 2010
WPF? Not sure what that is. Yeah I try to keep the example as simple as possible. If I add too many levels of indirection, it sort of clouds up the SproutCore essentials that I was trying to convey.
Another section on best practices and design patterns might be in order.
But sure, having the treeItem crap in the model is a violation of separations of concern and possibly MVC. It is JavaScript however, so we could just add the methods to the class in a separate file since you can add methods dynamically--we are using JavaScript after all. Creating a separate class and using a delegate seems like too much extra work to me.
I think this is ok for this tutorial as is.
florian said
at 2:06 pm on Mar 25, 2011
Sorry to read that, now i don't just have to get the idea behind Sproutcore, but also behind design decisions made in the tutorials...
Robert Allard said
at 1:44 pm on Nov 15, 2010
Missing taskList code.
Step 2 says:
"... put the list and tree in the tab view."
and then shows code using "taskList":
items: [
{title: 'Task List',
value: 'Todos.mainPage.taskList'},
]
but does not provide the code; here it is.
TaskList is the same code that was in "topLeftView" that was just replaced.
Add it to the mainPage (same as requested for "projectTreeView" in step 5.
taskList: SC.ScrollView.design({
hasHorizontalScroller: NO,
layout: { top: 36, bottom: 32, left: 0, right: 0 },
backgroundColor: 'white',
contentView: SC.ListView.design({
contentBinding: 'Todos.tasksController.arrangedObjects',
selectionBinding: 'Todos.tasksController.selection',
contentValueKey: "description",
contentCheckboxKey: "isDone",
rowHeight: 21,
canEditContent: YES,
canDeleteContent: YES,
target: "Todos.tasksController",
action: "toggleDone"
}) //end of contentView
}), //end of taskList
Robert Allard said
at 11:40 pm on Nov 15, 2010
The "Add Task" button generates the error "Cannot call method ''itemViewForContentIndex" of undefined.
To fix it modify the addTask function as follows; in tasks.js under the controller directory.
Change: //var list = Todos.mainPage.getPath('mainPane.middleView.topLeftView.contentView');
To: var list = Todos.mainPage.getPath('taskList.contentView');
Robert Allard said
at 11:43 pm on Nov 15, 2010
Works only when the "Task List" tab is chosen.
Generates a different errorwhen the "Project Tree" tab is chosen.
You don't have permission to comment on this page.