Presentación de efector-dom usando la lista de tareas de ejemplo

Muchos ya conocen al gerente estatal efector , alguien no solo lo vio, sino que también lo usó en la producción. Desde finales de otoño, su autor ha estado desarrollando activamente devtools para un efector, y en el proceso de este trabajo logró escribir una biblioteca muy interesante para presentar una aplicación: efector-dom .


Conoceremos este render: en este tutorial crearemos una aplicación Todo sencilla.



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.


Resultó que una aplicación tan simple se hizo especialmente lo más simple posible, con una separación mínima en entidades separadas.


Por supuesto, si lo desea, los mismos botones de filtro se pueden extraer en una entidad separada, que se puede utilizar para transmitir el tipo y el nombre del 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'});
});

Del mismo modo, gracias a su trabajo basado en la pila, efector-dom le permite representar libremente no solo elementos individuales, sino también un comportamiento general.


El código procesado se aplicará específicamente a los elementos necesarios, por ejemplo:


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

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

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

11. Total


Personalmente, me impresionó la simplicidad de trabajar con el nuevo render, especialmente porque ya tenía experiencia trabajando con efector en una aplicación grande.


Espero una versión estable, algún tipo de popularización para la oportunidad de usar el render en el producto.

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


All Articles