States | Lucid.js

Lucid.js / States

Introduction 🔗

States allow you snapshot a component at a given point in time, and return to that state later if needed. Any data associated with the component at that time will be repopulated into its data object when the state is later reinstated.

States come in two forms:

  • Indexed states - unnamed states that are created and traversed through just like undo/redo deltas common in software applications.
  • Persistent states - named states that can be returned to by name; they exist outside the undo/redo delta system of indexed states, so cannot be lost.

A component's starting state is 0.

States are not recursive. Resetting a component's state to an earlier time changes its own data object only, not the data of any child components.

Creating states 🔗

States are created via the newState() method of the components API.

The method takes an single, optional param which is used only when creating a persistent (rather than indexed state) - the state name.

this.newState('foobar');

Reinstating states 🔗

States can be reinstated later by calling the changeState() method of the components API.

The method accepts a single param, denoting the state to reinstate - an index (indexed state) or a string, denoting a state name (persistent state).

this.changeState('foobar');

Example 🔗

Let's build an interface which offers a series of randomly-coloured squares. When one is clicked, the user is prompted to change the colour of the block to one of their choice. We'll provide buttons so the user can toggle between the starting colours and their changed colours.

<style> div div { width: 50px; height: 50px; margin-right: 10px; display: inline-block; } </style> <div> <div data-i='{{$index}}' style='background: #{{$value}}'></div> <p> <button id='orig'>Original</button> <button id='user'>User</button> </p> </div>

Now for our JS. First we'll write a simple function to generate random, three-character hexadecimal colours, and then use it to create an array of five random colours. We'll then take a snapshot of our data in a persistent state, "orig", that we can return to later.

<script> function randColour() { let str = '0123456789abcdef', ret = ''; for (let i=0; i<3; i++) ret += str[Math.floor(Math.random() * str.length)]; return ret; } //set start colours and log as "orig" state this.data.blocks = Array.from({length: 5}, () => randColour()); this.newState('orig'); </script>

Next we'll use a repeater to output the inner div element once per colour (this was hinted at in the HTML template, which references {{$index}} and {{$value}}).

this.repeaters = { 'div div': () => this.data.blocks };

When the user clicks a block, we'll prompt them for a new colour and save it to the data model. We'll then update the "user" state with the change, and reprocess our repeaters to update the DOM.

this.DOM.addEventListener('click', evt => { //check is click to a block if (!evt.target.matches('[data-i]')) return; //get and check new colour let newCol = prompt('Enter a hexdex colour code:', ''); if (!/^#?[0-9a-f]{3,}$/i.test(newCol)) return alert('Bad input!'); //update data model with new colour and update "user" state this.data.blocks[evt.target.dataset.i] = newCol.replace(/^#/, ''); this.newState('user'); //reprocess repeaters with updated colours this.rr(); });

Finally, we just need to power our buttons which toggle between reverting to the original state and returning to the user state. Each time, we'll reprocess our repeaters so the blocks are re-evaluated and their colours updated accordingly.

this.DOM.addEventListener('click', evt => { if (!evt.target.matches('button')) return; this.changeState(evt.target.id); this.rr(); });

Et voila!

Re-rendering 🔗

A component's current active state persists across re-renderings - in other words, if the component re-renders itself, its active state does not get reset to 0. However, it will be reset to 0 if a parent or ancestor component is itself re-rendered; in this situation, the child component is effectively loaded again for the first time.

Be careful when re-rendering components that set state. When re-rendered, a component's JavaScript runs entirely again, so it's often necessary to check for the current state before running certain code.

In our example above, instead of reprocessing the repeaters, we could have just re-rendered the component to get the same effect - except, we'd need to conditionalise the first part of our code, which sets the "orig" state.

States persist across re-renders, and the default state is 0, so we can check for this.

//set start colours and log as "orig" state if (this.activeState !== 0) { this.data.blocks = Array.from({length: 5}, () => randColour()); this.newState('orig'); }

Alternatively, we could check if the component has been re-render, and only run the above code on the initial render.

if (this.rerendered) { this.data.blocks = Array.from({length: 5}, () => randColour()); this.newState('orig'); }

Did I help you? Feel free to be amazing and buy me a coffee on Ko-fi!