Skip to main content

How to add multiple copies of a React widget to a Drupal 8 site

Front-end Development
Drupal

In this article, I'll expand on the examples in the previous two articles covering React widgets and Drupal, specifically demonstrating how to support multiple copies of a React widget, each with its own configuration. Following the previous article showing how to pass Drupal data to a React widget, I'll demonstrate two methods for passing configuration - a simple method and a complex method. And as always, you can see sneak peek the final code.

Prerequisites

For either method, we need the ability to store a unique configuration for each block. To do this, we modify the block plugin to receive and store configuration.

Step #1: Add configuration form to block.

In our example, we'll store a simple text message and output it to React. You'll notice I've updated our build method to construct a $container variable and then attach it to the main $build variable. I did this to make method #2 easier to implement.

use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Form\FormStateInterface;
 
 class ReactExampleBlock extends BlockBase {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function blockForm($form, FormStateInterface $form_state) {
+    $form = parent::blockForm($form, $form_state);
+
+    $form['message'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Message'),
+      '#default_value' => $this->configuration['message'] ?? '',
+      '#required' => TRUE,
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function blockSubmit($form, FormStateInterface $form_state) {
+    $this->configuration['message'] = $form_state->getValue('message');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'message' => '',
+    ];
+  }
+
   /**
    * {@inheritdoc}
    */
   public function build() {
     $build = [];
 
-    $build[] = [
+    $container = [
       '#type' => 'container',
       '#attributes' => [
-        'id' => 'react-example',
+        'class' => 'react-example',
         'data-drupal' => json_encode([
           'time' => time(),
         ]),
@@ -42,6 +75,8 @@ class ReactExampleBlock extends BlockBase {
       ],
     ];
 
+    $build[] = $container;
+
     return $build;
   }

Step #2: Convert React mount element from ID to class

In order to support multiple mount elements, we need to change our use of a single ID to HTML classes. We've already made that change to the block file in step 1. Here in step 2, we need to loop through and mount our elements.

-const mount = document.getElementById('react-example');
+const elements = document.getElementsByClassName('react-example');
 
-ReactDOM.render(
-  <React.StrictMode>
-    {/* <App data={JSON.parse(mount.dataset.drupal)} /> */}
-    <App data={drupalSettings.react_example} />
-  </React.StrictMode>,
-  mount
-);
+for (let element of elements) {
+  ReactDOM.render(
+    <React.StrictMode>
+      {/* <App data={JSON.parse(mount.dataset.drupal)} /> */}
+      {/* <App data={drupalSettings.react_example} /> */}
+      <App data={element.dataset.drupal} />
+    </React.StrictMode>,
+    element
+  );
+}

Method #1: Data Attributes

Now that we've done the heavy lifting of storing configuration in our blocks, we just need to use it. Just like how we used data attributes previously, we can use them to pass block configuration. All we have to do is read the configuration and pass it as a data attribute.

   public function build() {
+    $message = $this->configuration['message'];
     $build = [];
 
     $container = [
       '#attributes' => [
         'class' => 'react-example',
         'data-drupal' => json_encode([
-          'time' => time(),
+          'message' => $message,
         ]),
       ],
       '#attached' => [

Method #2: Drupal Settings Global Variable

Using the global Drupal Settings variable is slightly more difficult because we need to associate the configuration for a given block and access it in JavaScript. Drupal doesn't make it easy for a block to access its instance ID. So we need to do a backhanded method of manually storing that in the block's configuration. Then we add that block ID as a data variable so JS knows which Drupal Setting to use on which mount.


   public function blockSubmit($form, FormStateInterface $form_state) {
     $this->configuration['message'] = $form_state->getValue('message');
+    $this->configuration['block_id'] = $form['id']['#value'];
   }
 
   public function defaultConfiguration() {
     return [
+      'block_id' => NULL,
       'message' => '',
     ];
   }

   public function build() {
+    $block_id = $this->configuration['block_id'];
     $message = $this->configuration['message'];
     $build = [];
 
     $container = [
       '#type' => 'container',
       '#attributes' => [
         'class' => 'react-example',
         'data-drupal' => json_encode([
           'message' => $message,
         ]),
+        'data-id' => $block_id,
       ],
       '#attached' => [
-        'drupalSettings' => [
-          'react_example' => [
-            'time' => time(),
-          ],
-        ],
       ],
     ];
 
+    $container['#attached']['drupalSettings']['react_example'][$block_id] = [
+      'message' => $message,
+    ];
+
     $build[] = $container;
 
     return $build;

index.js

     <React.StrictMode>
       {/* <App data={JSON.parse(mount.dataset.drupal)} /> */}
       {/* <App data={drupalSettings.react_example} /> */}
-      <App data={element.dataset.drupal} />
+      {/* <App data={element.dataset.drupal} /> */}
+      <App data={drupalSettings.react_example[element.dataset['id']]} />
     </React.StrictMode>,

Conclusion

So there you have it. With this quick overview of adding React widgets to Drupal sites, you have a complete approach to integrating modern front-end experiences with your modern CMS. Have questions? Got stuck? Hit us up on twitter.