A tiny event-based state manager Storeon for Velo by Wix.
import { createStoreon } from 'storeon-velo';
const app = (store) => {
store.on('@init', () => ({ count: 0 }));
store.on('increment', ({ count }) => ({ count: count + 1 }));
};
const { getState, setState, dispatch, connect, readyStore } = createStoreon([app]);
// Subscribe for state property 'count'.
// The callback function will be run when the store is redy `readyStore()`
// and each time when property 'count' would change.
connect('count', ({ count }) => {
$w('#text1').text = String(count);
});
$w.onReady(() => {
$w('#button1').onClick(() => {
// Emit event
dispatch('increment');
});
// initialize observe of the state changes
return readyStore();
});
You use the Package Manager to manage the npm packages in your Wix site.
npm install storeon-velo
#or
yarn add storeon-velo
Creates a store that holds the complete state tree of your app and returns 5 methods for work with the app state. (modules API)
const { getState, setState, dispatch, connect, readyStore } = createStoreon(modules);
Syntax
function createStoreon(Array<Module | false>): Store
type Store = {
getState: Function
setState: Function
dispatch: Function
connect: Function
readyStore: Function
}
Returns an object that holds the complete state of your app.
const state = getState();
Syntax
function getState(): object
Set partial state. Accepts an object that will assign to the state.
setState({ xyz: 123 });
Syntax
function setState(data: object): void
Emits an event with optional data.
dispatch('event/type', { xyz: 123 });
Syntax
function dispatch(event: string, data?: any): void
Connects to state by property key. It will return the function disconnect from the store.
const disconnect = connect('key', (state) => { });
disconnect();
You can connect for multiple keys, the last argument must be a function.
connect('key1', 'key2', (state) => { });
Syntax
function connect(...args: [key: string, ...key: string[], handler: ConnectHandler]): Disconnect
type ConnectHandler = (state: object) => void | Promise<void>
type Disconnect = () => void
Start to observe the state changes and calls of the connect()
callbacks.
It must be used inside $w.onReady()
when all the page elements have finished loading
$w.onReady(() => {
return readyStore();
});
Syntax
function readyStore(): Promise<any[]>
The store should be created with createStoreon()
function.
It accepts a list of the modules.
Each module is just a function, which will accept a store and bind their event listeners.
import { query } from 'wix-location-frontend';
import { createStoreon } from 'storeon-velo';
// Business logic
const appModule = (store) => {
store.on('@init', () => {
return {
items: [],
};
});
store.on('items/add', ({ items }, item) => {
return {
items: [...items, item],
};
});
};
// Devtools
export const logger = (store) => {
store.on('@dispatch', (state, [event, data]) => {
if (event === '@changed') {
console.info(
`%c @changed:%c ${Object.keys(data).join(', ')}\n`,
'color:#25a55a;font-weight:bold;',
'font-style:oblique;',
state,
);
} else {
console.info(
`%c action:%c ${event}`,
'color:#c161f0;font-weight:bold;',
'color:#f69891;',
typeof data !== 'undefined' ? data : '',
);
}
});
};
const { getState, setState, dispatch, connect, readyStore } = createStoreon([
appModule,
// Enable development logger if a query param in the URL is ?logger=on
query.logger === 'on' && logger,
]);
$w.onReady(() => {
return readyStore();
});
Syntax
function createStoreon(Array<Module | false>): Store
type Module = (store: StoreonStore) => void
type StoreonStore = {
dispatch: Function
on: Function
get: Function
set: Function
}
Emits an event with optional data.
store.dispatch('event/type', { xyz: 'abc' });
Syntax
function dispatch(event: string, data?: any): void
Adds an event listener. store.on()
returns cleanup function.
This function will remove the event listener.
const off = store.on('event/type', (state, data) => { });
off();
Syntax
function on(event: string, listener: EventListener): Unbind
type EventListener = (state: object, data?: any) => Result
type Unbind = () => void
type Result = object | void | Promise<void> | false
Returns an object that holds the complete state of your app. The app state is always an object.
const state = store.get();
Syntax
function get(): object
Set partial state. Accepts an object that will assign to the state. it can be useful for async event listeners.
store.set({ xyz: 123 });
Syntax
function set(data: object): void
There are 5 built-in events:
It will be fired in createStoreon()
. The best moment to set an initial state.
store.on('@init', () => { });
It will be fired in readyStore()
(it must be inside $w.onReady()
when all the page elements have finished loading).
store.on('@ready', (state) => { });
It will be fired on every new action (on dispatch()
calls and @changed
event).
It receives an array with the event name and the event’s data.
it can be useful for debugging.
store.on('@dispatch', (state, [event, data]) => { });
It will be fired when you use setState()
or store.set()
calls.
store.on('@set', (state, changes) => { });
It will be fired when any event changes the state. It receives object with state changes.
store.on('@changed', (state, changes) => { });
You can dispatch any other events. Just do not start event names with @
.
If the event listener returns an object, this object will update the state. You do not need to return the whole state, return an object with changed keys.
// 'products': [] will be added to state on initialization
store.on('@init', () => {
return { products: [] };
});
Event listener accepts the current state as a first argument and optional event object as a second.
So event listeners can be a reducer as well. As in Redux’s reducers, you should change immutable.
Reducer
store.on('products/add', ({ products }, product) => {
return {
products: [...products, product],
};
});
Dispatch
$w('#buttonAdd').onClick(() => {
dispatch('products/add', {
_id: uuid(),
name: $w('#inputName').value,
});
});
Connector
connect('products', ({ products }) => {
// Set new items to repeater
$w('#repeater').data = products;
// Update repeater items
$w('#repeater').forEachItem(($item, itemData) => {
$item('#text').text = itemData.name;
});
});
You can dispatch other events in event listeners. It can be useful for async operations.
Also, you can use store.set()
method for async listeners.
import wixData from 'wix-data';
import { createStoreon } from 'storeon-velo';
const appModule = (store) => {
store.on('@init', () => {
return {
products: [],
error: null,
};
});
store.on('@ready', async () => {
try {
// wait to fetch items from the database
const { items } = await wixData.query('Products').find();
// resolve
store.set({ products: items });
} catch (error) {
// reject
store.set({ error });
}
});
// Listener with the logic of adding new items to list
store.on('products/add', ({ products }, product) => {
return {
products: [product, ...products],
};
});
store.on('products/save', async (_, product) => {
try {
// wait until saving to database
await wixData.save('Products', product);
// resolve
store.dispatch('products/add', product);
} catch (error) {
// reject
store.set({ error });
}
});
}
const { getState, setState, dispatch, connect, readyStore } = createStoreon([
appModule,
]);
Use forEachItem()
for updating a $w.Repeater items into connect()
callback.
connect('products', ({ products }) => {
// Set new items to repeater
$w('#repeater').data = products;
// Update repeater items
$w('#repeater').forEachItem(($item, itemData) => {
$item('#text').text = itemData.name;
});
});
Never nest the event handler for repeated items into any repeater loop.
Use global selector $w()
instead and use context for retrieving repeater item data.
connect('products', ({ products }) => {
$w('#repeater').data = products;
$w('#repeater').forEachItem(($item, itemData) => {
$item('#text').text = itemData.name;
- $item('#repeatedContainer').onClick((event) => {
- dispatch('cart/add', itemData);
- });
});
});
$w.onReady(() => {
+ $w('#repeatedContainer').onClick((event) => {
+ const { products } = getState();
+ const product = products.find((i) => i._id === event.context.itemId);
+
+ dispatch('cart/add', product);
+ });
return readyStore();
});
more: