Customizing Page and Widget Property Editing

Example App: Icon picker extension

Scrivito lets you provide custom editing functionality to page or widget properties. Most often, the built-in editors for changing attribute values such as text, numbers, images, etc., are smart enough to satisfy even dainty editing demands. However, various use cases are conceivable in which special editing functionality not covered by Scrivito’s editing tools is desired or needed. This is where Scrivito’s UI extensibility jumps in. The Example App, for instance, includes an advanced icon widget with a sophisticated icon picker, which was built as a Scrivito extension.

Built-in editor for enum attributes

In this tutorial, we are going to develop an Instagram style picker extension for the Example App’s ImageWidget. This extension lets you assign one of a multitude of Instagram “filters” to the image in an ImageWidget.

With Scrivito, letting a user select one of several options can be done using an attribute of the enum type. Scrivito does have a built-in editor for enum values, but with a growing number of options, the properties dialog can get quite cluttered, so writing an extension seems obvious. Additionally, we’d like to display a preview of the styled image.

Getting the CSS and providing it to the app

First things first, so directly download the Instagram CSS file provided by Yan Zhu at GitHub.

Store the CSS file to your app’s “src/assets/stylesheets” directory, then open the “index.scss” file from the same directory and add the CSS file to the imports:

// ...

/* imports
================================================== */
@import "_colors";
@import "bootstrap/bootstrap";
@import "_social_cards";
@import "_grid_layout_editor";
@import "_icon_editor";
@import "instagram";

// ...

That was it! The styles are now ready to be used.

Add an attribute to the image widget class

For the selected Instagram “filter” name to be stored in image widget instances, we require an attribute; so open “src/Widgets/ImageWidget/ImageWidgetClass” and add an enum attribute to it. We’ll name it instagramStyle and provide it with the CSS class names we took from the repo:

src/Widgets/ImageWidget/ImageWidgetClass.js
Copy
import * as Scrivito from 'scrivito';

const ImageWidget = Scrivito.provideWidgetClass('ImageWidget', {
  attributes: {
    image: 'reference',
    alignment: ['enum', { values: ['left', 'center', 'right'] }],
    alternativeText: 'string',
    link: 'link',
    instagramStyle: ['enum', { values: [
      'filter-1977',
      'filter-aden',
      'filter-amaro',
      'filter-ashby',
      'filter-brannan',
      'filter-brooklyn',
      'filter-charmes',
      'filter-clarendon',
      'filter-crema',
      'filter-dogpatch',
      'filter-earlybird',
      'filter-gingham',
      'filter-ginza',
      'filter-hefe',
      'filter-helena',
      'filter-hudson',
      'filter-inkwell',
      'filter-kelvin',
      'filter-juno',
      'filter-lark',
      'filter-lofi',
      'filter-ludwig',
      'filter-maven',
      'filter-mayfair',
      'filter-moon',
      'filter-nashville',
      'filter-perpetua',
      'filter-poprocket',
      'filter-reyes',
      'filter-rise',
      'filter-sierra',
      'filter-skyline',
      'filter-slumber',
      'filter-stinson',
      'filter-sutro',
      'filter-toaster',
      'filter-valencia',
      'filter-vesper',
      'filter-walden',
      'filter-willow',
      'filter-xpro-ii',
    ] }],
  },
});

export default ImageWidget;

Note that we could have used a string attribute instead of an enum. Unlike string, however, enum ensures that only a predefined value from the component we’ll provide further down can be set.

Make the ImageWidget’s properties dialog use a Scrivito extension 

The properties dialog of a widget (or page) can be configured using a call to Scrivito.provideEditingConfig in the corresponding “*EditingConfig” file. So let’s introduce our not yet existing Instagram style selection component to the properties of image widgets; simply insert a propertiesGroups array with one element at the top level of the configuration like it’s done here underneath properties:

src/Widgets/ImageWidget/ImageWidgetEditingConfig
Copy
// ...
// For attributes on the 'General' tab use:

  properties: [
    'alignment',
    'alternativeText',
    'link',
  ],

// For a component rendered on an additional tab use:

  propertiesGroups: [
    { 
      title: 'Instagram style',
      component: 'InstagramStyleTab',
    },
  ],

Each element contained in propertiesGroups adds a tab to the properties dialog. Next to the title key, you can either specify a component to use for displaying the tab’s content, or the properties you want Scrivito to make editable on this tab.

We named our component InstagramStyleTab in accordance with the instagramStyle widget attribute we chose above. When using a component for a tab, the component is expected to take care of all the editing functionality on the tab, whereas properties are handled by Scrivito. (So, if a widget or page is equipped with a larger number of attributes, you can divide them up between any number of tabs instead of cluttering the “General” tab using properties at the top level.)

Providing the style selection component

With the Example App, UI extensions can be found in the “src/Components/ScrivitoExtensions” directory, so let’s place our Instagram style picker there, too:

src/Components/ScrivitoExtensions/InstagramStyleTab.js
Copy
import * as React from 'react';
import * as Scrivito from 'scrivito';

class InstagramStyleTab extends React.Component {
  constructor(props) {
    super(props);
    this.setInstagramStyle = this.setInstagramStyle.bind(this);
  }

  setInstagramStyle(event) {
    const style = event.target.value;
    this.props.widget.update({ instagramStyle: style === '' ? null : style });
  }

  render() {
    const InstagramStyles = [
        { value: '', title: 'None' },
        { value: 'filter-1977', title: '1977' },
        { value: 'filter-aden', title: 'Aden' },
        { value: 'filter-amaro', title: 'Amaro' },
        { value: 'filter-ashby', title: 'Ashby' },
        { value: 'filter-brannan', title: 'Brannan' },
        { value: 'filter-brooklyn', title: 'Brooklyn' },
        { value: 'filter-charmes', title: 'Charmes' },
        { value: 'filter-clarendon', title: 'Clarendon' },
        { value: 'filter-crema', title: 'Crema' },
        { value: 'filter-dogpatch', title: 'Dogpatch' },
        { value: 'filter-earlybird', title: 'Earlybird' },
        { value: 'filter-gingham', title: 'Gingham' },
        { value: 'filter-ginza', title: 'Ginza' },
        { value: 'filter-hefe', title: 'Hefe' },
        { value: 'filter-helena', title: 'Helena' },
        { value: 'filter-hudson', title: 'Hudson' },
        { value: 'filter-inkwell', title: 'Inkwell' },
        { value: 'filter-kelvin', title: 'Kelvin' },
        { value: 'filter-juno', title: 'Kuno' },
        { value: 'filter-lark', title: 'Lark' },
        { value: 'filter-lofi', title: 'Lo-Fi' },
        { value: 'filter-ludwig', title: 'Ludwig' },
        { value: 'filter-maven', title: 'Maven' },
        { value: 'filter-mayfair', title: 'Mayfair' },
        { value: 'filter-moon', title: 'Moon' },
        { value: 'filter-nashville', title: 'Nashville' },
        { value: 'filter-perpetua', title: 'Perpetua' },
        { value: 'filter-poprocket', title: 'Poprocket' },
        { value: 'filter-reyes', title: 'Reyes' },
        { value: 'filter-rise', title: 'Rise' },
        { value: 'filter-sierra', title: 'Sierra' },
        { value: 'filter-skyline', title: 'Skyline' },
        { value: 'filter-slumber', title: 'Slumber' },
        { value: 'filter-stinson', title: 'Stinson' },
        { value: 'filter-sutro', title: 'Sutro' },
        { value: 'filter-toaster', title: 'Toaster' },
        { value: 'filter-valencia', title: 'Valencia' },
        { value: 'filter-vesper', title: 'Vesper' },
        { value: 'filter-walden', title: 'Walden' },
        { value: 'filter-willow', title: 'Willow' },
        { value: 'filter-xpro-ii', title: 'X-Pro II' },
    ];

    const widget = this.props.widget;

    return (
      <div className="scrivito_tab_content">
        <div className="scrivito_tab_pane active">
          <div className='group'>
            <div className="scrivito_detail_content">
              <div className="scrivito_detail_label">
                <span>Style</span>
              </div>
              <div className="item_content">
                <select value={ widget.get('instagramStyle') || '' } onChange={ this.setInstagramStyle }>
                  {
                    InstagramStyles.map(style => {
                      return (
                        <option key={ style.value } value={ style.value }>
                          { style.title }
                        </option>
                      );
                    })
                  }
                </select>
              </div>
            </div>
            <div className="scrivito_detail_content">
              <div className="item_content">
                  <Scrivito.ImageTag
                    content={ widget }
                    attribute="image"
                    className={ widget.get('instagramStyle') }
                  />
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }
}

Scrivito.registerComponent('InstagramStyleTab', InstagramStyleTab);

We’ve put the list of filter names directly into the component, but you might alternatively import the list to keep the code lean. Basically, the render method iterates this list to generate option elements inside of a select. The select tag is given a value and an onChange attribute for specifying the selected option and, respectively, the event hander to use when the selection changes.

The event handler, setInstagramStyle, updates the widget’s instagramStyle attribute. It converts the empty string representing the “None” option to null, which is the enum attribute’s equivalent for “no value selected”. In the select tag, the current value is set using the reverse conversion (null becomes the empty string).

Note that the component doesn’t need to make use of state since the selected value is directly written to the widget which causes Scrivito to instantly update the widget instance on the page.

After generating the select element, the render method displays the image widget, applying the currently selected style to it, so the user can see the effect of their action.

For styling the tab, we are using the CSS classes from the Example App’s “Social cards” tab.

What’s next?

Instead of using a select element for offering the styles and applying the selected one to a preview, you could render a clickable thumbnail for each “filter” to give the user an impression of the whole set of styles.

The above component uses Scrivito.ImageTag to render the preview of the image in the widget. In editing mode, this makes the image selectable via the Content Browser. If you don’t want this, you can use an ordinary <img> element instead, like so:

Copy
<img src={ widget.get('image').contentUrl() }
  className={ widget.get('instagramStyle') }
/>