Skip to content

Model

Metro UI includes class Model to create reactive data model with two-way binding!

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.

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:

Terminal window
npm install @olton/model

or use CDN:

<script type="module">
import Model from "https://esm.run/@olton/model";
</script>
index.html
<div id="root">
<div>{{ name }}</div>
<div>{{ age }}</div>
</div>
index.js
import { Model } from "@olton/model";
const model = new Model({
name: "John",
age: 30
});
model.init("#root");
model.data.name = "Mike";
model.data.age = 31;

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
});

You can use model in loops.

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"]
});

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"
}
});

You can use conditionals in a model with directives data-if, data-else, and data-else-if.

Directive data-if is used to create a conditional block.

<div data-if="isVisible">Hello</div>

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>

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>

You can use nested objects in model.

<div>{{ user.name }}</div>
const model = new Model({
user: {
name: "John",
age: 30
}
});

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"]
});

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");
const model = new Model({
items: ["Item 1", "Item 2", "Item 3"]
});
model.store.applyArrayMethod('items', 'push', 'Item 4');
model.store.applyArrayChanges('items', (items) => items.push("Item 4"));

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]}

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;
}
});

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());
}
});

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"
});

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"
});

You can bind events to elements with directive @event.

<button @click="this.counter++">Click me</button>
<button @click="onClick()">Click me</button>
<button @click="onClick($model)">Get Model</button>
<button @click="onClick($event)">Get Event</button>
<button @click="onClick($data)">Get Data</button>
<button @click="onClick('text', 123)">Custom Arguments</button>

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();
});

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:

  • init - when model initialized
  • destroy - when model destroyed
  • change - when property changed
  • arrayChange - when array property changed
  • batchComplete - when batch complete
  • compute - when computed property changed
  • saveState - when model state saved
  • saveStateError - when model state not saved
  • restoreState - when model state restored
  • restoreStateError - when model state restored with error
  • createSnapshot - when snapshot created
  • restoreSnapshot - when snapshot restored
  • pluginRegistered - when plugin registered
  • pluginUnregistered - when plugin removed

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}`);
});

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);

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.

var model
document.addEventListener('DOMContentLoaded', () => {
model = new Metro.Model(null, {
id: 'model-state'
});
model.loadState()
model.init("#root")
})

To save model state to localStorage, you can use method save().

window.addEventListener('beforeunload', () => {
model.save();
})

To restore model state from localStorage, you can use method restore().

model.restore();
model.autoSave(5000); // save every 5 seconds
model.autoSave(false); // disable auto save

You can create model snapshots to save model state.

const snapshot = model.snapshot();
model.snapshot(snapshot);

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;
});

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
});

To validate model, you can use methods:

  • validate() - validate model for cycling dependencies and missing properties
  • validPath(path) - check if path is valid
const model = new Model({
name: "John",
age: 30,
});
model.validate();
model.validatePath('name'); // true
model.validatePath('lastname'); // false

You can use plugins in model. Plugins are useful when you want to extend model functionality.

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);

After registering plugin, you can use it in model.

const model = new Model({
name: "John",
age: 30,
})
const pluginOptions = {
...
}
model.usePlugin("my-plugin", pluginOptions);

To unregister plugin, you can use method Model.removePlugin(name).

Model.removePlugin('my-plugin');