Parts
Contents
Enonic provides a highly customizable component type called parts. Similar to page components parts must be defined in both Enonic and Next 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 Ascending or Descending. To work seamlessly with Content Studio’s WYSIWYG preview, in the Enonic app, parts need a special controller file that helps the preview reload the component when there are changes, without having to refreshing the entire page.
Add the preview controller file for the child-list part
This controller simply is triggered when a component is updated in the page editor, and simply forwards the request to the proxy - like all other requests.
src/main/resources/site/parts/child-list/child-list.jsvar proxy = require('/lib/nextjs/proxy'); exports.get = proxy.get;
Build and deploy your Enonic app once more to make the new part accessible to from Content Studio.
Add parts to the page
Once the app has deployed, head over to Content Studio. This time, edit the
Persons
folder item. Create the page and drag’n drop the new part into the region (right-click → Insert
, or drag-n-drop from theInsert panel`
on the right).The part will appear without making any changes to Next, this is because the Enonic Adapter contains fallback part rendering, as long as NextJS is running in DEV mode.
Task: Configure part rendering in Next
Heading over to the front-end code, we must register a new component that support actual rendering of this part.
Add the part implementation to the Next app
src/components/parts/ChildList.tsximport React from 'react' import {getUrl} from '@enonic/nextjs-adapter/UrlProcessor'; import {Context} from '@enonic/nextjs-adapter'; import {VariablesGetterResult} from '@enonic/nextjs-adapter/ComponentRegistry'; 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; }
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'; ... 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, then to the view which then renders the component.
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>
src/main/resources/site/parts/heading/heading.jsvar proxy = require('/lib/nextjs/proxy'); exports.get = proxy.get;
Redeploy the Enonic app and add the part to your page
Add and register the heading component in Next
src/components/parts/Heading.tsximport React from 'react' import {APP_NAME} from '@enonic/nextjs-adapter'; import {PartData} from '@enonic/nextjs-adapter/guillotine/getMetaData'; // 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'; ... ComponentRegistry.addPart(`${APP_NAME}:heading`, { view: Heading });
You should now have two configurable parts to play with.
In the next chapter we will make page composition even more interesting, with the introduction of layouts.