Introducing State to Widget Components 

With dynamic websites, many use cases exist in which one or several elements on a page need to keep track of their state. A slider, for example, must remember the slide it is displaying to be able to switch to the next or the previous one at the user’s request.

In this tutorial, we’ll illustrate how a widget could make use of state. Based on the Scrivito Example App, we’re going to develop a disclosure widget that requires the visitor to deliberately click a button to open a panel containing some information they might not want to have revealed, e.g. the solution to a riddle, or the end of a story or movie.

Our “DisclosureWidget” should be equipped with a button for showing and hiding the information provided by an editor. Depending on its state, the button should be labeled accordingly. Additionally, we want the information to be disclosed to have a heading and to be freely composable using widgets.

So let’s first create the widget model class to make the pieces of content (the attributes) we want to have for each actual “DisclosureWidget” known to Scrivito. Next, we’ll configure how the widget and which of its attributes should show up in Scrivito’s user interface. After that, finally, the rendering of the actual widgets is taken care of. Only then, state comes into play.

Create the widget model

In a Scrivito-based app, every widget is represented by a folder in the “src/Widgets” folder of the app. Create a subfolder named “DisclosureWidget” and add to it the files we are going to put together here.

src/Widgets/DisclosureWidget/DisclosureWidgetClass.js
Copy
import * as Scrivito from 'scrivito';

Scrivito.provideWidgetClass('DisclosureWidget', {
  attributes: {
    heading: 'string',
    body: 'widgetlist',
    labelHidden: 'string',
    labelDisclosed: 'string',
  },
});

This widget class (the model) makes the pieces of content (the attributes) we want to have for each actual “DisclosureWidget”  known to Scrivito: There’s a heading, a body (we wanted it to be a widgetlist attribute), and the two button labels, labelHidden and labelDisclosed.

Make the content editable

Now that we have the model, let’s figure out what needs to be configured for editors to use the widget. First, an editor should be enabled to recognize the widget when wanting to select one using the widget selection dialog. For this, we’re going to provide a title, a short description, and a thumbnail for it.

Second, regarding the attributes of a widget, some are predestined for being edited in place, while others should be made editable on the widget’s properties dialog. In general, all attributes that don’t require an editor to select something (e.g. a style or a color) and don’t respond to clicks (buttons, tabs, and the like, do) can be conveniently edited in place, first of all string, html, and widgetlist attributes.

Thus, in the case of our “DisclosureWidget”, the button texts should not be made editable directly on the page because, as indicated, the click needed to focus the button for editing one of its texts would trigger the button’s click event. This is why we’ll configure the button texts so that they show up on the properties dialog. Such a configuration of a widget attribute's editing control to be made available, as well as the widget’s appearance in the selection dialog, can be handed over to Scrivito using Scrivito.provideEditingConfig:

src/Widgets/DisclosureWidget/DisclosureWidgetEditingConfig.js
Copy
import * as Scrivito from 'scrivito';
import buttonWidgetIcon from '../../assets/images/button_widget.svg';

Scrivito.provideEditingConfig('DisclosureWidget', {
  title: 'Disclosure Widget',
  description: 'Button for opening an info panel',
  thumbnail: `/${buttonWidgetIcon}`,
  attributes: {
    labelHidden: {
      title: 'Button label (hidden)',
    },
    labelDisclosed: {
      title: 'Button label (disclosed)',
    },
  },
  properties: [
    'labelHidden',
    'labelDisclosed',
  ],
});

This is what the above configuration effects on the widget selection and properties dialogs:

Render the actual widgets

For rendering the actual widgets based on the model class we provided above, we’ll pass a React component to Scrivito that implements the behavior and the look we want the widgets to have.

For stateless display, it is sufficient to provide an anonymous rendering function via Scrivito.provideComponent, but with state involved, a React.Component class is required. Their built-in state object lets us specify and determine what the widget needs to take account of as it’s being rendered.

Using a React.Component class, we access the widget via props.widget, while with a functional component the widget is passed in directly as a function argument.

As you’ll see from the code below, we provide a constructor function for initializing instances of the “DisclosureWidget” React class. This is where the initial state of the component is set. We want the component to remember whether the button has been clicked and the information has been disclosed, so we named the state isDisclosed and set it to false for the first call to the render function. 

src/Widgets/DisclosureWidget/DisclosureWidgetComponent.js
Copy
import * as React from 'react';
import * as Scrivito from 'scrivito';

class DisclosureWidget extends React.Component {
  constructor() {
    super();
    this.state = { isDisclosed: false };
  }

  switchState() {
    this.setState({ isDisclosed: !this.state.isDisclosed });
  }

  render() {
    const widget = this.props.widget;
    const isDisclosed = this.state.isDisclosed;
    const label = isDisclosed ?
      widget.get('labelDisclosed') || 'Here we go!' :
      widget.get('labelHidden') || 'Disclose!';
    return (
    <div>
      <div className="text-center">
        <span className="btn btn-primary" onClick={ () => this.switchState() }>
          { label }
        </span>
      </div>
      <div className={ `panel panel-default ${isDisclosed ? '' : 'd-none'}` }>
        <Scrivito.ContentTag className='panel-heading' content={ widget } attribute='heading' />
        <Scrivito.ContentTag className='panel-body' content={ widget } attribute='body' />
      </div>
    </div>
    );
  }
}

Scrivito.provideComponent('DisclosureWidget', DisclosureWidget);

For the component to respond to clicks on its button, we’ve assigned an onClick event handler to the button’s <span> element, calling the switchState function provided above. switchState simply toggles the component’s state, causing it to alternately show and hide the information panel.

Clicking the component’s button changes its state. For the component to be rendered when its state changes, the component’s setState function must be used. Simply assigning the new value to isDisclosed won’t trigger the rendering action.

To take account of the isDisclosed state, the render function first assigns it to the isDisclosed constant – just for the ease of accessing it. Then, isDisclosed is used to determine the button label text to render and to add the d-none class (hidden in Bootstrap 3.x) to the panel CSS classes if the panel shouldn’t be displayed. Finally, the contents of the panel contained in the heading and body attributes is rendered using the Scrivito.ContentTag React component.

The very last statement connects the “DisclosureWidget” component class to Scrivito using Scrivito.provideComponent to make it available in the UI.

This is what the widget may look like after adding some content to it:

Final words

When having to handle state in a widget component, just keep in mind that you need a React.Component class that’s connected to Scrivito. You can change a component’s state as a response to any event. For this, provide an event handler, and use setState to change the state and have the component’s view updated. Take the state into account in the render function by applying CSS classes conditionally, or by outputting only the elements reflecting the current state.

React’s component state handling offers quite a bit more than what we covered in this guide. The lifecycle methods, for example, let you hook into the construction, rendering, and destruction processes of a component to trigger specific actions like loading additional data, or preventing the component from being rendered. Among the many primers available on the web, we found Scott Domes’ React Lifecycle Methods – how and when to use them to be a very helpful introduction to this topic.