Banyak yang sudah tahu manajer negara efektor , seseorang tidak hanya menontonnya, tetapi juga menggunakannya di prod. Sejak akhir musim gugur, penulisnya telah secara aktif mengembangkan devtools untuk seorang efektor, dan dalam proses karya ini ia berhasil menulis perpustakaan yang sangat menarik untuk membuat aplikasi - efektor-dom .
Kita akan mengetahui render ini - dalam tutorial ini kita akan membuat aplikasi Todo sederhana.

effector, — effector-dom.
todomvc-app-template todomvc-app-css tastejs.
1.
, npm, npm, webpack ( — webpack boilerplate).
, :
npm install effector@20.11.5 effector-dom@0.0.10 todomvc-app-css
2.
.
, , , - best practices .
, .
:
srs/
view/
app.js
title.js
footer.js
header.js
main.js
todoItem.js
model.js
index.js
3.
, : , , .
( undefined) Store, — Event.
combine, .
import {createStore, createEvent, combine} from 'effector';
export const $todos = createStore([]);
export const $activeFilter = createStore(null);
export const $filteredTodos = combine(
$todos,
$activeFilter,
(todos, filter) => filter === null
? todos
: todos.filter(todo => todo.completed === filter)
);
export const appended = createEvent();
export const toggled = createEvent();
export const removed = createEvent();
export const allCompleted = createEvent();
export const completedRemoved = createEvent();
export const filtered = createEvent();
.
, store.on
...
$todos
.on(appended, (state, title) => [...state, {title, completed: false}])
.on(removed, (state, title) => state.filter(item => item.title !== title))
.on(toggled, (state, title) => state.map(item => item.title === title
? ({...item, completed: !item.completed})
: item))
.on(allCompleted, state => state.map(item => item.completed
? item
: ({...item, completed: true})))
.on(completedRemoved, state => state.filter(item => !item.completed));
$activeFilter
.on(filtered, (_, filter) => filter);
. .
4. view
view effector-dom. , SwiftUI .
, , . dom- .
effector-dom dom , . using, dom- , :
import {using} from 'effector-dom';
import {App} from './view/app';
using(document.body, () => {
App();
});
h, dom- , .
dom- spec, :
import {h, spec} from 'effector-dom';
import classes from 'todomvc-app-css/index.css';
import {Header} from './header';
import {Main} from './main';
import {Footer} from './footer';
export const App = () => {
h('section', () => {
spec({attr: {class: classes.todoapp}});
Header();
Main();
Footer();
});
};
.
5.
h1 .
dom- , , . spec , :
import {h} from 'effector-dom';
export const Title = () => {
h('h1', {text: 'todos'});
};
6.
, effector-dom effector , , ..
, header. dom- spec ( ), handler.
, input , $value input.
sample . api sample — , — , : event = sample($store, triggerEvent).
import {h, spec} from 'effector-dom';
import {createEvent, createStore, forward, sample} from 'effector';
import classes from 'todomvc-app-css/index.css';
import {Title} from './title';
import {appended} from '../model';
export const Header = () => {
h('header', () => {
Title();
h('input', () => {
const keypress = createEvent();
const input = createEvent();
const submit = keypress.filter({fn: e => e.key === 'Enter'});
const $value = createStore('')
.on(input, (_, e) => e.target.value)
.reset(appended);
forward({
from: sample($value, submit).filter({fn: Boolean}),
to: appended,
});
spec({
attr: {
class: classes["new-todo"],
placeholder: 'What needs to be done?',
value: $value
},
handler: {keypress, input},
})
});
});
};
7.
, effector-dom list.
— , , . list($store, itemCallback) .
.
, todomvc-app-css - , . .
import {h, spec, list} from 'effector-dom';
import classes from 'todomvc-app-css/index.css';
import {TodoItem} from './todoItem';
import {$filteredTodos, allCompleted} from '../model';
export const Main = () => {
h('section', () => {
spec({attr: {class: classes.main}});
h('input', {
attr: {id: 'toggle-all', class: classes['toggle-all'], type: 'checkbox'}
});
h('label', {attr: {for: 'toggle-all'}, handler: {click: allCompleted}});
h('ul', () => {
spec({attr: {class: classes["todo-list"]}});
list({
source: $filteredTodos,
key: 'title',
fields: ['title', 'completed']
}, ({fields: [title, completed], key}) => TodoItem({title, completed, key}));
});
});
};
8.
, effector-dom - , ..
, toggled removed - — .
effector — event.prepend.
store.map
import {h, spec} from 'effector-dom';
import classes from 'todomvc-app-css/index.css';
import {toggled, removed} from '../model';
export const TodoItem = ({title, completed, key}) => {
h('li', () => {
spec({attr: {class: completed.map(flag => flag ? classes.completed : false)}});
h('div', () => {
spec({attr: {class: classes.view}});
h('input', {
attr: {class: classes.toggle, type: 'checkbox', checked: completed},
handler: {click: toggled.prepend(() => key)},
});
h('label', {text: title});
h('button', {
attr: {class: classes.destroy},
handler: {click: removed.prepend(() => key)},
});
});
});
};
9.
,
import {h, spec} from 'effector-dom';
import classes from 'todomvc-app-css/index.css';
import {$todos, $activeFilter, filtered, completedRemoved} from '../model';
export const Footer = () => {
h('footer', () => {
spec({attr: {class: classes['footer']}});
h('span', () => {
spec({attr: {class: classes['todo-count']}});
const $activeCount = $todos.map(
todos => todos.filter(todo => !todo.completed).length
);
h('strong', {text: $activeCount});
h('span', {text: $activeCount.map(count => count === 1
? ' item left'
: ' items left'
)});
});
h('ul', () => {
spec({attr: {class: classes.filters}});
h('li', () => {
h('a', {
attr: {class: $activeFilter.map(active => active === null
? classes.selected
: false
)},
text: 'All',
handler: {click: filtered.prepend(() => null)},
});
});
h('li', () => {
h('a', {
attr: {class: $activeFilter.map(completed => completed === false
? classes.selected
: false
)},
text: 'Active',
handler: {click: filtered.prepend(() => false)},
});
});
h('li', () => {
h('a', {
attr: {class: $activeFilter.map(completed => completed === true
? classes.selected
: false
)},
text: 'Completed',
handler: {click: filtered.prepend(() => true)},
});
});
});
h('button', {
attr: {class: classes['clear-completed']},
text: 'Clear completed',
handler: {click: completedRemoved},
});
});
};
10.
Aplikasi yang begitu sederhana ternyata, dibuat secara khusus sesederhana mungkin, dengan pemisahan minimal menjadi entitas yang terpisah.
Tentu saja, jika diinginkan, tombol filter yang sama dapat dibawa ke entitas yang terpisah, yang dapat digunakan untuk menyampaikan jenis dan nama filter.
const FilterButton = ({filter, text}) => {
h('li', () => {
h('a', {
attr: {class: $activeFilter.map(completed => completed === filter
? classes.selected
: false
)},
text: text,
handler: {click: filtered.prepend(() => filter)},
});
});
};
h('ul', () => {
spec({attr: {class: classes.filters}});
FilterButton({filter: null, text: 'All'});
FilterButton({filter: false, text: 'Active'});
FilterButton({filter: true, text: 'Completed'});
});
Dengan cara yang sama, berkat kerja berbasis stacknya, efektor-dom memungkinkan Anda untuk secara bebas membuat tidak hanya elemen individual, tetapi juga perilaku umum.
Kode yang diberikan akan diterapkan secara khusus ke elemen yang diperlukan, misalnya:
const WithFocus = () => {
const focus = createEvent();
focus.watch(() => console.log('focused'));
spec({handler: {focus}});
};
h('input', () => {
...
WithFocus();
...
});
11. Total
Saya pribadi terkesan oleh kesederhanaan bekerja dengan render baru, terutama karena saya sudah memiliki pengalaman bekerja dengan efektor dalam aplikasi besar.
Saya menantikan versi stabil, semacam popularisasi untuk kesempatan menggunakan render di prod.