Présentation d'effector-dom à l'aide de l'exemple de liste de tâches

Beaucoup connaissent déjà le directeur de l' État effecteur , quelqu'un non seulement l'a regardé, mais l'a également utilisé dans la prod. Depuis la fin de l'automne, son auteur développe activement des devtools pour un effecteur, et au cours de ce travail, il a réussi à écrire une bibliothèque très intéressante pour le rendu d'une application - effecteur-dom .


Nous allons apprendre à connaître ce rendu - dans ce tutoriel, nous allons créer une application Todo simple.



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.


Une telle application simple s'est avérée, elle a été spécialement rendue aussi simple que possible, avec une séparation minimale en entités distinctes.


Bien sûr, si vous le souhaitez, les mêmes boutons de filtre peuvent être retirés dans une entité distincte, qui peut être utilisée pour transmettre le type et le nom du filtre.


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

De la même manière, grâce à son travail basé sur la pile, effector-dom vous permet de rendre librement non seulement des éléments individuels, mais aussi un comportement général.


Le code rendu sera appliqué spécifiquement aux éléments nécessaires, par exemple:


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

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

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

11. Total


J'ai été personnellement impressionné par la simplicité de travailler avec le nouveau rendu, d'autant plus que j'avais déjà de l'expérience avec effector dans une grande application.


J'attends avec impatience une version stable, une sorte de vulgarisation pour avoir l'opportunité d'utiliser le rendu dans la prod.

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


All Articles