Model
Class Model
provides a simple way to create reactive two-way binding data model.
Using this class, you can create truly reactive sites on the Metro UI.
The model allows you to bind HTML elements to JavaScript variables in such a way that any changes to the data model are immediately reflected in the HTML.
Installation
Section titled “Installation”Metro UI already includes the Model
library.
You can use it without any additional installation, but if you want to use it in your project, you can install it with package manager:
npm install @olton/model
pnpm install @olton/model
yarn add @olton/model
or use CDN:
<script type="module"> import Model from "https://esm.run/@olton/model";</script>
<div id="root"> <div>{{ name }}</div> <div>{{ age }}</div></div>
import { Model } from "@olton/model";
const model = new Model({ name: "John", age: 30});
model.init("#root");
model.data.name = "Mike";model.data.age = 31;
Binding model to HTML
Section titled “Binding model to HTML”You can bind model to HTML elements.
<div>{{ age }}</div><input type="text" data-model="name"><button :disabled="isDisabled">Click me</button><div data-bind='{"title": "User Counter"}'>Counter: {{ counter }}</div>
const model = new Model({ name: "John", age: 30, isDisabled: false});
Loops (for, in)
Section titled “Loops (for, in)”You can use model in loops.
data-for
Section titled “data-for”Directive data-for
is used to create a loop by array.
<ul> <li data-for="item in items">{{ item }}</li></ul>
const model = new Model({ items: ["Item 1", "Item 2", "Item 3"]});
data-in
Section titled “data-in”Directive data-in
is used to create a loop by object.
<ul> <li data-in="key in obj">{{ key }} : {{ obj[key] }}</li></ul>
const model = new Model({ obj: { name: "Serhii", age: 52, address: "Kyiv, Ukraine" }});
Conditionals (if-else)
Section titled “Conditionals (if-else)”You can use conditionals in a model with directives data-if
, data-else
, and data-else-if
.
data-if
Section titled “data-if”Directive data-if
is used to create a conditional block.
<div data-if="isVisible">Hello</div>
data-else
Section titled “data-else”Directive data-else
is used to create an else block after data-if
or data-else-if
.
<div data-if="isVisible">Visible</div><div data-else>Invisible</div>
data-else-if
Section titled “data-else-if”Directive data-else-if
is used to create an else if block after data-if
.
<div data-if="counter < 0">Negative</div><div data-else-if="counter > 0">Positive</div><div data-else>Zero</div>
Nested objects
Section titled “Nested objects”You can use nested objects in model.
<div>{{ user.name }}</div>
const model = new Model({ user: { name: "John", age: 30 }});
Arrays
Section titled “Arrays”You can use arrays in the model.
<ul> <li data-for="item in items">{{ item }}</li></ul>
const model = new Model({ items: ["Item 1", "Item 2", "Item 3"]});
Mutation methods
Section titled “Mutation methods”Model is able to detect when a reactive array’s mutation methods are called and trigger necessary updates. These mutation methods are:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
model.data.items.push("Item 4");
applyArrayMethod()
Section titled “applyArrayMethod()”const model = new Model({ items: ["Item 1", "Item 2", "Item 3"]});
model.store.applyArrayMethod('items', 'push', 'Item 4');
applyArrayChanges()
Section titled “applyArrayChanges()”model.store.applyArrayChanges('items', (items) => items.push("Item 4"));
diffArrays()
Section titled “diffArrays()”You can use diffArrays(newArray, oldArray)
method to compare two arrays.
model.diffArrays([1, 2, 3], [1, 2, 4]); // {added: [3], removed: [4], changed: [2]}
Computed properties
Section titled “Computed properties”You can use computed properties in the model. The computed property must be a function, and calculated based on other properties.
<div>{{ fullName }}</div>
const model = new Model({ firstName: "John", lastName: "Doe", fullName() { return this.firstName + " " + this.lastName; }});
Asynchronous computed properties
Section titled “Asynchronous computed properties”You can use asynchronous computed properties in the model.
<ul> <li data-for="post in posts">{{ post.title }}</li></ul>
const model = new Model({ posts: [], async getPosts() { this.posts = await fetch("https://jsonplaceholder.typicode.com/posts").then(res => res.json()); }});
Binding Attributes
Section titled “Binding Attributes”data-bind
Section titled “data-bind”You can bind attributes to elements with directive data-bind
.
This directive allows you to set multiple element attributes at once, based on model properties.
<div data-bind='{"class": "counter >= 0 ? \"positive\" : \"negative\"", "title": "new_title"}'>Counter: {{ counter }}</div>
const model = new Model({ counter: 10, new_title: "New title"});
:attribute
Section titled “:attribute”You can use special syntax :attribute
to bind specified attribute to elements.
<div :class="counter >= 0 ? 'positive' : 'negative'">Counter: {{ counter }}</div><img :src="path_to_image" alt="Image"/>
const model = new Model({ counter: 10, path_to_image: "https://example.com/image.jpg"});
Binding Events
Section titled “Binding Events”You can bind events to elements with directive @event
.
<button @click="this.counter++">Click me</button><button @click="onClick()">Click me</button>
Access to Model
Section titled “Access to Model”<button @click="onClick($model)">Get Model</button>
Access to Event
Section titled “Access to Event”<button @click="onClick($event)">Get Event</button>
Access to Model data
Section titled “Access to Model data”<button @click="onClick($data)">Get Data</button>
Pass custom arguments
Section titled “Pass custom arguments”<button @click="onClick('text', 123)">Custom Arguments</button>
Middleware
Section titled “Middleware”You can use middleware in a model.
const model = new Model({ name: "John", age: 30, counter: 10,});
model.use(async (context, next) => { if (context.property === 'counter' && context.newValue < 0) { context.preventDefault = true; return; } await next();});
Model Events
Section titled “Model Events”You can use events in a model.
const model = new Model({ name: "John", age: 30,});
model.on("change", (property, newValue, oldValue) => { console.log(`Property ${property} changed from ${oldValue} to ${newValue}`);});
You can use next events in model:
General
Section titled “General”init
- when model initializeddestroy
- when model destroyed
Changes
Section titled “Changes”change
- when property changedarrayChange
- when array property changedbatchComplete
- when batch completecompute
- when computed property changed
saveState
- when model state savedsaveStateError
- when model state not savedrestoreState
- when model state restoredrestoreStateError
- when model state restored with errorcreateSnapshot
- when snapshot createdrestoreSnapshot
- when snapshot restored
Plugins
Section titled “Plugins”pluginRegistered
- when plugin registeredpluginUnregistered
- when plugin removed
Watchers
Section titled “Watchers”You can use watchers in model.
const model = new Model({ name: "John", age: 30,});
model.watch("name", (newValue, oldValue) => { console.log(`Name changed from ${oldValue} to ${newValue}`);});
Validation
Section titled “Validation”You can add validators to the model to validate user input.
const model = new Model({ name: "John", age: 30,});
model.addValidator('name', value => value.length >= 3);
Formatting
Section titled “Formatting”You can add formatters to the model to format user input.
const model = new Model({ name: "John", age: 30,});
model.addFormatter('name', value => value.toUpperCase());
You can save and restore model state from localstorage.
Load state
Section titled “Load state”var model
document.addEventListener('DOMContentLoaded', () => { model = new Metro.Model(null, { id: 'model-state' });
model.loadState() model.init("#root")})
Save state
Section titled “Save state”To save model state to localStorage, you can use method save()
.
window.addEventListener('beforeunload', () => { model.save();})
Restore state
Section titled “Restore state”To restore model state from localStorage, you can use method restore()
.
model.restore();
Auto save state
Section titled “Auto save state”model.autoSave(5000); // save every 5 seconds
model.autoSave(false); // disable auto save
Snapshots
Section titled “Snapshots”You can create model snapshots to save model state.
Create snapshot
Section titled “Create snapshot”const snapshot = model.snapshot();
Restore snapshot
Section titled “Restore snapshot”model.snapshot(snapshot);
Batch updates
Section titled “Batch updates”You can use batch updates in model. Batch updates are useful when you want to update multiple properties at once without triggering the change event for each property.
const model = new Model({ name: "John", age: 30,});
model.batch(() => { model.data.name = "Mike"; model.data.age = 31;});
DevTools
Section titled “DevTools”You can activate DevTools for a model. DevTools is useful when you want to debug your model.
const model = new Model({});
model.runDevTools({ enabled: true, timeTravel: true, maxSnapshots: 50});
Validating Model
Section titled “Validating Model”To validate model, you can use methods:
validate()
- validate model for cycling dependencies and missing propertiesvalidPath(path)
- check if path is valid
const model = new Model({ name: "John", age: 30,});
model.validate();model.validatePath('name'); // truemodel.validatePath('lastname'); // false
Plugins
Section titled “Plugins”You can use plugins in model. Plugins are useful when you want to extend model functionality.
Register plugin
Section titled “Register plugin”To register plugin, you must use static method Model.registerPlugin(name, class)
.
class MyPlugin { constructor(model, options) { this.model = model; }
init() { console.log('Plugin initialized'); }}
Model.registerPlugin('my-plugin', MyPlugin);
Use plugin
Section titled “Use plugin”After registering plugin, you can use it in model.
const model = new Model({ name: "John", age: 30,})
const pluginOptions = { ...}model.usePlugin("my-plugin", pluginOptions);
Unregister plugin
Section titled “Unregister plugin”To unregister plugin, you can use method Model.removePlugin(name)
.
Model.removePlugin('my-plugin');