Drupal 8 et React : block dynamique ou headless

2 tuto en 1, nous allons voir comment coupler Drupal avec react.js en headless et … avec Drupal lui même (pour rajouter un bloc react temps-réel).

D’abord nous activons les modules RESTful Web Services et Serialization.

Ensuite nous allons créer une vue sur les derniers commentaires postés avec un display REST export (j’ai mis plain text dans le format de display des champs par soucis de simplicité) :

Nous allons maintenant nous appuyer dessus pour faire des appels du webservice JSON via React. Notez pour plus tard le chemin d’appel du webservice : api/v1/comments.

Drupal Headless avec React

En mode headless, Drupal n’est pas utilisé pour le front-office, seulement pour le back-office. Nous avons besoin de charger les bibliothèques react (on peut le faire en ligne) dans une premier temps et de faire l’appel au webservice dans une second temps :

<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8" />
 <title>App</title>
 <link rel="stylesheet" href="https://netdna.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
 <link rel="stylesheet" type="text/css" href="stylesheets/style.css">
</head>

<body>

<div id="container">test</div>

<script src="https://npmcdn.com/react@15.3.1/dist/react.js"> </script>
<script src="https://npmcdn.com/react-dom@15.3.1/dist/react-dom.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.24/browser.min.js"></script>
<script type="text/babel" src="js/app.jsx"></script>

</body>
</html>

react.js et react-dom.js sont les libraries react à proprement parler. Mais en plus nous incluons axios qui permet de faire des appels de webservices en JSON. Quand à babel-core c’est le compiler qui transforme le JSX en javascript. Le JSX permet (entre autres) d’avoir des tags HTML directement dans le code JavaScript, ils sont convertis en React.createElement() directement.

Le fichier app.jsx :

class App extends React.Component {

constructor() {
 super();
 // Setting up initial state
 this.state = { 
   data: []
 }
}

// calling the componentDidMount() method after a component is rendered for the first time

componentDidMount() {
 var th = this;
 this.serverRequest = axios.get(this.props.source)
 .then(function(event) {
   th.setState({
     data: event.data
   });
 })
}

// calling the componentWillUnMount() method immediately before a component is unmounted from the DOM

componentWillUnmount() {
 this.serverRequest.abort();
}

render() {
 var titles = []
 this.state.data.forEach(item => {
   titles.push(<h3 className="events">{item.subject}</h3> );
 })
 return (
   <div className="container">
     <div className="row">
       <div className="col-md-6 col-md-offset-5">
         <h1 className="title">All Comments</h1>
         {titles}
       </div>
     </div>
   </div>
 );
 }
}

// rendering into the DOM
ReactDOM.render(
 <App source="http://test.box.local/drupal/web/api/v1/comments" />,
 document.getElementById('container')
);

Ce n’est pas un tuto react, je ne m’étendrais donc pas trop sur le sujet. Si vous voulez en savoir plus allez ici ou ici. La base de react c’est essentiellement son DOM virtuel et son moteur de rendu optimisé qui détecte les différences : ReactDOM.render et React.CreateElement (ici masqué dans du JSX), ainsi que la possibilité de créer ses propres « tags HTML » grâce au système de classe. C’est pour cela que <App .../> est compris par JSX comme un React.CreateElement('App', ...); est compris comme l’instanciation de la classe App (classe qui doit contenir une methode render et hériter de React.Component pour être reconnue par React).

C’est très basique comme application, mais ça fonctionne. Mais si l’on ne souhaite pas faire tout un front en react il est possible de l’utiliser pour améliorer l’ergonomie de Drupal en rajoutant React sur certains blocs pour les rendre « temps réel ». C’est ce que nous allons voir dans la 2ème partie de ce tutoriel.

Drupal real-time avec React

Tiré d’un tuto portugais dont l’objectif est de réaliser l’équivalent de la version en drupal 7.

Avec la console nous créons un module react_comment et un bloc ReactComments :

drupal generate:module --module="react_comment" --machine-name="react_comment" --module-path="/modules/custom" --description="React real time comments" --core="8.x" --package="Custom" --composer --learning --uri="http://default" --no-interaction

drupal generate:plugin:block --module="react_comment" --class="ReactComments" --label="React comments" --plugin-id="react_comments" --learning --uri="http://default" --no-interaction

Voici le fichier react_comment.libraries.yml qui permet d’inclure le javascript :

recent.comments:
 version: VERSION
 js:
   js/react-comments.js: {}
 dependencies: 
   - react_comment/reactjs

reactjs:
 version: VERSION
 js:
   js/react.min.js: {}

Note: il faut inclure une version de react.min.js dans le répertoire js en le téléchargeant à la main (moi je l’ai pris ici).

Voici le fichier src/Plugin/Block/ReactComments.php qui sert à inclure l’application react et à créer un div par défaut dans un block que l’application react pourra modifier à son gré :

<?php

namespace Drupal\react_comment\Plugin\Block, il n'y a que 2 lignes qui changent :

use Drupal\Core\Block\BlockBase;

/**
  * Provides a 'ReactComments' block.
  *
  * @Block(
  * id = "react_comments",
  * admin_label = @Translation("React comments"),
  * )
  */
class ReactComments extends BlockBase {
 
  /** 
    * {@inheritdoc}
    */
  public function build() {
    $build = [];
    $build['react_comments']['#markup'] = '<div id="recent-comments"></div>';
    $build['#attached']['library'][] = 'react_comment/recent.comments';
    return $build;
  }

}

Enfin la pièce maîtresse : le fichier js/react-comments.js :

/**
 * @file
 * Main JS file for react functionality.
 *
 */
 
(function ($) {
 
  Drupal.behaviors.react_blocks = {
    attach: function (context) {
 
      // A div with some text in it
      var CommentBox = React.createClass({displayName: 'CommentBox',
 
      loadCommentsFromServer: function() {
        $.ajax({
          url: this.props.url,
          dataType: 'json',
          success: function(data) {
            this.setState({data: data});
          }.bind(this),
          error: function(xhr, status, err) {
            console.error(this.props.url, status, err.toString());
          }.bind(this)
        });
      },
 
      getInitialState: function() {
        return {data: []};
      },
 
      componentDidMount: function() {
        this.loadCommentsFromServer();
        setInterval(this.loadCommentsFromServer, this.props.pollInterval);
      },
 
      render: function() {
          return (
            React.createElement("div", {className: "commentBox"},
              React.createElement("h3", null, React.createElement("b", null, "Check them out!")),
              React.createElement(CommentList, {data: this.state.data})
            )
          );
        }
      });
 
      var CommentList = React.createClass({displayName: 'CommentList',
        render: function() {
          var commentNodes = this.props.data.map(function (comment) {
            return (
              React.createElement(Comment, {name: comment.name, subject: comment.subject},
                comment.subject
              )
            );
          });
          return (
            React.createElement("div", {className: "commentList"},
              commentNodes
            )
          );
        }
      });
 
      var Comment = React.createClass({displayName: 'Comment',
        render: function() {
          return (
            React.createElement("div", {className: "comment"},
              React.createElement("h2", {className: "commentAuthor"},
                this.props.name
              ),
              this.props.subject
            )
          );
        }
      });
 
      // Render our reactComponent
      React.render(
        React.createElement(CommentBox, {url: "api/v1/comments", pollInterval: 2000}),
        document.getElementById('recent-comments')
      );
 
    }
  }
 
})(jQuery);

Ce fichier est écrit en JS et non en JSX, il est donc un peu plus lourd à lire.

Et voila le résultat :

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *