рдХрдИ рдкрд╣рд▓реЗ рд╕реЗ рд╣реА рдкреНрд░рднрд╛рд╡рд╢реАрд▓ рд░рд╛рдЬреНрдп рдкреНрд░рдмрдВрдзрдХ рдХреЛ рдЬрд╛рдирддреЗ рд╣реИрдВ , рдХрд┐рд╕реА рдиреЗ рди рдХреЗрд╡рд▓ рдЗрд╕реЗ рджреЗрдЦрд╛, рдмрд▓реНрдХрд┐ рдЗрд╕рдХрд╛ рдЙрдкрдпреЛрдЧ рдареЗрд╕ рдореЗрдВ рднреА рдХрд┐рдпрд╛ред рд╢рд░рдж рдЛрддреБ рдХреА рд╕рдорд╛рдкреНрддрд┐ рдХреЗ рдмрд╛рдж рд╕реЗ, рдЗрд╕рдХрд╛ рд▓реЗрдЦрдХ рдПрдХ рдкреНрд░рднрд╛рд╡рдХрд╛рд░ рдХреЗ рд▓рд┐рдП рд╕рдХреНрд░рд┐рдп рд░реВрдк рд╕реЗ рдбреЗрд╡рдЯреВрд▓реНрд╕ рд╡рд┐рдХрд╕рд┐рдд рдХрд░ рд░рд╣рд╛ рд╣реИ, рдФрд░ рдЗрд╕ рдХрд╛рдо рдХреА рдкреНрд░рдХреНрд░рд┐рдпрд╛ рдореЗрдВ рд╡рд╣ рдПрдХ рдЕрдиреБрдкреНрд░рдпреЛрдЧ - рдкреНрд░рднрд╛рд╡рдХрд╛рд░-рдбреЛрдо рдХреЗ рдкреНрд░рддрд┐рдкрд╛рджрди рдХреЗ рд▓рд┐рдП рдПрдХ рдмрд╣реБрдд рд╣реА рджрд┐рд▓рдЪрд╕реНрдк рдкреБрд╕реНрддрдХрд╛рд▓рдп рд▓рд┐рдЦрдиреЗ рдореЗрдВ рдХрд╛рдордпрд╛рдм рд░рд╣рд╛ ред
рд╣рдо рдЗрд╕ рд░реЗрдВрдбрд░ рдХреЛ рдЬрд╛рди рдкрд╛рдПрдВрдЧреЗ - рдЗрд╕ рдЯреНрдпреВрдЯреЛрд░рд┐рдпрд▓ рдореЗрдВ рд╣рдо рдПрдХ рд╕рд╛рдзрд╛рд░рдг рдЯреЛрдбреЛ рдПрдкреНрд▓реАрдХреЗрд╢рди рдмрдирд╛рдПрдВрдЧреЗред

рддрд░реНрдХ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рд╣рдо рдЕрдиреБрдкреНрд░рдпреЛрдЧ- рдкреНрд░рднрд╛рд╡рдХрд╛рд░-рдбреЛрдо рдХреЛ рдкреНрд░рд╕реНрддреБрдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдкреНрд░рднрд╛рд╡рдХрд╛рд░ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реЗрдВрдЧреЗ ред
рджреГрд╢реНрдп рднрд╛рдЧ рдХреЗ рд▓рд┐рдП, рдХреЗ рдкрд╣рд▓реЗ рд╕реЗ рддреИрдпрд╛рд░ рдХрд░рддреЗ рд╣реИрдВ todomvc рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рдЯреЗрдореНрдкрд▓реЗрдЯ рдЯреЗрдореНрдкрд▓реЗрдЯ рдХреЗ рд╕рд╛рде todomvc рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рд╕реАрдПрд╕рдПрд╕ рджреНрд╡рд╛рд░рд╛ рд╢реИрд▓рд┐рдпреЛрдВ tastejs рдХреЗ рд░реВрдк рдореЗрдВ рдЖрдзрд╛рд░ ред
1. рдкрд░рд┐рдпреЛрдЬрдирд╛ рдХреА рддреИрдпрд╛рд░реА
рдореБрдЭреЗ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рдЖрдк рдкрд╣рд▓реЗ рд╕реЗ рд╣реА рд╡реЗрдмрдкреИрдХ рдФрд░ npm рд╕реЗ рдкрд░рд┐рдЪрд┐рдд рд╣реИрдВ, рдЗрд╕рд▓рд┐рдП рд╣рдо npm рдХреЛ рд╕реНрдерд╛рдкрд┐рдд рдХрд░рдиреЗ, рд╡реЗрдмрдкреИрдХ рдХреЗ рд╕рд╛рде рдПрдХ рдкреНрд░реЛрдЬреЗрдХреНрдЯ рдмрдирд╛рдиреЗ рдФрд░ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рд▓реЙрдиреНрдЪ рдХрд░рдиреЗ рдХреЗ рдЪрд░рдг рдХреЛ рдЫреЛрдбрд╝ рджреЗрддреЗ рд╣реИрдВ (рдпрджрд┐ рд╣рдо рдкрд░рд┐рдЪрд┐рдд рдирд╣реАрдВ рд╣реИрдВ, рддреЛ google 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.
рдЗрд╕ рддрд░рд╣ рдХреЗ рдПрдХ рд╕рд╛рдзрд╛рд░рдг рдЖрд╡реЗрджрди рдирд┐рдХрд▓рд╛, рдЗрд╕реЗ рд╡рд┐рд╢реЗрд╖ рд░реВрдк рд╕реЗ рдпрдерд╛рд╕рдВрднрд╡ рд╕рд░рд▓ рдмрдирд╛рдпрд╛ рдЧрдпрд╛ рдерд╛, рдЬрд┐рд╕рдореЗрдВ рдЕрд▓рдЧ-рдЕрд▓рдЧ рд╕рдВрд╕реНрдерд╛рдУрдВ рдореЗрдВ рдиреНрдпреВрдирддрдо рдЕрд▓рдЧрд╛рд╡ рдерд╛ред
рдмреЗрд╢рдХ, рдпрджрд┐ рд╡рд╛рдВрдЫрд┐рдд рд╣реИ, рддреЛ рдПрдХ рд╣реА рдлрд╝рд┐рд▓реНрдЯрд░ рдмрдЯрди рдХреЛ рдПрдХ рдЕрд▓рдЧ рдЗрдХрд╛рдИ рдореЗрдВ рдирд┐рдХрд╛рд▓рд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ, рдЬрд┐рд╕рдХрд╛ рдЙрдкрдпреЛрдЧ рдлрд╝рд┐рд▓реНрдЯрд░ рдкреНрд░рдХрд╛рд░ рдФрд░ рдирд╛рдо рдХреЛ рд╡реНрдпрдХреНрдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИред
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'});
});
рдЙрд╕реА рддрд░рд╣, рдЗрд╕рдХреЗ рд╕реНрдЯреИрдХ-рдЖрдзрд╛рд░рд┐рдд рдХрд╛рд░реНрдп рдХреЗ рд▓рд┐рдП рдзрдиреНрдпрд╡рд╛рдж, рдкреНрд░рднрд╛рд╡рдХрд╛рд░-рдбреЛрдо рдЖрдкрдХреЛ рд╕реНрд╡рддрдВрддреНрд░ рд░реВрдк рд╕реЗ рди рдХреЗрд╡рд▓ рд╡реНрдпрдХреНрддрд┐рдЧрдд рддрддреНрд╡реЛрдВ рдХреЛ рдкреНрд░рд╕реНрддреБрдд рдХрд░рдиреЗ рдХреА рдЕрдиреБрдорддрд┐ рджреЗрддрд╛ рд╣реИ, рдмрд▓реНрдХрд┐ рд╕рд╛рдорд╛рдиреНрдп рд╡реНрдпрд╡рд╣рд╛рд░ рднреА рдХрд░рддрд╛ рд╣реИред
рдкреНрд░рд╕реНрддреБрдд рдХреЛрдб рд╡рд┐рд╢реЗрд╖ рд░реВрдк рд╕реЗ рдЖрд╡рд╢реНрдпрдХ рддрддреНрд╡реЛрдВ рдХреЗ рд▓рд┐рдП рд▓рд╛рдЧреВ рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛, рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП:
const WithFocus = () => {
const focus = createEvent();
focus.watch(() => console.log('focused'));
spec({handler: {focus}});
};
h('input', () => {
...
WithFocus();
...
});
11. рдХреБрд▓
рдореИрдВ рд╡реНрдпрдХреНрддрд┐рдЧрдд рд░реВрдк рд╕реЗ рдирдП рд░реЗрдВрдбрд░ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдиреЗ рдХреА рд╕рд╛рджрдЧреА рд╕реЗ рдкреНрд░рднрд╛рд╡рд┐рдд рдерд╛, рдЦрд╛рд╕рдХрд░ рдЬрдм рд╕реЗ рдореБрдЭреЗ рдкрд╣рд▓реЗ рд╕реЗ рд╣реА рдПрдХ рдмрдбрд╝реЗ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдореЗрдВ рдкреНрд░рднрд╛рд╡рдХрд╛рд░реА рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдиреЗ рдХрд╛ рдЕрдиреБрднрд╡ рдерд╛ред
рдореИрдВ рдПрдХ рд╕реНрдерд┐рд░ рд╕рдВрд╕реНрдХрд░рдг рдХреЗ рд▓рд┐рдП рддрддреНрдкрд░ рд╣реВрдВ, рдареЗрд╕ рдореЗрдВ рд░реЗрдВрдбрд░ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХреЗ рдЕрд╡рд╕рд░ рдХреЗ рд▓рд┐рдП рдХрд┐рд╕реА рдкреНрд░рдХрд╛рд░ рдХрд╛ рд▓реЛрдХрдкреНрд░рд┐рдпрдХрд░рдгред