Skip to content
This repository has been archived by the owner on Nov 4, 2022. It is now read-only.

js-widgets/example-widget

Repository files navigation

This project is deprecated please take a look at @js-widgets/registry-starterkit-react instead.


Example Widget semantic-release

This example contains documentation and example code for creating widgets using React.

View the demo

Requirements

Getting started

  1. On the example-widget project page click the "Use this template" button to setup a new widget.
  2. Give the new repository a name and a description and then click the "Create repository from template" button.
  3. Clone the new repository and change the project name, version, and homepage inside package.json.
  4. Change the name of the widget render function(renderExampleWidget) in src/index.js. Note: It is recommended to use something unique to the widget in order to avoid potential conflicts with other widgets, like the widget repository name.
  5. Change the renderFunctionName in the header of public/index.html to match the designated widget render function from the previous step (i.e. renderExampleWidget)
  6. Install the project dependencies using npm install
  7. Run the project locally using npm start

IMPORTANT: Make sure you check the widget development documentation when developing your own.

widget.json

There is a special file called widget.json in the root of the widget. Widget registries, like this example, need this file to learn about this widget. It contains the following keys:

Name Required Example Description
shortcode yes product-catalog The machine name identifier for the widget.
description no A catalog of products for our company. A longer description for the widget. This is shown in the widget catalog.
availableTranslations yes ['en', 'es'] Language codes this widget is available in for internationalization purposes.
settingsSchema no A JSON Schema object decribing the input parameters for the widgets at embed time.
externalPeerDependencies no List of runtime dependencies for this widget that embedders need to add along with the widget.
status yes stable One of stable, beta, wip, or deprecated.

Configurable widgets

Configuration parameters are specified during the embed process. Drupal will create a form element automatically to gather those parameters in the editorial screens. For the CMS to know what form element to use, the widget definition needs to include a JSON Schema definition for every parameter.

This repository contains an example of a configurable parameter. In this case it's the text of the button. This is described in widget.json as:

  "settingsSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "fields": {
        "type": "object",
        "properties": {
          "button-text": {
            "type": "string",
            "title": "Button text",
            "description": "Some random string to be displayed when the widget is rendered.",
            "examples": ["I am a button", "Please, click me", "CLICK"]
          }
        }
      }
    }
  },

And then accessed in the widget code Widget.jsx:

<p className="is-size-6 pb-4">
  <button className="button is-primary">{element.getAttribute('data-button-text')}</button>
</p>

Note that since the field name is button-text, the value is accessed as 'data-button-text'.

External dependencies

externalPeerDependencies is a tool designed to share JS dependencies across different widgets. This module provides an example how to avoid bundling react, react-dom, and react-intl with the widget's JS, while making Drupal (or any other available integrations) load the dependencies automatically.

For each dependency you will need to:

  1. Tell Webpack to not include the library in the resulting JS file(s) for this widget.
    // webpack.config.js or craco.config.js
    externals: {
        react: 'React',
        'react-dom': 'ReactDOM',
        'react-intl': 'ReactIntl',
    },
    // ...
  1. Tell the widget registry (in widget.json), and ultimately the CMS integrations where to find these libraries that were excluded.
"externalPeerDependencies": {
    "react": {"src": "https://unpkg.com/react@^17/umd/react.production.min.js"},
    "react-dom": {"src": "https://unpkg.com/react-dom@^17/umd/react-dom.production.min.js"},
    "react-intl": {"src": "https://unpkg.com/react-intl-bundle@^1/dist/react-intl.production.min.js"}
},

Continuous Integration

Testing and deployment scripts available inside this example repository using GitHub Actions.

Usage

Note: Changes made to the src/public/index.html file are for development and preview purposes only and will not be compiled into the production version of the widget.

Widgets are referenced in the header of the page and rendered using an orchestrator script. The document.loadWidget and document.loadWidgets functions allow rendering of multiple widgets, and multiple instances of a single widget.

loader.js

Within the <head> tag reference the widget load callback(renderFunctionName) and tell it which div(instanceId) to render the widget in.

Projects embedding widgets should include the loader script maintained in the Widget Registry. Include the script in the HTML as:

<script
  src="//js-widgets.github.io/widget-registry-boilerplate/widget-registry/production/loader.js"
  type="application/javascript"
></script>

Remember to use your version of the widget registry instead of widget-registry-boilerplate.

document.loadWidget()

document.loadWidget({
  renderFunctionName: 'renderExampleWidget',
  instanceId: 'example-widget-1',
  language: 'de',
  onRenderFinish: (renderedElement) => {
    alert('Render process finished.');
  },
});

Within the <body> tag add the instanceId div wherever you want this widget to render.

<div id="example-widget-1" data-button-text="foobar"></div>

document.loadWidgets()

document.loadWidgets({
  'widget-1': {
    renderFunctionName: 'renderExampleWidget',
    instanceId: 'example-widget-1',
  },
  'widget-2': {
    renderFunctionName: 'renderExampleWidget',
    instanceId: 'example-widget-2',
  },
});
<div id="example-widget-1" data-button-text="foo"></div>
<div id="example-widget-2" data-button-text="bar"></div>

Parameters

Name Required Default Example Description
renderFunctionName yes renderExampleWidget The render function callback.
instanceId yes example-widget-1 The already present HTML element ID where the react app will be rendered.
language no en de The language code for internationalization purposes.
origin no window.location.origin https://www.example.org Protocol and hostname where a JSONAPI endpoint is available.
onRenderFinish no A callback that executes after the widget has been rendered.

Attributes

Data attributes of the instanceId div are accessible from the <App /> React component using the getAttribute() method.

import React, { Component } from 'react';

class Widget extends Component {
  render() {
    return <div className="App">{this.props.obj.getAttribute('data-button-text')}</div>;
  }
}

export default Widget;

DIV attributes

<div id="example-widget-1" data-button-text="Hello world!"></div>

Query string values

http://localhost:3000/?data-button-text=Hello%20world!

Note: DIV attributes are the preferred method and will always take precedence over the use of query string values. Query string values are a good way to test a widget quickly without having to alter the HTML source code.

Translations

The boilerplate widget has built-in translation support using the react-intl module. Use the FormattedMessage to create translatable strings within the widget.

<FormattedMessage id="App.welcomeMsg" defaultMessage="Welcome!" />

Create all translation messages in src/messages.js, following the provided example. Generate locales using the npm run build:locales command. This will scan src/messages.js for translatable strings and compile them into JSON files for translation under src/locales/*.json.

Supported Languages

Language Code
Arabic ar
German de
English en
Spanish es
Latin American Spanish esla
French fr
Italian it
Japanese ja
Korean ko
Polish pl
Portuguese pt
Russian ru
Turkish tr
Simplified Chinese zh-cn
Traditional Chinese zh-tw

Typescript

In an effort to improve overall code quality and reduce run-time exceptions, we strongly encourage widget developers to use Typescript. We understand, though, that using Typescript can be challenging if you don't have prior experience with it and we don't want to impose that restriction upon you.

If you feel comfortable with Typescript or are willing to learn about it (here and here are good resources), you can run npm run use-typescript, which will install all the libraries you'll need, update file extensions, and make appropriate configuration changes.

Styling

In order to scope widget styles to the widget without adversely affecting the styles of the page within which it's embedded, we're using a library called craco. This library allows us to override configuration in apps bootstrapped with Create React App, which is otherwise a closed system. By using this method, we can make alterations to confuration without needing to eject the app. This along with a PostCSS plugin called postcss-wrap allows us to define a CSS class that will be prepended to all CSS selectors, effectively namespacing all styles.

Uncomment the style rule specified in index.css and start the app to see the prepended class from postcss.config.js added. Remember to define your custom namespace class in postcss.config.js and add it to the top-most element in the app, as seen in Widget.jsx. If you need to use this method, please remove the index.css file and where it's imported in index.js.

It is recommended to rely on the above method strictly for usage of installable component libraries. For custom styling, we recommend installing Emotion, which is a library for writing CSS-in-JS.

Testing

This boilerplate is setup to use Jest and @testing-library/react for testing. See Widget.test.js for an example of how to use these libraries.

Current Maintainer(s)