Next.XP and RichText in Part

Enonic version: newest
OS: WIN 11 or MAC OS

How can I handle an HTML Area Input of a part-configuration in a part-component on the frontend ?

I know, that I can access all configuration via the part-props. But here I only have the raw-property, so I can not use the RichTextView component.

Maybe I am following the wrong approach - any tipps are welcome :- :grinning:

Hi there.
You should have the processedHtml field there as well?
Which version of Guillotine are you using?

Hi @tsi ,
i am on 6.0.5.
For a content-component that is correct, but not so for a part-component.

In my app I have the following config for a part:

<input name="teaser" type="HtmlArea">
    <label>Text</label>
    <default>
        <p>Enter description here</p>
    </default>
</input>

This is the result, when I add a link in part-configuration

Part:
{
  "descriptor": "de.lyn.be:banner",
  "config": {
    "header": "neues Buch ",
    "teaser": "<p>see here:  <a href=\"content://d955b5ee-c605-4cdd-b3e6-663e2bc4fbc9\">Reliquie</a></p>\n",
    "image": "28911ad4-e29f-4533-8c1b-a27b59dd8678"
  }
}

And there also seems to be an issue with absolute path in frontend for images and links:
I also have a htmlArea in a content-type. When I add an image, the src is not absolute! So this works in content-studio with proxy. But not on the frontend!

<img alt="Katharina_m.jpg" src="/site/default/master/lyn/graphql/_/image/c74c551a-88c7-4ef7-96bc-f6c2ebfae41a:a882aada761bbb23f2abf2c6084ac6237faf8172/width-768/Katharina_m.jpg" data-image-ref="6843ff84-e187-4e7c-a20f-6c791abc4fb6" style="width:100%">

The path for a link from the htmlArea on the other hand looks the following:

http://localhost:3000/site/default/master/lyn/secretum
and should be:
http://localhost:3000/secretum

This is how my query looks:

import {APP_NAME_UNDERSCORED} from "../../_enonicAdapter/utils";

const getBook = `
query ($path: ID!) {
  guillotine {
    get(key: $path) {
      displayName
      ... on ${APP_NAME_UNDERSCORED}_Book {
        data {
          subtitle
          content {
            processedHtml
            images {
             image {
                _id               
             }
            }
            links {
              ref
            }            
          }
          cover {
            ... on media_Image {
              imageUrl: imageUrl(type: absolute, scale: "width(250)")
              attachments {
                name
              }
            }
          }
        }
      }
      parent {
        _path(type: siteRelative)
      }
    }
  }
}
`

export default getBook;

and this is my view:

import React from 'react';
import {FetchContentResult} from "../../_enonicAdapter/guillotine/fetchContent";
import {Container, Heading, Image} from "@chakra-ui/react";
import RichTextView from "../../_enonicAdapter/views/RichTextView";
import PropsView from "./Props";

const Book = (props: FetchContentResult) => {

    const {displayName, data, parent} = props.data?.get;
    return (
        <Container>
            <Heading>
                {displayName}
            </Heading>
            <Image src={data.cover.imageUrl}></Image>
            <RichTextView data={data.content} meta={props.meta}/>
            <pre>
                {JSON.stringify(props.meta, null, 2)}
            </pre>
            <PropsView {...props} />
        </Container>
    );
};

export default Book;

Thomas

Hey @luctho

Use the richTextQuery from the _enonicAdaptor to get the needed data. (I’m using the nextjs npm module)
The approach you are goin for would work, but it get really complicated when trying to get macroes to render.

import { RichTextData, richTextQuery } from "@enonic/nextjs-adapter/guillotine/getMetaData";
import RichTextView from "@enonic/nextjs-adapter/views/RichTextView";

// Graphql query
export const getPartQuery = () => `{
    ...Other properties to query...
    ${richTextQuery('text')}
    
}`


// Part view (Don't forget to add the meta data)
export const PartView = (props: PartProps) => {
    return <RichTextView data={props?.part?.config?.text || ''} meta={props.meta}></RichTextView>
}

Now you need to have the right context of the query. (It does not work on compile time :frowning: )

// _mappings.ts
ComponentRegistry.addPart(`${APP_NAME}:part`, {
    configQuery: (() => {
        return getPartQuery();
    })(),
    view: PartView,
});

I hope this helps to get the htmlArea in parts to render properly.

Yeah the urls to link will not work in live view unless you process them.

// Grpahql query (OtherContent is a ContentSelector)
export const GetLink = `{
    otherContent {
        pageUrl(type: absolute)
    }
}`

export const PartView = (props: PartProps) => {
    return <a href={getUrl(config?.otherContent?.pageUrl || '', props.meta)}>
}

They image url should work with an absolute url from the server.
I have had some issues with the next <Image> part since it seems to do some special handeling of urls and sizes of images

Hi @pkw - I do not filly understand your answer for the htmlArea.
My use-case is the following: The part should render a hero-banner with overlayed html-text.
Currently my query for the part fetches the data for the image.
This is the config of the part in the app:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<part xmlns="urn:enonic:xp:model:1.0">
    <display-name>Banner</display-name>
    <description>Banner mit Bild und OverlayText</description>
    <form>
        <input name="header" type="TextLine">
            <label>Überschrift</label>
            <occurrences minimum="1" maximum="1"/>
        </input>
        <input name="teaser" type="HtmlArea">
            <label>Text</label>
            <default>
                <p>Enter description here</p>
            </default>
        </input>
        <input name="image" type="ImageSelector">
            <label>Anzuzeigendes Bild</label>
            <occurrences minimum="0" maximum="1"/>
            <config>
                <allowPath>${site}/*</allowPath>
                <treeMode>true</treeMode>
                <showStatus>true</showStatus>
                <hideToggleIcon>false</hideToggleIcon>
            </config>
        </input>
    </form>
</part>

and this is my component in Next:

import React, {useEffect, useState} from "react";
import {PartProps} from "../../_enonicAdapter/views/BasePart";
import {Box, Flex, Heading, Img, Text} from "@chakra-ui/react";
import {VariablesGetterResult} from "../../_enonicAdapter/ComponentRegistry";
import {Context} from "../../_enonicAdapter/guillotine/fetchContent";
import PropsView from "../views/Props";
import RichTextView from "../../_enonicAdapter/views/RichTextView";
import HTMLReactParser from "html-react-parser";


const Banner = (props: PartProps) => {

    const {part, data, meta, common} = props;
    const {header, teaser} = part.config;
    const [teaserText, setTeaserText] = useState('');

    useEffect(() => {
        const parsedText =  HTMLReactParser(teaser);
        // @ts-ignore
        setTeaserText(parsedText);
    },[]);

    return (
        <>
            <Box backgroundColor={"gray.100"} as="section" h="100vH" display="flex" alignItems="center"
                 position="relative">
                <Box py={10} position="relative" zIndex={1} margin={"auto"}>
                    <Heading as="h1" size="4xl">
                        {header}
                    </Heading>

                    <Text my={5}>
                        {teaserText}
                    </Text>

                </Box>
                <Flex
                    id="image-wrapper"
                    position="absolute"
                    insetX="0"
                    insetY="0"
                    w="full"
                    h="full"
                    overflow="hidden"
                    align="center"
                >
                    <Box position="relative" w="full" h="full">
                        <Img
                            src={data.get?.imageUrl}
                            alt="Main Image"
                            w="full"
                            h="full"
                            objectFit="cover"
                            objectPosition="top bottom"
                            position="absolute"
                        />
                        <Box position="absolute" w="full" h="full" bg="blackAlpha.600"/>
                    </Box>
                </Flex>
            </Box>
            <PropsView {...props}/>
        </>
    );
};

export default Banner;

export const getBanner = {
    query: `query ($path: ID!) {
  guillotine {
    get(key: $path) {
      _id
      type      
      ... on media_Image {
        imageUrl(type: absolute, scale: "block(1024, 768)")
        displayName
        data {
          caption
        }
      }
    }
  }
}`,
    variables: function (path: string, context?: Context, config?: any): VariablesGetterResult {
        console.log("context::  ", context);
        return {
            path: config.image ? config.image : ""
        }
    }
}

and this my the mapping:

ComponentRegistry.addPart(`${APP_NAME}:banner`, {
    query: getBanner,
    view: Banner,
})

Where and how do I have to integrate the richTextQuery here ???

Here I have the same problem.
I am talking about a link I entered in htmlArea - linking to other Content via contentSelector works for me.

They image url should work with an absolute url from the server.

Here it is the same. As this is also from the htmlArea I have no control over the Url received from API !

Update:
I had a look at the code of RichTextView and found these lines:

 case UrlProcessor.IMG_TAG:
      ref = el.attribs[UrlProcessor.IMG_ATTR];
      const src = el.attribs['src'];
      // do not process content images in next to keep it absolute
      if (ref && src && UrlProcessor.isContentImage(ref, allData.images)) {
          el.attribs['src'] = getUrl(src, meta);
      }
      break;

In my case getUrl(..) returns a relative Url !!! Maybe this is a bug !

Where to insert the richTextQuery? This needs to be included in the query for the banner.
In the code you posted that would be getBanner.

GetBanner is probably a string so you just insert it like i posted inside the getPartQuery:

export const getPartQuery = () => `{
    ...Other properties to query...
    ${richTextQuery('text')}
    
}`

Don’t forget the to change the mapping as i posted above too:

// _mappings.ts
ComponentRegistry.addPart(`${APP_NAME}:part`, {
    configQuery: (() => {
        return getPartQuery();
    })(),
    view: PartView,
});

This is needed since the richTextQuery uses some runtime values when creating urls for images and links.

Hi Persijn,

now I have it :grinning: Thanks a lot for your support and patience
Just to share the complete solution with others, here we go:

query

export const getBanner = () => `{
  header 
  image {
      ... on media_Image {
             imageUrl: imageUrl(type: absolute, scale: "block(1024, 768)") 
          }
  }    
  ${richTextQuery('teaser')}
}`

mapping

ComponentRegistry.addPart(`${APP_NAME}:banner`, {
    configQuery:(()=> {
        return getBanner();
    })(),
    view: Banner,
})

view

const Banner = (props: PartProps) => {

    const {part, data, meta, common} = props;
    const {header, teaser, image} = part.config;


    return (
        <>
            <Box backgroundColor={"gray.100"} as="section" h="100vH" display="flex" alignItems="center"
                 position="relative">
                <Box py={10} position="relative" zIndex={1} margin={"auto"}>
                    <Heading as="h1" size="4xl">
                        {header}
                    </Heading>
                    <RichTextView data={teaser} meta={props.meta}/>

                </Box>
                <Flex
                    id="image-wrapper"
                    position="absolute"
                    insetX="0"
                    insetY="0"
                    w="full"
                    h="full"
                    overflow="hidden"
                    align="center"
                >
                    <Box position="relative" w="full" h="full">
                        <Img
                            src={image.imageUrl}
                            alt="Main Image"
                            w="full"
                            h="full"
                            objectFit="cover"
                            objectPosition="top bottom"
                            position="absolute"
                        />
                        <Box position="absolute" w="full" h="full" bg="blackAlpha.600"/>
                    </Box>
                </Flex>
            </Box>
            <PropsView {...props}/>
        </>
    );
};

export default Banner;

2 Likes