React + webpack + WordPress

Me llaman Ignacio

Me llaman Ignacio

Me llaman Ignacio

  • Especializado en WordPress Multisitio
  • Empecé con WordPress de resaca
  • Especializado en empezar cosas que nunca acabo
  • Mago frustrado

Agenda del día

ECMAScript 6

let y const

for (let i = 0; i < a.length; i++) {
    let x = a[i]
    …
}
for (let i = 0; i < b.length; i++) {
    let y = b[i]
    …
}

const myConst = 10;

Arrow functions

// ES5
function( param1, param2 ) {
    return param1 + param2;
}

// ES6
( param1, param2 ) => {
    return param1 + param2;
}

( param1, param2 ) => param1 + param2;

myList.map( ( element ) => {
    return element + 1;
});

$el.click( ( e ) => {
    // this no es el elemento.
});

Rest parameter

function foo(x, y, ...a) {
    return (x + y) * a.length
}
foo(1, 2, "hello", true, 7);

Property shorthand

// ES5
var x = 'value1';
var y = 'value2';
obj = { x: x, y: y }

// ES6
obj = { x, y }

Classes

class Shape {
    constructor (id, x, y) {
        this.id = id
        this.move(x, y)
    }
    move (x, y) {
        this.x = x
        this.y = y
    }
}

Babel

¿Qué es Babel?

  • Transpilador de ES6 (que no entienden los navegadores) a ES5 (lo que sí entienden)
  • Demo: https://babeljs.io/

webpack

¿Qué es webpack?

  • Bundler modular
  • Automatiza los procesos de transpilación (a través de Babel) y preprocesado de código (parecido a Grunt o Gulp)
  • Sólo entiende JavaScript con su instalación base
  • Se le indica un archivo de entrada JS y otro de salida. El primero puede llamar a otros archivos.
  • Durante el procesado, genera un gráfico de dependencias

Webpack: conceptos

  • Entry: Punto de entrada a la aplicación. Desde donde webpack generará el gráfico. Es el primer archivo que lanzará tu aplicación
  • Output: Fichero de salida. Donde irá el resultado del procesado.
  • Loaders: Capaces de entender archivos no-JS (CSS, Sass...).
  • Plugins: Pues plugins :)

webpack: Instalación

En la carpeta de nuestro proyecto:

npm run install --save-dev webpack

webpack: Configuración

  • Fichero webpack.config.js
const path = require('path');

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-first-webpack.bundle.js'
  }
};

webpack: Workflow

  • Durante el desarrollo: 

 

  • En producción:
webpack --watch
webpack -p

Una guía completa

React

¿Qué es React?

  • Librería para UIs
  • No es un Framework. Sólo se encarga de la vista.
  • Extremadamente declarativo.
  • Basado en componentes reutilizables
  • Los componentes reaccionan al estado de la aplicación.
  • JavaScript + JSX (XML para construir los componentes)
  • Genera un DOM Virtual
  • El DOM cambiará el DOM real sólo las partes que son necesarias

Componente sencillo con props

class Button extends React.Component {
    render() {
        return <button>Hello {this.props.name}</button>;
    }
}

// Dentro de mi aplicación
...
<Button name="ignacio" />
...

Componente sencillo con estado

class Button extends React.Component {
    constructor( props ) {
        super( props );
        this.state = {
            clickCounts: 0
        }
        
        this.increment = this.increment.bind( this );
    }

    increment() {
        this.setState( { clickCounts : this.state.clickCounts + 1 } );
    }

    render() {
        return <button onClick={ this.increment }>Hello {this.props.name}</button>;
    }
}

Aplicación práctica

Repositorio

Estado inicial: Un sencillo plugin

  • Sólo una página de administración debajo de Ajustes con un div#app
  • wp_enqueue_scripts para encolar js/app.js (no existe todavía)
  • _src/app.js: Crea una lista a partir de un array de todos
  • _src/helpers.js: Funciones de utilidad.
  • _src/app.js: Entry point para webpack.

Configuración inicial

Iniciar npm e instalar dependencias

npm init

npm install --save-dev webpack babel-core babel-loader babel-preset-es2015

Configuración inicial:webpack.config.js

webpack.config.js
let path = require('path');

module.exports = {
    entry: './_src/app.js',
    output: {
        path: path.resolve( __dirname, 'js' ),
        filename: 'app.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /(node_modules)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['es2015']
                    }
                }
            }
        ]
    },
};

Configuración inicial: ejecución de webpack

./node_modules/.bin/webpack

Hash: e4b05c7393919d7eba0d
Version: webpack 2.6.1
Time: 2677ms
 Asset     Size  Chunks             Chunk Names
app.js  3.32 kB       0  [emitted]  main
   [0] ./_src/helpers.js 267 bytes {0} [built]
   [1] ./_src/app.js 314 bytes {0} [built]

Pasar lista de todos desde PHP. meetup-plugin.php

$init_state = array(
	'todos' => array(
		array(
			'title' => 'Hacer la compra',
			'id' => 1,
		),
		array(
			'title' => 'Médico a las 11',
			'id' => 2
		)
	)
);

wp_localize_script( 'meetup', 'Meetup_Init_State', $init_state );

Pasar lista de todos desde PHP. _src/helpers.js

export const getTodos = () => {
    return Meetup_Init_State.todos;
};

Configuración inicial: npm scripts. package.json

...
"scripts": {
    "build": "webpack -p",
    "watch": "webpack --watch"
},
...

Configuración inicial: Ejecución npm

npm run build
npm run watch

Source Maps. webpack.config.js

    ...
    devtool: 'cheap-module-source-map'
}

Sass: Instalación de loaders

npm install sass-loader style-loader node-sass css-loader --save-dev

Sass: _src/scss/style.scss

ul.todos {
  li.todo-item {
    padding:20px;
    border:1px solid #CFCFCF;
    width:100%;
  }
}

Sass: _src/app.js

import { getTodos } from './helpers';
import styles from './scss/style.scss';
...

Un alto en el camino

Movemos _src/*.js a _src/js/ por tener más separada la cosa

Sass. webpack.config.js

...
module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /(node_modules)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['es2015']
                    }
                }
            },
            {
                test: /\.scss$/,
                use: [ 'style-loader', 'css-loader', 'sass-loader' ]
            }
        ]
    },
...

Sass

  • Un problema: Los estilos se han embebido en un tag <style>
  • Reto: Extraer los estilos a un .css

Extraer estilos a .css

npm install --save-dev extract-text-webpack-plugin

Extraer estilos a un .css. webpack.config.js

let path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractSass = new ExtractTextPlugin( '../css/style.css' );


module.exports = {
    ...
    module: {
        rules: [
            ...
            {
                test: /\.scss$/,
                loader: extractSass.extract(['css-loader', 'sass-loader'])
            }
        ]
    },
    devtool: 'cheap-module-source-map',
    plugins: [
        extractSass
    ]
};

Extraer estilos a un .css. meetup-plugin.php

...
$plugin_url = plugin_dir_url( __FILE__ );
wp_enqueue_script( 'meetup', $plugin_url . 'js/app.js', array(), '', true );
wp_enqueue_style( 'meetup', $plugin_url . 'css/style.css');
...

Custom Post Type: Todo. meetup-plugin.php

...
...
add_action( 'init', function() {
	register_post_type( 'todo', array(
		'description'         => 'Todos',
		'labels'              => array(
			'name'               => _x( 'Todos', 'post type general name', 'todo' ),
			'singular_name'      => _x( 'Todo', 'post type singular name', 'todo' ),
			'menu_name'          => _x( 'Todos', 'admin menu', 'todo' ),
			'name_admin_bar'     => _x( 'Todo', 'add new todo on admin bar', 'todo' ),
			'add_new'            => _x( 'Add New', 'post_type', 'todo' ),
			'add_new_item'       => __( 'Add New Todo', 'todo' ),
			'edit_item'          => __( 'Edit Todo', 'todo' ),
			'new_item'           => __( 'New Todo', 'todo' ),
			'view_item'          => __( 'View Todo', 'todo' ),
			'search_items'       => __( 'Search Todos', 'todo' ),
			'not_found'          => __( 'No todos found.', 'todo' ),
			'not_found_in_trash' => __( 'No todos found in Trash.', 'todo' ),
			'parent_item_colon'  => __( 'Parent Todo:', 'todo' ),
			'all_items'          => __( 'All Todos', 'todo' ),
		),
		'public'              => false,
		'hierarchical'        => false,
		'exclude_from_search' => true,
		'publicly_queryable'  => false,
		'show_ui'             => true,
		'show_in_menu'        => true,
		'show_in_nav_menus'   => false,
		'show_in_admin_bar'   => false,
		'menu_position'       => null,
		'menu_icon'           => null,
		'capability_type'     => 'post',
		'capabilities'        => array(),
		'map_meta_cap'        => null,
		'supports'            => array( 'title' ),
		'has_archive'         => false,
		'show_in_rest'        => true
	) );
});
...

REST API: Todo.

npm install --save-dev whatwg-fetch es6-promise

Instalamos una librería JS para hacer llamadas a la REST API fácilmente

REST API: Todo. _src/js/Fetcher.js

require( 'es6-promise' ).polyfill();
import { getRESTAPIUrl, getRESTAPINonce } from './helpers';
import 'whatwg-fetch';

function MeetupFetcher() {

    const url = getRESTAPIUrl();
    const nonce = getRESTAPINonce();

    const request = function( url, params ) {
        return fetch( url, params )
            .then( ( response ) => {
                return response.json();
            });
    };

    return {
        getTodos: () => {
            let params = {
                credentials: 'same-origin',
                headers: {
                    'X-WP-Nonce': nonce
                }
            };
            return request( `${url}/todo`, params );
        }
    };
}

const Fetcher = new MeetupFetcher();
export default Fetcher;

REST API: Todo. _src/js/app.js

import { getTodos } from './helpers';
import styles from '../scss/style.scss';
import Fetcher from './Fetcher';

let ul = document.createElement( 'ul' );
ul.classList.add( 'todos' );

let app = document.getElementById( 'app' );

Fetcher.getTodos()
    .then( ( response ) => {
        response.map( ( todo ) => {
            let li = document.createElement( 'li' );
            li.classList.add( 'todo-item' );
            li.innerHTML = todo.title.rendered;
            ul.append( li );
        });

        app.innerHTML = '';
        app.append( ul );
    });

REST API: Todo. _src/js/helpers.js

export const getRESTAPIUrl = () => {
    return Meetup_Init_State.apiUrl;
};

export const getRESTAPINonce = () => {
    return Meetup_Init_State.apiNonce;
};

REST API: Todo. meetup-plugin.php

...
$init_state = array(
	'apiUrl' => esc_url_raw( rest_url( 'wp/v2' ) ),
	'apiNonce' => wp_create_nonce( 'wp_rest' )
);

...
<div class="wrap">
	<div id="app">Loading...</div>
</div>

...

Configuración para React: Dependencias

npm install --save-dev babel-preset-react react react-dom

Configuración para React: webpack.config.js

...
presets: ['es2015', 'react']
...

Primer componente en React: _src/js/app.js

import { getTodos } from './helpers';
import styles from '../scss/style.scss';
import Fetcher from './Fetcher';
import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {
    render() {
        return <ul className="todos">
            <li className="todo-item">One todo</li>
            <li className="todo-item">Another todo</li>
        </ul>;
    }
}


ReactDOM.render( <App />, document.getElementById( 'app' ) );

Nos olvidamos por un momento de la REST API

Añadiendo estado a la App: _src/js/app.js

...
class App extends React.Component {
    constructor() {
        super();
        this.state = {
            todos: [
                {
                    title: 'One todo',
                    id: 1
                },
                {
                    title: 'Another todo',
                    id: 2
                }
            ]
        }
    }

    render() {
        const todos = this.state.todos.map( ( todo ) => {
            return <li key={ todo.id } className="todo-item">{ todo.title }</li>
        });

        return <ul className="todos">
            { todos }
        </ul>;
    }
}
...

Eliminar Todos: _src/js/app.js

...
deleteTodo( todoId ) {
    const todos = this.state.todos.filter( ( todo ) => {
        return ! ( todo.id === todoId );
    });

    this.setState( { todos } );
}

...
const todos = this.state.todos.map( ( todo ) => {
    return <li key={ todo.id } className="todo-item">
            { todo.title }
            <span 
                className="todo-delete" 
                onClick={ this.deleteTodo.bind( this, todo.id ) }>
            Delete
            </span>
    </li>
});

Estilos para el componente: _src/scss/style.scss

ul.todos {
  li.todo-item {
    padding:20px;
    border:1px solid #CFCFCF;
    width:80%;

    span.todo-delete {
      color:red;
      cursor:pointer;
      float:right;
    }
  }
}

De vuelta a la REST API: _src/js/app.js

...
constructor() {
    super();
    this.state = {
        todos: []
    };
}

loadTodos() {
    Fetcher.getTodos()
        .then( ( todos ) => {
            todos = todos.map( ( todo ) => {
                return {
                    id: todo.id,
                    title: todo.title.rendered
                }
            });
            this.setState( { todos } );
        });
}
...

Iniciamos los todos como una lista vacía y  añadimos nuevo método para cargarlos

¿Cuándo los cargamos? Ciclo de vida de componentes

  • Un componente pasa por diferentes estados durante un ciclo de vida

Ciclo de vida de componentes: Mounting

  • constructor()
  • componentWillMount()
  • render()
  • componentDidMount()

Ciclo de vida de componentes: Updating

  • componentWillReceiveProps()
  • shouldComponentUpdate()
  • componentWillUpdate()
  • render()
  • componentDidUpdate()

Ciclo de vida de componentes: Umounting

  • componentWillUnmount()
  • shouldComponentUpdate()
  • componentWillUpdate()
  • render()
  • componentDidUpdate()

Cargando todos: _src/js/app.js

...
componentDidMount() {
    this.loadTodos();
}
...

Cargando todos: 

meetup-plugin.php

...
function render_meetup_plugin_menu() {
	?>
	<div class="wrap">
		<div id="app">Loading...</div>
	</div>
	<?php
}

Borrando todos con la REST API: _src/js/app.js

...
deleteTodo( todoId ) {
    const todos = this.state.todos.filter( ( todo ) => {
        if ( todo.id === todoId ) {
            Fetcher.deleteTodo( todo.id );
            return false;
        }
        return true;
    });
    
    this.setState( { todos } );
}
...

Borrando todos con la REST API: _src/js/Fetcher.js

...
deleteTodo: ( id ) => {
    let params = {
        credentials: 'same-origin',
        headers: {
            'X-WP-Nonce': nonce,
            'Content-type': 'application/json'
        },
        method: 'delete'
    };

    return request( `${url}/todo/${id}`, params );
}
...

Separación de componentes: _src/js/components/TodoItem.js

import React, {PropTypes} from 'react';

export default class TodoItem extends React.Component {
    render() {
        return <li className="todo-item">
            { this.props.title }
            <span
                className="todo-delete"
                onClick={ this.props.onDelete }
            >
                Delete
            </span>
        </li>;
    }
}

Separación de componentes: _src/js/app.js

import TodoItem from './components/TodoItem';
...

render() {
        const todos = this.state.todos.map( ( todo ) => {
            return <TodoItem
                onDelete={ this.deleteTodo.bind( this, todo.id ) }
                key={ todo.id }
                title={ todo.title }
            />
        });
...

This is

The End

This is

The alt End

React-webpack-WordPress

By Ignacio Cruz Moreno