defi.js

defi.js — это библиотека, включающая в себя дюжину функций, которые добавляют интересные фичи любым JavaScript объектам с помощью геттеров и сеттеров.


Гифка для привлечения внимания (3.5МБ)
Репозиторий


В качестве Hello World создадим небольшой виджет, состоящий из поля имени, фамилии и приветствия (демо).


<input class="first">
<input class="last">
<output class="greeting"></output>

// данные по умолчанию
const obj = {
  first: 'John',
  last: 'Doe'
};

// слушаем изменения в свойствах first и last
// если произошло изменение, сообщим об этом в консоли
defi.on(obj, 'change:first', () => console.log('First name is changed'));
defi.on(obj, 'change:last', () => console.log('Last name is changed'));

// автоматически генерируем приветствие (свойство greeting) каждый раз,
// когда first или last изменились
defi.calc(obj, 'greeting', ['first', 'last'], (first, last) => `Hello, ${first} ${last}`);

// объявляем двусторонний байндинг между свойствами 
// и соответствующими элементами на странице
defi.bindNode(obj, {
  first: '.first',
  last: '.last',
  greeting: '.greeting'
});

В итоге, если first или last изменились, обработчики события сообщают об этом в консоли, свойство greeting автоматически обновляется, а его элемент получает новое значение (по умолчанию, "Hello, John Doe"). Это случается каждый раз, когда свойства меняются, причем не имеет значения, каким образом. Можно установить значение с помощью кода obj.first = 'Jane', либо изменив значение поля, и все остальные изменения произойдут автоматически.


В случае, если есть необходимость синхронизации свойства объекта и innerHTML произвольного HTML элемента (в примере мы использовали тэг output), в функцию bindNode нужно передать так называемый "байндер", который отвечает за то, как синхронизировать состояние элемента и свойства объекта. По умолчанию bindNode не знает, как работать с нодами, не являющимися элементами форм.


const htmlBinder = {
  setValue: (value, binding) => binding.node.innerHTML = value,
};
// изменение свойства obj.greeting обновит innerHTML любого элемента
defi.bindNode(obj, 'greeting', '.greeting', htmlBinder)

Кроме этого, можно воспользоваться html из библиотеки common-binders (это коллекция байндеров общего назначения).


const { html } = require('common-binders');
// изменение свойства obj.greeting обновит innerHTML любого элемента
defi.bindNode(obj, 'greeting', '.greeting', html())

Методы API


Подробную документацию ко всем методам, доступные вариации вызова, флаги и др. можно прочесть на defi.js.org. Стоит упомянуть, что кроме методов ниже, у defi.js есть библиотека для роутинга: defi-router.


  • bindNode — Связывает свойство объекта и DOM узел для двустороннего байндинга.

// обычное использование 
// (для стандартных HTML5 элементов форм, см. defaultBunders)
defi.bindNode(obj, 'myKey', '.my-element');

// кастомный байндинг
defi.bindNode(obj, 'myKey', '.my-element', {
    // событие, которое говорит об изменении элемента
    // (можно использовать функцию для не-DOM событий)
    on: 'click',
    // как извлечь текущее состояние элемента?
    getValue: ({ node }) => someLibraryGetValue(node),
    // как установить состояние элемента при изменении свойства?
    setValue: (v, { node }) => someLibrarySetValue(node, v),
    // как инициализировать элемент (библиотеку или виджет)?
    // это можно сделать любым способом
    // но 'initialize' добавляет немного синтаксического сахара
    initialize: ({ node }) => someLibraryInit(node),
});

obj.myKey = 'some value'; // обновит элемент

  • calc — Создает зависимость одного свойства объекта от других свойств (в том числе, из других объектов).

defi.calc(obj, 'a', ['b', 'c'], (b, c) => b + c);
obj.b = 1;
obj.c = 2;
console.log(obj.a); // 3

  • mediate — Модифицирует значение свойства при его изменении.

defi.mediate(obj, 'x', value => String(value));

obj.x = 1;

console.log(obj.x); // "1"
console.log(typeof obj.x); // "string"

  • on — Добавляет обработчик событий. На сайте с документацией есть небольшая статья, описывающая все возможные события.

defi.on(obj, 'change:x', () => {
    alert(`obj.x now equals ${obj.x}`);
});

obj.x = 1;

  • off — Удаляет обработчик события.

defi.off(obj, 'change:x bind');

  • trigger — Триггерит событие.

defi.on(obj, 'foo bar', (a, b, c) => {
    alert(a + b + c);
});
defi.trigger(obj, 'foo', 1, 2, 3); // вызывает alert(6)

  • unbindNode — Отключает связывание элемента и DOM узла.

defi.bindNode(obj, 'myKey', '.my-element');

defi.unbindNode(obj, 'myKey', '.my-element');

  • bound —  Возвращает DOM элемент, связанный с заданным свойством.

defi.bindNode(obj, 'myKey', '.my-element');
const node = defi.bound(obj, 'myKey'); // вернет document.querySelector('.my-element')

  • chain — Используется для цепочечного вызова функций defi.js.

defi.chain(obj)
    .calc('a', 'b', b => b * 2)
    .set('b', 3)
    .bindNode('c', '.node');

  • defaultBinders — Массив функций, возвращающих соответствующий байндер или undefined. Позволяет bindNode связывать кастомные элементы и свойства без явного указания байндера.

defi.defaultBinders.unshift(element => {
    // если у элемента есть класс "foo"
    if(element.classList.contains('foo')) {
        // значит, нужно использовать этот байндер 
        return {
            on: ...,
            getValue: ...,
            setValue: ...
        };
    }
});

// ...

defi.bindNode(obj, 'myKey', '.foo.bar');

  • lookForBinder — Возвращает байндер, соответствующий элементу, если таковой вернула одна из функций defaultBinders.

const element = document.createElement('input');
element.type = 'text';

console.log(defi.lookForBinder(element));

  • remove — Удаляет свойство объекта и соотвествующие обработчики событий.

defi.remove(obj, 'myKey');

  • set — Устанавливает свойство объекта, как и сеттер, но дает возможность передать какие-нибудь данные в обработчик события change:KEY. Так же можно сделать установку значения свойства "тихим", т. е. не вызывать change:KEY вовсе.

defi.set(obj, 'myKey', 3, { silent: true });



defi.js — это переработанный и упрощенный хард-форк фреймворка Matreshka.js, который включал в себя рендеринг массивов, несколько классов и больше методов. Некоторые методы, которые потенциально могли бы попасть в defi.js были заменены на опции к другим методам, например, вместо once и onDebounce можно использовать метод on, передав опции once: true или debounce: number.


Спасибо, что прочли до конца. Всем хорошего дня.

Комментарии (13)


  1. Serg_de_Adelantado
    21.01.2019 08:40

    Осталось добавить рендер и будет Vue :) Правда, она потихоньку на Proxy мигрирует.


    1. Kr0nic
      21.01.2019 12:32

      и Virtual DOM…


  1. artalar
    21.01.2019 10:23

    Крайне не рекомендую использовать паттерн «заместитель» в проде, апи к любому функционалу должно быть явным и не должно быть скрыто за стандартными конструкциями ЯП.
    youtu.be/dCXvQkvSyQg?t=1907


    1. Sirion
      21.01.2019 13:33
      +1

      Должно кому?


      1. artalar
        21.01.2019 13:34

        Семантике


        1. Sirion
          21.01.2019 14:07

          Для семантики будет лучше, если мы будем на каждый вызов писать десяток лишних символов, относящихся не к тому, что мы хотим сделать, а к тому, как мы хотим это сделать?


          1. artalar
            21.01.2019 14:16

            «каждый», «десятки» — как-то очень абстрактно. Чтение \ запись и подписка \ обновление — это разные процессы и описывать их нужно по разному


            1. Sirion
              21.01.2019 14:26
              +1

              Весь смысл абстракции — в том, чтобы скрыть какие-то детали реализации. Допустим, наследование в JS скрывает, вызываем мы метод объекта или какого-то из прототипов. Наследование тоже зло?


              1. artalar
                21.01.2019 14:43

                Отвечает Егор Бугаенко =D


                1. Sirion
                  21.01.2019 14:58

                  Если это отсылка к видео, линк на которое вы привели в начале ветки, то, во-первых, я его не могу сейчас посмотреть по техническим причинам. Отсюда плавно вытекает «во-вторых»: использование видео там, где достаточно текста — моветон, от которого недалеко до вставки кода картинками.


                  1. artalar
                    21.01.2019 15:25

                    Нет, Егор Бугаенко — это довольно известная личность (хоть и из мира Java). И он выступает «против» наследования. Рекомендую ознакомится с его докладами и статьями, ЯП в них играет не самую важную роль.

                    P.S. он так же выступает против геттеров \ сеттеров, но по другой причине… Подробнее где-то в этом докладе youtu.be/lfdAwl3-X_c


    1. justboris
      21.01.2019 23:12
      +1

      Спасибо за ссылку!

      Тоже видел проблемы когда люди злоупотребляли геттерами/сеттерами. Теперь буду знать как этот паттерн называется.


  1. sanchezzzhak
    22.01.2019 17:19
    -1

    До моделей состоящие из одного obj сгодится и то трижды подумать придется перед использованием, есть proxy негативное решение, для большого списка obj как у меня 500к+, я бы точно не использовал подобное.