SenchaCon 2016: Handle Real-World Data with Confidence - Fredric Berling

Post on 12-Jan-2017

82 views 4 download

Transcript of SenchaCon 2016: Handle Real-World Data with Confidence - Fredric Berling

Handle real world data with confidence.

Fredric Berling@FredricBerling

fredric@entence.se

• 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