senchacon 2016: handle real-world data with confidence - fredric berling
TRANSCRIPT
• Reading & saving simple dataWhen we live in a perfect world. Domain model is perfect. We decide everything.
• Real world data and demandsStill simple data but add real world scenarios and demands. Learn how to configure to cope with it.
• AssociationsHow to handle associate data.
• Multi model scenariosLearn about Ext.data.Session to track changes in multiple models and associations.
• Errors
2
Agenda
Reading simple data
Model View ViewModel ViewController
Sencha MVVM recap
Model View ViewModel ViewController
Sencha MVVM recap
Ext.data.Model Ext.Component Ext.app.ViewModel Ext.app.ViewController
Model View ViewModel ViewController
Sencha MVVM recap
Ext.data.Model Ext.Component Ext.app.ViewModel Ext.app.ViewController
View Package
CRM.view.Viewport CRM.view.ViewportModel CRM.view.ViewportController
Our sample application View Package
resources/getCompanyById.json
• Id field is lowercase
• Date is formatted in a understandable format
{ id : 100, companyName : 'Ikea', address : 'Storgatan 1', city : 'Stockholm', country : 'Sweden',
updatedDate : '1973-11-17 05:30'}
The data model
• Fields config are not really needed for simple data types.
.. except date
• Simple proxy config
Ext.define('CRM.model.CompanyProfile', { extend: 'Ext.data.Model', fields:[ {
name : 'updatedDate',type : 'date’
} ], proxy: {
url : 'resources/getCompanyById.json' }});
Data loading
• linkTo function
Creates a link to a record by loading data from configured proxy and setting it on the view model with a simple name.
"companyProfile" will be our reference in all bindings referring to company data record.
Ext.define('CRM.view.ViewportController', { extend : 'Ext.app.ViewController', alias : 'controller.viewportcontroller',
init: function() {this.getViewModel().linkTo('companyProfile',{
type : 'CRM.model.CompanyProfile', id : 100 }); }});
Ext.define('CRM.view.Viewport', { extend : 'Ext.Container', viewModel : 'viewportmodel', controller : 'viewportcontroller', padding : 20, items: [ { xtype : 'textfield', fieldLabel : 'Company name', bind : '{companyProfile.companyName}' }, { xtype : 'textfield', fieldLabel : 'Address', bind : '{companyProfile.address}' }
...
]});
Bind fields to view
Ext.define('CRM.view.Viewport', { extend : 'Ext.Container', viewModel : 'viewportmodel', controller : 'viewportcontroller', padding : 20, items: [ '''
]});
• viewModel- connects this view to the viewmodel with
alias "viewportmodel"
• controller- connects this view to the viewController
with alias "viewportcontroller"
View configs
Live demoSimple read
Saving simple data
Save buttonExt.define('CRM.view.ViewportController', { extend : 'Ext.app.ViewController', alias : 'controller.viewportcontroller', onSave:function(){ this.getViewModel().get('companyProfile').save(); }});
Ext.define('CRM.view.Viewport', { extend : 'Ext.Panel', viewModel : 'viewportmodel', controller : 'viewportcontroller', padding : 20, buttons:[ { text:'Save', handler:'onSave' } ], items: [ { xtype : 'textfield', fieldLabel : 'Company name', bind : '{companyProfile.companyName}' } ...
Save function
Live demoSimple Read/Save
HTTP Post{ "id": 100, "companyName": "Ikea AB", "address": "Storgatan 1", "city": "Malmö", "country": "Sweden", "updatedDate": "2016-08-23 19:47"}
{ id:100, companyName:"Ikea AB"}
Required response
Noteworthy
• Only dirty field data was sent.
• Id was always sent
• Same URL as read
• Response data was automatically realized in view
19
Real world data and demands
Real world data from Customer
• Observations- id field is not "id"
{ _id : "57bb5d639baeb676ced2b0de", companyName : 'Ikea', address : {
street : 'Storgatan 1',city : 'Stockholm',country : 'Sweden'
}, updatedDate : '2016-08-23T21:26:08.358Z'
}
Real world data from Customer
• Observations- id field is not "id"
- Some data resides is in objects
{ _id : "57bb5d639baeb676ced2b0de", companyName : 'Ikea', address : {
street : 'Storgatan 1',city : 'Stockholm',country : 'Sweden'
}, updatedDate : '2016-08-23T21:26:08.358Z'
}
Real world data from Customer
• Observations- id field is not "id"
- Some data resides is in objects
- Date format
{ _id : "57bb5d639baeb676ced2b0de", companyName : 'Ikea', address : {
street : 'Storgatan 1',city : 'Stockholm',country : 'Sweden'
}, updatedDate : '2016-08-23T21:26:08.358Z'
}
Scenarios/Customer demands
• Id must be "_id". We are using Mongo DB.
• Updated date should ignore timezones
• The exact same JSON structure must be sent on save.
• Always send all data on save.
• Save must be on separate url.
24
Specify your own id field
idPropertyConsider doing this in abstract base class or even override Ext.data.Model.
Ext.define('CRM.model.CompanyProfile', { extend: 'Ext.data.Model', idProperty:'_id',
fields:[ {
name : 'updatedDate',type : 'date’
} ], proxy: {
url : 'resources/getCompanyById.json' }});
Read data from nested objects
mappingSpecify a dot notated path in the fields array of data model.
fields:[ { name : 'street', mapping : 'address.street', type : 'string' }, { name : 'city', mapping : 'address.city', type : 'string' }, { name : 'country', mapping : 'address.country', type : 'string' },
... ],
Send all fields on save
writeAllFieldsProxy writer configs to ensure all fields will be sent.
proxy: {type : 'ajax',url : '/companies',reader:{
type:'json'},
writer:{type : 'json',writeAllFields : true
}}
Keep object nesting on save { _id : "57bb5d639baeb676ced2b0de", companyName : 'Ikea', address : {
street : 'Storgatan 1',city : 'Stockholm',
country: 'Sweden' },
updatedDate : '2016-08-23T21:26:08.358Z'}
Keep object nesting on save
expandDataMakes sure the mapped field data sent back is in a nested format.
namePropertyProperty used to read the key for each value that will be sent to the server.
proxy: {type : 'ajax',url : '/companies',reader:{
type:'json'},
writer:{type : 'json',writeAllFields : true,nameProperty : 'mapping',expandData : true
}}
Live demoReal world data
Date should not add timezone
By default you will get a localized date in the current browsers timezone
{ _id : "57bb5d639baeb676ced2b0de", companyName : 'Ikea', address : {
street : 'Storgatan 1',city : 'Stockholm',
country: 'Sweden' },
updatedDate : '2016-08-23T21:26:08.358Z'}
dateFormat
Adding the exact date format will make sure date is not altered.
fields:[ ''' { name:'updatedDate', type:'date', dateFormat:'Y-m-d\\TH:i:s.u\\Z' }],
Read/Update on separate Url´s
Instead of using the proxy standard url config, we change to the more versatile api config
proxy: { type : 'ajax', url : '/companies' reader:{ type:'json' }, writer:{ type : 'json', nameProperty : 'mapping', writeAllFields : true, expandData : true } }
Read/Update on separate Url´s
Instead of using the proxy standard url config, we change to the more versatile api config
proxy: { type : 'ajax', api : { read: '/companies', update: '/updateCompanies' }, reader:{ type:'json' }, writer:{ type : 'json', nameProperty : 'mapping', writeAllFields : true, expandData : true } }
Associations
Associated licenses{ "_id": "57bf32fe9baeb676ced2b0e1", "companyName": "Ikea", "address": { "street": "Storgatan 3", "city": "Malmö", "country": "Sweden" }, "updatedDate": "2016-08-25T18:51:39.671Z", "licenses": [ { "id": 1, "product": "ExtJS 2.1", "amount": 5 }, { "id": 2, "product": "ExtJS 4.1", "amount": 5 }, .......
]
License references Company Ext.define('CRM.model.License', { extend: 'Ext.data.Model', idgen:'uuid', idProperty:'_id', fields:[ {name:'product', type:'string'}, {name:'amount', type:'float'}, {
name: 'companyId',reference: {
parent : 'CRM.model.CompanyProfile',inverse : {
role : 'licenses'}
} }
Company
License
License
License
License references Company Ext.define('CRM.model.License', { extend: 'Ext.data.Model', idgen:'uuid', idProperty:'_id', fields:[ {name:'product', type:'string'}, {name:'amount', type:'float'}, {
name: 'companyId',reference: {
parent : 'CRM.model.CompanyProfile',inverse : {
role : 'licenses'}
} }
Company
License
License
License
License references Company Ext.define('CRM.model.License', { extend: 'Ext.data.Model', idgen:'uuid', idProperty:'_id', fields:[ {name:'product', type:'string'}, {name:'amount', type:'float'}, {
name: 'companyId',reference: {
parent : 'CRM.model.CompanyProfile',inverse : {
role : 'licenses'}
} }
Company
License
License
License
Useful?
• Automatically created stores.Company model function licenses() will return accociated licenses.
var viewModel = this.getViewModel(),companyProfile = viewModel.get('companyProfile'),store =
companyProfile.licenses();
Useful?
• Automatically created stores.Company model function licenses() will return accociated licenses.
• License function getCompany() will return Company model.
var viewModel = this.getViewModel(),companyProfile = viewModel.get('licenses'),store =
companyProfile.licenses(),firstLicense = store.first(),
console.log(firstCompany.getCompany());
// Returns companyProfile
Useful?
• Automatically created stores.Company model function licenses() will return accociated licenses.
• License function getCompany() will return Company model.
• Nice bindings. companyProfile.licenses references the store
{xtype : 'grid',plugins : 'cellediting',columnLines:true,bind : {
store:'{companyProfile.licenses}'},columns:[
{text: 'Licence', dataIndex:'product'}, {text: 'Amount', dataIndex:'amount'} ]}
• Inline associationsLicence data is array in company JSON and should be saved in same call as company profile.
• Proxy/Remote associations Licence data is fetched from its own web service and CRUD operations are handled here
43
Possible business scenarios
Inline associations{ "_id": "57bf32fe9baeb676ced2b0e1", "companyName": "Ikea", "address": { "street": "Storgatan 3", "city": "Malmö", "country": "Sweden" }, "updatedDate": "2016-08-25T18:51:39.671Z", "licenses": [ { "id": 1, "product": "ExtJS 2.1", "amount": 5 }, { "id": 2, "product": "ExtJS 4.1", "amount": 5 }, .......
]
GET
Inline associations{ "_id": "57bf32fe9baeb676ced2b0e1", "companyName": "Ikea", "address": { "street": "Storgatan 3", "city": "Malmö", "country": "Sweden" }, "updatedDate": "2016-08-25T18:51:39.671Z", "licenses": [ { "id": 1, "product": "ExtJS 2.1", "amount": 5 }, { "id": 2, "product": "ExtJS 4.1", "amount": 5 }, .......
]
POST
allDataOptions
Proxy writer needs to be told to save associated data.
.
proxy: { type : 'ajax',
api : { read: '/companies',
update: '/updateCompanies'},
writer:{ type : 'json', nameProperty : 'mapping', writeAllFields : true, expandData : true, allDataOptions :{ associated:true } } }
Company model
Associated store data is not realized on save.
Live example
47
Saving associated data in a big JSON is generally a bad idea
The associated data is probably saved in a table in a database
Empty arrays will have to force delete
48
3 Possible solutions
• Re-load all data after successful save- extra call
- safe
onSave:function(){this.getViewModel().get('companyProfile').save({
callback:function(){this.getViewModel().linkTo('companyProfile',{
type : 'CRM.model.CompanyProfile', id : '57bf32fe9baeb676ced2b0e1' });
},scope:this
}); }
3 Possible solutions
• Re-load all data after successful save- extra call
- safe
• Forced commitChanges()- no extra call
- response data is ignored.
onSave:function(){var store = this.getViewModel().get('companyProfile.licenses')this.getViewModel().get('companyProfile').save({
callback:function(){store.commitChanges(); },
},scope:this
});}
3 Possible solutions
• Re-load all data after successful save- extra call
- safe
• Forced commitChanges()- no extra call
- response data is ignored.
• Code around it- complex
What happens if license array is missing?
Demo!
52
Associated stores will default to a ajax proxy.
Avoid remote reads by changing this to a memory proxy.
Ext.define('CRM.model.License', { extend: 'Ext.data.Model', fields:[ {name:'product', type:'string'}, {name:'amount', type:'float'},
{ name: 'companyId', reference: { parent : 'CRM.model.CompanyProfile', inverse : { role:'licenses', storeConfig:{ proxy:{ type:'memory' } } }
} . . .
Store config
Proxy/Remote associationsExt.define('CRM.view.ViewportController', { extend : 'Ext.app.ViewController', alias : 'controller.viewportcontroller',
onSave:function(){var vm = this.getViewModel();
vm.get('companyProfile').save(); vm.get('companyProfile.licenses').sync(); },
...
• Call save() on model.
• Call sync() on stores.
Proxy/Remote associationsExt.define('CRM.model.License', {
extend: 'Ext.data.Model',idProperty:'_id',fields:[..],proxy: {
type : 'ajax',api : {
read: '/licenses',create : '/addLicenses',update : '/updateLicenses',destroy : '/deleteLicenses'
}, writer:{
type : 'json', writeAllFields : true } }});
• Specify all api´s on associated data model
Proxy/Remote associations{
xtype: 'grid',plugins: 'cellediting',columnLines: true,tbar: [{text: 'Add', handler: 'addLicense'}],bind: {
store: '{companyProfile.licenses}'}
},
• Associated data will only load if store is used in a binding
Proxy/Remote associations
filter:[{
"property":"companyId",
"value":"57bcbaa29baeb676ced2b0e0","exactMatch":true
}]
• On read, a filter will be posted.- property is the reference name,
- value is the reference Id
Backend must honor this filter and return the correct subset.
• Live Example
HTTP GET /licenses
Multi models scenarios
59
Ext.data.Session
The primary job of a Session is to manage a collection of records of many different types and their associations. This often starts by loading records when requested and culminates when it is time to save to the
60
View with multiple models
61
Company
Licenses
Pool cars
Feedback
View with multiple models
62
Company
Licenses
Pool cars
Feedback
Data model + association
View with multiple models
63
Company
Licenses
Pool cars
Feedback
Data model + association
Store
View with multiple models
64
Company
Licenses
Pool cars
Feedback
Data model + association
Store
Data model
• getChanges()- Returns an object describing all of the modified fields, created or dropped records
maintained by this session.
- Used to track if ANY data is dirty
• getSaveBatch()- Returns an Ext.data.Batch containing the Ext.data.operation.Operation instances that are
needed to save all of the changes in this session
65
Session features
One point save onSave:function(){if(this.getSession().getChanges()){
this.getSession().getSaveBatch().start();}
},
Enable session Ext.define('CRM.view.Viewport', { extend : 'Ext.Panel', viewModel : 'viewportmodel', controller : 'viewportcontroller', bodyPadding : 20, session:true, buttons:[ { text:'Save', handler:'onSave' } ], layout:{ type:'vbox', align:'stretch' }, . . . .
• Enable session in view
Enable session var store = this.getViewModel().getStore('cars');
store.setSession(this.getSession());• Enable session in view
• Add stores to session
Enable session CRM.model.Feedback.load('57d072659baeb676ced2b0e5',{
success:function(record){this.getViewModel().set('feedback', record);
},scope:this
}, this.getSession());
• Enable session in view
• Add stores to session
• Provide session in model load
Enable session this.getViewModel().linkTo('companyProfile',{type : 'CRM.model.CompanyProfile',id : '57bcbaa29baeb676ced2b0e0'
});• Enable session in view
• Add stores to session
• Provide session in model load
• Use LinkTo- uses session from viewModel
Live demoMulti Model
Sending extra parameters
Extra proxy parameters proxy: { type : 'ajax', extraParams:{ appName:'CRM' }, api : {
read: '/companies', update: '/updateCompanies' }, reader:{ type:'json' }, writer:{ type : 'json', } }
• Set in proxy using extraParams config
Extra proxy parameters var proxy = Ext.ClassManager.get(<class>).getProxy();
proxy.setExtraParam('appName', 'CRM'); • Set in proxy using extraParams config
• Set from controller using the setExtraParam function
Operation parameters vm.get('companyProfile').save({params:{
appHeight:this.getView().getHeight();}
});vm.get('companyProfile.licenses').sync({
params:{appHeight:this.getView().getHeight();
}});
• Send params config into operational methods,
Live demoExtra Parameters
Errors with messages
Handle error response { "IsSuccess": false, "ErrorMessage": "Missing company"}Error returned from the server in a json
response when logic fails or data is missing.
Configure the proxy reader proxy: {type : 'ajax',api : {
read: '/companies',update: '/updateCompanies'
},reader:{
type:'json',successProperty:'IsSuccess',messageProperty:'ErrorMessage'
}
. . .
• successProperty
The property indicating that the operation was successful or not.
• messageProperty
The property where the error message is returned.
Listen for proxy exception Ext.mixin.Observable.observe(Ext.data.proxy.Server);
Ext.data.proxy.Server.on('exception',function(proxy, resp, operation){
Ext.Msg.alert('Error', operation.getError());
});
• exception- fires when "success" is false
- fires on HTTP exceptions
• operation.getError()- returns messageProperty content
Live demoError handling
Please Take the Survey in the Mobile App
• Navigate to this session in the mobile app
• Click on “Evaluate Session”
• Respondents will be entered into a drawing to win one of five $50 Amazon gift cards