Документация на русском
Github репозиторий
Всем привет! В этой статье я расскажу, как пользоваться Матрешкой на трех несложных примерах. Мы рассмотрим базовые возможности Матрешки, познакомимся с тем, как работать с данными и разберем коллекции.
Пост является краткой компиляцией первых четырех статей о Матрешке с актуальными ссылками на документацию, обновленными методами и синтаксисом.
Напомню, Матрешка — front-end фреймворк, соблюдающий несколько важных принципов, среди которых
- Никакой логики в HTML
- Минимум сущностей
- Произвольная архитектура
Матрешка реализует простой синтаксис двустороннего связывания данных и активно использует акцессоры (геттеры и сеттеры).
this.bindNode( 'x', 'input.my-node' );
this.on( 'change:x', function() {
alert( this.x );
});
this.x = 'Wow!';
Программист имеет доступ к данным, как к обычным свойствам объекта, а коллекции генерируют HTML дерево самостоятельно при добавлении, удалении и сортировке элементов. Задав несколько правил, описанных в документации, вы можете не обращать внимания на состоянии представления и работать только с данными.
1. Hello World!
Давайте начнем с самого простого: подключим нужные скрипты на страницу и свяжем свойство
x
с двумя узлами на странице: с полем ввода (двусторонняя привязка) и обычным <div>
(односторонняя привязка). При изменении свойства выведем сообщение в консоль.Сначала создадим HTML файл.
<!DOCTYPE html>
<html>
<head>
<title>Моё первое приложение на базе Матрешки</title>
</head>
<body>
<input type="text" class="my-input">
<div class="my-output"></div>
<script src="http://cdn.jsdelivr.net/matreshka/latest/matreshka.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>
Теперь создадим JS файл js/app.js со следующим содержимым:
var Application = Class({
'extends': Matreshka,
constructor: function() {
// связываем свойство x и текстовое поле
this.bindNode( 'x', '.my-input' );
// связываем свойство x и блок с классом my-output
this.bindNode( 'x', '.my-output', {
setValue: function( v ) {
this.innerHTML = v;
}
});
// если свойство х изменилось, сообщаем об этом в консоли
this.on( 'change:x', function() {
console.log( 'x изменен на ' + this.x );
});
}
});
var app = new Application();
Теперь откройте консоль и введите:
app.x = 'Wow!';
Как вы можете заметить, произошло три вещи:
- Обновилось значение поля ввода
- Обновилось HTML содержимое блока
- В консоль вывелась информация о том, что
x
поменяли
При вводе текста в текстовое поле:
- Обновилось свойство
x
- Обновилось HTML содержимое блока
- В консоль вывелась информация о том, что
x
поменяли
Как видите, не нужно вручную отлавливать событие ввода в поле текста; при изменении значения свойства не нужно вручную устанавливать значения HTML узлам; не нужно объявлять дескриптор самостоятельно.
Не забывайте, что это работает даже в Internet Explorer 8.
Живой пример
2. Форма авторизации. Знакомимся с «моделью» (Matreshka.Object)
Следующий пример — реализация формы авторизации на сайте. У нас есть два текстовых поля: логин и пароль. Есть два чекбокса: «показать пароль» и «запомнить меня». Есть одна кнопка: «войти». Скажем, что валидация формы пройдена тогда, когда длина логина не меньше 4 символов, а длина пароля не меньше 5 символов.
Немного теории:
Matreshka.Object
играет роль класса, создающего объекты типа ключ-значение. В каждом экземпляре класса можно отделить свойства, отвечающие заданные (то что будет передано не сервер, например) от других свойств (то, что серверу не нужно, но определяет поведение приложения). В данном случае, логин, пароль и “запомнить меня” являются данными, которые мы отправляем на сервер, а свойство, говорящее о том, валидна ли форма — нет.Подробная и актуальная информация об этом классе находится в документации.
Итак, создадим класс, который наследуется от
Matreshka.Object
(или более кратко: MK.Object
).var LoginForm = Class({
'extends': MK.Object,
constructor: function () {
// ...
}
});
Так как “приложение” очень небольшое, всю логику можно разместить в конструкторе класса.
Перво-наперво, объявим данные по умолчанию.
.jset({
userName: '',
password: '',
rememberMe: true
})
Метод jset не только устанавливает значения, но и объявляет свойства, отвечающие за данные. Т. е.
userName
, password
и rememberMe
должны быть переданы на сервер (в этом примере просто выведем JSON на экран).Объявляем свойство,
isValid
, которое зависит от свойств userName
и password. При изменении любого из этих свойств (из кода, консоли или с помощью привязанного элемента), свойство isValid
тоже изменится..linkProps( 'isValid', 'userName password', function( userName, password ) {
return userName.length >= 4 && password.length >= 5;
})
isValid
будет равно true
, если длина имени пользователя не меньше четырех, а длина пароля — не меньше пяти. Метод linkProps — это еще одна крутая возможность фреймворка. Одни свойства могут зависеть от других, другие от третьих, в третьи вообще от свойств другого объекта. При этом, вы защищены от цикличных ссылок. Метод прекращает работу если встречается с опасными зависимостями.Теперь связываем свойства объекта и элементы на странице. Первым делом объявляем песочницу. Песочница нужна для того, чтоб ограничить влияние экземпляра одним элементом на странице и избежать конфликтов (например, если на странице есть два элемента с одним и тем же классом). Затем привязываем остальные элементы.
bind
и bind:KEY
). Первые два аргумента — обязательны.Правило привязки (байндер, “привязчик”) — это объект состоящий из трех основных свойств (четвертое —
initialize
, но нам оно пока не интересно), отвечающих за то как связать свойство экземпляра класса с HTML узлом.Давайте разберем на примере. Скажем, вы хотите связать свойство
x
со значением текстового поля.Первое, что нужно узнать — это то, какое событие HTML узла говорит нам о том, что значение элемента изменилось. В случае с текстовым полем — это событие
keyup
.on: 'keyup'
Второе — как извлечь значение элемента. В данном случае, нужно вытащить значение
value
элемента. Функция будет вызываться каждый раз, после срабатывания события keyup
, а возвращаемое значение присваиваться соответствующему свойству объекта (в данном случае, свойству x
).getValue: function() {
return this.value;
}
Используя jQuery, код будет выглядеть так:
getValue: function() {
return $( this ).val();
}
Третье — как установить новое значение элементу. Функция вызывается каждый раз, когда меняется значение свойства
x
.setValue: function( v ) {
this.value = v;
}
Или c jQuery:
setValue: function( v ) {
$( this ).val( v );
}
Теперь, собираем всё вместе:
this.bindNode( 'x', '.my-input', {
on: 'keyup',
getValue: function() {
return this.value;
},
setValue: function( v ) {
this.value = v;
}
});
Или, в случае использования jQuery:
this.bindNode( 'x', '.my-input', {
on: 'keyup',
getValue: function() {
return $( this ).val();
},
setValue: function( v ) {
$( this ).val( v );
}
});
Обратите внимание, в Матрешке реализована привязка всех без исключения HTML5 полей ввода без указания третьего аргумента. Поэтому, вместо громоздкой писанины, можно было бы сделать так:
this.bindNode( 'x', '.my-input' );
Матрешка сама поймет, что это текстовое поле и самостоятельно выберет необходимый байндер.
В случае, если Матрешка не знает о том, как связать переданный элемент со свойством, а программист не передал байндер в качестве третьего аргумента, то значение свойства и состояние элемента не будут синхронизироваться.
Более подробную информацию вы найдете в документации к методу bindNode.
// альтернативный синтаксис метода позволяет передать объект ключ-элемент в качестве первого аргумента,
// что несколько уменьшает количество кода
.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: function( v ) {
$( this ).toggleClass( 'disabled', !v );
}
})
Вместо такой записи можно использовать более краткую:
.bindNode( 'isValid', ':sandbox .submit', MK.binders.className( '!disabled' ) )
См. документацию к объекту binders.
Связываем поле с паролем и свойство
showPassword
(“показать пароль”) и меняем тип инпута в зависимости от значения свойства..bindNode( 'showPassword', ':bound(password)', {
getValue: null,
setValue: function( v ) {
this.type = v ? 'text' : 'password';
}
})
getValue: null
означает то, что мы переопределяем стандартное поведение фреймворка при привязке элементов формы.Добавляем событие отправки формы.
.on( 'submit::sandbox', function(evt) {
this.login();
evt.preventDefault();
})
submit
— обычное, произвольное DOM или jQuery событие, sandbox
— это наша форма (.login-form
). Такое событие и ключ должны быть разделены двоеточием. Это синтаксический сахар DOM событий, т. е. событие можно навешать любым другим способом, в том числе, и используя addEventListener
:this.bound( 'sandbox' ).addEventListener( 'submit', function() { … } );
В обработчике вызываем метод
login
, который объявим ниже, и предотвращаем перезагрузку страницы, отменяя стандартное поведение браузера используя preventDefault
.Последний штрих — метод
login
. Для примера, метод выводит на экран результирующий объект, если форма валидна. В реальном приложении, содержимым функции, очевидно, должен быть ajax запрос на сервер.login: function () {
if( this.isValid ) {
alert( JSON.stringify( this.toJSON() ) );
}
return this;
}
В самом конце создаём экземпляр класса.
var loginForm = new LoginForm();
Можете снова открыть консоль и изменить свойства вручную:
loginForm.userName = 'Chuck Norris';
loginForm.password = 'roundhouse_kick';
loginForm.showPassword = true;
Круто?
Ссылка на живой пример
3. Список пользователей. Разбираемся с коллекциями (Matreshka.Array)
С данными вида ключ-значения разобрались. Пришло время рассмотреть коллекции. Скажем, задача звучит так: вывести список неких людей в виде таблицы.
Чтобы не усложнять пример, поместим подготовленные данные в переменную
data
.var 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
(более кратко — MK.Array
).var Users = Class({
'extends': MK.Array,
});
Укажем свойство
itemRenderer
, которое отвечает за то как элементы массива будут рендериться на странице.itemRenderer: '#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, определяя класс элементов, содержащихся в коллекции (такой синтаксис должен быть знаком пользователям Backbone).
Model: User,
Класс
User
мы создадим немного позже, для начала определим конструктор новосозданного класса коллекции.constructor: function( data ) {
this
.bindNode( 'sandbox', '.users' )
.bindNode( 'container', ':sandbox tbody' )
.recreate( data )
;
}
При создании экземпляра класса
- Связываются свойство
sandbox
и элемент'.users'
создавая песочницу (границы влияния класса на HTML). - Связываются свойство
container
и элемент':sandbox tbody'
, определяя HTML узел, куда будут вставляться отрисованные элементы массива. - Добавляем переданные данные в массив методом recreate.
Теперь объявляем “Модель”: класс
User
, который наследуется от уже знакомого нам Matreshka.Object
.var User = Class({
'extends': MK.Object,
constructor: function( data ) {}
});
Устанавливаем данные, переданные в конструктор методом jset.
this.jset( data );
Затем, дожидаемся события
render
, которое срабатывает тогда, когда соответствующий HTML элемент был создан, но еще не вставлен на страницу. В обработчике привязываем соответствующие свойства соответствующим HTML элементам. Когда значение свойства изменится, innerHTML
заданного элемента тоже поменяется.this.on( 'render', function() {
this
.bindNode({
name: ':sandbox .name',
email: ':sandbox .email',
phone: ':sandbox .phone'
}, {
setValue: function( v ) {
this.innerHTML = v;
}
})
;
})
В конце создадим экземпляр класса
Users
, передав данные в качестве аргументаvar users = new Users( data );
Всё. При обновлении страницы вы увидите таблицу со списком юзеров.
Ссылка на живой пример
Теперь откройте консоль и напишите:
users.push({
name: 'Gene L. Bailey',
email: 'bailey@rhyta.com',
phone: '562-657-0985'
});
Как видите, в таблицу добавился новый элемент. А теперь вызовите
users.reverse();
Или любой другой метод массива (
sort
, splice
, pop
...). MK.Array
, кроме собственных методов, содержит все без исключения методы стандартного JavaScript массива. Затем,users[0].name = 'Vasily Pupkin';
users[1].email = 'mail@example.com'
Как видите, вам не нужно вручную следить за изменениями в коллекции, фреймворк самостоятельно ловит изменения данных и меняет DOM. Это невероятно удобно.
Не забывайте, что
MK.Array
поддерживает собственный набор событий. Вы можете отлавливать любое изменение в коллекции: добавление, удаление, пересортировку элементов методом on.users.on( 'addone', function( evt ) {
console.log( evt.added.name );
});
users.push({
name: 'Clint A. Barnes'
})
(выведет в консоль имя добавленного пользователя)
В качестве еще одного примера коллекции можете рассмотреть реализацию простейшего поисковика музыки, основанного на Soundcloud API.
Пока такой возможности из коробки не планируется. Шаблоны могут подгружаются с помощью AMD:
define([
'matreshka',
'text!templates/template.html'
], function( MK, TEMPLATE ) {
return MK.Class({
'extends': MK.Array,
itemRenderer: TEMPLATE
// ...
})
});
Или в случае использования Babel:
import MK from 'matreshka';
import TEMPLATE from 'text!templates/template.html';
export default class extends MK.Array {
itemRenderer() {
return TEMPLATE;
}
// ...
}
При отсутствии AMD, можно запросить шаблон любым способом (например, в конструкторе), а затем вызвать метод rerender. Этот метод как раз и создан для случаев, когда itemRenderer устанавливается динамически.
var MyClass = Class({
'extends': MK.Array,
constructor: function() {
// произвольная функция для получения шаблона
ajax( 'templates/template.html', function( TEMPLATE ) {
this.itemRenderer = TEMPLATE;
this.rerender();
}.bind( this ) );
}
});
4. TodoMVC
Теперь, когда мы рассмотрели возможности Матрешки на простых примерах, взгляните на реализацию известного эталонного приложения. Описание к коду находится здесь, а здесь находится репозиторий.
Вывод
Матрешка — простой фреймворк, решающий своими простыми функциями огромное поле задач. При этом, не требуется описывать байндинги в HTML коде, об обработчиках «знает» только JavaScript код, а структура приложения может быть совершенно произвольной. Матрешка не ограничивает творчество разработчика, являясь, скорее, библиотекой, чем фреймворком.
Новые ресурсы:
Gitter (en)
Gitter (ru)
Спасибо всем тем, кто сообщал об опечатках на сайте. Всем добра!
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (23)
kiselev_dv
08.04.2015 20:18+1Вам бы примеров побольше, например
— простенькое дерево категорий (на генерацию хтмля)
— взаимодействие со сторонними библиотеками (например с картами, добавляем десяток точек они появляются на карте, связываем клик по маркеру и содержимое попапа с моделью)
— работа с hash частью url'a и историей (я понимаю что это немного за рамками библиотеки, но всеже)
Glebcha
10.04.2015 16:10Давно присматривался и уже забыл, а тут ваша статья.
Ни капли злой иронии, но почему я должен посмотреть Matreshka.js если мне безумно крутым и легким показался Mithril.js?
Двустороннее связывание интересно посмотреть как у вас реализовано, но и без него из коробки можно обойтись, раньше же как-то получалось.
Теперь классы — а почему бы не ES6 синтаксис сразу в документации, прям жирным шрифтом? IE8 идет лесом как мне кажется, уже давно и навсегда.
А вообще побольше примеров и, желательно, посложнее и более приближенных к реальным задачам (например гриды, формы и так далее).Finom Автор
10.04.2015 18:31но почему я должен посмотреть Matreshka.js если мне безумно крутым и легким показался Mithril.js?
Все фреймворки хороши по-своему. Вы мне ничего не должны :)
Теперь классы — а почему бы не ES6 синтаксис сразу в документации, прям жирным шрифтом?
Когда ES6 будет использоваться повсеместно, так и сделаем. Сейчас нет в этом смысла. Многие даже не в курсе, что это такое.
IE8 идет лесом как мне кажется, уже давно и навсегда.
К сожалению, это не так. Еще как минимум год разработчикам прийдется мучиться. Посмотрите любую статистику.
А вообще побольше примеров и, желательно, посложнее и более приближенных к реальным задачам (например гриды, формы и так далее).
Полностью согласен. Но очень сложно соблюсти баланс между максимальной понятностью и функционалом примера.
Glebcha
10.04.2015 18:59Но вы же хотите привлечь внимание, назовите причины по которым это стоит сделать с оглядкой на упомянутый крутой фрэймворк.
Неужели вам не хочется понять в сравнении преимущества своего детища? Я же не издеваюсь ни капли.
Да уж я с вами не соглашусь, про ES6 знают и его хотят использовать. Многие ждали и верили, дождались и используют.
Я уже год назад отказался от поддержки IE < 8 и не собираюсь возвращаться и извращаться. Того же и всем желаю.
Так для новичков будут простые примеры, а из реальной жизни — для тех кто поигрался и захотел на продакшне.Finom Автор
10.04.2015 19:17Промазал с ответом: habrahabr.ru/company/matreshka/blog/254889/#comment_8371889
Finom Автор
10.04.2015 19:17Но вы же хотите привлечь внимание, назовите причины по которым это стоит сделать с оглядкой на упомянутый крутой фрэймворк.
Неужели вам не хочется понять в сравнении преимущества своего детища? Я же не издеваюсь ни капли.
Я вряд ли когда-то буду говорить, что такой-то фреймворк плохой, а этот лучше всех. Почему стоит использовать или не использовать Матрешку, решаете вы сами. Главная задача разработчика любого свободного инструмента — дать возможность определиться, нужно ли его использовать или нет. Мне кажется, здесь с этим всё в порядке: есть сайт, есть «рекламирующие» тексты, есть документация, есть примеры (в том числе и средних размеров: TodoMVC).
Я уже год назад отказался от поддержки IE < 8 и не собираюсь возвращаться и извращаться.
Я и не против, но клиенты требуют.
Delphinum
13.04.2015 00:01Зачем же вы добавляете в ваше решение излишний (на мой взгляд) функционал? Нет желания пользоваться AngularJS только от того, что мне не нужны их фильтры да сервисы, а вот связывание хочется.
Думаю перейти на «матрешку», но можно ли уменьшить ее вес, выпилив все, кроме связывания?Finom Автор
13.04.2015 12:06Связывание тут реализуется достаточно просто. Любой программист со средним опытом может реализовать standalone функцию bindNode с помощью Object.defineProperty и addEventListener.
Finom Автор
14.04.2015 19:14Решил для небольшой тренеровки после обеда написать отдельную функцию
bindNode
.
Вот, если вам интересно:
window.bindNode = function bindNode( object, key, node, binder ) { var value = object[ key ]; Object.defineProperty( object, key, { get: function() { return value; }, set: function( v ) { binder.setValue.call( node, v ); } }); node.addEventListener( binder.on, function() { value = binder.getValue.call( node ); }); };
Функция поддерживает только один набор аргументов, аргумент node должен быть DOM нодой, нет стандартных байндеров и пр., но вы можете сами дописать, это не трудно.
jsbin.com/mabetap/2/edit?html,js,console,output
Введитеdata.x = 'Hello world'
в консоли.Delphinum
14.04.2015 22:50Спасибо. Возможно было бы очень удобно, если бы вы разделили фреймворк на части, чтобы можно было наращивать функционал постепенно, по мере надобности.
Использую ваш фреймворк в связке с React. Думаю об использовании балалайки )Finom Автор
15.04.2015 00:55Возможно было бы очень удобно, если бы вы разделили фреймворк на части, чтобы можно было наращивать функционал постепенно, по мере надобности.
Тогда фреймворк уеньшится на 20%, по моим оценкам, что очень немного, при этом не отразившись на производительности. Поэтому, я не вижу в этом смысла.
Использую ваш фреймворк в связке с React
Буду благодарен, если поделитесь опытом (здесь или в личке). Очень интересно.
Delphinum
15.04.2015 11:09Буду благодарен, если поделитесь опытом (здесь или в личке). Очень интересно.
Постараюсь что нибудь написать специально для вас (распространять имеющиеся исходники не имею права)
Delphinum
15.04.2015 15:42Идея в том, чтобы позволить программисту рендерить структуру виджета с помощью React, но обновлять конкретные значения виджета через связывание.
Пример использованияdefine('components/Timer', ['react', 'matreshka'], function (React, Matreshka) { return React.createClass({ displayName: 'Timer', // Увеличиваем значение таймера используя модель tick: function () { this.model.value = Number(this.model.value) + 1; }, // Обнуляем таймер используя модель clean: function(){ this.model.value = 0; }, // Создаем модель с использованием Matreshka initModel: function(){ this.model = new (Matreshka.Class({ 'extends': Matreshka.Object, constructor: function (node, value) { this.value = value; this.bindNode('value', node, { setValue: function (v) { this.innerHTML = v; } }); } }))(this.getDOMNode().lastChild, this.props.value); }, // Вешаем обработчики на виджет componentDidMount: function () { this.initModel(); this.timer = setInterval(this.tick, 500); this.getDOMNode().lastChild.addEventListener('click', this.clean); }, // Удаляем обработчики с виджета componentDidUnmount: function () { clearInterval(this.timer); this.getDOMNode().lastChild.removeEventListener('click', this.clean); }, // рендерим виджет render: function () { return React.createElement('div', null, [ React.createElement('span', null, 'Timer: '), React.createElement('span', {title: 'Click to reset'}) ]); } }); });
Delphinum
15.04.2015 16:21Более сложный пример. Головной элемент состоит из подэлемента, который способен изменять свою HTML структуру (благодаря React). Тот, в свою очередь, состоит из еще более мелких элементов, которые могут изменять свое значение (благодаря Matreshka).
Пример использованияvar todo = React.createClass({ inc: function(){ this.model.value = Number(this.model.value) + 1; }, componentDidMount: function(){ this.model = new (Matreshka.Class({ 'extends': Matreshka.Object, constructor: function (node, value) { this.value = value; this.bindNode('value', node, { setValue: function (v) { this.innerHTML = v; } }); } }))(React.findDOMNode(this.refs.data), this.props.value); React.findDOMNode(this.refs.button).addEventListener('click', this.inc); }, render: function(){ return React.createElement('li', null, [ React.createElement('span', {ref: 'data'}, this.props.value), React.createElement('input', {type: 'button', value: 'inc', ref: 'button'}) ]); } }); var todoList = React.createClass({ getInitialState: function(){ return {items: this.props.items}; }, addLi: function(text){ var newState = this.state.items; newState.push(React.createElement(todo, {value: text})); this.setState(newState); }, render: function(){ return React.createElement('ul', null, this.state.items) } }); var todoApp = React.createClass({ addTodo: function(){ this.refs.todoList.addLi(React.findDOMNode(this.refs.text).value); React.findDOMNode(this.refs.text).value = ''; }, componentDidMount: function(){ React.findDOMNode(this.refs.button).addEventListener('click', this.addTodo); }, componentDidUnmount: function(){}, render: function(){ return React.createElement('div', null, [ React.createElement(todoList, {ref: 'todoList', items: []}), React.createElement('input', {type: 'text', ref: 'text'}), React.createElement('input', {type: 'button', value: 'Add', ref: 'button'}) ]); } });
HeadWithoutBrains
Finom Автор
habrahabr.ru/company/matreshka/blog/253909/#comment_8362671
4dmonster
Видимо авторам статей о фреймворках лучше самим добавлять эту картинку :).
4dmonster
А вам ещё и приписывать про гипотетический метод toMatreshka (или просто to).