GrabDuck

Matreshka.js 2: От простого к простому

:

image

Документация на русском
Github репозиторий

Всем привет! В этой статье я расскажу, как пользоваться Matreshka.js на трех несложных примерах. Мы рассмотрим базовые возможности фреймворка, познакомимся с тем, как работать с данными и разберем коллекции.

Пост является краткой компиляцией переводов из этого репозитория.



1. Hello World!

Для начала стоит ознакомиться с приложением Hello World, которое вынесено из поста на сайт.

// ...
// связываем свойство x и текстовое поле
this.bindNode('x', '.my-input');

// связываем свойство x и блок с классом my-output
this.bindNode('x', '.my-output', htmlBinder());

// слушаем изменения свойства x
this.on('change:x', () =>
            console.log(`x изменен на "${this.x}"`));
// ...

При обновлении свойства x произойдет три вещи:


  • Обновится значение поля ввода
  • Обновится HTML содержимое блока
  • В консоль будет выведена информация о том, что x поменяли

При вводе текста в текстовое поле:


  • Обновится свойство x
  • Обновится HTML содержимое блока
  • В консоль будет выведена информация о том, что x поменяли

Как видите, не нужно вручную отлавливать событие ввода в поле текста; при изменении значения свойства не нужно вручную устанавливать значения HTML узлам; не нужно объявлять дескриптор самостоятельно.

Демо


2. Форма авторизации. Знакомимся с Matreshka.Object

Следующий пример — реализация формы авторизации на сайте. У нас есть два текстовых поля: логин и пароль. Есть два чекбокса: «показать пароль» и «запомнить меня». Есть одна кнопка: «войти», которая активна только тогда, когда форма валидна. Скажем, что валидация формы пройдена если длина логина не меньше 4 символов, а длина пароля не меньше 5 символов.


image

Немного теории: Matreshka.Object играет роль класса, создающего объекты типа ключ-значение. В каждом экземпляре класса можно отделить свойства, отвечающие за данные (то что будет передано не сервер, например) от других свойств (то, что серверу не нужно, но определяет поведение приложения). В данном случае, логин, пароль и “запомнить меня” являются данными, которые мы отправляем на сервер, а свойство, говорящее о том, валидна ли форма — нет.

Подробная и актуальная информация об этом классе находится в документации.

Итак, создадим класс, который наследуется от Matreshka.Object.

class LoginForm extends Matreshka.Object {
    constructor () {
        // ...
    }
}

Так как “приложение” очень небольшое, всю логику можно разместить в конструкторе класса.

Перво-наперво, объявим данные по умолчанию.

super();

this.setData({
    userName: '',
    password: '',
    rememberMe: true
})

Метод setData не только устанавливает значения, но и объявляет свойства, отвечающие за данные. Т. е. userName, password и rememberMe должны быть переданы на сервер (в этом примере просто выведем JSON на экран).

Так как при разработке приложений, рекомендуется ипользовать всю мощь ECMAScript 2015, и, так как конструктор Matreshka.Object вызвает setData с переданным в него аргументом, мы инициализируем дефольные данные с помощью единственного вызова super (который сделал бы то же семое, что и Matreshka.Object.call(this, { ... })) для того, чтоб код стал симпатичнее. Код ниже делает то же самое, что и предыдущий:

super({
    userName: '',
    password: '',
    rememberMe: true
})

Объявляем свойство, isValid, которое зависит от свойств userName и password. При изменении любого из этих свойств (из кода, консоли или с помощью привязанного элемента), свойство isValid тоже изменится.

.calc('isValid', ['userName', 'password'], (userName, password) => {
    return userName.length >= 4 && password.length >= 5;
})

isValid будет равно true, если длина имени пользователя не меньше четырех, а длина пароля — не меньше пяти. Метод calc — это еще одна крутая возможность фреймворка. Одни свойства могут зависеть от других, другие от третьих, в третьи вообще от свойств другого объекта. При этом, вы защищены от цикличных ссылок. Метод прекращает работу если встречается с опасными зависимостями.

Теперь, связываем свойства объекта и элементы на странице. Первым делом объявляем песочницу. Песочница нужна для того, чтоб ограничить влияние экземпляра одним элементом на странице и избежать конфликтов (например, если на странице есть два элемента с одним и тем же классом). Затем привязываем остальные элементы.

// альтернативный синтаксис метода позволяет передать объект ключ-элемент в качестве первого аргумента,
// что несколько уменьшает количество кода
.bindNode({
    sandbox: '.login-form',
    userName: ':sandbox .user-name',
    password: ':sandbox .password',
    showPassword: ':sandbox .show-password',
    rememberMe: ':sandbox .remember-me'
})

Как видите, для остальных элементов используется нестандартный селектор :sandbox, ссылающийся на песочницу (на элемент с классом .login-form). В данном случае это не обязательно, так как страница содержит только нашу форму. В ином случае, если на странице есть несколько форм или других виджетов, настоятельно рекомендуется ограничивать выбираемые элементы песочницей.

Затем, связываем кнопку, отвечающую за отправку формы, и свойство isValid. Когда isValid равно true, добавляем элементу класс "disabled", когда false — убираем. Это пример одностороннего связывания, а точнее, значение свойства объекта влияет на состояние HTML элемента, но не наоборот.

.bindNode("isValid", ":sandbox .submit", {
    setValue(v) {
        this.classList.toggle("disabled", !v);
    }
})

Вместо такой записи можно использовать более краткую:

.bindNode('isValid', ':sandbox .submit',
    Matreshka.binders.className('disabled', false))

См. документацию к объекту binders.

Связываем поле с паролем и свойство showPassword (“показать пароль”) и меняем тип инпута в зависимости от значения свойства (:bound(KEY) — это последний нестендартный селектор).

.bindNode("showPassword", ":bound(password)", {
    getValue: null,
    setValue(v) {
        this.type = v ? "text" : "password";
    }
})

getValue: null означает то, что мы переопределяем стандартное поведение фреймворка при привязке элементов формы.

Добавляем событие отправки формы.

.on("submit::sandbox", evt => {
    this.login();
    evt.preventDefault();
})

submit — обычное, произвольное DOM или jQuery событие, sandbox — наша форма (.login-form). Такое событие и ключ должны быть разделены двоеточием. Это синтаксический сахар DOM событий, т. е. событие можно навешать любым другим способом, в том числе, и используя addEventListener:

this.nodes.sandbox.addEventListener("submit", evt => { ... });

В обработчике вызываем метод login, который объявим ниже, и предотвращаем перезагрузку страницы, отменяя стандартное поведение браузера используя preventDefault.

Последний штрих — метод login. Для примера, метод выводит на экран результирующий объект, если форма валидна. В реальном приложении, содержимым функции, очевидно, должен быть ajax запрос на сервер.

login() {
    if(this.isValid) {
        alert(JSON.stringify(this));
    }

    return this;
}

В самом конце создаём экземпляр класса.

const loginForm = new LoginForm();

Можете снова открыть консоль и изменить свойства вручную:

loginForm.userName = "Chuck Norris";
loginForm.password = "roundhouse_kick";
loginForm.showPassword = true;

Демо


3. Список пользователей. Разбираемся с коллекциями (Matreshka.Array)

С данными вида ключ-значения разобрались. Рассмотрим коллекции. Скажем, задача звучит так: вывести список неких людей в виде таблицы.


Чтобы не усложнять пример, поместим заранее подготовленные данные в переменную data.

const data = [{
        name: 'Ida T. Heath',
        email: 'ida@dayrep.com',
        phone: '507-879-9766'
    }, {
        name: 'Robert C. Burkhardt',
        email: 'rburkhardt@teleworm.us',
        phone: '321-252-5698'
    }, {
        name: 'Gerald S. Reaves',
        email: 'gsr@rhyta.com',
        phone: '765-431-5347'
}];

(имена и телефоны получены с помощью генератора случайных данных)

Для начала, как обычно, создаём HTML разметку.

<table class="users">
    <thead>
        <th>Name</th>
        <th>Email</th>
        <th>Phone</th>
    </thead>
    <tbody><!-- здесь будет список пользователей --></tbody>
</table>

Объявим коллекцию Users, которая наследуется от Matreshka.Array.

class Users extends Matreshka.Array {

}

Укажем свойство itemRenderer, которое отвечает за то как элементы массива будут рендериться на странице.

get itemRenderer() {
    return '#user_template';
}

В данном случае, указан селектор в качестве значения, ссылающийся на шаблон в HTML коде.

<script type="text/html" id="user_template">
    <tr>
        <td class="name"></td>
        <td class="email"></td>
        <td class="phone"></td>
    </tr>
</script>

Свойство itemRenderer может принимать и другие значения, в том числе, функцию или HTML строку.

И укажем значение свойства Model, определяя класс элементов, содержащихся в коллекции.

get Model() {
    return User;
}

Класс User мы создадим немного позже, для начала определим конструктор новосозданного класса коллекции.

constructor(data) {
    super();
    this
        .bindNode("sandbox", ".users")
        .bindNode("container", ":sandbox tbody")
        .recreate(data);
}

При создании экземпляра класса


  • Связываются свойство sandbox и элемент '.users' создавая песочницу (границы влияния класса на HTML).
  • Связываются свойство container и элемент ':sandbox tbody', определяя HTML узел, куда будут вставляться отрисованные элементы массива.
  • Добавляем переданные данные в массив методом recreate.

Отлично. Но мы собираемся использовать как можно больше возможностей ECMAScript 2015, улучшающих код. Поэтому, мы будем использовать вызов super для заполнения массива.

constructor(data) {
    super(...data)
        .bindNode('sandbox', '.users')
        .bindNode('container', ':sandbox tbody')
        .rerender();
}

  • Добавляются айтемы в коллекуию с помощью вызова super (который делает то же самое, что и вызов Matreshka.Array.apply(this, data)).
  • Связываются свойство sandbox и элемент '.users'
  • Связываются свойство container и элемент ':sandbox tbody'
  • Вызывается метод rerender котрый рендерит коллекцию (мы должны его вызвать, так как привязали container после того, как добавили данные).

Теперь объявляем “Модель”: класс User, который наследуется от уже знакомого нам Matreshka.Object.

class User extends Matreshka.Object {
    constructor(data) { ... }
}

Устанавливаем данные, переданные в конструктор методом setData, или, как обычно, вызываем super.

super(data);

Затем, дожидаемся события render, которое срабатывает тогда, когда соответствующий HTML элемент был создан, но еще не вставлен на страницу. В обработчике привязываем соответствующие свойства соответствующим HTML элементам. Когда значение свойства изменится, innerHTML заданного элемента тоже поменяется.

this.on( 'render', function() {
    this
        .bindNode({
            name: ':sandbox .name',
            email: ':sandbox .email',
            phone: ':sandbox .phone'
        }, Matreshka.binders.html())
    ;
})

Есть возможность заменить прослушиваение события "render" созданием специального метода onRender (см. доку), но, чтоб не усложнять этот пример, оставим так.

В конце создадим экземпляр класса Users, передав данные в качестве аргумента

const users = new Users(data);

Всё. При обновлении страницы вы увидите таблицу со списком юзеров.

Демо

Теперь откройте консоль и напишите:

users.push({
    name: 'Gene L. Bailey',
    email: 'bailey@rhyta.com',
    phone: '562-657-0985'
});

Как видите, в таблицу добавился новый элемент. А теперь вызовите

users.reverse();

Или любой другой метод массива (sort, splice, pop...). Matreshka.Array, кроме собственных методов, содержит все без исключения методы стандартного JavaScript массива. Затем,

users[0].name = 'Vasily Pupkin';
users[1].email = 'mail@example.com'

Как как видно из примеров, не нужно вручную следить за изменениями в коллекции, фреймворк самостоятельно ловит изменения данных и меняет DOM.

Не забывайте, что Matreshka.Array поддерживает собственный набор событий. Вы можете отлавливать любое изменение в коллекции: добавление, удаление, пересортировку элементов методом on.

users.on("addone", evt => {
    console.log(evt.addedItem.name);
});
users.push({
    name: "Clint A. Barnes"
});

(выведет в консоль имя добавленного пользователя)



Как говорится в документации к itemRenderer можно определять рендерер на уровне класса Model. Это ответ на частозадаваемый вопрос: почему я должен определять рендерер на уровне коллекции. Вместо определения itemRenderer для класса Users можно определить свойство renderer для класса User.

class User extends Matreshka.Object {
    get renderer() {
        return '#user_template';
    }
    constructor(data) { ... }
}

На самом деле, есть несколько способов имплементации такого приложения. Не нужно определять Model если объекты, входящие в массив не имеют никакой серьезной логики. Приложение, описанное выше можно реализовать, используя один единственный класс.

class Users extends Matreshka.Array {
    get itemRenderer() {
        return '#user_template';
    }
    constructor(data) {
        super(...data)
            .bindNode('sandbox', '.users')
            .bindNode('container', ':sandbox tbody')
            .rerender();
    }
    onItemRender(item) {
        // item - это обычный объект, а не экзепляр Matreshka, поэтомы мы воспользуемся
        // статичной версией метода bindNode
        Matreshka.bindNode(item, {
            name: ':sandbox .name',
            email: ':sandbox .email',
            phone: ':sandbox .phone'
        }, Matreshka.binders.html());
    }
}

Так же, можно использовать парсер байндингов, который по умолчанию используется классом Matreshka.Array, определив рендерер прямо в классе и не добавляя ничего в HTML.

class Users extends Matreshka.Array {
    get itemRenderer() {
        return `
        <tr>
          <td class="name">{{name}}</td>
          <td class="email">{{email}}</td>
          <td class="phone">{{phone}}</td>
        </tr>`;
    }
    constructor(data) {
        super(...data)
            .bindNode('sandbox', '.users')
            .bindNode('container', ':sandbox tbody')
            .rerender();
    }
}

Спасибо всем тем, кто сообщал об опечатках на сайте. Всем добра!