senchacon 2016: handling undo-redo in sencha applications - nickolay platonov
TRANSCRIPT
Handling Undo-Redo in Sencha Applications
Nickolay Platonov@Bryntum
About me
• Nickolay Platonov- Senior ExtJS developer at Bryntum
- Using ExtJS since version 2.0
- SamuraiJack1 on the forums
- Creator of Joose 3 class system for JavaScript, http://joose.it
- Co-creator of Siesta and Robo tools
2
Undo/Redo in general
Undo/Redo as a feature
• Very useful
• Standard feature for any serious desktop application
4
Undo/Redo as a feature
• Very useful
• Standard feature for any serious desktop application
• Rarely seen in Sencha applications
• Probably since it is considered too complex to implement
5
Undo/Redo as a feature
• Very useful
• Standard feature for any serious desktop application
• Rarely seen in Sencha applications
• Probably since it is considered too complex to implement
• But doable, as you will see by the end of this talk
6
Undo/Redo – formal task definition
• It’s all about the application state
• State is presented with data structures
// Data structure for shapevar shape = { shapeType : ‘circle’,
x : 100,y : 100,radius : 50
}
Undo/Redo – formal task definition
• It’s all about the application state
• State is presented with data structures
• For example, for simple graphical editor a state would be – an array of shapes.
// Data structure for shapevar shape = { shapeType : ‘circle’,
x : 100,y : 100,radius : 50
}
// Graphical editor statevar appState = [ shape1, shape2, shape3 ]
Undo/Redo – formal task definition
• Every user action switches (advances) the application to a new state
• User moves a circle
• User adds a shape
{ { shapeType : ‘circle’, shapeType : ‘circle’, x : 100, x : 200, y : 100, y : 200, radius : 50 radius : 50} }
[ shape1, shape 2 ] [ shape1, shape2, shape3 ]
Undo/Redo – formal task definition
• State change is normally a one-way flow
10
state1 state2 state3
Undo/Redo – formal task definition
• We want to make the flow bidirectional
11
state1 state2 state3
Approach 1 (naive) – Save full snapshot
• Serialize full snapshot of application state
• Deserialize the snapshot and place it as a new application state
12
state1 state2 state3
Approach 1 (naive) – Save full snapshot
• Serialize full snapshot of application state
• Deserialize the snapshot and place it as a new application state
• Pros- Very simple implementation
• Cons- Performance (full scan of the dataset)
- Memory consumption (every checkpoint contains all the data)
13
state1 state2 state3
Approach 2 – Save the diff between the states
• Calculate the diff between the application states
• Apply the diff to initial state, to get the new state
14
state1 state2 state3diff_1 diff_2
Approach 2 – Save the diff between the states
• Calculate the diff between the application states
• Apply the diff to initial state, to get the new state
• Pros- Memory consumption (only the actually changed data is gathered)
• Cons- Complexity of diff operation
- Performance of diff operation (both for gathering diffs and applying diffs)
15
state1 state2 state3diff_1 diff_2
Approach 3 – Save the changelog between the states
• Every diff between the states is a list of actions
• Actions are small, atomic and reversible
16
state1 state2 state3Action1 Action2 Action3 Action4
Approach 3 – Save the changelog between the states
• Every diff between the states is a list of actions
• Actions are small, atomic and reversible
• Pros- Performance & memory consumption (only the actually changed data is gathered/stored)
- Relatively simple implementation
• Cons- Application needs to be aware about the undo/redo feature
17
state1 state2 state3Action1 Action2 Action3 Action4
Architecture requirements
• App should follow the MVC pattern
• No state to be kept in the views or controllers (or at least as less as possible)
18
Controller
ModelView
Robo
Robo
• Undo/redo functionality framework, developed by Bryntum
• Targets Sencha applications
• Robo supports ExtJS 5.1.2 / 6.0.1 / 6.0.2 / 6.2.0
• Out of the box, operates on ExtJS data stores (Ext.data.Store and Ext.data.TreeStore)
• Can be customized to a specific application needs
20
Design & terminology
• The transition between application states is called “transaction”
• Every transaction may contain several smaller “actions”, which are all atomic
• Listens to events from stores, and creates actions from them
21
Robo.Transaction
state1 state2Robo.Action 1 Robo.Action 2
22
http://www.bryntum.com/examples/robo-latest/examples/basic/
Transaction boundaries
• The application may define complex processing rules for data
• Robo is not aware of them
• Developer can choose between the 2 strategies for defining the state checkpoints:- Timeout (default) – finish the transaction after some time
- Manual – finish every transaction manually (it will start automatically on any change in any store)
23
Action example
• Robo.action.flat.Add
• Every action has “undo” and “redo” methods
Ext.define('Robo.action.flat.Add', { extend : 'Robo.action.Base',
store : null, records : null,
index : null,
undo : function () {
this.store.remove(this.records); },
redo : function () { this.store.insert(this.index, this.records); } });
Action example
• Robo.action.flat.Remove
• Every action has “undo” and “redo” methods
Ext.define('Robo.action.flat.Remove', { extend : 'Robo.action.Base',
store : null, records : null,
index : null,
undo : function () { var me = this;
me.store.insert(me.index, me.records); },
redo : function () { var me = this;
me.store.remove(me.records); } });
Integration with Sencha application
• Add Robo.data.Model mixing to the models
Ext.define('Example.model.Employee', { extend : 'Ext.data.Model', modelName : 'Employee', mixins : { robo : 'Robo.data.Model' }, ...})
Integration with Sencha application
• Add Robo.data.Model mixing to the models
• Create an instance of Robo.Manager
Ext.define('Example.model.Employee', { extend : 'Ext.data.Model', modelName : 'Employee', mixins : { robo : 'Robo.data.Model' }, ...})
var robo = new Robo.Manager({ stores : [ 'store1', 'store2', 'store3‘ ]});
Integration with Sencha application
• Add Robo.data.Model mixing to the models
• Create an instance of Robo.Manager
• Add stores to it
Ext.define('Example.model.Employee', { extend : 'Ext.data.Model', modelName : 'Employee', mixins : { robo : 'Robo.data.Model' }, ...})
var robo = new Robo.Manager({ stores : [ 'store1', 'store2', 'store3‘ ]});
// or add the stores after instantiationrobo.addStore(store1)robo.addStore(store2)
Integration with Sencha application
• Add Robo.data.Model mixing to the models
• Create an instance of Robo.Manager
• Add stores to it
• Start monitoring data changes
Ext.define('Example.model.Employee', { extend : 'Ext.data.Model', modelName : 'Employee', mixins : { robo : 'Robo.data.Model' }, ...})
var robo = new Robo.Manager({ stores : [ 'store1', 'store2', 'store3‘ ]});
// or add the stores after instantiationrobo.addStore(store1)robo.addStore(store2)
// start monitoring (after data load)robo.start();
Integration with Sencha application
• Add Robo.data.Model mixing to the models
• Create an instance of Robo.Manager
• Add stores to it
• Start monitoring data changes
• Use the robo.undo() robo.redo() API calls
Ext.define('Example.model.Employee', { extend : 'Ext.data.Model', modelName : 'Employee', mixins : { robo : 'Robo.data.Model' }, ...})
var robo = new Robo.Manager({ stores : [ 'store1', 'store2', 'store3‘ ]});
// or add the stores after instantiationrobo.addStore(store1)robo.addStore(store2)
// start monitoring (after data load)robo.start();
// at some point laterrobo.undo()
robo.redo()
Dependent data
• Data objects often depends on each other
• Change in one object triggers a change in another (possibly in another store)
// user actionUSER: employee1.set(‘hourlyRate’, 100)
// application triggers (activated by ‘update’ event)APP: employee1.set(‘monthlySalary’, 16000)APP: employee1.set(‘yearlySalary’, 192000)
Dependent data
• Data objects often depends on each other
• Change in one object triggers a change in another (possibly in another store)
• Robo performs the undo/redo using the standard data package API (will trigger standard events)
• App may react on every data change triggered by Robo
// user actionUSER: employee1.set(‘hourlyRate’, 100)
// application triggers (activated by ‘update’ event)APP: employee1.set(‘monthlySalary’, 16000)APP: employee1.set(‘yearlySalary’, 192000)
// robo.undo()
ROBO: employee1.set(‘yearlySalary’, 96000)APP: employee1.set(‘hourlyRate’, 50)APP: employee1.set(‘monthlySalary’, 8000)ROBO: employee1.set(‘monthlySalary’, 8000)APP: employee1.set(‘hourlyRate’, 50)APP: employee1.set(‘yearlySalary’, 96000)ROBO: employee1.set(‘hourlyRate’, 50)APP: employee1.set(‘monthlySalary’, 8000)APP: employee1.set(‘yearlySalary’, 96000)
Solution
• Application needs to be aware about current data flow “mode” – “normal/undo/redo”
• Skip the data propagation rules in “undo/redo” mode
33
Technically
• Add the Robo.data.Store mixin to the store
• Use the isUndoingOrRedoing() method to check if current flow is undo/redo
Ext.define('Example.store.EmployeeList', { extend : 'Ext.data.Store', mixins : { robo : 'Robo.data.Store' }, onRecordUpdate : function (...) { if (!this.isUndoingOrRedoing()) { ... } }});
Suspended events
• Robo can’t record anything, if events on a store are suspended
• Moreover, a missed action leads to inconsistent undo/redo queue state
• Application should not change store data if events are suspended (or, suspend events with the queuing option)
• Or, create missing actions manually roboManager.currentTransaction.addAction()
35
Robo widgets. Transaction titles
• Robo provides two simple buttons – undo and redo
• Every button contains a menu with an item for every transaction to undo/redo
• Developer can define a title for the transaction by implementing the “getTitle()” method on the models
36
Advanced Robo showcase
Bryntum Ext Gantt
• 5 stores, one of them is a TreeStore
• Very complex processing rules (change in one store propagates to others)
me.undoManager = new Gnt.data.undoredo.Manager({ stores : [ calendarManager, taskStore, resourceStore, assignmentStore, dependencyStore ]});
Bryntum Ext Gantt
• 5 stores, one of them is a TreeStore
• Very complex processing rules (change in one store propagates to others)
• Works like a charm
me.undoManager = new Gnt.data.undoredo.Manager({ stores : [ calendarManager, taskStore, resourceStore, assignmentStore, dependencyStore ]});
Bryntum Ext Gantt
• 5 stores, one of them is a TreeStore
• Very complex processing rules (change in one store propagates to others)
• Works like a charm
• Required some customization
me.undoManager = new Gnt.data.undoredo.Manager({ stores : [ calendarManager, taskStore, resourceStore, assignmentStore, dependencyStore ]});
41
http://www.bryntum.com/examples/gantt-latest/examples/advanced/advanced.html#en
Conclusion
• With Robo, the undo-redo functionality is easy to add to any Sencha application, following a few simple rules during development
• There are already several successful implementations
42
Conclusion
• With Robo, the undo-redo functionality is easy to add to any Sencha application, following a few simple rules during development
• There are already several successful implementations
• Next time you hear the request for undo/redo – don’t reject it immediately.
43
Learn more
• http://www.bryntum.com/products/robo/
• http://www.bryntum.com/docs/robo/#!/guide/robo_getting_started
• Any questions?
44