Custom flow syntax

Contents

Custom flow syntax

Lesson overview

In this example things get a bit more complex. We’ll make another Part, and re-use the Page controller, Template and Content type we made previously.

  • However, we won’t use the React4xp.render shorthand function from before. Instead, we’ll look at a more explicit react4xp syntax:

    1. It constructs one or more data-holding React4xp objects. This gives you better control and opportunities for logic steps when you handle your entry; you can manipulate the object(s) or extract data from them before rendering.

    2. Here, each entry is rendered in two separate methods: renderBody and renderPageContributions (full details: React4xp data object API)

    3. You can pretty easily render multiple entries from the same controller, by chaining together their rendering.

  • We will also use a thymeleaf template for one of the entries - and take advantage of the react4xp data object to send its ID to the thymeleaf model - just to demonstrate a way to combine react and thymeleaf rendering.

  • In addition to chain-rendering more than one entry, we will use jsxPaths to point to entries in a different location. Actually, let’s just recycle the entries from before:

Files involved:
site/parts/
    custom-flow/            (1)
        custom-flow.xml
        custom-flow.es6
        custom-flow-view.html

    color/
        color.jsx           (2)

site/pages/
    hello-react/
        hello-react.jsx     (3)
1 The new Part we’re making, custom-flow.
2 The already existing entry color.jsx: a different folder from our custom-flow Part, and different name.
3 We’re also reusing another existing entry hello-react.jsx (with props, not the first hardcoded one), located under site/pages/hello-react/. So even though what we’re making here is a Part, an entry from a Page, or anywhere, is fine: when using jsxPaths, all entries are equal.


Code

Part definition

The Part definition again defines some base data for us to play with. This is exactly the same definitions as before, just merged together into one Part: the greeting and bottle/thing counter from hello-react.xml, and the color from color.xml.

custom-flow.xml:
<part>
    <display-name>CustomFlow example</display-name>
    <description>Single part with multiple entries</description>
    <form>
        <input name="color" type="TextLine">
            <label>What's the color of the thing?</label>
            <occurrences minimum="1" maximum="1"/>
            <default>red</default>
        </input>

        <input type="TextLine" name="greeting">
            <label>What's the greeting?</label>
            <default>Hello</default>
            <occurrences minimum="1" maximum="1"/>
        </input>

        <input type="TextLine" name="greetee">
            <label>Who shall we greet?</label>
            <occurrences minimum="1" maximum="1"/>
            <default>world</default>
        </input>

        <input type="TextLine" name="things">
            <label>What are the things on the wall?</label>
            <occurrences minimum="1" maximum="1"/>
            <default>bottles of beer</default>
        </input>

        <input type="Long" name="startCount">
            <label>How many of them are there?</label>
            <occurrences minimum="1" maximum="1"/>
            <default>99</default>
        </input>
    </form>
</part>


Thymeleaf view

Next, we’re adding a regular thymeleaf view template file.

This forms the initial HTML base, with color and targetId inserted by thymeleaf. This is actually not different from before: previously we’ve used a hardcoded string as the HTML base that react is rendered into. Now we’re just letting thymeleaf make it. Same thing, the base HTML string can come from anywhere.

The controller will render two entries into this base HTML. To be clear: even though we’re using two entries, this is still one single XP Part. The first entry is color.jsx which will be inserted into the element <div data-th-id="${targetId}"></div>. The second one, hello-react.jsx will also be rendered and inserted into this base HTML, but it will have a react4xp-ID that does not match any element ID here. This will cause react4xp to revert to the default behavior: generate a new container <div> and insert it right at the end of the root element: after </section> here:

custom-flow-view.html:
<div class="custom-flow-view">
    <section class="color-section">
        <h2 data-th-text="|Ain't nothing but a ${color} thing|"></h2>
        <p>Here it comes:</p>
        <div data-th-id="${targetId}"></div>
    </section>
</div>


Part controller

The controller now has more complexity than before, so here’s an overview:

  • Each entry is used to set up a data-holding reactxp object, in steps before the rendering is called. In these steps, data (options and props) can be both injected into the react4xp objects, and extracted from them.

  • The output of one rendering is used as the base for the next; chaining them together and gradually building up the final output.

  • HTML body and page contributions are rendered separately for each entry. So there is one multi-entry flow for body, and another for page contributions.

custom-flow.es6
const portal = require('/lib/xp/portal');
const React4xp = require('/lib/enonic/react4xp');
const thymeleaf = require('/lib/thymeleaf');

const view = resolve('custom-flow-view.html');


exports.get = function(request) {
    // Fetching data from the part config:
    const component = portal.getComponent();
    const partConfig = (component || {}).config || {};



    // Setting up the data-holding object for hello-react.jsx:
    const helloObj = new React4xp(`site/pages/hello-react/hello-react`);     (1)
    helloObj.setProps({                                                      (2)
            message: partConfig.greeting,
            messageTarget: partConfig.greetee,
            droppableThing: partConfig.things,
            initialCount: partConfig.startCount
        })


    // Setting up colorObj, the data-holding object for color.jsx:
    const colorObj = new React4xp(`site/parts/color/color`);
    colorObj                                                                 (3)
        .setProps({ color: partConfig.color })
        .setId("myColorThing")                                               (4)
        .uniqueId()                                                          (5)


    // Using thymeleaf to render container HTML,
    // inserting the colorObj's ID into the target container where colorObj will be rendered:
    const thymeleafModel = {
        color: colorObj.props.color,
        targetId: colorObj.react4xpId
    }
    const colorSectionContainer = thymeleaf.render(view, thymeleafModel);    (6)


    // Render the color.jsx entry into the same-ID target container in the container HTML:
    const colorBody = colorObj.renderBody({
        body: colorSectionContainer                                          (7)
    });
    // Rendering the activating page contributions of color.jsx.
    const colorPageContributions = colorObj.renderPageContributions({
        pageContributions: {                                                 (8)
            bodyEnd: `<script>console.log('Created: ${colorObj.props.color} thing.');</script>`
        }
    });


    // Determining if the rendering context is not inside Content Studio:
    const isOutsideContentStudio = (                                         (9)
        request.mode === 'live' ||
        request.mode === 'preview'
    );


    // Rendering helloObj's entry into colorBody (which is basically custom-flow-view.html with color.jsx added),
    // using client-side rendering only outside of Content Studio:
    const finalBody = helloObj.renderBody({
        body: colorBody,                                                     (10)
        clientRender: isOutsideContentStudio
    });
    // Adding helloObj's page contributions to the previously rendered page contributions,
    // duplicating clientRender between renderPageContributions and renderBody (pair-wise for each entry).
    const finalPageContributions = helloObj.renderPageContributions({
        pageContributions: colorPageContributions,                           (11)
        clientRender: isOutsideContentStudio
    });


    // Finally, returning the response object in the standard XP-controller way:
    return {
        body: finalBody,
        pageContributions: finalPageContributions
    }
};
1 Constructing the data-holding react4XP object helloObj from the hello-react.jsx entry we finalized before. The constructor takes one argument, which is mandatory: an entry reference. This can be an XP component object like before, OR like we’re doing here: a jsxPath. This entry reference is used the same way as the first argument, entry, in React.render.
2 setProps modifies helloObj, to add some props. This of course corresponds to the second argument, props, in React4xp.render.
3 After creating a react4xp object colorObj for the second entry, color.jsx, we’re modifying that too, starting with adding props. Note the builder-like pattern here: each of the setter methods (setProps, setId and uniqueId) returns the react4xp object itself. This allows you to run them directly after each other like this, so this example is just a shorter way of writing colorObj.setProps({ color: partConfig.color }); colorObj.setId("myColorThing"); colorObj.uniqueId();.
4 setId sets the ID of the react4xp object and the target element that the rendering will look for in the HTML. If an ID has previously been set for the react4xp object, setId will overwrite it.
5 uniqueId makes sure the react4xp object has a globally unique ID. It can work in two ways. If an ID has not been set previously, a simple random ID is generated. If an ID has been set, like here in step 4, the random number is appended after the existing ID. So the order between setId and uniqueId matters - what we get here is "myColorThing" plus a random number (separated by an underscore), giving us something recognizable in the output but still ensuring that the element ID is not repeated in cases where this part is used more than once on a page. Had setId been run after uniqueId` however, setId would just overwrite the previous unique ID with the supplied string - and possibly repeated.
6 So, since there’s a random component in the ID string of the react4xp object and we want that ID to match a specific element in the HTML, we read the ID from colorObj.react4xpId and inject it into the thymeleaf template as targetId.
7 We render colorObj into a new HTML string, based on the HTML output of the thymeleaf rendering…​
8 …​and render the page contributions for activating it in the client. We add a small extra script just to demonstrate that extra pageContributions can be added in renderPageContributions as well, by passing them through as before. Now we have both the HTML body and page contributions from the first entry, color.jsx.
9 But we’re going to add a second entry to this Part, just because we can. That entry’s going to be clientside rendered (as opposed to color.jsx which gets serverside-rendered because no clientRender was flagged). However, since this syntax doesn’t automatically handle in-content-studio rendering (unlike React4xp.render which does handle that), we need to determine if this rendering is happening inside or outside Content Studio.
10 Rendering the HTML body of second entry, hello-react.jsx, into the HTML body from the first entry: colorBody from before. ID and target element is handled the same way as in React4xp.render: since no ID is set (we created helloObj without running .setId) a random ID will be used. And since that ID doesn’t match any ID in the base HTML body (colorBody), the rendering will just create a target container element inside the root element of colorBody, after other content. We’re letting the value of clientRender depend on whether or not we’re rendering inside Content Studio (if we are, then isOutsideContentStudio is false and serverside-rendering is forced, while we get clientside rendering outside).
11 Rendering the activating page contributions for helloObj, adding them to the previous colorPageContributions by passing them through the rendering. We’re using the same clientRender: isOutsideContentStudio here too:

The rendering mode (client- or serverside) must match between renderBody and .renderPageContributions for an entry!

This is on an entry-by-entry basis: there’s no problem mixing multiple entries in the same controller like in this example, where one entry is serverside and the other is clientside rendered - as long as each entry’s renderBody and renderPageContributions have a matching clientRender.

And we’re done, our new custom-flow Part is now ready.


Setup

All of this amounted to a new Part, custom-flow. It can be added to any Region, so just follow the same setup steps in Content Studio to add and see it.

Again, if you add more than one custom-flow Part to a Region, you’ll see that they are independent both in behavior and output; separated by their unique ID.

Further reading

The API behind the custom flow syntax: React4xp data objects.




Contents