Documentation

Report Edit

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)

batch method is used to perform multiple Store operations silently and afterwards send a notification only once. This causes the application to be re-rendered only once even if multiple changes occurred.

The Store instance is passed to the callback function.

Store.copy(from, to)

Copies the value stored under the from path and saves it under the to path.

Store.delete(path)

Removes data from the Store stored under the given path.

Store.dispatch(action)

dispatch method is used for dispatching actions. This method is available only if the application Store is based on a Redux store (See cx-redux package).

Store.get(path)

The get method is used to read the data from the store under the given path. The method can take multiple arguments or an array of strings representing paths.

Store.getData()

Returns a reference to the object representing the application state.

Store.init(path, value)

Saves value in the Store under the given path. If the path is already taken, it returns false without overwriting the existing value. Otherwise, saves the value and returns true.

Store.load(data)

Loads data object into the Store. This method is used to restore the application state when doing Hot Module Replacement.

Store.move(from, to)

Copies the value stored under the from path and saves it under the to path. Afterwards, the from entry is deleted from the Store.

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 path argument is provided to indicate where the change occurred.

Store.set(path, value)

Saves value in the Store under the given path. Any existing data stored under that path gets overwritten.

Store.silently(callback)

silently method is used to perform data changes without notifications. Changes made this way will not reflect in the UI until the application is rendered again.

The Store instance is passed to the callback function.

Store.toggle(path)

Toggles the boolean value stored under the given path.

Store.update(path, updateFn, ...args)

Applies the updateFn to the data stored under the given path. args can contain additional parameters that will be passed to the updateFn.

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.

ControllerIndex
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}!`);
    }
}
Copied!Cx Fiddle

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.

Code
<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>
Copied!Cx Fiddle

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.

Code
<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>
Copied!Cx Fiddle

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.

Code
<div layout={LabelsTopLayout}>
    <TextField value-bind="$page.name" label="Name" />
    <Button onClick={(e, {store}) =>
        store.delete('$page.name')
    }>
        Clear
    </Button>
</div>
Copied!Cx Fiddle

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.

Code
<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>
Copied!Cx Fiddle

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.

Code
<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>
Copied!Cx Fiddle

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:

  • path under 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.

Code
<div layout={LabelsTopLayout}>
    <NumberField label="Count" value-bind="$page.count" style="width: 50px"/>
    <Button onClick={(e, {store}) => {
        store.update('$page.count', count => count + 1);
    }}>+1</Button>
</div>
Copied!Cx Fiddle
import { append, filter, findTreeNode,.. } from 'cx/data';

CxJS provides a set of commonly used update functions, which are listed below.

Methods
append(array, ...items)

append function takes a number of arguments. First argument is the array to which all subsequent arguments will be appended.

filter(array, callback)

filter function works just like the Array.prototype.filter function the difference being that it returns the original array if none of the items were filtered out.

findTreeNode(array, criteria, childrenProperty)

findTreeNode scans the tree using the depth-first search algorithm until it finds a node that satisfies the given search criteria. criteria is a predicate function that takes a node object as input and returns true or false, based on the search criteria. childrenProperty specifies where child nodes are stored. Default value is $children. findTreeNode returns the first node object that satisfies the search criteria.

merge(item, data)

merge function takes two arguments, item and data, and merges them into a single object. The function returns the original object if no changes were made.

removeTreeNodes(array, criteria, childrenProperty)

removeTreeNodes removes all tree nodes that satisfy the given criteria. childrenProperty specifies where child nodes are stored. Default value is $children.

updateArray(array, updateCallback, itemFilter, removeFilter)

updateArray function takes four arguments: array that needs to be updated, updateCallback, itemFilter and removeFilter functions. itemFilter is optional and it can be used to select elements that need to be updated. removeFilter is also optional and it can be used to filter out elements from the list. If no changes are made, the function will return the original array.

updateTree(array, updateCallback, itemFilter, childrenProperty, removeFilter)

updateTree is similar to updateArray, the difference being that it can be applied to tree structures on multiple levels. It basically applies the updateArray function to each item's children. childrenProperty specifies where child nodes are stored. Default value is $children. If no changes were made, the function returns the original array.

updateArray

updateArray takes three arguments: array that needs to be updated, updateCallback and itemFilter functions.

Todo List



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.

Code
<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>
Copied!Cx Fiddle

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
Folder 1
Folder 2
Folder 3
file_1.txt
file_2.txt
file_3.txt
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.

Controller
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);
  }
Copied!Cx Fiddle