Store
import { Store } from 'cx/data';CxJS widgets are tightly connected to a central data repository called Store.
- Widgets access stored data to calculate data required for rendering (data binding process).
- Widgets react to user inputs and update the Store either directly (two-way bindings) or by dispatching actions which are translated into the new application state.
- The Store sends a change notification which produce a new rendering of the widget tree and update the DOM.
Principles
- The whole application state is stored in the object tree within a single Store.
- The state is immutable. On every change, a new copy of the state is created containing the updated values.
- The only way to change the state is through the use of two-way data binding or the Store methods.
Store methods
In order to simplify working with immutable data, the Store exposes a set of public methods that can be used to manage the application state.
| Methods |
|---|
Store.batch(callback)
The Store instance is passed to the |
Store.copy(from, to)Copies the value stored under the |
Store.delete(path)Removes data from the Store stored under the given |
Store.dispatch(action)
|
Store.get(path)The |
Store.getData()Returns a reference to the object representing the application state. |
Store.init(path, value)Saves |
Store.load(data)Loads |
Store.move(from, to)Copies the value stored under the |
Store.notify(path)Notifies Store subscribers about the change. Usually, notifications cause the application to re-render.
This method automatically occurs whenever a change is made.
Optional |
Store.set(path, value)Saves |
Store.silently(callback)
The Store instance is passed to the |
Store.toggle(path)Toggles the boolean value stored under the given |
Store.update(path, updateFn, ...args)Applies the |
Examples
In the examples below we will explore the most common ways to use the Store in CxJS:
- inside Controllers (store is available via
this.store)n - through two-way data binding (explained here)
- inside event handlers
init
The init method is typically used inside the Controller's onInit method to initialize the data.
It takes two arguments, path and value. The path is a string which is used as a key for storing the
value. If the path is already taken, the method returns false without overwriting the existing value.
Otherwise, it saves the value and returns true.
class PageController extends Controller { onInit() { this.store.init('$page', { name: 'Jane', disabled: true, todoList: [ { id: 1, text: 'Learn Cx', done: true }, { id: 2, text: "Feed the cat", done: false }, { id: 3, text: "Take a break", done: false } ], count: 0 }); } greet() { let name = this.store.get('$page.name') MsgBox.alert(`Hello, ${name}!`); } }
get
The get method is used to read data from the Store. It takes any number of arguments or an array of strings
representing paths and it returns the corresponding values.
In the previous example, the greet method inside the controller is
using the Store.get method to read the name from the Store.
You will notice that we are able to directly access a nested property ($page.name) by using the . in our
path
string. Think of path as a property accessor.
The set method is used to update data in the Store. It takes two arguments, path and value.
Any existing data stored under the given path will be overwritten.
In this example, we are accessing the Store from inside an event handler.
In CxJS, all event handlers receive at least two arguments, event and instance.
The instance represents the CxJS widget that triggered the event and we can use it to obtain the Store.
In this example, the computable function is used to dynamically
calculate the button text, depending on the $page.disabled value.
<div layout={LabelsTopLayout} > <TextField label="Name" value-bind="$page.name" disabled-bind="$page.disabled" /> <Button onClick={(e, instance) => { let {store} = instance; store.set('$page.disabled', !store.get('$page.disabled')); }} text={computable('$page.disabled', (disabled) => disabled ? "Enable input" : "Disable input")} /> </div>
toggle
The toggle method is used for inverting boolean values inside the Store.
Below is the same example, only this time done using toggle.
You can also make the code more compact by doing destructuring right inside the function declaration.
<div layout={LabelsTopLayout} > <TextField label="Name" value-bind="$page.name" disabled-bind="$page.disabled" /> <Button onClick={(e, {store}) => { store.toggle('$page.disabled'); }} text={computable('$page.disabled', (disabled) => disabled ? "Enable input" : "Disable input")} /> </div>
The delete method is used to remove data from the Store. It takes a single parameter it being the path
under which the value is stored.
The copy method is used to copy data from one path to another. It takes two parameters, the origin path
and the destination path. Any existing data stored under the destination path is overwritten.
<div layout={LabelsTopLayout}> <TextField label="Origin" value-bind="$page.name" /> <TextField label="Destination" value-bind="$page.copyDestination" placeholder="click Copy" /> <Button onClick={(e, {store}) => { store.copy('$page.name', '$page.copyDestination'); }}>Copy</Button> </div>
The move method is similar to the copy method. The only difference is that it removes the data
from the Store after creating a copy. Any existing data stored under the destination path is overwritten.
<div layout={LabelsTopLayout}> <TextField label="Origin" value-bind="$page.name" /> <TextField label="Destination" value-bind="$page.moveDestination" placeholder="click Move" /> <Button onClick={(e, {store}) => { store.move('$page.name', '$page.moveDestination'); }}>Move</Button> </div>
update
The update method is primarily used to perform an update that is dependant on the previous state.
This simplifies use-cases where the developer would use the get method to read a value, perform
calculation, and then use the set method to save the result to the Store.
update method requires two parameters:
pathunder which the value is stored in the Store- update function
updateFn - optionally, any additional arguments will be passed over to the update function
The simplest example of when to use the update method is the counter widget. On click, the update method
reads the current count from the Store, passes it to the updateFn, takes the returned value and writes
it back to the Store.
updateFn receives the initial value as a first argument followed by any additional arguments that
are provided by the developer. The function must return either the updated value, or the initial value if no
changes are made.
This helps the Store to determine the state changes more efficiently. It's important to note that updateFn
should be a pure function, without any side effects, e.g. direct object or array mutations.
import { append, filter, findTreeNode,.. } from 'cx/data';CxJS provides a set of commonly used update functions, which are listed below.
| Methods |
|---|
append(array, ...items)
|
filter(array, callback)
|
findTreeNode(array, criteria, childrenProperty)
|
merge(item, data)
|
removeTreeNodes(array, criteria, childrenProperty)
|
updateArray(array, updateCallback, itemFilter, removeFilter)
|
updateTree(array, updateCallback, itemFilter, childrenProperty, removeFilter)
|
updateArray
updateArray takes three arguments: array that needs to be updated, updateCallback and itemFilter functions.
Each item is passed through the itemFilter function, if one is provided. If itemFilter
returns true, the item is then passed to the updateCallback function, which returns the
updated value. Finally, updateArray function either creates the updated copy, or returns
the original array if no changes were made.
<div class="widgets"> <div layout={LabelsLeftLayout}> <strong>Todo List</strong> <Repeater records-bind="$page.todoList"> <Checkbox value-bind="$record.done" text-bind="$record.text" /> <br /> </Repeater> <Button onClick={(e, { store }) => { store.update( "$page.todoList", updateArray, item => ({ ...item, done: true }), item => !item.done ); }} > Mark all as done </Button> </div> </div>
updateTree
updateTree takes five arguments: array (tree structure) that needs to be updated,
updateCallback, itemFilter, childrenField (name of the property under which child
items are stored), and removeFilter that returns true for each removed tree item,
otherwise false.
| Name |
|---|
| Name |
|---|
onRowDrop function is called when the file/folder being dragged gets dropped
into another folder. Then, we can easily extract source and target nodes to
create an updated tree. First, we create a copy of the source node, and then
we remove the original source node from the tree. In updateCallback, we create
a copy and update the children array of the target node, while in removeFilter we
specify original source node's id to remove the node from the tree.
onRowDrop(e) { const sourceNode = e.source.record.data; const targetNode = e.target.record.data; const data = this.store.get("data"); const newTree = updateTree( data, (n) => { const nodeChildren = n.$children ?? []; nodeChildren.push({ ...sourceNode, // Creates a copy id: this.id++ // with updated id }); return { ...n, $children: nodeChildren }; }, (n) => n.name == targetNode.name, "$children", (n) => n.id == sourceNode.id // Removes the original source node ); this.store.set("data", newTree); }