Integrating React.js with PHP projects

  • View
    256

  • Download
    11

  • Category

    Internet

Preview:

Citation preview

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

https://github.com/shakacode/react_on_rails

Based on

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

nacho@limenius.com

http://limenius.com Formation, Consulting and

Development.