Реактивный огород

Реактивность

Реактивность

			#! let a = 1;
			#! let b = 2;
			#! let c = a + b;
			#! console.log(c); // 3
			#! a = 3;
			#! console.log(c); // 5
		

Реализация в JS

Основная идея, это создать контейнер для всех переменных, который будет иметь get/set. Чтобы ближе познакомиться в с темой, я написал малюсенькую либу, ReactiveDot или просто rdot.

a + b

			#! const a = rdot(1);
			#! const b = rdot(2);
			#! const c = rdot(() => a + b); // (!) Зависимость
			#! c.onValue(val => console.log("c: " + val)); // "c: 3"
			#! a.set(3); // "c: 5"
			#! b.set(7); // "c: 10"
		

Пример

			<form name="counter">
				<button name="up" type="button">+</button>
				<button name="down" type="button">-</button>
				<input name="result" readonly/>
			</form>
		

NativeJS

			const form = document.forms.counter;
			#! let sum = 0;
			#!+ function counter(x) {
				sum += x;
				#! render();
			#!- }
			#!+ function render() {
				form.result.value = sum;
			#! }
			#! form.up.addEventListener("click", render.bind(null, +1));
			#! form.down.addEventListener("click", render.bind(null, -1));
			#! render();
		

«Реактивность»

			const form = document.forms.counter;

			#!+ // Счетчик + сеттер, который при получении нового значения, прибовляет к нему старое
			#!- const counter = rdot(0, (newValue, oldValue) => newValue + (oldValue|0));

			#! // Создаем «реактивный поток» на основе события `click`:
			#! rdot.fromEvent(form.up, "click").onValue(() => counter.set(+1));
			#! rdot.fromEvent(form.down, "click").onValue(() => counter.set(-1));

			#!+ // Связываем значение реактивной переменной с элементом формы
			#!- counter.assign(form.result, "value");
		

Ещё пример

			<form name="reg">
				<div>
					<input placeholder="Username" name="username"/>
					<span></span>
				</div>
				<div>
					<input placeholder="Fullname" name="fullname"/>
					<span></span>
				</div>
				<button name="send">Reg</button>
			</form>
		
			const form = document.forms.reg;

			#!+ // Создаем реактивную двухстороннюю связку с Input-элементом
			const username = rdot.dom(form.username);
			#!- const fullname = rdot.dom(form.fullname);

			#!+ // Реактивное правило валидации
			#!- const validate = rdot(() => username().length > 0 && fullname().length > 0);

			#!+ // Связываем кол-во введенных символов и их вывод в соответствующем DOM-элементе
			[username, fullname].forEach(dot => {
				#!+ dot
				#!-	.map(value => value.length)
				#!	.assign(dot.el.nextElementSibling);
			#!- });

			#!+ // Связываем правило валидации с состоянием кнопки
			#!- validate.not().assign(form.send, "disabled");
		
			const KEY_ENTER = 13;
			const FILTER_ALL = "/";
			const FILTER_ACTIVE = "/active";
			const FILTER_COMPLETED = "/completed";

			const newTodoEl = document.querySelector(".new-todo");
			const listEl = document.querySelector(".todo-list");
			const footerEl = document.querySelector(".footer");
			const filtersEl = footerEl.querySelectorAll(".filters a");
			const todoCountEl = footerEl.querySelector(".todo-count");
			const clearCompletedEl = footerEl.querySelector(".clear-completed");
		
			// Реактивные переменные
			#!+ const location = rdot
								.fromEvent(window, "hashchange")
			#!- 					.map(value => window.location.href.split('#')[1] || FILTER_ALL);

			#!+ // Полле ввода нового todo
			#!- const newTodo = rdot.dom(newTodoEl);

			#!+ // Реактивный список дел
			#!- const todosStorage = new rdot.Model.List();

			#!+ // Статистика
			 const stats = new rdot.Model({
				#! left: 0, // сколько осталось
				#! completed: 0 // всего
			#!- });
		
			// Слушаем `Enter`
			#!+ rdot.fromEvent(newTodo.el, "keydown").onValue(evt => {
				if (evt.keyCode === KEY_ENTER) {
					#!+ todosStorage.unshift(new rdot.Model({
						value: newTodo(),
						completed: false
					#!- }));

					#! newTodo.set(""); // clear
				}
			#!- });
		
			// Видимость подвала
			const footerVisiblity = todosStorage
																.map(todos => todos.length > 0);

			#!+ footerVisiblity
			#!	.map(state => state ? "" : "none")
			#!	.assign(footerEl.style, "display");

			#!+ // Выбранный фильтр
			#!- location
			#!	.filter(filter => footerVisiblity()) // (!) Зависимость
			#!+	.onValue(filter => {
					[].forEach.call(filtersEl, a => {
						a.className = a.href.split("#")[1] === filter ? "selected" : "";
					});
			#!-	});
		
			// Подсчет статистики
			todosStorage.fetch(todos => {
				let left = 0;
				let completed = 0;

				#!+ todos.forEach(todo => {
					left += !todo.completed();
					completed += todo.completed();
				#!- });

				#! stats.left.set(left);
				#! stats.completed.set(completed);
			});
		
			// Осталось дел
			stats.left
			#!	.map(cnt => `${cnt} ${cnt === 1 ? 'item' : 'items'}`)
			#!	.assign(todoCountEl);

			#!+ // Выполненых дел
			#!- stats.completed
			#!	.map(cnt => cnt ? "" : "none")
			#!	.assign(clearCompletedEl.style, "display");
		
			// Обновление списка
			#!+ rdot
				#! .combine([location, todosStorage]) // (!) Зависимость
				#!+ .arrayFilter(([filter, todos]) => ({
					#! array: todos,
					#!+ callback(todo) {
						return (
							(FILTER_ALL === filter) ||
							(FILTER_ACTIVE === filter && !todo.completed()) ||
							(FILTER_COMPLETED === filter && todo.completed())
						);
					#!- }
				#!- }))
				#! .onValue(renderTodosList)
			#!- ;
		

The End

Учебный пример реализации