integrating react.js with php projects
TRANSCRIPT
PHP UK Conference February 2017
Integrating React.js with PHP Projects
Nacho Martín @nacmartin
My name is Nacho Martin.
Almost every project needs a rich frontend, for one reason or another.
We build tailor-made projects.
So we have been thinking about this for some time.
I write code at Limenius
Why (as a PHP developer) should I care about the frontend?
What is React.js?
Pour eggs in the pan
The fundamental premise
How to cook an omeletteBuy eggs
Break eggs
Pour eggs in the pan
Beat eggs
The fundamental premise
How to cook an omeletteBuy eggs
Break eggs
Options:
The fundamental premise
Options:
The fundamental premise
1: Re-render everything.
Options:
The fundamental premise
1: Re-render everything. Simple
Options:
The fundamental premise
1: Re-render everything. Simple Not efficient
Options:
2: Find in the DOM where to insert elements, what to move, what to remove…
The fundamental premise
1: Re-render everything. Simple Not efficient
Options:
2: Find in the DOM where to insert elements, what to move, what to remove…
The fundamental premise
1: Re-render everything. Simple
Complex
Not efficient
Options:
2: Find in the DOM where to insert elements, what to move, what to remove…
The fundamental premise
1: Re-render everything. Simple
EfficientComplex
Not efficient
Options:
2: Find in the DOM where to insert elements, what to move, what to remove…
The fundamental premise
1: Re-render everything. Simple
EfficientComplex
Not efficient
React allows us to do 1, although it does 2 behind the scenes
Give me a state and a render() method that depends on it and forget about how and when to render.*
The fundamental premise
Give me a state and a render() method that depends on it and forget about how and when to render.*
The fundamental premise
* Unless you want more control, which is possible.
Click me! Clicks: 0
Our first component
Click me! Clicks: 1Click me!
Our first component
Our first componentimport 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)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); }}
export default Counter;
Our first componentimport 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)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); }}
export default Counter;
ES6 Syntax (optional but great)
Our first componentimport 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)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); }}
export default Counter;
ES6 Syntax (optional but great)
Initial state
Our first componentimport 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)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); }}
export default Counter;
ES6 Syntax (optional but great)
Set new state
Initial state
Our first componentimport 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)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); }}
export default Counter;
ES6 Syntax (optional but great)
Set new state
render(), called by React
Initial state
Working with state
Working with state
constructor(props) { super(props); this.state = {count: 1};}
Initial state
Working with state
constructor(props) { super(props); this.state = {count: 1};}
Initial state
this.setState({count: this.state.count + 1});
Assign state
Working with state
constructor(props) { super(props); this.state = {count: 1};}
Initial state
this.setState({count: this.state.count + 1});
Assign state
this.state.count = this.state.count + 1;
Just remember: avoid this
render() and JSX
render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> );
It is not HTML, it is JSX. React transforms it internally to HTML elements.
Good practice: make render() as clean as possible, only a return.
render() and JSX
render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> );
It is not HTML, it is JSX. React transforms it internally to HTML elements.
Some things change
Good practice: make render() as clean as possible, only a return.
render() and JSX
render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> );
It is not HTML, it is JSX. React transforms it internally to HTML elements.
Some things change
We can insert JS expressions between {}
Good practice: make render() as clean as possible, only a return.
Thinking in React
render() {
return ( <div className="App"> <button onClick={this.tick.bind(this)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> );}
Thinking in React
render() {
return ( <div className="App"> <button onClick={this.tick.bind(this)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> );}
Here we don’t modify state
Thinking in React
render() {
return ( <div className="App"> <button onClick={this.tick.bind(this)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> );}
Here we don’t make Ajax calls
Thinking in React
render() {
return ( <div className="App"> <button onClick={this.tick.bind(this)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> );}
Here we don’t calculate decimals of PI and send an e-mail with the result
Important: think our hierarchy
Important: think our hierarchy
Component hierarchy: 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)}> Click me! {this.props.name} </button> <span>Clicks: {this.state.count}</span> </div> );}
and in Counter…
Component hierarchy: 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)}> Click me! {this.props.name} </button> <span>Clicks: {this.state.count}</span> </div> );}
and in Counter…
Component hierarchy: propsclass CounterGroup extends Component { render() { return ( <div> <Counter name="amigo"/> <Counter name="señor"/> </div> ); }}
Pro tip: Stateless components
const Greeter = (props) => { <div> <div>Hi {props.name}!</div> </div>}
Tip: Presentational and Container components
const TaskList = (props) => { <div> {props.tasks.map((task, idx) => { <div key={idx}>{task.name}</div> })} </div>}
class TasksListContainer extends React.Component { constructor(props) { super(props) this.state = {tasks: []} } componentDidMount() { // Load data with Ajax and whatnot } render() { return <TaskList tasks={this.state.tasks}/> }}
Everything depends on the state, therefore we can:
Everything depends on the state, therefore we can:
•Reproduce states,
Everything depends on the state, therefore we can:
•Reproduce states,•Rewind,
Everything depends on the state, therefore we can:
•Reproduce states,•Rewind,•Log state changes,
Everything depends on the state, therefore we can:
•Reproduce states,•Rewind,•Log state changes,•Make storybooks,
Everything depends on the state, therefore we can:
•Reproduce states,•Rewind,•Log state changes,•Make storybooks,•…
Learn once, write everywhere
What if instead of this…
render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); }
…we have something like this?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> );}
…we have something like this?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
Recommended setup
WebpackPros
Webpack
• Manages dependenciesPros
Webpack
• Manages dependencies• Allows several environments: production, development, ….
Pros
Webpack
• Manages dependencies• Allows several environments: production, development, ….• Automatic page reload (even hot reload).
Pros
Webpack
• Manages dependencies• Allows several environments: production, development, ….• Automatic page reload (even hot reload).• Can use preprocessors/“transpilers”, like Babel.
Pros
Webpack
• Manages dependencies• Allows several environments: production, development, ….• Automatic page reload (even hot reload).• Can use preprocessors/“transpilers”, like Babel.
Pros
Cons
Webpack
• Manages dependencies• Allows several environments: production, development, ….• Automatic page reload (even hot reload).• Can use preprocessors/“transpilers”, like Babel.
Pros
Cons• It has a non trivial learning curve.
Webpack
• Manages dependencies• Allows several environments: production, development, ….• Automatic page reload (even hot reload).• Can use preprocessors/“transpilers”, like Babel.
Pros
Cons• It has a non trivial learning curve.
I maintain a sandbox: https://github.com/Limenius/symfony-react-sandbox
Insertion
<div id="react-placeholder"></div>
import ReactDOM from 'react-dom';
ReactDOM.render( <Counter name="amigo">, document.getElementById('react-placeholder'));
HTML
JavaScript
Integration with PHP https://github.com/Limenius/ReactRenderer
ReactRenderer
{{ react_component('RecipesApp', {'props': props}) }}
import ReactOnRails from 'react-on-rails';import RecipesApp from './RecipesAppServer';
ReactOnRails.register({ RecipesApp });
Twig:
JavaScript:
ReactRenderer
{{ react_component('RecipesApp', {'props': props}) }}
import ReactOnRails from 'react-on-rails';import RecipesApp from './RecipesAppServer';
ReactOnRails.register({ RecipesApp });
Twig:
JavaScript:
ReactRenderer
{{ 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=“[my Array in JSON]" data-trace=“false" data-dom-id=“sfreact-57d05640f2f1a”></div>
Generated HTML:
Server-side rendering
Give me a state and a render() method that depends on it and forget about how and when to render.
The fundamental premise
Give me a state and a render() method that depends on it and forget about how and when to render.
The fundamental premise
We can render components in the server
Give me a state and a render() method that depends on it and forget about how and when to render.
The fundamental premise
We can render components in the server
• SEO friendly.
Give me a state and a render() method that depends on it and forget about how and when to render.
The fundamental premise
We can render components in the server
• SEO friendly.• Faster perceived page loads.
Give me a state and a render() method that depends on it and forget about how and when to render.
The fundamental premise
We can render components in the server
• SEO friendly.• Faster perceived page loads.• We can cache.
Client-side + Server-side
{{ react_component('RecipesApp', {'props': props, rendering': 'both'}}) }}
TWIG
Client-side + Server-side
{{ react_component('RecipesApp', {'props': props, rendering': 'both'}}) }}
TWIG
HTML returned by the server<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 + Server-side
{{ react_component('RecipesApp', {'props': props, rendering': 'both'}}) }}
TWIG
HTML returned by the server<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>
An then React in the browser takes control over the component
Universal applications: Options
Option 1: Call a node.js subprocessMake a call to node.js using Symfony Process component
* Easy (if we have node.js installed).
* Slow.
Library: https://github.com/nacmartin/phpexecjs
Option 2: v8jsUse PHP extension v8js
* Easy (although compiling the extension and v8 is not a breeze).
* Currently slow, maybe we could have v8 preloaded using php-pm so it is not destroyed after every request-response cycle.
Library: https://github.com/nacmartin/phpexecjs
Option 3: External node.js server
We have “stupid” node.js server used only to render React components.
It has <100 LoC, and it doesn’t know anything about our logic.
* “Annoying” (we have to keep it running, which is not super annoying either).
* Faster.
There is an example a dummy server for this purpose at https://github.com/Limenius/symfony-react-sandbox
Options 1 & 2
$renderer = new PhpExecJsReactRenderer(‘path_to/server-bundle.js’);$ext = new ReactRenderExtension($renderer, 'both');
$twig->addExtension($ext);
phpexecjs detects the presence of the extension v8js, if not, calls node.js
Option 3
$renderer = new ExternalServerReactRenderer(‘../some_path/node.sock’);$ext = new ReactRenderExtension($renderer, 'both');
$twig->addExtension($ext);
The best of the two worlds
In development use node.js or v8js with phpexecjs.
In production use an external server.
If we can cache server-side responses, even better.
Server side rendering, is it worth it?
Server side rendering, is it worth it?
Sometimes yes, but it introduces complexity
Redux support (+very brief introduction to Redux)
Redux: a matter of state
save
Your name: John
Hi, John
John’s stuff
Redux: a matter of state
save
Your name: John
Hi, John
John’s stuff
Redux: a matter of state
save
Your name: John
Hi, John
John’s stuff
state.name
callback to change it
dispatch(changeName(‘John'));
Component
dispatch(changeName(‘John'));
Component
changeName = (name) => { return { type: ‘CHANGE_NAME', name }}
Action
dispatch(changeName(‘John'));
Component
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(‘John'));
Component
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 == ‘John';dispatch(changeName(‘John'));
Component
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 with ReactRenderer
Sample code in 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 with ReactRenderer
Sample code in 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('AnotherComponent') }}
Share store between components
React
ReactReact
Twig
Twig
React
By sharing store they can share state
Twig
Share store between components
Forms, a special case
Dynamic forms, why?
•Inside of React components.
•Important forms where UX means better conversions.
•Very specific forms.
•Very dynamic forms that aren’t boring (see Typeform for instance).
Typically PHP frameworks have a Form Component
Typically PHP frameworks have a Form Component$form (e.g. Form Symfony Component)
Typically PHP frameworks have a Form Component$form (e.g. Form Symfony Component)
Initial values
Typically PHP frameworks have a Form Component$form (e.g. Form Symfony Component)
Initial values
UI hints (widgets, attributes)
Typically PHP frameworks have a Form Component$form (e.g. Form Symfony Component)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Typically PHP frameworks have a Form Component$form (e.g. Form Symfony Component)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Typically PHP frameworks have a Form Component$form (e.g. Form Symfony Component)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Validate
Typically PHP frameworks have a Form Component$form (e.g. Form Symfony Component)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Validate
Return errors
Typically PHP frameworks have a Form Component$form (e.g. Form Symfony Component) $form->createView() (helpers in other Fws not Sf)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Validate
Return errors
Typically PHP frameworks have a Form Component$form (e.g. Form Symfony Component) $form->createView() (helpers in other Fws not Sf)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Validate
Return errors
Render view
Typically PHP frameworks have a Form Component$form (e.g. Form Symfony Component) $form->createView() (helpers in other Fws not Sf)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Validate
Return errors
Render view
Show errors after Submit
Typically PHP frameworks have a Form Component$form (e.g. Form Symfony Component) $form->createView() (helpers in other Fws not Sf)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Validate
Return errors
Render view
Some client-side validation (HTML5)
Show errors after Submit
Using forms in an API$form $form->createView() (helpers in other Fws not Sf)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Validate
Return errors
Render view
Some client-side validation (HTML5)
Show errors after Submit
…and we want more$form $form->createView() (helpers in other Fws not Sf)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Validate
Return errors
Render view
On Submit validation
Some client-side validation (HTML5)
…and we want more$form $form->createView() (helpers in other Fws not Sf)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Validate
Return errors
Render view
On blur sync validation
On Submit validation
Some client-side validation (HTML5)
…and we want more$form $form->createView() (helpers in other Fws not Sf)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Validate
Return errors
Render view
On blur sync validation
On Submit validation
On blur async validation
Some client-side validation (HTML5)
…and we want more$form $form->createView() (helpers in other Fws not Sf)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Validate
Return errors
Render view
On blur sync validation
On Submit validation
On blur async validation
Some client-side validation (HTML5)
All the dynamic goodies
Suppose this Symfony form
public function buildForm(FormBuilderInterface $builder, array $options){ $builder ->add('country', ChoiceType::class, [ 'choices' => [ 'United Kingdom' => 'gb', 'Deutschland' => 'de', 'España' => 'es',
] ]) ->add('addresses', CollectionType::class, ...);};
Forms rendered to HTML
$form->createView();
state.usuario
Forms rendered to HTML
$form->createView();
state.usuario
Forms rendered to HTML
$form->createView();
submit
Country: España
DeutschlandEspaña
Addresses:
Some St.-+state.usuario
Forms rendered to HTML
$form->createView();
submit
Country: España
DeutschlandEspaña
Addresses:
Some St.-+state.usuario
POST well formed with country:’es’
and not ‘España’, ‘espana', ‘spain', ‘0’…
Forms rendered to HTML
$form->createView();
$form->submit($request);
submit
Country: España
DeutschlandEspaña
Addresses:
Some St.-+state.usuario
POST well formed with country:’es’
and not ‘España’, ‘espana', ‘spain', ‘0’…
state.usuario
Forms in APIs
$form;
submit
Country: España
DeutschlandEspaña
Addresses:
Some St.-+state.usuario
Forms in APIs
$form; ✘How do we know the visible choices or values?
Read the docs!
submit
Country: España
DeutschlandEspaña
Addresses:
Some St.-+state.usuario
Forms in APIs
$form;
$form->submit($request); POST “I'm Feeling Lucky”
✘How do we know the visible choices or values?
Read the docs!
submit
Country: España
DeutschlandEspaña
Addresses:
Some St.-+state.usuario This form should not contain extra fields!!1
Forms in APIs
$form;
$form->submit($request); POST “I'm Feeling Lucky”
✘How do we know the visible choices or values?
Read the docs!
submit
Country: España
DeutschlandEspaña
Addresses:
Some St.-+state.usuario This form should not contain extra fields!!1
The value you selected is not a valid choice!!
Forms in APIs
$form;
$form->submit($request); POST “I'm Feeling Lucky”
✘How do we know the visible choices or values?
Read the docs!
submit
Country: España
DeutschlandEspaña
Addresses:
Some St.-+state.usuario This form should not contain extra fields!!1
The value you selected is not a valid choice!!One or more of the given values is invalid!! :D
Forms in APIs
$form;
$form->submit($request); POST “I'm Feeling Lucky”
✘How do we know the visible choices or values?
Read the docs!
submit
Country: España
DeutschlandEspaña
Addresses:
Some St.-+state.usuario This form should not contain extra fields!!1
The value you selected is not a valid choice!!One or more of the given values is invalid!! :DMUHAHAHAHAHA!!!!!
Forms in APIs
$form;
$form->submit($request); POST “I'm Feeling Lucky”
✘How do we know the visible choices or values?
Read the docs!
Define, maintain and keep in sync in triplicate
Form Server API docs Form Client
:_(How many devs does it take to write a form?
Wizard type form?
“While you code this we will be preparing different versions for
other use cases”
Case: Complex Wizard Form
Case: Complex Wizard Form
Case: Complex Wizard Form
What we need
$form->createView();
HTML
API$mySerializer->serialize($form);
What we need
$form->createView();
HTML
Serialize! Ok, into which format?
API$mySerializer->serialize($form);
JSON Schema
json-schema.org
How does it look like{ "$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"]}
Definitions Types, Validation rules :_)
New resource: my-api/products/form
Liform & LiformBundle
https://github.com/Limenius/Liform
use Limenius\Liform\Resolver;use Limenius\Liform\Liform;
$resolver = new Resolver();$resolver->setDefaultTransformers();$liform = new Liform($resolver);
$form = $this->createForm(CarType::class, $car, ['csrf_protection' => false]);$schema = json_encode($liform->transform($form));
Transform piece by piece {"title":"task", "type":"object", "properties":{ "name":{ "type":"string", "title":"Name", "default":"I'm a placeholder", "propertyOrder":1 }, "description":{ "type":"string", "widget":"textarea", "title":"Description", "description":"An explanation of the task", "propertyOrder":2 }, "dueTo":{ "type":"string", "title":"Due to", "widget":"datetime", "format":"date-time", "propertyOrder":3 } }, “required":[ “name", "description","dueTo"]}
public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name', Type\TextType::class, [ 'label' => 'Name', 'required' => true, 'attr' => ['placeholder' => 'I\'m a placeholder’]])
->add('description', Type\TextType::class, [ 'label' => 'Description', 'liform' => [ 'widget' => 'textarea', 'description' => 'An explanation of the task', ]])
->add('dueTo', Type\DateTimeType::class, [ 'label' => 'Due to', 'widget' => 'single_text'] ) ; }
Transform piece by piece {"title":"task", "type":"object", "properties":{ "name":{ "type":"string", "title":"Name", "default":"I'm a placeholder", "propertyOrder":1 }, "description":{ "type":"string", "widget":"textarea", "title":"Description", "description":"An explanation of the task", "propertyOrder":2 }, "dueTo":{ "type":"string", "title":"Due to", "widget":"datetime", "format":"date-time", "propertyOrder":3 } }, “required":[ “name", "description","dueTo"]}
public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name', Type\TextType::class, [ 'label' => 'Name', 'required' => true, 'attr' => ['placeholder' => 'I\'m a placeholder’]])
->add('description', Type\TextType::class, [ 'label' => 'Description', 'liform' => [ 'widget' => 'textarea', 'description' => 'An explanation of the task', ]])
->add('dueTo', Type\DateTimeType::class, [ 'label' => 'Due to', 'widget' => 'single_text'] ) ; }
Transform piece by piece {"title":"task", "type":"object", "properties":{ "name":{ "type":"string", "title":"Name", "default":"I'm a placeholder", "propertyOrder":1 }, "description":{ "type":"string", "widget":"textarea", "title":"Description", "description":"An explanation of the task", "propertyOrder":2 }, "dueTo":{ "type":"string", "title":"Due to", "widget":"datetime", "format":"date-time", "propertyOrder":3 } }, “required":[ “name", "description","dueTo"]}
public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name', Type\TextType::class, [ 'label' => 'Name', 'required' => true, 'attr' => ['placeholder' => 'I\'m a placeholder’]])
->add('description', Type\TextType::class, [ 'label' => 'Description', 'liform' => [ 'widget' => 'textarea', 'description' => 'An explanation of the task', ]])
->add('dueTo', Type\DateTimeType::class, [ 'label' => 'Due to', 'widget' => 'single_text'] ) ; }
Transform piece by piece {"title":"task", "type":"object", "properties":{ "name":{ "type":"string", "title":"Name", "default":"I'm a placeholder", "propertyOrder":1 }, "description":{ "type":"string", "widget":"textarea", "title":"Description", "description":"An explanation of the task", "propertyOrder":2 }, "dueTo":{ "type":"string", "title":"Due to", "widget":"datetime", "format":"date-time", "propertyOrder":3 } }, “required":[ “name", "description","dueTo"]}
public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name', Type\TextType::class, [ 'label' => 'Name', 'required' => true, 'attr' => ['placeholder' => 'I\'m a placeholder’]])
->add('description', Type\TextType::class, [ 'label' => 'Description', 'liform' => [ 'widget' => 'textarea', 'description' => 'An explanation of the task', ]])
->add('dueTo', Type\DateTimeType::class, [ 'label' => 'Due to', 'widget' => 'single_text'] ) ; }
Transformers
Transformers extract information from each Form Field.
We can extract a lot of information: •Default values and placeholders. •Field attributes. •Validation rules.
Also important
•FormView serializer for initial values. •Form serializer that extracts errors.
{ "code":null, "message":"Validation Failed", "errors":{ "children":{ "name":{ "errors":[ "This value should not be equal to Beetlejuice." ] }, "description":[], "dueTo":[] } }}
So far we have:$form $form->createView() (helpers in other Fws not Sf)
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Validate
Return errors
Render view
On blur sync validation
On Submit validation
On blur async validation
Some client-side validation (HTML5)
All the dynamic goodies
Form Server API docs Form Client
$form Json-schemaLiform
Leverage json-schema: Validators
let valid = ajv.validate(schema, data)if (!valid) console.log(ajv.errors)
https://github.com/epoberezkin/ajv
Leverage json-schema: Form generators
• mozilla/react-jsonschema-form: React.
• limenius/liform-react: React + Redux; integrated with redux-form (we ♥ redux-form).
• …
• Creating our own generator is not so difficult (you typically only need all the widgets, only a subset)
liform-react
By using redux-form we can:
• Have sane and powerful representation of state in Redux.
• Integrate on-blur validation, async validation & on Submit validation.
• Define our own widgets/themes.
• Know from the beginning that it is flexible enough.
liform-react
import React from 'react'import { createStore, combineReducers } from 'redux'import { reducer as formReducer } from 'redux-form'import { Provider } from 'react-redux'import Liform from 'liform-react'
const MyForm = () => { const reducer = combineReducers({ form: formReducer }) const store = createStore(reducer) const schema = { //… } } return ( <Provider store={store}> <Liform schema={schema} onSubmit={(v) => {console.log(v)}}/> </Provider> )}
liform-react{ "properties": { "name": { "title":"Task name", "type":"string", "minLength": 2 }, "description": { "title":"Description", "type":"string", "widget":"textarea" }, "dueTo": { "title":"Due to", "type":"string", "widget":"datetime", "format":"date-time" } }, "required":["name"]}
Form Server API docs Form Client
$form Json-schema React formLiform liform-react
=:D$form $form->createView()
Initial values
UI hints (widgets, attributes)
Bind incoming data
Deserialize
Validate
Return errors
Render view
On blur sync validation
On Submit validation
On blur async validation
Some client-side validation (HTML5)
All the dynamic goodies
https://github.com/Limenius/symfony-react-sandbox
Example with • Webpack • ReactRenderer/ReactBundle • Liform/LiformBudle & liform-react
MADRID · NOV 27-28 · 2015
Thanks!@nacmartin
http://limenius.com Formation, Consulting and
Development.