يعرف الكثير بالفعل مدير الدولة المستجيب ، شخص لم يشاهده فقط ، ولكنه استخدمه أيضًا في حث. منذ نهاية الخريف ، كان مؤلفه يعمل بنشاط على تطوير أدوات devtools لأحد المستجيبين ، وفي عملية هذا العمل ، تمكن من كتابة مكتبة مثيرة للاهتمام للغاية لتقديم تطبيق - المستجيب-دوم .
سنتعرف على هذا العرض - في هذا البرنامج التعليمي سنقوم بإنشاء تطبيق Todo بسيط.

للعمل مع المنطق ، سوف نستخدم المستجيب ، لتقديم التطبيق - المستجيب دوم .
بالنسبة للجزء البصرية، لنأخذ الجاهزة قالب todomvc التطبيق قالب مع todomvc التطبيق-المغلق الأساليب التي كتبها tastejs لل أساس .
1. إعداد المشروع
أفترض أنك مألوف بالفعل مع webpack و npm ، لذلك فإننا نتخطى خطوة تثبيت npm ، وإنشاء مشروع باستخدام webpack وتشغيل التطبيق (إذا لم نكن على دراية ، 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- المجموع
لقد تأثرت شخصيًا ببساطة العمل مع العرض الجديد ، خاصة وأنني كنت بالفعل لدي خبرة في العمل مع المستجيب في تطبيق كبير.
إنني أتطلع إلى إصدار مستقر ، نوع من التعميم لإتاحة الفرصة لاستخدام العرض في المنتج.