Building a Child Navigation Widget

In this tutorial, we are going to extend the Scrivito Example App by a more advanced feature that lets editors place a navigation on any page.

For this, we'll develop a widget that utilizes the hierarchical relationship between CMS objects that accounts for the top-down structure a website built with Scrivito may have. 

With Scrivito, web pages can be organized in a reversed-tree-like structure, meaning that pages can act as parents (or nodes) to which child pages can be added (each of which may again be a node). The page at the very top, the origin of all descendants, is the root page. The unique position each CMS page object occupies in the hierarchy is identified by the value of its respective _path attribute. In a path like “/company/staff”, the root page is the first ”/”; “company” would be one of the root’s children and, at the same time, the parent of “staff”.

You normally don’t need to take care of paths because Scrivito maintains them for you. As an editor, you can create subpages using the page menu. And if a navigation is rendered using the built-in Scrivito.ChildListTag component, then subordinate pages can be created using the blue handles of the navigation, like shown on the screenshot.

We want the starting point of our navigation to be configurable so that it can be used to list the child pages of any page on your website, not just the ones underneath the page containing the navigation widget. Additionally, the widget should render an “up-by-one-level” link and support adding a title.

We’ll also show you how to apply custom styles to the navigation, as an example of how to adapt the look of a Scrivito application in general.

First things first: provide the model

Every CMS object or widget type needs a model that defines its attributes, so let’s create one. In a Scrivito app, the files a widget type is made up of are located in a folder named after the widget, underneath the “src/Widgets” folder. We’ll name the widget “ChildNavigationWidget” and provide it with two attributes to cover its features outlined above:

Copy
// src/Widgets/ChildNavigationWidget/ChildNavigationWidgetClass.js

import * as Scrivito from 'scrivito';
Scrivito.provideWidgetClass('ChildNavigationWidget', {
  attributes: {
    parentPage: 'reference',
    navTitle: 'string',
  },
});

As you can see, the model has an attribute named parentPage. It is meant for customizing the starting point of the navigation. Its type is reference, which is exactly the attribute type needed for pointing to CMS objects. It lets editors set the attribute value conveniently using the Content Browser. The second attribute is navTitle, a string for storing the navigation title. That’s all at this point. If you like the idea behind this widget type and want to extend it later on, you’ll surely require one or two further attributes.

Make the attributes editable

Before we go into the rendering component, let’s quickly provide the configuration required for setting the value of the parentPage attribute via the properties dialog of actual widgets of this type. From the icon that’s imported (and which we didn't change) you can see that we used the “PageListWidget” as a template for our  “ChildNavigationWidget”:

Copy
// src/Widgets/ChildNavigationWidget/ChildNavigationWidgetEditingConfig.js

import * as Scrivito from 'scrivito';
import pageListWidgetIcon from '../../assets/images/page_list_widget.svg';

Scrivito.provideEditingConfig('ChildNavigationWidget', {
  title: 'Child Navigation',
  description: 'Displays a navigation from the children of a page',
  thumbnail: `/${pageListWidgetIcon}`,
  attributes: {
    parentPage: {
      title: 'Parent page',
      description: 'Leave empty for current page',
    },
  },
  properties: [
    'parentPage',
  ],
});

Next to the properties for visualizing the widget class in the widget selection dialog (title, description, thumbnail), there is only this above-mentioned attribute configuration for parentPage. For the other attribute, navTitle, no configuration is required because its value can be edited in place.

In general, it’s a good idea to place only those attributes on a properties dialog that cannot be edited directly on the page. This helps preventing properties dialogs from overflowing.

The “infrastructure” of our widget type is now complete, so let’s turn to the rendering part.

Render the widget instances

The “ChildNavigationWidget” component is meant to do the following:

  1. Render the navTitle attribute.
  2. Render an “up-one-level” link.
  3. Render links to the children of the parentPage object.

Note that it’s not obligatory for an editor to specify a parentPage. If none is given, the page containing the widget (i.e. the current page) is used as the parentPage

Copy
// src/Widgets/ChildNavigationWidget/ChildNavigationWidgetComponent.js

import * as React from 'react';
import * as Scrivito from 'scrivito';
Scrivito.provideComponent('ChildNavigationWidget', ({ widget }) => {
  let parentPage = widget.get('parentPage');

  if (!parentPage) {
    parentPage = widget.obj();
  }

  return (
  <div className='childNav'>
    <Scrivito.ContentTag
      tag='h4'
      className='title'
      content={ widget }
      attribute='navTitle'
    />
    <ParentLink page={ parentPage } />

    <Scrivito.ChildListTag
      tag='ul'
      className='page-list'
      parent={ parentPage }
      renderChild={ renderChild }
    />
  </div>
  );
});

Rendering the parent link

The rendering of the “up-one-level” link requires some logic, so it’s delegated to a separate component, ParentLink. To enable this component to access content, we connect it to Scrivito using Scrivito.connect. Add the following code to the “ChildNavigationWidgetComponent.js” file:

Copy
const ParentLink = Scrivito.connect(props => {
  let target = props.page;
  if (!target || !target.parent()) {
    return null;
  }

  if (target.id() === Scrivito.currentPage().id()) {
    target = target.parent();
  }

  return (
    <div className='parent'>
      <Scrivito.LinkTag to={ target }>
        { target.get('title') }
      </Scrivito.LinkTag>
    </div>
  );
});

The ParentLink component receives the parent object set for the widget as a prop. It renders the link only if none of the CMS objects involved is null. This precautionary measure is advisable because Scrivito loads content asynchronously, meaning that the objects might or might not be present when the component is invoked.

Also, if the passed-in CMS object is the current page, the parent of this object is linked so that the link doesn’t point to the page already being displayed.

Rendering the links to the children

The links pointing to the children of the parent object (the actual navigation) are rendered using the built-in Scrivito.ChildListTag component (see above for how it’s invoked). As a default, Scrivito.ChildListTag wraps each link in an <li> element, but you can customize this behavior by specifying your own rendering function via the renderChild prop. This is what our rendering function looks like (add this code to the “ChildNavigationWidgetComponent.js” file, too):

Copy
function renderChild(child) {
  let className = '';

  if (child.id() === Scrivito.currentPage().id()) {
    className = 'strong';
  }

  return (
    <li className={ `child ${className}` } >
      <Scrivito.LinkTag to={ child }>
        { child.get('title') }
      </Scrivito.LinkTag>
    </li>
  );
}

The additional logic we wanted to have highlights the navigation link that points to the current page. This can only apply to one of the navigation items if the widget’s parentPage points to the parent of the page containing the widget.

Make it fit your website’s style

The “ChildNavigationWidget” component applies the (not yet existing) “childNav” CSS class to its wrapper <div> element as it renders the navigation.

To provide a “childNav” CSS class, just save it to a .scss file in the “src/assets/stylesheets” folder of your project directory. Then import the file in the “index.scss” file as described in Extending the Example App Styles.

The “childNav” CSS class for styling the navigation widget could look like the one given here (we named the file “_childnav.scss”).

After saving and importing the file, the class should be instantly effective. To make the linked list items look even better, you could replace the child dummy class in the renderChild function with a built-in icon class: fa fa-caret-right.

Copy
.childNav { 
    .title { 
        text-transform:uppercase;
        font-weight:500;
        color:#FF6B6B;  
    }
    .parent {
        a { 
            text-transform:uppercase;
            font-weight:600;  
        }
    }
    .page-list {
        li:hover:before { color:#FF6B6B }
        a {
            &:link { 
                font-family:"Source Sans Pro";
                display:inline-block;
                padding:5px; 
                   
            }
            &:visited { }
            &:hover { color:#3ac7bd }
        }
    }
}

See the screenshot below for what the new CSS effects. Remember to place the navigation widget on a page with children (the homepage or the “Pages & Widgets” page), or set the parent page accordingly. Otherwise, it won’t contain any items.

That’s it! You now have a widget for placing a children-based navigation on any page! As with many new page and widget types, this one has potential, too: It displays the navigation independently of whether the parentPage has children or not. Also, it would be nice if editors had the option to specify the horizontal alignment of the navigation, for example. Both extensions shouldn’t be too difficult to implement...