Today I'm going to show you how to display a Drupal 8 block with recent comments. Displayed dynamically using React.js.
You can see it in action below:
I have a vanilla Drupal site installed using the Standard profile so I have the Article content type with comments, which I'm going to use for this project.
Before we go further let's look at the prerequisites:
Let's get started.
First I'm going to create the folder called drupal_block_reactive inside my modules/custom folder, then I'll put my .info.yml file inside the folder.
drupal_block_reactive.info.yml
name: Recent comments (React) type: module description: 'Latest comments block with React.js' package: Development version: '8.x-1.x-dev' core: '8.x' dependencies: - block - comment - node - rest - user
I have Rest module as a required module here since I'm going to crate a View with the Rest display to get comments so note that the urls will be "/api/comments". So this will be the only connection to fetch data from the backend.
Now let me add my Views export here so you can import it using Drupal 8 configurations import.
views.view.recent_comments_react.yml
langcode: en status: true dependencies: module: - comment - node - rest - user id: recent_comments_react label: 'Recent comments react' module: views description: 'Recent comments.' tag: default base_table: comment_field_data base_field: cid core: 8.x display: default: display_plugin: default id: default display_title: Master position: 0 display_options: access: type: perm options: perm: 'access comments' cache: type: tag query: type: views_query exposed_form: type: basic pager: type: some options: items_per_page: 10 offset: 0 style: type: html_list options: grouping: { } row_class: '' default_row_class: true type: ul wrapper_class: item-list class: '' row: type: fields options: default_field_elements: true inline: subject: subject changed: changed separator: ' ' hide_empty: false relationships: node: field: node id: node table: comment_field_data required: true plugin_id: standard fields: subject: id: subject table: comment_field_data field: subject relationship: none group_type: group admin_label: '' label: '' exclude: false alter: alter_text: false text: '' make_link: false path: '' absolute: false external: false replace_spaces: false path_case: none trim_whitespace: false alt: '' rel: '' link_class: '' prefix: '' suffix: '' target: '' nl2br: false max_length: 0 word_boundary: false ellipsis: false more_link: false more_link_text: '' more_link_path: '' strip_tags: false trim: false preserve_tags: '' html: false element_type: '' element_class: '' element_label_type: '' element_label_class: '' element_label_colon: false element_wrapper_type: '' element_wrapper_class: '' element_default_classes: true empty: '' hide_empty: false empty_zero: false hide_alter_empty: true click_sort_column: value type: string settings: link_to_entity: false group_column: value group_columns: { } group_rows: true delta_limit: 0 delta_offset: 0 delta_reversed: false delta_first_last: false multi_type: separator separator: ', ' field_api_classes: false plugin_id: field entity_type: comment entity_field: subject changed: id: changed table: comment_field_data field: changed relationship: none plugin_id: field group_type: group admin_label: '' label: '' exclude: false alter: alter_text: false text: '' make_link: false path: '' absolute: false external: false replace_spaces: false path_case: none trim_whitespace: false alt: '' rel: '' link_class: '' prefix: '' suffix: '' target: '' nl2br: false max_length: 0 word_boundary: true ellipsis: true more_link: false more_link_text: '' more_link_path: '' strip_tags: false trim: false preserve_tags: '' html: false element_type: '' element_class: '' element_label_type: '' element_label_class: '' element_label_colon: false element_wrapper_type: '' element_wrapper_class: '' element_default_classes: true empty: '' hide_empty: false empty_zero: false hide_alter_empty: true type: timestamp_ago settings: future_format: '@interval hence' past_format: '@interval ago' granularity: 2 entity_type: comment entity_field: changed cid: id: cid table: comment_field_data field: cid relationship: none group_type: group admin_label: '' label: '' exclude: false alter: alter_text: false text: '' make_link: false path: '' absolute: false external: false replace_spaces: false path_case: none trim_whitespace: false alt: '' rel: '' link_class: '' prefix: '' suffix: '' target: '' nl2br: false max_length: 0 word_boundary: true ellipsis: true more_link: false more_link_text: '' more_link_path: '' strip_tags: false trim: false preserve_tags: '' html: false element_type: '' element_class: '' element_label_type: '' element_label_class: '' element_label_colon: false element_wrapper_type: '' element_wrapper_class: '' element_default_classes: true empty: '' hide_empty: false empty_zero: false hide_alter_empty: true click_sort_column: value type: number_integer settings: thousand_separator: '' prefix_suffix: false group_column: value group_columns: { } group_rows: true delta_limit: 0 delta_offset: 0 delta_reversed: false delta_first_last: false multi_type: separator separator: ', ' field_api_classes: false entity_type: comment entity_field: cid plugin_id: field filters: status: value: true table: comment_field_data field: status id: status plugin_id: boolean expose: operator: '' group: 1 entity_type: comment entity_field: status status_node: value: true table: node_field_data field: status relationship: node id: status_node plugin_id: boolean expose: operator: '' group: 1 entity_type: node entity_field: status sorts: created: id: created table: comment_field_data field: created relationship: none group_type: group admin_label: '' order: DESC exposed: false expose: label: '' plugin_id: date entity_type: comment entity_field: created cid: id: cid table: comment_field_data field: cid relationship: none group_type: group admin_label: '' order: DESC exposed: false plugin_id: field entity_type: comment entity_field: cid title: 'Recent comments' empty: area_text_custom: id: area_text_custom table: views field: area_text_custom relationship: none group_type: group admin_label: '' label: '' empty: true content: 'No comments available.' tokenize: false plugin_id: text_custom display_extenders: { } cache_metadata: contexts: - 'languages:language_content' - 'languages:language_interface' - user.permissions max-age: -1 tags: { } rest_export_1: display_plugin: rest_export id: rest_export_1 display_title: 'REST export' position: 3 display_options: display_extenders: { } path: api/comments style: type: serializer options: uses_fields: true formats: json: json filters: { } defaults: filters: false filter_groups: false filter_groups: operator: AND groups: 1: AND row: type: data_field options: field_options: subject: alias: '' raw_output: false changed: alias: '' raw_output: false cache_metadata: max-age: -1 contexts: - 'languages:language_content' - 'languages:language_interface' - request_format - user.permissions tags: { }
Okay the backend if ready now, let's add the library dependencies, so in Drupal 8 how do we do that is we use .libraries.yml to add any js dependencies.
drupal_block_reactive.libraries.yml
reactjs: remote: https://github.com/facebook/react version: 15.1.0 license: name: BSD url: https://github.com/facebook/react/blob/master/LICENSE gpl-compatible: true js: # Non-minified version: development friendly, debugging is possible. //cdn.jsdelivr.net/react/15.1.0/react.js: { type: external, minified: false } //cdn.jsdelivr.net/react/15.1.0/react-dom.js: { type: external, minified: false } component: version: VERSION js: js/dist/index.js: {} dependencies: - core/drupal
So what this code does is, this will get the React.js 15.1.0 added together with component which is what I'm going to detail next.
React.js Components let you split the UI into independent, reusable pieces, so each comment will be a Component and Comment container will be another Component.
I'm gonna create two files for those two components inside js/src/components directory.
js/src/components/Comment.js
// Comment component. class Comment extends React.Component { render() { return ( <div className = "Comment" ><span> {this.props.subject} < /span> | <span>{this.props.changed}</span ></div>); } } export default Comment
js/src/components/CommentBox.js
import Comment from './Comment' // CommentBox component. class CommentBox extends React.Component { constructor() { super(); // Setting initial state. this.state = { comments: [] } } // Data from a service. _fetchData() { jQuery.ajax({ url: this.props.url, dataType: 'json', success: (comments) => this.setState({ comments }), error: (xhr, status, err) => { console.error(this.props.url, status, err.toString()); } }); } // Gets data from state, returns a list components. _getComments() { // Get the list of comments from the state. const commentsList = this.state.comments; // Return an array of sub-components. if (commentsList.length > 0) { return commentsList.map((comment) => { return <Comment subject = {comment.subject} changed = {comment.changed} key = {comment.cid}/> }); } return ( <p> {Drupal.t('No comments.')} </p> ); } componentDidMount() { this._fetchData(); setInterval(this._fetchData.bind(this), this.props.timeInterval); } render() { const commentsNodes = this._getComments(); return ( <div className = "CommentBox" > {commentsNodes} </div> ); } } export default CommentBox
Now we have components added, let's put them together in the gatekeeper index.js file.
js/src/index.js
/** * @file * Attaching React component via Drupal behaviors.. */ import CommentBox from './components/CommentBox' Drupal.behaviors.drupal_block_reactive = { attach: (context) => { // Render our component. ReactDOM.render( <CommentBox url='/api/comments' timeInterval={2000}/>, document.getElementById('recent-comments-react') ); } };
Now let's add the package.json for the dependencies.
js/package.json
{ "name": "drupal-block-reactive", "version": "1.0.0", "description": "A drop of reactivity in Drupal 8", "main": "gulpfile.js", "private": true, "author": "Kalin Chernev", "url": "https://github.com/kalinchernev", "repository": { "type": "git", "url": "https://github.com/kalinchernev/drupal_block_reactive.git" }, "devDependencies": { "babel-preset-es2015": "^6.9.0", "babel-preset-react": "^6.5.0", "gulp": "3.9.1", "browserify": "13.1.0", "gulp-babel": "6.1.2", "vinyl-source-stream": "1.1.0", "babelify": "7.3.0" } }
js/gulpfile.js
I'm going to use Gulp to compile the JS.
const gulp = require('gulp'); const babel = require('gulp-babel'); const browserify = require('browserify'); const source = require('vinyl-source-stream'); gulp.task('default', () => { return browserify({ entries: 'src/index.js', extensions: ['.jsx'], debug: true }) .transform('babelify', { presets: ['es2015', 'react'] }) .bundle() .pipe(source('index.js')) .pipe(gulp.dest('dist')); });
Now let's connect this JS in Drupal UI using a static HTML block.
src/Plugin/Block/RecentCommentsBlock.php
<?php namespace Drupal\drupal_block_reactive\Plugin\Block; use Drupal\Core\Block\BlockBase; /** * Provides a 'Recent comments (reactive)' block. * * @Block( * id = "recent_comments_reactive", * admin_label = @Translation("Recent comments (reactive)"), * category = "Development" * ) */ class RecentCommentsBlock extends BlockBase { /** * {@inheritdoc} */ public function build() { return [ '#markup' => '<div id="recent-comments-react"></div>', '#attached' => [ 'library' => [ 'drupal_block_reactive/reactjs', 'drupal_block_reactive/component', ], ], ]; } }
Now everything is set up, you can go to the js folder and run npm install to install all the dependencies. Then run gulp to compile the JS.
Once those steps are completed you can enable the block in a sidebar or content region and add couple of comments to see it in action.
You can also find the complete code here in this GitHub repository.