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.
-
Add the
child-list
part to the Enonic appsrc/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. -
Build and deploy your Enonic app once more to make the new part available in Content Studio (happens automatically if you started
enonic project dev
). -
Add parts to the page
Back in Content Studio, edit the
/hmdb/persons
folder item. Expand the page preview section by clicking display icon in the top right corner and selectMain page
controller in the dropdown in the middle of page. Create the page and add a new part to the region (right-click → Insert
, or drag-n-drop from theInsert panel`
on the right hand side). Finally select theChild list
in the part descriptor dropdown.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.
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.
-
Add the part implementation to the Next.js app
src/components/parts/ChildList.tsximport {Context, PartProps, VariablesGetterResult} from '@enonic/nextjs-adapter'; import Link from 'next/link'; import React from 'react' const FORBIDDEN_TYPES_REGEXP = "^media:.*|portal:fragment|portal:template-folder|portal:page-template$"; const ChildList = (props: PartProps) => { const {data, meta} = props; const children = data.get.children; if (!children || children.length === 0) { return null; } const prefix = meta.baseUrl + (meta.locale && meta.locale !== meta.defaultLocale ? meta.locale + '/' : ''); return ( <main style={{ margin: `0 auto`, maxWidth: 960, padding: `0 1.0875rem`, }}> { children && <ul>{ children.map((child: any, i: number) => ( <li key={i}> <Link href={prefix + child._path}>{child.displayName}</Link> </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 type } } } }` }, variables: function (path: string, context?: Context, config?: any): VariablesGetterResult { return { path, order: config?.sorting } } }; export async function childListProcessor(data: any, context?: Context, config?: any): Promise<any> { // exclude forbidden types data.get.children = data.get.children?.filter((child: any) => !child.type.match(FORBIDDEN_TYPES_REGEXP)); return data; }
-
Then, register the components in _mappings.ts:
Update _mappings.ts with the following new lines:
src/components/_mappings.tsimport 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.
Like macros, all page components support configQuery
. Here though, ifconfigQuery
is omitted, theconfig
prop will fallback to the raw form values viaconfigAsJson
.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:
-
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>
-
Redeploy the Enonic app and add the part to your page (happens automatically if you started
enonic project dev
). -
Add the heading component in Next.js.
src/components/parts/Heading.tsximport 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.tsimport 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.