Apresentando effector-dom usando a lista de tarefas de exemplo

Muitos já conhecem o gerente de estado efetor , alguém não apenas o assistiu, mas também o usou no prod. Desde o final do outono, seu autor desenvolve ativamente devtools para um efetor e, no processo deste trabalho, ele conseguiu escrever uma biblioteca muito interessante para renderizar um aplicativo - effector-dom .


Vamos conhecer essa renderização - neste tutorial, criaremos um aplicativo Todo simples.



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, .


// src/model.js
import {createStore, createEvent, combine} from 'effector';

// 

//  
export const $todos = createStore([]); 

//  ,    null/true/false
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


// src/model.js
...

$todos
  //   
  .on(appended, (state, title) => [...state, {title, completed: false}])
  //  .     title
  .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- , :


// src/index.js
import {using} from 'effector-dom';
import {App} from './view/app';

using(document.body, () => {
  App();
});

h, dom- , .


dom- spec, :


// src/view/app.js
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 = () => {
  //  section 
  h('section', () => {
    //    
    spec({attr: {class: classes.todoapp}});

    //     
    Header();
    Main();
    Footer();
  });
};

.


5.


h1 .


dom- , , . spec , :


// src/view/title.js
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).


// src/view/header.js
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, to})
      forward({ 
        //    $value   submit,
        //       
        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 - , . .


// src/view/main.js
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     
      }, ({fields: [title, completed], key}) => TodoItem({title, completed, key})); 
    });
  });
};

8.


, effector-dom - , ..


, toggled removed - — .


effector — event.prepend.


store.map


// src/view/todoItem.js
import {h, spec} from 'effector-dom';
import classes from 'todomvc-app-css/index.css';
import {toggled, removed} from '../model';

// title  completed -    
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.


,


// src/view/footer.js
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.


Um aplicativo tão simples acabou sendo feito o mais simples possível, com uma separação mínima em entidades separadas.


Obviamente, se desejado, os mesmos botões de filtro podem ser retirados em uma entidade separada, que pode ser usada para transmitir o tipo e o nome do filtro.


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'});
});

Da mesma forma, graças ao seu trabalho baseado em pilha, o effector-dom permite renderizar livremente não apenas elementos individuais, mas também comportamento geral.


O código renderizado será aplicado especificamente aos elementos necessários, por exemplo:


const WithFocus = () => {
  const focus = createEvent();
  focus.watch(() => console.log('focused'));

  spec({handler: {focus}});
};

h('input', () => {
  ...
  WithFocus();
  ...
});

11. Total


Fiquei pessoalmente impressionado com a simplicidade de trabalhar com a nova renderização, principalmente porque já tinha experiência em trabalhar com o effector em um aplicativo grande.


Estou ansioso por uma versão estável, algum tipo de popularização pela oportunidade de usar a renderização no prod.

Source: https://habr.com/ru/post/undefined/


All Articles