• 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 - Alternate Path Step 7 - Forms and Validation

Page history last edited by Drew Hart 13 years, 3 months ago Saved with comment

Creating a view to hold our form fields

 

We want to create a form like this:

 

 

Here is a short video describing behavior for initial form.

 

Let's start out by creating a new view that we put in the bottomRightView of the splitView. This view should have some labels. It should have a check box for the isDone property. It should have a text area for the description. We can bind directly to the object controller (taskController) because an object controller just delegates to its content, i.e., in our case a Task record object.

 

Here is the form in all of its glory taken from ./apps/todos/resources/main_page.js.

 

Todos.mainPage = SC.Page.design({   

   mainPane: SC.MainPane.design({

   ...

    middleView: SC.SplitView.design({

    ...

 

        bottomRightView: SC.View.design({

          classNames: "todolabel".w(),

          childViews: "prompt okButton descriptionLabel descriptionText isDoneCheckbox".w(),

 

          // PROMPT

          prompt: SC.LabelView.design({

            layout: { top: 12, left: 20, height: 18, right: 20 },

            value: "Edit the task below:"

          }),

 

          // INPUTS 

 

          descriptionLabel: SC.LabelView.design({

            layout: { top: 40, left: 20, width: 70, height: 18 },

            textAlign: SC.ALIGN_RIGHT,

            value: "Description:" 

          }),

 

          descriptionText: SC.TextFieldView.design({

            layout: { top: 40, left: 240, height: 80, width: 600 },

            hint: "Enter task description here".loc(),

            isTextArea: YES,

            valueBinding: "Todos.taskController.description"

          }),

 

          isDoneCheckbox: SC.CheckboxView.design({

            layout: { top: 146, left: 100, right: 20, height: 40 },

            title: "done?".loc(),

            valueBinding: "Todos.taskController.isDone" 

          }),

 

          okButton: SC.ButtonView.design({

            layout: { bottom: 20, right: 20, width: 90, height: 24 },

            title: "OK".loc(),

            isDefault: YES,

            target: "Todos.taskController",

            action: "saveTask"

          }),

 

 

        }),

    }),

 

 

Notice that we bind directly to the properties of the object controller, i.e., Todos.taskController.description and Todos.taskController.isDone shown as follows:

 

          descriptionText: SC.TextFieldView.design({

            ... 

            isTextArea: YES,

            valueBinding: "Todos.taskController.description",

          }),

 

          isDoneCheckbox: SC.CheckboxView.design({

            ... 

            valueBinding: "Todos.taskController.isDone" 

          }),

 

There is an ok button that calls a saveTask method on our controller as well.

 

          okButton: SC.ButtonView.design({

            layout: { bottom: 20, right: 20, width: 90, height: 24 },

            title: "OK".loc(),

            isDefault: YES,

            target: "Todos.taskController",

            action: "saveTask"

          }),

 

The button is bound to the saveTask on the controller. The saveTask on the controller just looks up the record and calls commit on it as show below:

 

 

 Todos.taskController = SC.ObjectController.create({

    contentBinding: SC.Binding.single('Todos.tasksArrayController.selection'),

 

    saveTask: function() {

        var taskRecord = this.get("content");

        if (taskRecord && taskRecord.isRecord) {

            taskRecord.commitRecord();

        } else {

            alert("You must select a task first");

        }

    },

 

 });

 

The saveTask method make sure the task record is present. If it is, it commits the record. For now, if the end user has not selected a record, we display an error message (old school). Later, we will not enable the "Save Task" button until a record is selected.

 

By the way, using alert is not a very sproutcore thing to do. Again, we will fix this later. So far we have a working form.

 

If you are following along on the home version of family fued, then you will have a working form. Let me demonstrate what we have so far. Notice some problem areas, like the "Save Task" not getting disabled.

 

Here is a short video doing a code walk through so far.

 

Making the form behave

 

'Save Task' button should be grayed out until we select a record and start editing it or until we create a new record.

 

We want to be notified whenever the status of record changes. If the record is in the state READY_DIRTY or READY_NEW, we want to enable the save button. Otherwise, we want to disable the 'Save Task' button.

 

Now would be a good time to refer to the wiki documentation for data store and the SC.Record reference documentation, or you can continue and fill in the gaps later. 

 

Rather than try to explain in detail what we want, let me show you. To see it, watch the video at the end of this lesson.

 

To accomplish the behavior in the video, we need to bind the okButton's isEnabledBinding property to a new property on our taskController called isSaveOk as shown below: 

 

          okButton: SC.ButtonView.design({

            layout: { bottom: 20, right: 20, width: 90, height: 24 },

            title: "Save Task",

            isDefault: YES,

            isEnabledBinding: "Todos.taskController.isSaveOk",

            target: "Todos.taskController",

            action: "saveTask"

          }),

 

The isSaveOk property controls whether or not the okButton is enabled. The isSaveOk is set by listening, nee, observing the state of the current record. We listen to the state of the current record using a relative chained observer. You can find out more about this by going to Runtime->KVO->Relative Observer section in the WIKI.

 

Below we setup an observer method called observeRecordState which listens to changes in content and/or content status property of the ObjectController by using the pattern "*content.status".

 

Todos.taskController = SC.ObjectController.create({ ...

 

    contentBinding: SC.Binding.single('Todos.tasksArrayController.selection'),  

    isSaveOk: NO,

 

    saveTask: function() {

        ... //as before

    },

 

    observeRecordState: function() {        

        var taskRecord = this.get("content");

        if (taskRecord.get("status") === SC.Record.READY_DIRTY ||

            taskRecord.get("status") === SC.Record.READY_NEW) {

            this.set("isSaveOk", YES);

        } else {

            this.set("isSaveOk", NO);           

        }

    }.observes("*content.status"),

 

... 

 

Notice the addition of isSaveOk property and how we set it to YES if the record is dirty or new. Also notice how we use the .observers("*content.status") to listen to changes. You may recall that the content is bound to the currently selected record in the array controller with this binding contentBinding: SC.Binding.single('Todos.tasksArrayController.selection'). Thus the content is an SC.Record. Thus, "*content.status" listens to see if the record status changed.

 

Here is a video describing the last bit of code and showing how the application works so far.

 

 

Field Validation

 

Let's say we have a field called projectCode. This projectCode can only take 3 letters and a dash and then three more letters. When the end user tabs out of this field, we want to validate it. Thus when the text field for projectCode loses focus we want to validate the current value of the text field. Let me demonstrate with a brief video:

 

Here is a video demo of the validation feature that we are about to demonstrate.

 

The first step is to add the projectCode to field to the model object. You can find the model object at /apps/todos/model/task.js as follows:

 

 

Todos.Task = SC.Record.extend(

/** @scope Todos.Task.prototype */ {

 

  isDone: SC.Record.attr(Boolean),

  description: SC.Record.attr(String),

  projectCode: SC.Record.attr(String),

 

}) ;

 

Next we want a label that shows up next to the field when there is a validation error. You can add this right under the descriptionText text field in main_page.js as follows:

 

        bottomRightView: SC.View.design({

          childViews: "prompt okButton descriptionLabel descriptionText isDoneCheckbox projectCodeLabel projectCodeText projectCodeMessage".w(),

 

                    ...

 

          projectCodeLabel: SC.LabelView.design({

            layout: { top: 145, left: 20, width: 100, height: 18 },

            textAlign: SC.ALIGN_RIGHT,

            value: "Project Code:" 

          }),

 

          projectCodeText: SC.TextFieldView.design({

            layout: { top: 145, left: 240, height: 20, width: 200 },

            hint: "Project code 'abc-zxc' not '123', needs dash",

            valueBinding: "Todos.taskController.projectCode",

          }),

 

 

          projectCodeMessage: SC.LabelView.design({

            classNames: "errorLabel".w(),

            layout: { top: 145, left: 450, width: 400, height: 18 },

            isVisibleBinding: "Todos.taskController.isProjectCodeMessageOn",

            textAlign: SC.ALIGN_CENTER,

            backgroundColor: "red",

            valueBinding: "Todos.taskController.projectCodeMessage

          }),

 

 

 

Next we need to add a validation rule. For this we can use straight-up JavaScript as follows:

...

    var REGEX_MATCH_PROJECT_CODE = /[a-z]{3}-[a-z]{3}/i;

...

            if (!projectCode.match(REGEX_MATCH_PROJECT_CODE)) {

                this.set("projectCodeMessage", "must be 3 letters dash 3 letters");

                this.set("isProjectCodeMessageOn", YES);

                return NO;              

            } else {

                this.clearValidationMessages();

                return YES;

            }

 

 

Now the taskController is going to have a lot of stuff going on that is no one's business. How it does validation is not part of the public API. You need to encapsulate it. You can change the call to SC.ObjectController.create and define an anonymous function that we invoke. Inside of this anonymous function, you can define local variables that become private variables to the object literal that we are going to return. This is how you do private variables in JavaScript.This is all done through the magic of JavaScript closures. If this is a foreign concept, go get the book JavaScript the Good Parts by Douglas Crockford. It is a short book. Impress your friends by reading it in a single weekend. 

 

Also, you need to be able to detect when the projectCodeText text field loses focus. You do this by listening to its isKeyResponder property. If isKeyResponder is YES, then the projectCodeText text field just got focus.

Here is the new and improved taskController with private variables, validation logic and the ability to listen to the isKeyResponder of projectCodeText:

 

 

Todos.taskController = SC.ObjectController.create(function() {

 

    //Private variables

    var RELATIVE_PATH_TO_FORM = "mainPage.mainPane.middleView.bottomRightView";

    var PATH_TO_FORM = "Todos." + RELATIVE_PATH_TO_FORM;

    var REGEX_MATCH_PROJECT_CODE = /[a-z]{3}-[a-z]{3}/i;

 

 

 

    //Private methods

    var loadForm = function() {

        return Todos.getPath(RELATIVE_PATH_TO_FORM);

    }

    var projectCodeText = function() {

        return loadForm().get("projectCodeText");

    }   

 

 

    return { //returns object literal

 

    contentBinding: SC.Binding.single('Todos.tasksArrayController.selection'),  

    isSaveOk: NO,

    projectCodeMessage: "message goes here",

    isProjectCodeMessageOn: NO,

 

    saveTask: function() {

        var taskRecord = this.get("content");

        if (this.validateProjectCodeField()) {

            taskRecord.commitRecord();

        }

    },

 

    observeRecordState: function() {        

        var taskRecord = this.get("content");

        if (taskRecord.get("status") === SC.Record.READY_DIRTY ||

            taskRecord.get("status") === SC.Record.READY_NEW) {

            this.set("isSaveOk", YES);

        } else {

            this.set("isSaveOk", NO);           

        }

        this.clearValidationMessages();

    }.observes("*content.status"),

 

    clearValidationMessages: function() {

        this.set("projectCodeMessage", "");

        this.set("isProjectCodeMessageOn", NO);                     

    },

 

    validateProjectCodeField: function() {

        var taskRecord = this.get("content");

        if (!taskRecord) {

            return YES;

        }

        var projectCode = taskRecord.get("projectCode");

        if (projectCode) {

            //check to see if the project code matches our regex rule

            if (!projectCode.match(REGEX_MATCH_PROJECT_CODE)) {

                this.set("projectCodeMessage", "invalid project code: must be 3 letters dash 3 letters");

                this.set("isProjectCodeMessageOn", YES);

                return NO;              

            } else {

                this.clearValidationMessages();

                return YES;

            }

        }else {

               return YES;

        }      

    },

 

    observeProjectCodeKeyResponder: function() {

        var component = projectCodeText();

        //We just left the project code field, let's validate

        if (component.get("isKeyResponder")==NO) {

            this.validateProjectCodeField();

        }

    }.observes(PATH_TO_FORM + ".projectCodeText.isKeyResponder"),

 

}}());

 

The above is explained in more detail with this video.

 

Here is a video showing the validation code walk through.

 

In the next lesson, we are going to cover setting up a relationship between tasks and a new model object called project. Then we are going to build a tree view of projects with tasks in them.

 

In Step 8, we are going to add a relationship to a new model object, then we are going to use a TreeView and a Tab View. 

 

Alternative Path Step 8 - Model relationships, Tree View, Tab View and Tree controller.

 

 

 

 

 

 

Comments (2)

Richard Hightower said

at 1:43 pm on Sep 10, 2010

The validation method has a few more bugs than I would like to admit....

Try this one instead.

validateProjectCodeField: function() {
var taskRecord = this.get("content");
if (!taskRecord) {
return YES;
}
var projectCode = taskRecord.get("projectCode");
if (projectCode) {
//check to see if the project code matches our regex rule
if (!projectCode.match(REGEX_MATCH_PROJECT_CODE)) {
this.set("projectCodeMessage", "invalid project code: must be 3 letters dash 3 letters");
this.set("isProjectCodeMessageOn", YES);
return NO;
} else {
this.clearValidationMessages();
return YES;
}
} else {
return YES;
}
},

It works much better. I was wondering why my records would not get committed when I did an update.

David M said

at 7:13 am on Sep 11, 2010

Would it be more appropriate / better practice to use the SC.Validator facilities available ? Also, would this kind of validation logic be better placed in the model layer, instead of the controller ? Thanks!

You don't have permission to comment on this page.