Integrando React.js en aplicaciones Symfony (deSymfony 2016)
-
Upload
ignacio-martin -
Category
Software
-
view
771 -
download
1
Transcript of Integrando React.js en aplicaciones Symfony (deSymfony 2016)
deSymfony 16-17 septiembre 2016 Madrid
INTEGRANDO REACT.JS EN APLICACIONES SYMFONYNacho Martín
deSymfony
¡Muchas gracias a nuestros patrocinadores!
Programo en Limenius
Casi todos los proyectos necesitan un frontend rico, por una razón o por otra
Hacemos aplicaciones a medida
Así que le hemos dado unas cuantas vueltas
Objetivo: Mostrar cosas que nos encontramos al usar React desde Symfony, en tierra de nadie, y que ninguno de los dos va a documentar.
¿A mí qué me importa el frontend?
¿Qué es React.js?
Echar huevos en sartén
La premisa fundamental
Cómo hacer una tortillaComprar huevos
Romper huevos
Echar huevos en sartén
Batir huevos
La premisa fundamental
Cómo hacer una tortillaComprar huevos
Romper huevos
Opciones:
La premisa fundamental
Opciones:
La premisa fundamental
1: Repintamos todo.
Opciones:
La premisa fundamental
1: Repintamos todo. Simple
Opciones:
La premisa fundamental
1: Repintamos todo. Simple Poco eficiente
Opciones:
2: Buscamos en el DOM dónde insertar, qué mover, qué eliminar.
La premisa fundamental
1: Repintamos todo. Simple Poco eficiente
Opciones:
2: Buscamos en el DOM dónde insertar, qué mover, qué eliminar.
La premisa fundamental
1: Repintamos todo. Simple
Complejo
Poco eficiente
Opciones:
2: Buscamos en el DOM dónde insertar, qué mover, qué eliminar.
La premisa fundamental
1: Repintamos todo. Simple
Muy eficienteComplejo
Poco eficiente
Opciones:
2: Buscamos en el DOM dónde insertar, qué mover, qué eliminar.
La premisa fundamental
1: Repintamos todo. Simple
Muy eficienteComplejo
Poco eficiente
React nos permite hacer 1, aunque en la sombra hace 2
Dame un estado y una función render() que dependa de él, y olvídate de cómo y cuándo pinto.*
La premisa fundamental
Dame un estado y una función render() que dependa de él, y olvídate de cómo y cuándo pinto.*
La premisa fundamental
* A menos que quieras tener control absoluto.
¡Clícame! Clicks: 0
Nuestro primer componente
¡Clícame! Clicks: 1
Nuestro primer componente
¡Clícame!
Nuestro primer componenteimport React, { Component } from 'react';
class Counter extends Component { constructor(props) { super(props); this.state = {count: 1}; }
tick() { this.setState({count: this.state.count + 1}); }
render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> ); }}
export default Counter;
Nuestro primer componenteimport React, { Component } from 'react';
class Counter extends Component { constructor(props) { super(props); this.state = {count: 1}; }
tick() { this.setState({count: this.state.count + 1}); }
render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> ); }}
export default Counter;
Sintaxis ES6 (opcional)
Nuestro primer componenteimport React, { Component } from 'react';
class Counter extends Component { constructor(props) { super(props); this.state = {count: 1}; }
tick() { this.setState({count: this.state.count + 1}); }
render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> ); }}
export default Counter;
Sintaxis ES6 (opcional)
Estado inicial
Nuestro primer componenteimport React, { Component } from 'react';
class Counter extends Component { constructor(props) { super(props); this.state = {count: 1}; }
tick() { this.setState({count: this.state.count + 1}); }
render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> ); }}
export default Counter;
Sintaxis ES6 (opcional)
Modificar estado
Estado inicial
Nuestro primer componenteimport React, { Component } from 'react';
class Counter extends Component { constructor(props) { super(props); this.state = {count: 1}; }
tick() { this.setState({count: this.state.count + 1}); }
render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> ); }}
export default Counter;
Sintaxis ES6 (opcional)
Modificar estado
render(), lo llama React
Estado inicial
Trabajar con el estado
Trabajar con el estado
constructor(props) { super(props); this.state = {count: 1};}
Estado inicial
Trabajar con el estado
constructor(props) { super(props); this.state = {count: 1};}
Estado inicial
this.setState({count: this.state.count + 1});
Asignar estado
Trabajar con el estado
constructor(props) { super(props); this.state = {count: 1};}
Estado inicial
this.setState({count: this.state.count + 1});
Asignar estado
this.state.count = this.state.count + 1;
Simplemente recordar evitar
render() y JSXrender() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> );}
No es HTML, es JSX. React lo transforma internamente a elementos. Buena práctica: Dejar render() lo más limpio posible, solo un return.
render() y JSXrender() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> );}
No es HTML, es JSX. React lo transforma internamente a elementos.
Algunas cosas cambian
Buena práctica: Dejar render() lo más limpio posible, solo un return.
render() y JSXrender() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> );}
No es HTML, es JSX. React lo transforma internamente a elementos.
Algunas cosas cambian
Entre {} podemos insertar expresiones JS
Buena práctica: Dejar render() lo más limpio posible, solo un return.
Thinking in Reactrender() {
return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> );}
Thinking in Reactrender() {
return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> );}
Aquí no modificar el estado
Thinking in Reactrender() {
return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> );}
Aquí no Ajax
Thinking in Reactrender() {
return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> );}
Aquí no calcular decimales de pi y enviar un email
Importante: pensar la jerarquía
Importante: pensar la jerarquía
Jerarquía de componentes: propsclass CounterGroup extends Component { render() { return ( <div> <Counter name="amigo"/> <Counter name="señor"/> </div> ); }}
render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}> Clícame {this.props.name} </button> <span>Clicks: {this.state.count}</span> </div> );}
y en Counter…
Jerarquía de componentes: propsclass CounterGroup extends Component { render() { return ( <div> <Counter name="amigo"/> <Counter name="señor"/> </div> ); }}
render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}> Clícame {this.props.name} </button> <span>Clicks: {this.state.count}</span> </div> );}
y en Counter…
Jerarquía de componentes: propsclass CounterGroup extends Component { render() { return ( <div> <Counter name="amigo"/> <Counter name="señor"/> </div> ); }}
Pro tip: componentes sin estado
const Saludador = (props) => { <div> <div>Hola {props.name}</div> </div>}
Todo depende del estado, por tanto:
Todo depende del estado, por tanto:
•Podemos reproducir estados,
Todo depende del estado, por tanto:
•Podemos reproducir estados,•rebobinar,
Todo depende del estado, por tanto:
•Podemos reproducir estados,•rebobinar,•loguear cambios de estado,
Todo depende del estado, por tanto:
•Podemos reproducir estados,•rebobinar,•loguear cambios de estado,•hacer álbum de estilo
Todo depende del estado, por tanto:
•Podemos reproducir estados,•rebobinar,•loguear cambios de estado,•hacer álbum de estilo•…
Learn once, write everywhere
¿Y si en lugar de algo así…
render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> ); }
…tenemos algo así?render () { return ( <View> <ListView dataSource={dataSource} renderRow={(rowData) => <TouchableOpacity > <View> <Text>{rowData.name}</Text> <View> <SwitchIOS onValueChange={(value) => this.setMissing(item, value)} value={item.missing} /> </View> </View> </TouchableOpacity> } /> </View> );}
…tenemos algo así?render () { return ( <View> <ListView dataSource={dataSource} renderRow={(rowData) => <TouchableOpacity > <View> <Text>{rowData.name}</Text> <View> <SwitchIOS onValueChange={(value) => this.setMissing(item, value)} value={item.missing} /> </View> </View> </TouchableOpacity> } /> </View> );}
React Native
React Targets
• Web - react-dom • Mobile - React Native • Gl shaders - gl-react • Canvas - react-canvas • Terminal - react-blessed
react-blessed (terminal)
Setup
¿Assetic?
Setup recomendado
Setup recomendado
WebpackPros
Webpack
• Gestiona dependencias por nosotros.Pros
Webpack
• Gestiona dependencias por nosotros.• Permite varios entornos: producción, desarrollo, ….
Pros
Webpack
• Gestiona dependencias por nosotros.• Permite varios entornos: producción, desarrollo, ….• Recarga automática de página (e incluso hot reload).
Pros
Webpack
• Gestiona dependencias por nosotros.• Permite varios entornos: producción, desarrollo, ….• Recarga automática de página (e incluso hot reload).• Uso de preprocesadores/“transpiladores”, como Babel.
Pros
Webpack
• Gestiona dependencias por nosotros.• Permite varios entornos: producción, desarrollo, ….• Recarga automática de página (e incluso hot reload).• Uso de preprocesadores/“transpiladores”, como Babel.• Los programadores de frontend no nos arquearán la ceja.
Pros
Webpack
• Gestiona dependencias por nosotros.• Permite varios entornos: producción, desarrollo, ….• Recarga automática de página (e incluso hot reload).• Uso de preprocesadores/“transpiladores”, como Babel.• Los programadores de frontend no nos arquearán la ceja.
Pros
Contras
Webpack
• Gestiona dependencias por nosotros.• Permite varios entornos: producción, desarrollo, ….• Recarga automática de página (e incluso hot reload).• Uso de preprocesadores/“transpiladores”, como Babel.• Los programadores de frontend no nos arquearán la ceja.
Pros
Contras• Tiene su curva de aprendizaje.
Webpack
• Gestiona dependencias por nosotros.• Permite varios entornos: producción, desarrollo, ….• Recarga automática de página (e incluso hot reload).• Uso de preprocesadores/“transpiladores”, como Babel.• Los programadores de frontend no nos arquearán la ceja.
Pros
Contras• Tiene su curva de aprendizaje.
Ejemplo completo: https://github.com/Limenius/symfony-react-sandbox
Inserción
<div id="react-placeholder"></div>
import ReactDOM from 'react-dom';
ReactDOM.render( <Counter name="amigo">, document.getElementById('react-placeholder'));
HTML
JavaScript
Integración con Symfony https://github.com/Limenius/ReactBundle
ReactBundle
{{ react_component('RecipesApp', {'props': props}) }}
import ReactOnRails from 'react-on-rails';import RecipesApp from './RecipesAppServer';
ReactOnRails.register({ RecipesApp });
Twig:
JavaScript:
ReactBundle
{{ react_component('RecipesApp', {'props': props}) }}
import ReactOnRails from 'react-on-rails';import RecipesApp from './RecipesAppServer';
ReactOnRails.register({ RecipesApp });
Twig:
JavaScript:
ReactBundle
{{ react_component('RecipesApp', {'props': props}) }}
import ReactOnRails from 'react-on-rails';import RecipesApp from './RecipesAppServer';
ReactOnRails.register({ RecipesApp });
Twig:
JavaScript:
ReactBundle
{{ react_component('RecipesApp', {'props': props}) }}
import ReactOnRails from 'react-on-rails';import RecipesApp from './RecipesAppServer';
ReactOnRails.register({ RecipesApp });
Twig:
JavaScript:
<div class="js-react-on-rails-component" style="display:none" data-component-name=“RecipesApp” data-props=“[mi Array en JSON]" data-trace=“false" data-dom-id=“sfreact-57d05640f2f1a”></div>
HTML generado:
Aplicaciones universales
Dame un estado y una función render() que dependa de él, y olvídate de cómo y cuándo pinto.
La premisa fundamental
Dame un estado y una función render() que dependa de él, y olvídate de cómo y cuándo pinto.
La premisa fundamental
Podemos renderizar componentes desde el servidor.
Dame un estado y una función render() que dependa de él, y olvídate de cómo y cuándo pinto.
La premisa fundamental
Podemos renderizar componentes desde el servidor.
• SEO friendly.
Dame un estado y una función render() que dependa de él, y olvídate de cómo y cuándo pinto.
La premisa fundamental
Podemos renderizar componentes desde el servidor.
• SEO friendly.• Carga de página más rápida.
Dame un estado y una función render() que dependa de él, y olvídate de cómo y cuándo pinto.
La premisa fundamental
Podemos renderizar componentes desde el servidor.
• SEO friendly.• Carga de página más rápida.• Podemos cachear.
Client-side
{{ react_component('RecipesApp', {'props': props, 'rendering': 'client-side'}}) }}
TWIG
Client-side
{{ react_component('RecipesApp', {'props': props, 'rendering': 'client-side'}}) }}
TWIG
Client-side
{{ react_component('RecipesApp', {'props': props, 'rendering': 'client-side'}}) }}
TWIG
<div class="js-react-on-rails-component" style="display:none" data-component-name=“RecipesApp” data-props=“…” data-dom-id=“sfreact-57d05640f2f1a”></div>
HTML que devuelve el servidor
Client-side
{{ react_component('RecipesApp', {'props': props, 'rendering': 'client-side'}}) }}
TWIG
<div class="js-react-on-rails-component" style="display:none" data-component-name=“RecipesApp” data-props=“…” data-dom-id=“sfreact-57d05640f2f1a”></div>
HTML que devuelve el servidor
Generado en el navegador<div id="sfreact-57d05640f2f1a"><div data-reactroot="" data-reactid="1" data-react-checksum="2107256409"><ol class="breadcrumb" data-reactid="2"><li class="active" data-reactid=“3">Recipes</li>……</div>
Client-side y server-side
{{ react_component('RecipesApp', {'props': props, 'rendering': 'both'}}) }}
TWIG
Client-side y server-side
{{ react_component('RecipesApp', {'props': props, 'rendering': 'both'}}) }}
TWIG
Client-side y server-side
{{ react_component('RecipesApp', {'props': props, 'rendering': 'both'}}) }}
TWIG
HTML que devuelve el servidor<div id="sfreact-57d05640f2f1a"><div data-reactroot="" data-reactid="1" data-react-checksum="2107256409"><ol class="breadcrumb" data-reactid="2"><li class="active" data-reactid=“3">Recipes</li>……</div>
Client-side y server-side
{{ react_component('RecipesApp', {'props': props, 'rendering': 'both'}}) }}
TWIG
HTML que devuelve el servidor<div id="sfreact-57d05640f2f1a"><div data-reactroot="" data-reactid="1" data-react-checksum="2107256409"><ol class="breadcrumb" data-reactid="2"><li class="active" data-reactid=“3">Recipes</li>……</div>
Y React en el navegador toma el control al evaluar el código
Aplicaciones universales: Opciones
Opción 1: llamar a subproceso node.jsLlamamos a node.js con el componente Process de Symfony
* Cómodo (si tenemos node.js instalado).
* Lento.
Librería: https://github.com/nacmartin/phpexecjs
Opción 2: v8jsUsamos la extensión de PHP v8js
* Cómodo (aunque puede que haya que compilar la extensión y v8).
* Lento por ahora, potencialmente podríamos tener v8 precargada usando php-pm.
Librería: https://github.com/nacmartin/phpexecjs
Configuración Opciones 1 y 2
limenius_react: serverside_rendering: mode: "phpexecjs"
phpexecjs detecta si tenemos la extensión v8js, y si no llama a node.js
config.yml:
Opción 3: Servidor externoTenemos un servidor node.js “tonto” que nos renderiza React.
Es un servidor de <100 líneas, que es independiente de nuestra lógica.
* “Incómodo” (hay que mantener el servidor node.js corriendo, que tampoco es para morirse).
* Rápido.
Ver ejemplo en https://github.com/Limenius/symfony-react-sandbox
Configuración Opción 3
limenius_react: serverside_rendering: mode: “external” server-socket-path: “../tal_y_tal/node.sock”
config.yml:
Lo mejor de los dos mundosEn desarrollo usar llamada a node.js o v8js con phpexecjs.
En producción tener un servidor externo.
Si podemos cachear, menos problema.
Es decir:
limenius_react: serverside_rendering: mode: “external” server-socket-path: “../tal_y_tal/node.sock”
config.yml:
limenius_react: serverside_rendering: mode: "phpexecjs"
config_dev.yml:
¿Vale la pena una app universal?
¿Vale la pena una app universal?
En ocasiones sí, pero introduce complejidad.
Soporte para Redux (+brevísima introducción a Redux)
Redux: una cuestión de estado
guardar
Tu nombre: Juan
Hola, Juan
Cosas de Juan:
Redux: una cuestión de estado
guardar
Tu nombre: Juan
Hola, Juan
Cosas de Juan:
Redux: una cuestión de estado
guardar
Tu nombre: Juan
Hola, Juan
Cosas de Juan:
state.name
callback para cambiarlo
dispatch(changeName(‘Juan'));
Componente
dispatch(changeName(‘Juan'));
Componente
changeName = (name) => { return { type: ‘CHANGE_NAME', name }}
Action
dispatch(changeName(‘Juan'));
Componente
changeName = (name) => { return { type: ‘CHANGE_NAME', name }}
Action
const todo = (state = {name: null}, action) => { switch (action.type) { case 'CHANGE_USER': return { name: action.name } }}
Reducer
dispatch(changeName(‘Juan'));
Componente
changeName = (name) => { return { type: ‘CHANGE_NAME', name }}
Action
const todo = (state = {name: null}, action) => { switch (action.type) { case 'CHANGE_USER': return { name: action.name } }}
Reducer
Store
this.props.name == ‘Juan';dispatch(changeName(‘Juan'));
Componente
changeName = (name) => { return { type: ‘CHANGE_NAME', name }}
Action
const todo = (state = {name: null}, action) => { switch (action.type) { case 'CHANGE_USER': return { name: action.name } }}
Reducer
Store
Redux en ReactBundle
Ver ejemplo en https://github.com/Limenius/symfony-react-sandbox
import ReactOnRails from 'react-on-rails';import RecipesApp from './RecipesAppClient';import recipesStore from '../store/recipesStore';
ReactOnRails.registerStore({recipesStore})ReactOnRails.register({ RecipesApp });
Twig:
JavaScript:
{{ redux_store('recipesStore', props) }}{{ react_component('RecipesApp') }}
Redux en ReactBundle
Ver ejemplo en https://github.com/Limenius/symfony-react-sandbox
import ReactOnRails from 'react-on-rails';import RecipesApp from './RecipesAppClient';import recipesStore from '../store/recipesStore';
ReactOnRails.registerStore({recipesStore})ReactOnRails.register({ RecipesApp });
Twig:
JavaScript:
{{ redux_store('recipesStore', props) }}{{ react_component('RecipesApp') }}{{ react_component('OtroComponente') }}
Compartir store entre componentes
Compartir store entre componentesReact
ReactReact
Twig
Twig
React
Al compartir store comparten estado
Twig
Formularios, un caso especial
Formularios muy dinámicos•Dentro de aplicaciones React.
•Formularios importantísimos en los que un detalle de usabilidad vale mucho dinero.
•Formularios muy específicos.
•Formularios muy dinámicos que no cansen (ver typeform por ejemplo).
El problema
Supongamos un form así
public function buildForm(FormBuilderInterface $builder, array $options){ $builder ->add('country', ChoiceType::class, [ 'choices' => [ 'España' => 'es', 'Portugal' => 'pt', ] ]) ->add('addresses', CollectionType::class, ...);};
Forms en HTML
$form->createView();
state.usuario
Forms en HTML
$form->createView();
state.usuario
Forms en HTML
$form->createView();
submit
país: España
EspañaPortugal
direcciones:
C\ tal-+state.usuario
Forms en HTML
$form->createView();
submit
país: España
EspañaPortugal
direcciones:
C\ tal-+state.usuario
Forms en HTML
$form->createView();
submit
país: España
EspañaPortugal
direcciones:
C\ tal-+state.usuario
POST bien formadito con country:’es’
y no ‘España’, ‘espana', ‘spain', ‘0’…
Forms en HTML
$form->createView();
$form->submit($request);
submit
país: España
EspañaPortugal
direcciones:
C\ tal-+state.usuario
POST bien formadito con country:’es’
y no ‘España’, ‘espana', ‘spain', ‘0’…
Forms en API
$form;
state.usuario
Forms en API
$form;
submit
país: España
EspañaPortugal
direcciones:
C\ tal-+state.usuario
✘
Forms en API
$form;
submit
país: España
EspañaPortugal
direcciones:
C\ tal-+state.usuario
✘¿Cómo sabemos los campos,
o los choices? ¡A documentar! :(
Forms en API
$form;
$form->submit($request);
submit
país: España
EspañaPortugal
direcciones:
C\ tal-+state.usuario
POST “voy a tener suerte”
✘¿Cómo sabemos los campos,
o los choices? ¡A documentar! :(
Forms en API
$form;
$form->submit($request);
submit
país: España
EspañaPortugal
direcciones:
C\ tal-+state.usuario
POST “voy a tener suerte”
✘¿Cómo sabemos los campos,
o los choices? ¡A documentar! :(
This form should not contain extra fields!!1
Forms en API
$form;
$form->submit($request);
submit
país: España
EspañaPortugal
direcciones:
C\ tal-+state.usuario
POST “voy a tener suerte”
✘¿Cómo sabemos los campos,
o los choices? ¡A documentar! :(
This form should not contain extra fields!!1The value you selected is not a valid choice!!
Forms en API
$form;
$form->submit($request);
submit
país: España
EspañaPortugal
direcciones:
C\ tal-+state.usuario
POST “voy a tener suerte”
✘¿Cómo sabemos los campos,
o los choices? ¡A documentar! :(
This form should not contain extra fields!!1The value you selected is not a valid choice!!One or more of the given values is invalid!! :D
Forms en API
$form;
$form->submit($request);
submit
país: España
EspañaPortugal
direcciones:
C\ tal-+state.usuario
POST “voy a tener suerte”
✘¿Cómo sabemos los campos,
o los choices? ¡A documentar! :(
This form should not contain extra fields!!1The value you selected is not a valid choice!!One or more of the given values is invalid!! :DMUHAHAHAHAHA!!!!!
Definir (y mantener) por triplicado
Form SF API docs Form Cliente
:(
¿Cuántos programadores hacen falta para hacer un formulario?
Caso: Wizard complejo
Caso: Wizard complejo
Caso: Wizard complejo
Lo que necesitamos
$form->createView();
HTML
API$miTransformador->transform($form);
Lo que necesitamos
$form->createView();
HTML
¡Serializar! Vale, ¿A qué formato?
API$miTransformador->transform($form);
JSON Schema
Qué pinta tiene{ "$schema": "http://json-schema.org/draft-04/schema#", "title": "Product", "description": "A product from Acme's catalog", "type": "object", "properties": { "name": { "description": "Name of the product", "type": "string" }, "price": { "type": "number", "minimum": 0, "exclusiveMinimum": true }, "tags": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true } }, "required": ["id", "name", "price"]}
definiciones, tipos, reglas de validación :)
Nuevo recurso: mi-api/products/form
A partir del schema generamos form
Generadores client-side
• jdorn/json-editor: no React, es un veterano.
• mozilla/react-jsonschema-form: React. creado por un antiguo Symfonero (Niko Perriault).
• limenius/liform-react: React + redux; integrado con redux-form (I ♥ redux-form)
• …
• Crear el nuestro puede ser conveniente.
Generadores client-side: diferenciasCada uno amplía json-schema a su manera para
especificar detalles UI: Orden de campos, qué widget específico usar, etc.
Si queremos usarlos al máximo hay que generar un json-schema específico para cada uno
(no son totalmente intercambiables).
Ejemplo: usar editor Wysiwyg en un campo texto
Ejemplo: LiformBundle y liform-react
limenius/LiformBundle: Genera json-schema a partir de formularios Symfony.
limenius/liform-react: Generador de formularios React (con redux-form) a partir de json-schema.
Son Work in progress
Cómo serializar: resolvers + transformers
$transformer = $resolver->resolve($form);
$jsonSchema = $transformer->transform($form);
Ejemplo de esta técnica: https://github.com/Limenius/LiformBundle
Resolver
public function resolve(FormInterface $form){ $types = FormUtil::typeAncestry($form);
foreach ($types as $type) { if (isset($this->transformers[$type])) { return $this->transformers[$type]; } }}
Misión: Encuentra el transformer apropiado para el form
TransformerMisión: Inspecciona el form y crea un array.
Si es compuesto resuelve+transforma los hijos.class IntegerTransformer extends AbstractTransformer{ public function transform(FormInterface $form) { $schema = [ 'type' => 'integer', ]; if ($liform = $form->getConfig()->getOption('liform')) { if ($format = $liform['format']) { $schema['format'] = $format; } } $this->addCommonSpecs($form, $schema);
return $schema; }} protected function addLabel($form, &$schema) { if ($label = $form->getConfig()->getOption('label')) { $schema['title'] = $label; } }
TransformerRecopila información de cada Form Field.
Podemos sacar mucha información: •Valores por defecto & placeholders. •Atributos del formulario. •Validadores.
Ejemplo: validación ‘pattern’
protected function addPattern($form, &$schema){ if ($attr = $form->getConfig()->getOption('attr')) { if (isset($attr['pattern'])) { $schema['pattern'] = $attr['pattern']; } }}
Esta técnica vale también para Angular, Backbone, mobile…
Repaso:
Repaso:• Qué es React • Setup • Apps universales (ReactBundle) • Para qué sirve Redux • El problema de los formularios • Cómo aliviarlo con JSON Schema
MADRID · NOV 27-28 · 2015
¡Gracias!@nacmartin
http://limenius.com Formación, consultoría
y desarrollo de proyectos