├── .gitignore ├── Gemfile ├── public ├── favicon.ico ├── app │ ├── stores │ │ └── Users.js │ ├── app.js │ ├── views │ │ ├── Viewport.js │ │ ├── ErrorField.js │ │ └── users │ │ │ ├── List.js │ │ │ └── Form.js │ ├── models │ │ └── User.js │ └── controllers │ │ └── Users.js ├── stylesheets │ └── styles.css └── index.html ├── config.ru └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | steps 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem "rack" 4 | gem "serve" 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalearn/Forms-demo/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | use Rack::Static, :urls => { "/" => "index.html" }, :root => "public" 2 | run Rack::Directory.new("public") 3 | 4 | -------------------------------------------------------------------------------- /public/app/stores/Users.js: -------------------------------------------------------------------------------- 1 | App.stores.users = new Ext.data.Store({ 2 | model: 'User', 3 | autoLoad: true 4 | }); 5 | -------------------------------------------------------------------------------- /public/stylesheets/styles.css: -------------------------------------------------------------------------------- 1 | .invalid-field .x-form-label { color: red; } 2 | 3 | .errorfield { font-size: 0.75em; } 4 | .errorfield ul { margin: 10px; } 5 | -------------------------------------------------------------------------------- /public/app/app.js: -------------------------------------------------------------------------------- 1 | App = new Ext.Application({ 2 | name: "App", 3 | 4 | launch: function() { 5 | this.views.viewport = new this.views.Viewport(); 6 | 7 | this.views.usersList = this.views.viewport.down('#usersList'); 8 | this.views.usersForm = this.views.viewport.down('#usersForm'); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /public/app/views/Viewport.js: -------------------------------------------------------------------------------- 1 | App.views.Viewport = Ext.extend(Ext.Panel, { 2 | fullscreen: true, 3 | layout: 'card', 4 | 5 | initComponent: function() { 6 | Ext.apply(this, { 7 | items: [ 8 | { xtype: 'App.views.UsersList', id: 'usersList' }, 9 | { xtype: 'App.views.UsersForm', id: 'usersForm' }, 10 | ] 11 | }); 12 | App.views.Viewport.superclass.initComponent.apply(this, arguments); 13 | }, 14 | 15 | reveal: function(target) { 16 | var direction = (target === 'usersList') ? 'right' : 'left' 17 | this.setActiveItem( 18 | App.views[target], 19 | { type: 'slide', direction: direction } 20 | ); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /public/app/views/ErrorField.js: -------------------------------------------------------------------------------- 1 | App.views.ErrorField = Ext.extend(Ext.Component, { 2 | 3 | initComponent: function() { 4 | config = { 5 | xtype: 'component', 6 | id: this.fieldname + 'ErrorField', 7 | cls: 'errorfield', 8 | tpl: [ 9 | '', 10 | ' ', 15 | '' 16 | ], 17 | hidden: true 18 | }; 19 | 20 | Ext.apply(this, config); 21 | App.views.ErrorField.superclass.initComponent.call(this); 22 | }, 23 | 24 | }); 25 | Ext.reg('App.views.ErrorField', App.views.ErrorField); 26 | -------------------------------------------------------------------------------- /public/app/models/User.js: -------------------------------------------------------------------------------- 1 | App.models.User = Ext.regModel('User', { 2 | fields: [ 3 | { 4 | name: 'id', 5 | type: 'int' 6 | }, { 7 | name: 'name', 8 | type: 'string' 9 | }, { 10 | name: 'email', 11 | type: 'string' 12 | }, { 13 | name: 'phone', 14 | type: 'string' 15 | } 16 | ], 17 | 18 | validations: [ 19 | { 20 | type: 'presence', 21 | name: 'name' 22 | }, { 23 | type: 'format', 24 | name: 'email', 25 | matcher: /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/, 26 | message: 'must be a valid email' 27 | } 28 | ], 29 | 30 | proxy: { 31 | type: 'localstorage', 32 | id: 'sencha-users' 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sencha Touch 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/app/controllers/Users.js: -------------------------------------------------------------------------------- 1 | Ext.regController('Users', { 2 | store: App.stores.users, 3 | 4 | index: function() { 5 | App.views.viewport.reveal('usersList'); 6 | }, 7 | 8 | newForm: function() { 9 | var model = new App.models.User() 10 | App.views.usersForm.load(model); 11 | App.views.viewport.reveal('usersForm'); 12 | }, 13 | 14 | editForm: function(params) { 15 | var model = this.store.getAt(params.index); 16 | App.views.usersForm.load(model); 17 | App.views.viewport.reveal('usersForm'); 18 | }, 19 | 20 | save: function(params) { 21 | params.record.set(params.data); 22 | var errors = params.record.validate(); 23 | 24 | if (errors.isValid()) { 25 | this.store.create(params.data); 26 | this.index(); 27 | } else { 28 | params.form.showErrors(errors); 29 | } 30 | }, 31 | 32 | update: function(params) { 33 | var tmpUser = new App.models.User(params.data), 34 | errors = tmpUser.validate() 35 | 36 | if (errors.isValid()) { 37 | params.record.set(params.data); 38 | params.record.save(); 39 | this.index(); 40 | } else { 41 | params.form.showErrors(errors); 42 | } 43 | }, 44 | 45 | remove: function(params) { 46 | this.store.remove(params.record); 47 | this.store.sync(); 48 | this.index(); 49 | } 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /public/app/views/users/List.js: -------------------------------------------------------------------------------- 1 | App.views.UsersList = Ext.extend(Ext.Panel, { 2 | initComponent: function(){ 3 | var addButton, titlebar, list; 4 | 5 | addButton = { 6 | itemId: 'addButton', 7 | iconCls: 'add', 8 | iconMask: true, 9 | ui: 'plain', 10 | handler: this.onAddAction, 11 | scope: this 12 | }; 13 | 14 | titlebar = { 15 | dock: 'top', 16 | xtype: 'toolbar', 17 | title: 'Users', 18 | items: [ { xtype: 'spacer' }, addButton ] 19 | }; 20 | 21 | list = { 22 | xtype: 'list', 23 | itemTpl: '{name}', 24 | store: App.stores.users, 25 | listeners: { 26 | scope: this, 27 | itemtap: this.onItemtapAction 28 | } 29 | }; 30 | 31 | Ext.apply(this, { 32 | html: 'placeholder', 33 | layout: 'fit', 34 | dockedItems: [titlebar], 35 | items: [list] 36 | }); 37 | 38 | App.views.UsersList.superclass.initComponent.call(this); 39 | }, 40 | 41 | onAddAction: function() { 42 | Ext.dispatch({ 43 | controller: 'Users', 44 | action: 'newForm' 45 | }); 46 | }, 47 | 48 | onItemtapAction: function(list, index, item, e) { 49 | Ext.dispatch({ 50 | controller: 'Users', 51 | action: 'editForm', 52 | index: index 53 | }); 54 | } 55 | }); 56 | 57 | Ext.reg('App.views.UsersList', App.views.UsersList); 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This Git repository includes all of the source code used in creating a tutorial about [working with Sencha Touch forms][t]. 2 | 3 | The tutorial includes several checkpoints: 4 | 5 | * [Blank slate][00] 6 | * [Creating a user interface][01] ([view diff][00-01]) 7 | * [Creating a form][02] ([view diff][01-02]) 8 | * [Implementing the create action][03] ([view diff][02-03]) 9 | * [Implementing the update action][04] ([view diff][03-04]) 10 | * [Validating form fields][05] ([view diff][04-05]) 11 | * [Implementing the delete action][06] ([view diff][05-06]) 12 | 13 | ## Using this repository to follow the screencast 14 | 15 | First, you'll have to clone this repository: 16 | 17 | git clone git://github.com/senchalearn/Forms-demo.git 18 | 19 | Change into the directory: 20 | 21 | cd Forms-demo 22 | 23 | By default, the git clone command will only create the master branch locally. If you want to study the code at each checkpoint, you will have to fetch each of the other branches. You can do so by running the following: 24 | 25 | git checkout -b 00_blank_slate origin/00_blank_slate 26 | git checkout -b 01_user_interface origin/01_user_interface 27 | git checkout -b 02_create_new_form origin/02_create_new_form 28 | git checkout -b 03_implement_save_action origin/03_implement_save_action 29 | git checkout -b 04_create_edit_form origin/04_create_edit_form 30 | git checkout -b 05_validate_form_fields origin/05_validate_form_fields 31 | git checkout -b 06_implement_delete_action origin/06_implement_delete_action 32 | 33 | ## Live demo 34 | 35 | You can try out the demo here: 36 | 37 | * [http://sencha-forms.heroku.com/][t] 38 | 39 | [t]: http://www.sencha.com/learn/working-with-forms/ 40 | 41 | [00]: https://github.com/senchalearn/Forms-demo/tree/00_blank_slate 42 | [01]: https://github.com/senchalearn/Forms-demo/tree/01_user_interface 43 | [02]: https://github.com/senchalearn/Forms-demo/tree/02_create_new_form 44 | [03]: https://github.com/senchalearn/Forms-demo/tree/03_implement_save_action 45 | [04]: https://github.com/senchalearn/Forms-demo/tree/04_create_edit_form 46 | [05]: https://github.com/senchalearn/Forms-demo/tree/05_validate_form_fields 47 | [06]: https://github.com/senchalearn/Forms-demo/tree/06_implement_delete_action 48 | 49 | [00-01]: https://github.com/senchalearn/Forms-demo/compare/00_blank_slate...01_user_interface 50 | [01-02]: https://github.com/senchalearn/Forms-demo/compare/01_user_interface...02_create_new_form 51 | [02-03]: https://github.com/senchalearn/Forms-demo/compare/02_create_new_form...03_implement_save_action 52 | [03-04]: https://github.com/senchalearn/Forms-demo/compare/03_implement_save_action...04_create_edit_form 53 | [04-05]: https://github.com/senchalearn/Forms-demo/compare/04_create_edit_form...05_validate_form_fields 54 | [05-06]: https://github.com/senchalearn/Forms-demo/compare/05_validate_form_fields...06_implement_delete_action 55 | -------------------------------------------------------------------------------- /public/app/views/users/Form.js: -------------------------------------------------------------------------------- 1 | App.views.UsersForm = Ext.extend(Ext.form.FormPanel, { 2 | defaultInstructions: 'Please enter the information above.', 3 | 4 | initComponent: function(){ 5 | var titlebar, cancelButton, buttonbar, saveButton, deleteButton, fields; 6 | 7 | cancelButton = { 8 | text: 'cancel', 9 | ui: 'back', 10 | handler: this.onCancelAction, 11 | scope: this 12 | }; 13 | 14 | titlebar = { 15 | id: 'userFormTitlebar', 16 | xtype: 'toolbar', 17 | title: 'Create user', 18 | items: [ cancelButton ] 19 | }; 20 | 21 | saveButton = { 22 | id: 'userFormSaveButton', 23 | text: 'save', 24 | ui: 'confirm', 25 | handler: this.onSaveAction, 26 | scope: this 27 | }; 28 | 29 | deleteButton = { 30 | id: 'userFormDeleteButton', 31 | text: 'delete', 32 | ui: 'decline', 33 | handler: this.onDeleteAction, 34 | scope: this 35 | }; 36 | 37 | buttonbar = { 38 | xtype: 'toolbar', 39 | dock: 'bottom', 40 | items: [deleteButton, {xtype: 'spacer'}, saveButton] 41 | }; 42 | 43 | fields = { 44 | xtype: 'fieldset', 45 | id: 'userFormFieldset', 46 | title: 'User details', 47 | instructions: this.defaultInstructions, 48 | defaults: { 49 | xtype: 'textfield', 50 | labelAlign: 'left', 51 | labelWidth: '40%', 52 | required: false, 53 | useClearIcon: true, 54 | autoCapitalize : false 55 | }, 56 | items: [ 57 | { 58 | name : 'name', 59 | label: 'name', 60 | autoCapitalize : true 61 | }, 62 | { 63 | xtype: 'App.views.ErrorField', 64 | fieldname: 'name', 65 | }, 66 | { 67 | name: 'email', 68 | label: 'email', 69 | xtype: 'emailfield', 70 | }, 71 | { 72 | xtype: 'App.views.ErrorField', 73 | fieldname: 'email', 74 | }, 75 | { 76 | name: 'phone', 77 | label: 'phone', 78 | xtype: 'numberfield', 79 | }, 80 | { 81 | xtype: 'App.views.ErrorField', 82 | fieldname: 'phone', 83 | }, 84 | ] 85 | }; 86 | 87 | Ext.apply(this, { 88 | scroll: 'vertical', 89 | dockedItems: [ titlebar, buttonbar ], 90 | items: [ fields ], 91 | listeners: { 92 | beforeactivate: function() { 93 | var deleteButton = this.down('#userFormDeleteButton'), 94 | saveButton = this.down('#userFormSaveButton'), 95 | titlebar = this.down('#userFormTitlebar'), 96 | model = this.getRecord(); 97 | 98 | if (model.phantom) { 99 | titlebar.setTitle('Create user'); 100 | saveButton.setText('create'); 101 | deleteButton.hide(); 102 | } else { 103 | titlebar.setTitle('Update user'); 104 | saveButton.setText('update'); 105 | deleteButton.show(); 106 | } 107 | }, 108 | deactivate: function() { this.resetForm() } 109 | } 110 | }); 111 | 112 | App.views.UsersForm.superclass.initComponent.call(this); 113 | }, 114 | 115 | onCancelAction: function() { 116 | Ext.dispatch({ 117 | controller: 'Users', 118 | action: 'index' 119 | }); 120 | }, 121 | 122 | onSaveAction: function() { 123 | var model = this.getRecord(); 124 | 125 | Ext.dispatch({ 126 | controller: 'Users', 127 | action : (model.phantom ? 'save' : 'update'), 128 | data : this.getValues(), 129 | record : model, 130 | form : this 131 | }); 132 | }, 133 | 134 | onDeleteAction: function() { 135 | Ext.Msg.confirm("Delete this user?", "", function(answer) { 136 | if (answer === "yes") { 137 | Ext.dispatch({ 138 | controller: 'Users', 139 | action : 'remove', 140 | record : this.getRecord() 141 | }); 142 | } 143 | }, this); 144 | }, 145 | 146 | showErrors: function(errors) { 147 | var fieldset = this.down('#userFormFieldset'); 148 | this.fields.each(function(field) { 149 | var fieldErrors = errors.getByField(field.name); 150 | 151 | if (fieldErrors.length > 0) { 152 | var errorField = this.down('#'+field.name+'ErrorField'); 153 | field.addCls('invalid-field'); 154 | errorField.update(fieldErrors); 155 | errorField.show(); 156 | } else { 157 | this.resetField(field); 158 | } 159 | }, this); 160 | fieldset.setInstructions("Please amend the flagged fields"); 161 | }, 162 | 163 | resetForm: function() { 164 | var fieldset = this.down('#userFormFieldset'); 165 | this.fields.each(function(field) { 166 | this.resetField(field); 167 | }, this); 168 | fieldset.setInstructions(this.defaultInstructions); 169 | this.reset(); 170 | }, 171 | 172 | resetField: function(field) { 173 | var errorField = this.down('#'+field.name+'ErrorField'); 174 | errorField.hide(); 175 | field.removeCls('invalid-field'); 176 | return errorField; 177 | } 178 | }); 179 | 180 | Ext.reg('App.views.UsersForm', App.views.UsersForm); 181 | --------------------------------------------------------------------------------