Parts

Contents

Enonic provides a highly customizable component type called parts. Similar to page components parts must be defined in both Enonic and Next.js in order to fully work.

Task: Add part to Enonic

We’ll start off by adding a simple component that lists child items in the tree structure.

  1. Add the child-list part to the Enonic app

    src/main/resources/site/parts/child-list/child-list.xml
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <part xmlns="urn:enonic:xp:model:1.0">
        <display-name>Child list</display-name>
        <description>Lists children of current item</description>
        <form>
            <input name="sorting" type="ComboBox">
                <label>Sort order</label>
                <occurrences minimum="0" maximum="1"/>
                <config>
                    <option value="displayName ASC">Ascending</option>
                    <option value="displayName DESC">Descending</option>
                </config>
                <default>displayName ASC</default>
            </input>
        </form>
    </part>
    This component also contains a form, similar to content types. This means editors can configure it - sorting the items Ascending or Descending.
  2. Build and deploy your Enonic app once more to make the new part available in Content Studio.

  3. Add parts to the page

    Back in Content Studio, edit the /hmdb/persons folder item. Create the page and place the new part into the region (right-click → Insert, or drag-n-drop from the Insert panel` on the right hand side).

    The part will appear without making any changes to the front-end, this is because the Enonic Adapter contains fallback part rendering, as long as Next.js is running in DEV mode.

    missing component

Task: Configure part rendering in Next.js

Heading over to the front-end, you must register a new component that supports rendering of this part.

  1. Add the part implementation to the Next.js app

    src/components/parts/ChildList.tsx
    import React from 'react'
    import {Context, getUrl, VariablesGetterResult} from '@enonic/nextjs-adapter';
    import {PartProps} from '@enonic/nextjs-adapter/views/BasePart';
    
    const ChildList = (props: PartProps) => {
        const {data, meta} = props;
        const children = data.get.children;
        if (!children || children.length === 0) {
            return null;
        }
        return (
            <main style={{
                margin: `0 auto`,
                maxWidth: 960,
                padding: `0 1.0875rem`,
            }}>
                {
                    children &&
                    <ul>{
                        children.map((child: any, i: number) => (
                            <li key={i}>
                                <a href={getUrl(child._path, meta)}>
                                    {child.displayName}
                                </a>
                            </li>
                        ))
                    }</ul>
                }
            </main>
        );
    };
    
    export default ChildList;
    
    export const getChildList = {
        query: function (path: string, context?: Context, config?: any): string {
            return `query($path:ID!, $order:String){
                  guillotine {
                    getSite {
                      displayName
                    }
                    get(key:$path) {
                      displayName
                      children(sort: $order) {
                          _path(type: siteRelative)
                          _id
                          displayName
                      }
                    }
                  }
                }`
        },
        variables: function (path: string, context?: Context, config?: any): VariablesGetterResult {
            return {
                path,
                order: config?.sorting
            }
        }
    };
    
    export async function childListProcessor(common: any, context?: Context, config?: any): Promise<any> {
        common.modifiedBy = 'childListProcessor';
        return common;
    }
  2. Then, register the components in _mappings.ts:

    Update _mappings.ts with the following new lines:

    src/components/_mappings.ts
    import ChildList, {childListProcessor, getChildList} from './parts/ChildList';
    
    // Part mappings
    ComponentRegistry.addPart(`${APP_NAME}:child-list`, {
        query: getChildList,
        processor: childListProcessor,
        view: ChildList
    });

    As you may have noticed this component has a query, a view and a processor. Processors are optional JavaScript functions that can do whatever you need it to. For instance fetch data from another source than the CMS, or post-process the query response before it is passed to the view etc.

    The Enonic adapter will execute the query, pass the result via the processor, and then to the view which renders the component.

    child list rendered

    Try customizing the part configuration and see what happens.

Optional Task: The heading part

For more components to play with, you may add another part:

  1. Add files to the Enonic app

    This part definition includes a form, with an input field called heading.This makes it possible for editors to override the heading (aka displayName) coming from the content item.

    src/main/resources/site/parts/heading/heading.xml
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <part xmlns="urn:enonic:xp:model:1.0">
        <display-name>Heading</display-name>
        <description>Demo heading</description>
        <form>
            <input type="TextLine" name="heading">
                <label>Override heading</label>
            </input>
        </form>
    </part>
  2. Redeploy the Enonic app and add the part to your page

  3. Add the heading component in Next.js.

    src/components/parts/Heading.tsx
    import React from 'react'
    import {APP_NAME, PartData} from '@enonic/nextjs-adapter';
    
    // fully qualified XP part name:
    export const HEADING_PART_NAME = `${APP_NAME}:heading`;
    
    export interface HeadingData {
        part: PartData;
        common: any;
    }
    
    const HeadingView = ({part, common}: HeadingData) => (
        <h2>{part?.config?.heading || common?.get?.displayName}</h2>
    );
    
    export default HeadingView;

    Add the following lines to _mappings.ts:

    src/components/_mappings.ts
    import Heading from './parts/Heading';
    
    // Part mappings
    ComponentRegistry.addPart(`${APP_NAME}:heading`, {
        view: Heading
    });

    You should now have two configurable parts to play with.

In the next chapter you will make page composition even more interesting, with the introduction of layouts.


Contents

Contents