Note
This tutorial is for Sproutcore 1.X. I’ve written a similar tutorial for Sproutcore 2
here.
OK. I’ve reach the point in my Sproutcore understanding where I feel I can attempt to write a CRUD demo app.
Get the code here.
See it in action here.
The demo app allows the user to:
- Create a user record via a modal “details” pane
- Read user records by listing them in username order in a SC.TableView
- Update a user record via a modal “details” pane
- Delete a user record via pressing the DELETE key in SC.TableView or via a button on the modal “details” pane
The demo app implements:
- Field level validation – username must be [a-z]
- Page level validation – username is required
- Nested stores so that local changes can be discarded before commit.
- Simulated server response with 1 second latency
- Simulated server error – check for duplicate keys (username)
- Datasource error handling
- Display field, page and data source (server) errors to users
Here’s some screenshots.



1. Pre-requisites
Do the ToDo tutorial as well as read the programming guide (especially 03. DataStore) in wiki.sproutcore.com.
I’m assuming you have a basic knowledge of SC.Records, SC.Store and SC.DataSource.
Also, to use tables, you will need to modify your Buildfile as follows.
config :all, :required => [:sproutcore, 'sproutcore/table']
2. Data Model
The data model is represented by CrudSample.UserRecord. Points to note:
- I am using the userID property as the primary key rather than the standard ‘guid’. To do this, I’ve set the primaryKey property to the name of my primary key.
- The isAdminString property is required to help bind the isAdmin property to a SelectFieldView at the UI layer. It is stored as a boolean and this computed property allows us to convert a boolean to a string that can be used to bind to a SelectFieldView.
- I’ve added the function to backup and restore properties. I found I needed this feature to restore a record to its original state if there was an error after committing the record. For example, when a user edits a record, changes the username to one that already exists, and commits the record the data source will throw an exception because of a duplicate username. If I then present the user record to the user to edit again and the user clicks Discard, I want to restore the original values to the record in the store.
CrudSample.UserRecord = SC.Record.extend({
properties: 'userID username department userStatus isAdminString lastLoggedInDate'.w(),
primaryKey: 'userID',
userID: SC.Record.attr(Number), //not required on create
username: SC.Record.attr(String, { isRequired: YES }),
department: SC.Record.attr(String),
userStatus: SC.Record.attr(String, { isRequired: YES, key: "status" }),
isAdmin: SC.Record.attr(Boolean, { defaultValue: NO, isRequired: YES }),
isAdminString: function(key, value) {
// writing
if (value !== undefined) {
this.writeAttribute('isAdmin', value == 'YES' ? YES : NO); // write into data hash
}
// reading
value = this.readAttribute('isAdmin');
value = value ? 'YES' : 'NO';
return value;
}.property().cacheable(),
lastLoggedInDate: SC.Record.attr(SC.DateTime, {userIsoDate: YES}),
/**
* Return an object containing a backup of the properties
* @returns SC.Object object containing properties to backup
*/
backupProperties: function() {
var backup = SC.Object.create();
for (var i = 0; i < this.properties.length; i++) {
var p = this.properties[i];
backup.set(p, this.get(p));
}
return backup;
},
/**
* Restores properties from a backup crated by backupProperties().
*/
restoreProperties: function(backup) {
for (var i = 0; i < this.properties.length; i++) {
var p = this.properties[i];
this.set(p, backup.get(p));
}
return;
}
});
3. Fixtures and Data Source
3.1 Patch Sproutcore 1.4.2 Bug
There is a small bug around the caching of record status. I’ve submitted a pull request to fix it. In the mean time, you will have to edit veebs-sproutcore-samples/frameworks/sproutcore/frameworks/datastore/models/record.js. Don’t make the status property cacheable as below.
status: function() {
return this.store.readStatus(this.storeKey);
}.property(), //.property('storeKey').cacheable(),
3.2 Create our own Data Source to simulate calling remote server
I created numeric_id_fixtures.js and extended SC.FixturesDataSource to get it to simulate a server executing server side business rules.
- I override the simulateRemoteResponse and latency properties to simulate a delay of 1 second. I like to set a longer delay so we can see what happens at the UI layer in a worse case scenario.
- I override the generateIdFor() function so we allocate numbers for our primary key rather than a string (the default FixturesDataSource behaviour). This will mimic a database auto increment primary key.
- I override _createRecords() and _updateRecords() functions to allow checking for unique usernames. In these functions, we call store.dataSourceDidComplete() if update was successful and store.dataSourceDidError() if there was an error. Both these store functions sets the user record’s status property to READY_CLEAN or ERROR respectively.
CrudSample.NumericIdFixturesDataSource = SC.FixturesDataSource.extend(
/** @scope CrudSample.AutoIdFixturesDataSource.prototype */ {
/**
* Let's simulate calling a remote server for CRUD operations
*/
simulateRemoteResponse: YES,
/**
* Assume we have a slow server that takes 1 second to respond
*/
latency: 1000,
/**
* The next number to allocate to a primary key
*/
nextNumber: 1000000,
/**
* Override this method so that we can allocate ID based on a number that starts at 1,000,000. We don't start at
* 1 because that is within range of our primary key in our fixtures. We also want to return a number and not a
* string.
*
* @param recordType
* @param dataHash
* @param store
* @param storeKey
*/
generateIdFor: function(recordType, dataHash, store, storeKey) {
return this.nextNumber++;
},
/**
* Override _createRecords so that we can check for unique usernames
*
* @param store
* @param storeKeys
*/
_createRecords: function(store, storeKeys) {
storeKeys.forEach(function(storeKey) {
try {
var id = store.idFor(storeKey),
recordType = store.recordTypeFor(storeKey),
dataHash = store.readDataHash(storeKey),
fixtures = this.fixturesFor(recordType);
this.validateUniqueUsername(dataHash);
if (!id) {
id = this.generateIdFor(recordType, dataHash, store, storeKey);
}
this._invalidateCachesFor(recordType, storeKey, id);
fixtures[id] = dataHash;
store.dataSourceDidComplete(storeKey, null, id);
} catch (e) {
// We have an error
store.dataSourceDidError(storeKey, e);
}
}, this);
},
/**
* Override _updateRecords so that we can check for unique usernames
* @param store
* @param storeKeys
*/
_updateRecords: function(store, storeKeys) {
storeKeys.forEach(function(storeKey) {
try {
var hash = store.readDataHash(storeKey);
this.validateUniqueUsername(hash);
this.setFixtureForStoreKey(store, storeKey, hash);
store.dataSourceDidComplete(storeKey);
} catch (e) {
// We have an error
store.dataSourceDidError(storeKey, e);
}
}, this);
},
/**
* Checks if the username is unique in local store
* This simulates checking on the server side
*
* @param storeKey Store key of the user record to check
* @throws SC.Error
*/
validateUniqueUsername: function(dataHash) {
var username = dataHash.username;
var query = SC.Query.local(CrudSample.UserRecord, {
conditions: '(username = {name})',
name: username
});
var userRecords = CrudSample.store.find(query);
var count = userRecords.get('length');
if (count == 0) {
return;
} else {
if (count == 1) {
// Check that we are not matching ourselves
var dataHashPrimaryKey = dataHash.userId;
if (dataHashPrimaryKey && dataHashPrimaryKey != undefined) {
var primaryKey = userRecords.objectAt(0).get('id');
if (dataHashPrimaryKey != primaryKey) {
throw SC.Error.desc('Username already exists', 'username');
}
}
} else {
// Error - more than 1 match for whatever reason
throw SC.Error.desc('Username already exists', 'username');
}
}
}
});
3.3 Create Store using Fixture Data
I create my sample initial user data in fixtures/user.js. These entries simulate what will be returned from a real life server.
CrudSample.UserRecord.FIXTURES = [
{
userID: 1,
username: "michael",
department: "Accounts",
status: "Active",
isAdmin: YES,
lastLoggedInDate: "2010-01-01T10:20:30Z"
},
... more records ...
];
We then load the fixture data into our store in core.js.
CrudSample = SC.Application.create(
/** @scope CrudSample.prototype */ {
NAMESPACE: 'CrudSample',
VERSION: '0.1.0',
store: SC.Store.create().from('CrudSample.NumericIdFixturesDataSource')
});
4. Controllers
Controllers is where I keep the client side business rules.
4.1 Controller for List of User Records
controllers/user_array.js is where I’ve defined CrudSample.userRecordArrayController.
- This is a standard record array controller like the one in the ToDo tutorial in the wiki.
- I’ve added the isReady property to allow us to check if our data source is busy waiting for records to be returned from the server. If busy, I notify the user appropriately in the view layer.
- I’ve added the summary property to return the latest status to the user. If we are busy waiting on the data source to fetch data from the server, we show “Loading…”. If the data source is ready and has fetched records, show the number of users.
4.2 Controller for Reading a Single User Record
controllers/user.js is where I’ve defined CrudSample.userRecordController.
It is a standard object controller (like the one in the ToDo tutorial) bound to the selected user record in CrudSample.userRecordArrayController.
Every time the selected record is changed in the userRecordArrayController, the userRecordController will point to the selected record.
4.3 Controller for Creating or Updating a Single User Record
If I display and edit the selected user record in the userRecordController, every time the user makes a change, all UI elements bound to the record will be updated. I don’t want this to happen until the user saves the record.
Also, if the user decides to cancel editing a record, it is difficult to rollback changes.
controllers/user.js is where I’ve defined CrudSample.userViewController to help overcome these problems. I’ve called it a view controller because it is used by the view layer to create/update records. The view controller is my wrapper around the nested data store which gives us the ability to make batch changes before committing.
- I’m using a nested store so that that I can batch commit/rollback changes. You can see in the createNew() and updateCurrent() functions, a nested store is created.
- I’ve implemented callback functionality upon saving. The view layer just needs to pass a target object and method to the save() function and it will be called back when saving is complete. This simplifies the view layer.
- To trigger the callback, I’ve implemented the savingUserRecordStatusDidChange property that observes the status property of the user record being saved. I will invoke the callback when the record is no longer busy – i.e. the status property is set to READY_CLEAN or ERROR
- If there was an error during saving, I need to modify the status property of the record so that the user can make further changes to the record and fix the user. This is where fixSaveError() comes in. If the user was creating a record, I destroy the previously committed record, create a new record and restore the data so the user does not have to re-key. If the user was updating the previously committed record, I change the status property of the record from ERROR to READ_DIRTY.
- If the user decides to discard changes, I only need to discard the record in the nested store. However, the user was updating the record and there was an error during save, the record in the store will be left with a status of READY_DIRTY. I need to restore the properties and reset the record status to READY_CLEAN. A better way will be to reload the record from the server but this is not possible in this simulated demo app.
- I’ve added validateUsername() and validateRecord() provide sample record and field level validation.
- Note that in the updateCurrent() function, I used the id property to retrieve the primary key rather than the userId property. This is because Sproutcore does not keep the primary key in the record up to date with that has been set in the store. See here.
CrudSample.userViewController = SC.ObjectController.create({
/**
* Boolean flag to denote if the record has change (is dirty)
*/
contentIsChanged: NO,
/**
* Nested store that was used to retrieve the record
*/
nestedStore: null,
/**
* Empty object that we use to set the content so that any views bound to this content will not throw an
* exception like "cannot call method 'get' of null" when it tries to get a property value
*/
emptyContent: SC.Object.create(),
/**
* Current nested content we are proxying
*/
content: this.emptyContent,
/**
* Target to callback when saving is finished
*/
savingCallbackTarget: null,
/**
* Method of target to callback when saving is finished
*/
savingCallbackMethod: null,
/**
* The user record in the store that in being saved. NOTE - this is the actual record out of the store and not
* the temporary record from the nested store.
*/
savingUserRecord: null,
/**
* If editing, we store the original properties just in case saving fails and we have to restore the original values
*/
originalProperties: null,
/**
* Sets up the content of this controller with a new user record to create
*/
createNew: function() {
var ns = CrudSample.store.chain();
var nsUserRecord = ns.createRecord(CrudSample.UserRecord, {
status: 'Active',
isAdmin: NO
});
this._reset(ns, nsUserRecord, null);
},
/**
* Sets up the content of this controller with the current record in userController
* for updating.
*/
updateCurrent: function() {
// Get the primary key using the 'id' property and NOT 'userID' because sproutcore caches the primary key in an
// array within the store. When records are created,the primary key in the array is updated but the primary key
// property in the record is NOT!
var ns = CrudSample.store.chain();
var pk = CrudSample.userRecordController.get('id');
var nsUserRecord = ns.find(CrudSample.UserRecord, pk);
this._reset(ns, nsUserRecord, nsUserRecord.backupProperties());
},
/**
* Reset our state so that we are ready to add or edit a record
*
* @param {SC.NestedStore} Nested Data store for this editing session
* @param {CrudSample.UserRecord} user record from the nested store that we are going to edit. This will be set
* as the content of this controller.
* @param {SC.Object} object containing properties of the original record we are editing
*/
_reset: function(ns, nsUserRecord, originalProperties) {
this.set('savingUserRecord', null);
this.set('savingCallbackTarget', null);
this.set('savingCallbackMethod', null);
this.set('nestedStore', ns);
this.set('content', nsUserRecord);
this.set('originalProperties', originalProperties);
},
/**
* Saves changes back to the parent store
*
* @param {Object} callbackTarget Insurance of object containing the callbackMethod
* @param {Method} callbackMethod Method to callback when save is complete.
*/
save: function(callbackTarget, callbackMethod) {
this.validateRecord();
var nsUserRecord = this.get('content');
var ns = this.get('nestedStore');
ns.commitChanges();
ns.destroy();
var userRecord = CrudSample.store.find(nsUserRecord);
this.set('savingUserRecord', userRecord);
this.set('savingCallbackTarget', callbackTarget);
this.set('savingCallbackMethod', callbackMethod);
userRecord.commitRecord();
},
/**
* Wait for user record being save to be saved and check for error
*/
savingUserRecordStatusDidChange: function() {
var userRecord = this.get('savingUserRecord');
if (userRecord != null) {
var callbackTarget = this.get('savingCallbackTarget');
var callbackMethod = this.get('savingCallbackMethod');
var status = userRecord.get("status");
if (status === SC.Record.READY_CLEAN) {
// Saved OK - select object in UI
CrudSample.userRecordArrayController.selectObject(userRecord);
// Callback UI to clean up
callbackMethod.call(callbackTarget, null);
// Init variables to get read for next view session
this._reset(null, this.emptyContent, null);
} else {
// Error
if (userRecord.get('isError')) {
callbackMethod.call(callbackTarget, userRecord.get('errorObject'));
this.fixSaveError(userRecord);
}
}
}
}.observes('*savingUserRecord.status'),
/**
* Sets up the content of this controller with the current record for which there's been an error.
* When an error happens, the error record is sitting in our main store.
*
* @param {CrudSample.UserRecord} userRecord in the store that is to be loaded for fixing
*/
fixSaveError: function(userRecord) {
var store = userRecord.get('store');
var isCreating = SC.none(userRecord.get('id'));
var nsUserRecord;
var ns = CrudSample.store.chain();
if (isCreating) {
// Was adding. Delete from store and try again.
// Backup and restore properties so the user don't have to re-key
var backup = userRecord.backupProperties();
userRecord.destroy();
nsUserRecord = ns.createRecord(CrudSample.UserRecord, {});
nsUserRecord.restoreProperties(backup);
} else {
// Was updating, we change status to READY_DIRTY and try again
nsUserRecord = ns.materializeRecord(userRecord.get('storeKey'));
store.writeStatus(userRecord.get('storeKey'), SC.Record.READY_DIRTY);
userRecord.propertyDidChange('status');
}
this._reset(ns, nsUserRecord, this.get('originalProperties'));
},
/**
* Discard changes
*/
discard: function() {
var ns = this.get('nestedStore');
ns.discardChanges();
ns.destroy();
// If updating and there was an error in the save, we have to restore our properties and reset the
// record to clean (alternatively, we can reload the record from the data store)
var nsUserRecord = this.get('content');
if (!SC.none(nsUserRecord.get('id'))) {
var userRecord = CrudSample.store.find(nsUserRecord);
var store = userRecord.get('store');
var status = userRecord.get("status");
if (status === SC.Record.READY_DIRTY) {
userRecord.restoreProperties(this.get('originalProperties'));
store.writeStatus(userRecord.get('storeKey'), SC.Record.READY_CLEAN);
userRecord.propertyDidChange('status');
}
}
// Init for next time
this._reset(null, this.emptyContent, null);
},
/**
* Listens to changes in content and/or content status property of the ObjectController by observing the
* pattern "*content.status". We set the contentIsChanged property to YES if the record is dirty or new.
*/
contentStatusDidChange: function() {
var userRecord = this.get('content');
if (userRecord == null) {
this.set('contentIsChanged', NO);
} else {
var status = userRecord.get("status");
if (status === SC.Record.READY_DIRTY || status === SC.Record.READY_NEW) {
this.set('contentIsChanged', YES);
} else {
this.set('contentIsChanged', NO);
}
}
}.observes('*content.status'),
/**
* Checks if the current user's username is valid. This is an example of a field level check.
*
* SC.Error Exception thrown if error.
*/
validateUsername: function() {
var pattern = /^[a-z]+$/
var nsUserRecord = this.get('content');
var username = nsUserRecord.get('username');
if (!SC.empty(username)) {
if (!pattern.test(username)) {
throw SC.Error.desc('Username can only contain a-z in lower case and no spaces.', 'username');
}
}
return;
},
/**
* Checks if the current user record is valid. This is an example of a page check.
*
* SC.Error Exception thrown if error.
*/
validateRecord: function() {
var nsUserRecord = this.get('content');
var userName = nsUserRecord.get('username');
if (SC.empty(userName)) {
throw SC.Error.desc('Username is required', 'username');
} else {
this.validateUsername();
}
var userStatus = nsUserRecord.get('userStatus');
if (SC.empty(userStatus)) {
throw SC.Error.desc('Status is required', 'userStatus');
}
var isAdmin = nsUserRecord.get('isAdmin');
if (userStatus == 'InActive' && isAdmin) {
throw SC.Error.desc('Administrators must be active', 'isAdmin');
}
return;
}
});
5. Views
I’ve implemented my views in resources/main_page.cs.
5.1 SC.TableView to List User Records
The main page is split into 3 sections
- topView – contains the title and the add button.
- middleView – contains a table listing our user records.
- bottomView – contains a label letting the user know the current data source status from the userRecordArrayController.
I’ve used SC.TableView to display the user records.
- I’ve bound the contents of the table and the selected object to the userRecordArrayController.
- I want the user to be able to press the DELETE key. SC.TableView extends SC.ListView so I’ve followed the example in the ToDo tutorial. I’ve set the canDeleteContent property and implemented the collectionViewDeleteContent() delegate in the userRecordArrayController.
- When the user double clicks on a record, I want the details pane to be displayed. As such, I set the target and action properties to point to the detailPane.showForUpdate() function.
CrudSample.mainPage = SC.Page.design({
mainPane: SC.MainPane.design({
childViews: 'middleView topView bottomView'.w(),
topView: SC.ToolbarView.design({
layout: { top: 0, left: 0, right: 0, height: 36 },
childViews: 'titleLabel addButton'.w(),
anchorLocation: SC.ANCHOR_TOP,
titleLabel: SC.LabelView.design({
layout: { centerY: 0, height: 24, left: 8, width: 200 },
controlSize: SC.LARGE_CONTROL_SIZE,
fontWeight: SC.BOLD_WEIGHT,
value: 'Users'
}),
addButton: SC.ButtonView.design({
layout: { centerY: 0, height: 24, right: 12, width: 100 },
title: 'Add User',
target: 'CrudSample.mainPage.detailPane',
action: 'showForCreate',
isEnabledBinding: 'CrudSample.userRecordArrayController.isReady'
})
}),
middleView: SC.ScrollView.design({
hasHorizontalScroller: NO,
layout: { top: 36, bottom: 32, left: 0, right: 0 },
backgroundColor: 'white',
contentView: SC.TableView.design({
layout: { left: 15, right: 15, top: 15, bottom: 15 },
backgroundColor: "white",
columns: [
SC.TableColumn.create({
key: 'username',
label: 'Username',
width: 100
}),
SC.TableColumn.create({
key: 'department',
label: 'Department',
width: 200
}),
SC.TableColumn.create({
key: 'userStatus',
label: 'User Status',
width: 100
}),
SC.TableColumn.create({
key: 'isAdmin',
label: 'Is Admin?',
formatter: function(v) {
return v ? 'Yes' : 'No';
},
width: 100
}),
SC.TableColumn.create({
key: 'lastLoggedInDate',
label: 'Last Logged In Date',
formatter: function(v) {
return v == null ? '' : v.toFormattedString('%Y-%m-%d %H:%M:%S %Z');
},
width: 300
})
],
contentBinding: 'CrudSample.userRecordArrayController.arrangedObjects',
selectionBinding: 'CrudSample.userRecordArrayController.selection',
selectOnMouseDown: YES,
canDeleteContent: YES,
exampleView: SC.TableRowView,
recordType: CrudSample.UserRecord,
target: "CrudSample.mainPage.detailPane",
action: "showForUpdate"
})
}),
bottomView: SC.ToolbarView.design({
layout: { bottom: 0, left: 0, right: 0, height: 32 },
childViews: 'summaryLabel'.w(),
anchorLocation: SC.ANCHOR_BOTTOM,
summaryLabel: SC.LabelView.design({
layout: { centerY: 0, height: 18, left: 20, right: 20 },
valueBinding: 'CrudSample.userRecordArrayController.summary'
})
})
... more code ...
});
5.2 Details Pane to Create and Update a User Record
When the user clicks the Add button or double clicks an existing record, the modal details page is displayed over the top of the mainPane.
- To make the detailPane modal, I extend from SC.PanelPane.
- All the fields in detailPane are bound to the userViewController – NOT the userRecordController. This is because we want to use the nested store’s batch commit/discard functionality.
- To display the detailPane, call showForCreate() or showForUpdate(). This sets the detailsIsVisible property which will trigger detailIsVisibleDidChange to append this panel to the page. It also sets up the userViewController with a new record in a nested store if adding or an existing record in a nested store if updating.
- When saving, I supply saveComplete() as the callback method. When called, saveComplete() will display the error or hide the details pane if no error.
- Errors are displayed using the showError() function. It uses SC.AlertPane to popup a nice looking modal dialog rather than a bland javascript alert(). One trickly feature I’ve added is to place the cursor on the field that triggered the error. E.g. if username is required, then put the cursor on the username. This is achieved by, as a convention, putting the name of the property that triggered the error in the SC.Error.label property. I then lookup the property in the view and set focus by calling the field’s becomeFirstResponder() method.
CrudSample.mainPage = SC.Page.design({
... some code ...
detailPane: SC.PanelPane.create({
layout: { width:400, height:250, centerX:0, centerY:-50},
contentView: SC.View.extend({
childViews: 'title username department userStatus isAdmin lastLoggedInDate savingImage savingMessage deleteButton saveButton cancelButton'.w(),
title: SC.LabelView.design({
layout: { left: 17, right: 17, top: 17, height: 26 },
value: 'User Details',
textAlign: SC.ALIGN_CENTER,
fontWeight: SC.BOLD_WEIGHT
}),
username: SC.View.design({
layout: { left: 17, right: 17, top: 44, height: 26 },
childViews: 'label field'.w(),
label: SC.LabelView.design({
layout: { left: 0, width: 95, height: 18, centerY: 0 },
value: 'Username',
textAlign: SC.ALIGN_RIGHT,
fontWeight: SC.BOLD_WEIGHT
}),
field: SC.TextFieldView.design({
layout: { width: 250, height: 22, right: 3, centerY: 0 },
valueBinding: 'CrudSample.userViewController.username',
isEnabledBinding: 'CrudSample.mainPage.detailPane.isEnabled'
})
}),
department: SC.View.design({
layout: { left: 17, right: 17, top: 71, height: 26 },
childViews: 'label field'.w(),
label: SC.LabelView.design({
layout: { left: 0, width: 95, height: 18, centerY: 0 },
value: 'Department',
textAlign: SC.ALIGN_RIGHT,
fontWeight: SC.BOLD_WEIGHT
}),
field: SC.TextFieldView.design({
layout: { width: 250, height: 22, right: 3, centerY: 0 },
valueBinding: 'CrudSample.userViewController.department',
isEnabledBinding: 'CrudSample.mainPage.detailPane.isEnabled'
})
}),
userStatus: SC.View.design({
layout: { left: 17, right: 17, top: 98, height: 26 },
childViews: 'label field'.w(),
label: SC.LabelView.design({
layout: { left: 0, width: 95, height: 18, centerY: 0 },
value: 'User Status',
textAlign: SC.ALIGN_RIGHT,
fontWeight: SC.BOLD_WEIGHT
}),
field: SC.SelectFieldView.design({
layout: { width: 250, height: 22, right: 3, centerY: 0 },
objects: [
{name:'Active', value: 'Active'},
{name:'InActive', value: 'InActive'},
{name:'Locked', value: 'Locked;'},
],
nameKey: 'name',
valueKey: 'value',
acceptsFirstResponder: function() {
return this.get('isEnabled');
}.property('isEnabled'),
valueBinding: 'CrudSample.userViewController.userStatus',
isEnabledBinding: 'CrudSample.mainPage.detailPane.isEnabled'
})
}),
isAdmin: SC.View.design({
layout: { left: 17, right: 17, top: 125, height: 26 },
childViews: 'label field'.w(),
label: SC.LabelView.design({
layout: { left: 0, width: 95, height: 18, centerY: 0 },
value: 'Is Admin',
textAlign: SC.ALIGN_RIGHT,
fontWeight: SC.BOLD_WEIGHT
}),
field: SC.SelectFieldView.design({
layout: { width: 250, height: 22, right: 3, centerY: 0 },
objects: [
{name:'Yes', value: 'YES'},
{name:'No', value: 'NO'},
],
nameKey: 'name',
valueKey: 'value',
acceptsFirstResponder: function() {
return this.get('isEnabled');
}.property('isEnabled'),
valueBinding: 'CrudSample.userViewController.isAdminString',
isEnabledBinding: 'CrudSample.mainPage.detailPane.isEnabled'
})
}),
lastLoggedInDate: SC.View.design({
layout: { left: 17, right: 17, top: 152, height: 26 },
childViews: 'label field'.w(),
label: SC.LabelView.design({
layout: { left: 0, width: 95, height: 18, centerY: 0 },
value: 'Last Logged In Date',
textAlign: SC.ALIGN_RIGHT,
fontWeight: SC.BOLD_WEIGHT
}),
field: SC.LabelView.design({
layout: { width: 250, height: 22, right: 3, centerY: 0, top: 4 },
valueBinding: 'CrudSample.userViewController.lastLoggedInDate',
formatter: function(v) {
if (SC.kindOf(v, SC.DateTime)) {
return v == null ? '' : v.toFormattedString('%Y-%m-%d %H:%M:%S %Z');
} else {
return '';
}
}
})
}),
savingImage: SC.ImageView.design({
layout: { bottom: 15, left: 175, height:16, width: 16 },
value: sc_static('images/loading'),
useImageCache: NO,
isVisibleBinding: SC.Binding.from('CrudSample.mainPage.detailPane.isEnabled').bool().transform(
function(value, isForward) {
return !value;
})
}),
savingMessage: SC.LabelView.design({
layout: { bottom: 8, left: 195, height:24, width: 100 },
value: 'Saving ...',
classNames: ['saving-message'],
isVisibleBinding: SC.Binding.from('CrudSample.mainPage.detailPane.isEnabled').bool().transform(
function(value, isForward) {
return !value;
})
}),
deleteButton: SC.ButtonView.design({
layout: {bottom: 10, left: 20, height:24, width:80},
title: 'Delete',
action: 'deleteRecord',
isVisibleBinding: SC.Binding.from('CrudSample.userViewController.contentIsChanged').bool().transform(
function(value, isForward) {
return !value;
})
}),
saveButton: SC.ButtonView.design({
layout: {bottom: 10, right: 110, height:24, width:80},
title: 'Save',
action: 'save',
isDefault: YES,
isEnabledBinding: 'CrudSample.userViewController.contentIsChanged',
isVisibleBinding: 'CrudSample.mainPage.detailPane.isEnabled'
}),
cancelButton: SC.ButtonView.design({
layout: {bottom: 10, right: 20, height:24, width:80},
title: 'Cancel',
action: 'cancel',
isCancel: YES,
isVisibleBinding: 'CrudSample.mainPage.detailPane.isEnabled'
})
}),
/**
* Methods to show/hide the details pane
* Thanks Charles:
* http://markmail.org/message/miobpqe7y34w7rht#query:sproutcore%20panelpane+page:1+mid:miobpqe7y34w7rht+state:results
*/
detailIsVisible: NO,
/**
* observer - show/hide panel
*/
detailIsVisibleDidChange: function() {
var panel = CrudSample.mainPage.get('detailPane');
if (this.get('detailIsVisible')) {
// Show
panel.append();
// Set focus on the username field
CrudSample.mainPage.detailPane.contentView.username.field.becomeFirstResponder();
}
else {
// Hide
panel.remove();
}
}.observes('detailIsVisible'),
/**
* Show this form for a new user
*/
showForCreate: function() {
this.set('detailIsVisible', YES);
CrudSample.userViewController.createNew();
},
/**
* Show this form and load details of the current user for editing
*/
showForUpdate: function() {
this.set('detailIsVisible', YES);
CrudSample.userViewController.updateCurrent();
},
/**
* Save changes
* Note that the save button is only visible if there has been changes in the current user record
*/
save: function() {
try {
this.set('isEnabled', NO);
CrudSample.userViewController.save(this, this.saveComplete);
} catch (e) {
this.showError(e);
this.set('isEnabled', YES);
}
},
/**
* Check if saving has finished
*/
saveComplete: function(errorObject) {
this.set('isEnabled', YES);
if (SC.none(errorObject)) {
this.set('detailIsVisible', NO);
} else {
this.showError(errorObject);
}
},
/**
* Discard changes
*/
cancel: function() {
CrudSample.userViewController.discard();
this.set('detailIsVisible', NO);
},
/**
* Delete current user.
* Note that the delete button is only visible if there are no changes to the current user being edited
*/
deleteRecord: function() {
CrudSample.userViewController.discard();
CrudSample.mainPage.mainPane.middleView.contentView.deleteSelection();
this.set('detailIsVisible', NO);
},
/**
* Show an error message
* @param e Error object to show
*/
showError: function(e) {
if (SC.instanceOf(e, SC.Error)) {
SC.AlertPane.error(e.message);
if (!SC.empty(e.label)) {
var view = CrudSample.mainPage.detailPane.contentView.getPath(e.label);
if (view) {
view.field.becomeFirstResponder();
}
}
} else {
SC.AlertPane.error(e);
}
}
}) //detailPane
});
5.3 Show Main Page
To kick start the app, I show the main page and load records into the userRecordArrayController in main.js.
CrudSample.main = function main() {
// Step 1: Instantiate Your Views
// The default code here will make the mainPane for your application visible
// on screen. If you app gets any level of complexity, you will probably
// create multiple pages and panes.
CrudSample.getPath('mainPage.mainPane').append();
// Step 2. Set the content property on your primary controller.
var query = SC.Query.local(CrudSample.UserRecord, {orderBy: 'username'});
var users = CrudSample.store.find(query);
CrudSample.userRecordArrayController.set('content', users);
};