• If you are citizen of an European Union member nation, you may not use this service unless you are at least 16 years old.

  • You already know Dokkio is an AI-powered assistant to organize & manage your digital files & messages. Very soon, Dokkio will support Outlook as well as One Drive. Check it out today!

View
 

Todos - Alternative 8 Model Relationships, Tab View, Tree View and TreeController

Page history last edited by Richard Hightower 13 years, 7 months ago

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:

 

  1. Step 1 Define a new model object, and relationships
    1. Define the model object
    2. Define the relationship
    3. Define the fixture data 
  2. Step 2 Define a tab view to put the tree view
  3. Step 3 Make the model object behave like a tree node
  4. Step 4 Create a tree controller
  5. Step 5 Add a tree view
  6. Step 6 Keep the existing controllers in sync with tree view selections
  7. 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.