referencias
http://facebook.github.io/flux/docs/overview.html#content
https://reactjs.org/blog/2014/07/30/flux-actions-and-the-dispatcher.html
https://github.com/facebook/flux/tree/master/examples/flux-todomvc/
Práctica de Flux
Github paso a paso de la práctica
https://github.com/jalbertomr/my-todomvc
Al realizar la práctica nos requiere de unos prerequisitos, estos son conocer ES6 ya que sa hacen uso de algunas de las nuevas características que se le incorporan al javascript. también no requiere tener noción de React y Flux.
1.- En el primer paso Getting Started, clonamos e instalamos el flux repo desde git a nuestra máquina, para después copiar e instalar el subdirectorio examples/flux-shell a examples/my-todomvc. al ejecutar el servicio npm run watch, veremos un hello World! en examples/my-todomvc/index.html
esto es por que tiene una configuración de aplicación minima
<DIR> src
.babelrc
.gitignore
package.json
README.md
webpack.config.js
En src/root.js
'use strict';
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<div>Hello World!</div>, document.getElementById('root'));
2.- En el segundo paso se copian archivos de configuración examples/todomvc-common
que tiene pocos archivos de utilerías comunes, entre ellos base.css que da formato de estilo a la página. index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Flux • TodoMVC</title>
<link rel="stylesheet" href="todomvc-common/base.css">
</head>
<body>
<section id="todoapp"></section>
<footer id="info">
<p>Double-click para editar tarea</p>
<p>Parte de <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="./bundle.js"></script>
</body>
</html>
Se actualiza \my-todomvc\src\root.js para que haga referencia a <section id="todoapp"...
y se renderé.
ReactDOM.render(<div>Hola Mundito!</div>, document.getElementById('todoapp'));
Con esto veremos Hola Mundito! con presentación estilizada.
3.- En el tercer paso, se instala flux, con la siguiente estuctura de directorios y archivos.
src
├── containers
│ └── AppContainer.js
├── data
│ ├── TodoActions.js
│ ├── TodoActionTypes.js
│ ├── TodoDispatcher.js
│ └── TodoStore.js
├── root.js
└── views
└── AppView.js
En TodoDispatcher, se importa de flux y se instancia.
import {Dispatcher} from 'flux';
export default new Dispatcher();
Se crean Actions y ActionTypes, TodoAction.js y TodoActionTypes.js que tendrán todas las actions de la aplicación.
data/TodoActionTypes.js tiene un enumerado de las acciones de la aplicación.
const ActionTypes = {
ADD_TODO: 'ADD_TODO',
};
export default ActionTypes;
En data/TodoActions.js, cada function despacha una action.
import TodoActionTypes from './TodoActionTypes';
import TodoDispatcher from './TodoDispatcher';
const Actions = {
addTodo(text) {
TodoDispatcher.dispatch({
type: TodoActionTypes.ADD_TODO,
text,
});
},
};
export default Actions;
Ahora se levanta el Store. data/TodoStore.js este almacenará toda la información de los objetos de la aplicación. Se usa un map Immutable como state.
Immutable.js es una open source de facebook que nos permite manejar collections de datos, con métodos tradicionales más métodos nuevos referentes a la inmutabilidad.
Immutable es una forma de almacenar datos de forma eficiente con la característica de no modificar el valor original de los datos, sino considerar que cambian de estado. Al decir eficiente, significa que no se duplican valores del estado, sino solo los pequeños cambios se agregan al nuevo estado de los datos, esto se hace por medio de Structural Sharing, algo así como arboles que apuntan a los datos, y las raices de los arboles representan los estados.
import Immutable from 'immutable';
import {ReduceStore} from 'flux/utils';
import TodoActionTypes from './TodoActionTypes';
import TodoDispatcher from './TodoDispatcher';
class TodoStore extends ReduceStore {
constructor() {
super(TodoDispatcher);
}
getInitialState() {
return Immutable.OrderedMap();
}
reduce(state, action) {
switch (action.type) {
case TodoActionTypes.ADD_TODO:
// Do nothing for now, we will add logic here soon!
return state;
default:
return state;
}
}
}
export default new TodoStore();
se hace una simple vista usando React, views/AppView.js
import React from 'react';
function AppView() {
return <div>Hola desde Flux!</div>;
}
export default AppView;
Los CONTAINERS son los que conectan el STATE de los STORES a las VIEWS.
En containers/AppContainer.js se tiene:
import AppView from '../views/AppView';
import {Container} from 'flux/utils';
import TodoStore from '../data/TodoStore';
function getStores() {
return [
TodoStore,
];
}
function getState() {
return {
todos: TodoStore.getState(),
};
}
export default Container.createFunctional(AppView, getStores, getState);
Finalmente se actualiza root.js de la aplicación para que renderé AppContainer.
import AppContainer from './containers/AppContainer';
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<AppContainer />, document.getElementById('todoapp'));
La aplicación dirá Hola desde Flux!
4.- Se renderean Todos.
Se usa Immutable.js para retener los datos de cada tarea. Esto nos da un a bonita API para actualizar información sin necesidad de preocuparnos por una actualización accidental de un Todo,
data/Todo.js
import Immutable from 'immutable';
const Todo = Immutable.Record({
id: '',
complete: false,
text: '',
});
export default Todo;
Ahora actuializamos data/TodoStore.js, utilizando la estructura Todo, y Counter.js para implementar la acción ADD_TODO.
import Counter from './Counter';
import Todo from './Todo';
class TodoStore extends ReduceStore {
...
reduce(state, action) {
switch (action.type) {
case TodoActionTypes.ADD_TODO:
// Don't add todos with no text.
if (!action.text) {
return state;
}
const id = Counter.increment();
return state.set(id, new Todo({
id,
text: action.text,
complete: false,
}));
default:
return state;
}
}
}
Actualizamos la VISTA para que renderé los Todos que son almacenados. views/AppView.js
function AppView(props) {
return (
<div>
<Header {...props} />
<Main {...props} />
<Footer {...props} />
</div>
);
}
function Header(props) {
return (
<header id="header">
<h1>todos</h1>
</header>
);
}
function Main(props) {
if (props.todos.size === 0) {
return null;
}
return (
<section id="main">
<ul id="todo-list">
{[...props.todos.values()].reverse().map(todo => (
<li key={todo.id}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={todo.complete}
onChange={
// Empty function for now, we will implement this later.
() => {}
}
/>
<label>{todo.text}</label>
<button
className="destroy"
onClick={
// Empty function for now, we will implement this later.
() => {}
}
/>
</div>
</li>
))}
</ul>
</section>
);
}
function Footer(props) {
if (props.todos.size === 0) {
return null;
}
return (
<footer id="footer">
<span id="todo-count">
<strong>
{props.todos.size}
</strong>
{' items left'}
</span>
</footer>
);
}
Para probar la aplicación, creamos algunos Todos de prueba , después del rendereo inicial.
import AppContainer from './containers/AppContainer';
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<AppContainer />, document.getElementById('todoapp'));
// We will remove these lines later:
import TodoActions from './data/TodoActions';
TodoActions.addTodo('Mi Primer Tarea');
TodoActions.addTodo('Otra Tarea');
TodoActions.addTodo('Terminando el tutorial');
Aún no hay interactividad, por lo que los botones no funcionan aún.
5.- Agregando Interactividad.
Se agregan Actions a sata/TodoActionTypes.js y data/TodoActions.js
TodoActionTypes.js
const ActionTypes = {
ADD_TODO: 'ADD_TODO',
DELETE_TODO: 'DELETE_TODO',
TOGGLE_TODO: 'TOGGLE_TODO',
};
TodoActions.js
const Actions = {
addTodo(text) {
TodoDispatcher.dispatch({
type: TodoActionTypes.ADD_TODO,
text,
});
},
deleteTodo(id) {
TodoDispatcher.dispatch({
type: TodoActionTypes.DELETE_TODO,
id,
});
},
toggleTodo(id) {
TodoDispatcher.dispatch({
type: TodoActionTypes.TOGGLE_TODO,
id,
});
},
};
Actualizamos data/TodoStore.js
class TodoStore extends ReduceStore {
...
reduce(state, action) {
switch (action.type) {
...
case TodoActionTypes.DELETE_TODO:
return state.delete(action.id);
case TodoActionTypes.TOGGLE_TODO:
return state.update(
action.id,
todo => todo.set('complete', !todo.complete),
);
...
}
}
}
Ahora el Store ya es capaz de borrar y alternar una Tarea. En una aplicación de Flux el único lugar que debería tener conocimiento de Flux es el CONTAINER, esto significa que debemos definir los callbacks o funciones de llamado en AppContainer y hacer la transferencia a AppView, La Vista no despacha las acciones directamente. Esto hace fácil de usar, probar y cambiar las vistas.
Se actualiza container/AppContainer.js
import TodoActions from '../data/TodoActions';
function getState() {
return {
todos: TodoStore.getState(),
onDeleteTodo: TodoActions.deleteTodo,
onToggleTodo: TodoActions.toggleTodo,
};
}
Ahora necesitamos utilizar estos callbacks en la vista, para desplegar el número de Tareas completadas.
Actualizamos views/AppViews.js
function Main(props) {
if (props.todos.size === 0) {
return null;
}
return (
<section id="main">
<ul id="todo-list">
{[...props.todos.values()].reverse().map(todo => (
<li key={todo.id}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={todo.complete}
onChange={() => props.onToggleTodo(todo.id)}
/>
<label>{todo.text}</label>
<button
className="destroy"
onClick={() => props.onDeleteTodo(todo.id)}
/>
</div>
</li>
))}
</ul>
</section>
);
}
function Footer(props) {
if (props.todos.size === 0) {
return null;
}
const remaining = props.todos.filter(todo => !todo.complete).size;
const phrase = remaining === 1 ? ' item left' : ' items left';
return (
<footer id="footer">
<span id="todo-count">
<strong>
{remaining}
</strong>
{phrase}
</span>
</footer>
);
}
Al refrescar la página podremos borrar y alternar el estado de las Tareas.
6.- Agregando funcionalidad Extra
Una vez familiarizados con la estructura de la aplicación de tareas pendientes, la guía nos indica los siguientes pasos en orden adecuado, y es de gran ayuda consultar el ejemplo original.
1.- Crear una vista NewTodo
AppView.js se integra <NewTodo {...props} />
- Crear TodoDraftStore la cual dará seguimiento al contenido de NewTodo Input, está responderá a dos acciones:
- UPDATE_DRAFT el cual cambia el contenido redactado.
- ADD_TODO el cual limpia el contenido redactado.
Se integran estas acciones a TodoActionTypes.js, TodoActions.js
Se crea TodoDraftStore.js
import {ReduceStore} from 'flux/utils';
import TodoActionTypes from './TodoActionTypes';
import TodoDispacher from './TodoDispatcher';
class TodoDraftStore extends ReduceStore{
constructor() {
super(TodoDispacher);
}
getInitialState() {
return '';
}
reduce(state, action){
switch (action.type) {
case TodoActionTypes.ADD_TODO:
return '';
case TodoActionTypes.UPDATE_DRAFT:
return action.text;
default:
return state;
}
}
}
export default new TodoDraftStore();
- Crear la acción updateDraft y pasarla a través del CONTAINER.
Un FluxContainer es usado para subscribir un componente React a multiples STORES.
El Componente recibe información desde los STORES por medio del STATE.
Se integra a AppContainer.js
Al final de appContainer.js se exporta la función
export default Container.createFunctional(AppView, getStores, getState);
Esta es una función alterna a la de Container.
create(base: ReactClass, options: ?Object): ReactClass
create(...) es usada para transformar una clase React en un CONTAINER que actualiza el STATE cuando cambios relevantes ocurren en STORE.
La cual está documentada en la página, pero no createFunctional(...
Así que tendremos que ver su definición en las flux/utils, concretemente en \flux\lib\FluxContainer.js
function createFunctional(viewFn, _getStores, _calculateState, options) {...
esta función nos permite conectar una functional stateless VIEW al STORE. viewFn es el AppView que contiene componentes React. La forma simple de viewFn es la siguiente:
// FooView.js
*
* function FooView(props) {
* return <div>{props.value}</div>;
* }
*
* module.exports = FooView;
*
*
* // FooContainer.js
*
* function getStores() {
* return [FooStore];
* }
*
* function calculateState() {
* return {
* value: FooStore.getState();
* };
* }
*
* module.exports = FluxContainer.createFunctional(
* FooView,
* getStores,
* calculateState,
* );
En nuestro caso la AppView tiene la misma estructura, solo que un poco más elaborada.
- Amarra a la Vista.
2.- Agregar un botón de Limpiar Completadas en el footer
se crea la accion deleteCompletedTodos en TodoActionTypes.js, TodoActions.js, En CONTAINER
se mapea en el STATE la llamada onDeleteCompletedTodos (AppContainer.js). y en el STORE se agrega el código en CASE para modificar el STATE. esto es quitando (filter) los todos.completed.
3.- Agregar un botón "Completadas Todas", en la sección Main
de la vista.
- Si algún TODO esta incompleto, marcarlos todos como
completos.
- Si todos están Completados, Marcarlos como InCompletos.
Es de Observar como se aplican operaciones de Programación funcional al
STATE, como map, every, set, delete, filter...
4.- Agregar la habilidad de editar las Tareas con Doble click.
- Crear TodoEditStore que dara seguimiento al ID del TODO que será editado.
- Crear la acciones startEditingTodo y StopEditingTodo.
- Crear la accion editTodo.
- Crear el componente de vista TodoEdit, y darle funcionalidad.
Continua...