Rendering basics

Contents

In this chapter you will add a new component that renders persons

Person content type

The Headless Movie DB contains sample content, and content types. One of these is Person. From Content Studio, you may find and list persons. By selecting the JSON preview, you can also study the properties of a person:

Person JSON
{
    "_id": "a8b374a2-c532-45eb-9aa1-73d1c37cd681",
    "_name": "lea-seydoux",
    "_path": "/hmdb/persons/lea-seydoux",
    "creator": "user:system:su",
    "modifier": "user:system:su",
    "createdTime": "2021-11-23T10:01:16.711Z",
    "modifiedTime": "2021-11-23T13:06:38.493Z",
    "owner": "user:system:su",
    "type": "com.enonic.app.hmdb:person",
    "displayName": "Léa Seydoux",
    "hasChildren": true,
    "language": "en",
    "valid": true,
    "childOrder": "modifiedtime DESC",
    "data": {
        "photos": "09b3af0e-6da3-4bcf-88d9-11cbe9c41283",
        "bio": "French actress Léa Seydoux was born in 1985 in Paris, France, to Valérie Schlumberger, a philanthropist, and Henri Seydoux, a businessman.",
        "dateofbirth": "1985-07-01"
    },
    "x": {},
    "page": {},
    "attachments": {},
    "publish": {
        "from": "2024-11-13T13:32:39.427Z",
        "first": "2024-11-13T13:32:39.427Z"
    },
    "workflow": {
        "state": "READY",
        "checks": {}
    }
}

To implement the rendering we need to add code that will perform a two-step operation:

Fetch the data

  1. Create a data fetcher

    /src/main/resources/react4xp/components/content/PersonProcessor.ts
    import {get as getContentByKey} from '/lib/xp/content';
    import {imageUrl} from '/lib/xp/portal';
    import {toArray} from "/react4xp/utils/arrayUtils";
    import {parentPath} from '/react4xp/utils/path';
    import {PageDescriptor} from '@enonic-types/core';
    import type {Content} from '@enonic-types/lib-content';
    import type {ComponentProcessorFunction} from '@enonic-types/lib-react4xp/DataFetcher';
    
    function fetchAdditionalPhotos(photosIds) {
        return photosIds.map(photoId => {
            const photoContent = getContentByKey<Content>({key: photoId});
            return {
                _id: photoContent._id,
                title: photoContent.displayName,
                imageUrl: imageUrl({id: photoContent._id, scale: 'block(175, 175)'})
            };
        });
    }
    
    export const personProcessor: ComponentProcessorFunction<PageDescriptor>
        = (params) => {
    
        const photos: string[] = toArray<string>(params.content.data.photos as string | string[])
        const firstPhotoId = photos[0] || '';
        const remainingPhotoIds = photos.slice(1) || '';
    
        const {_id, displayName} = getContentByKey<Content>({key: firstPhotoId});
    
        const extraPhotos = fetchAdditionalPhotos(remainingPhotoIds);
    
        return {
            displayName: `${params.content.displayName}`,
            photo: {
                _id,
                title: displayName,
                imageUrl: imageUrl({id: _id, scale: 'block(1200, 675)'})
            },
            birthDate: params.content.data.dateofbirth,
            restPhotos: extraPhotos,
            parent: parentPath(params.request.path)
        };
    };
  2. Then add it to the dataFetcher, with these lines:

    dataFetcher
    import {personProcessor} from './components/content/PersonProcessor';
    
    dataFetcher.addContentType('com.enonic.app.hmdb:person', {processor: personProcessor});

We are now able to fetch the Person data, next up we need to render it.

Register component

The component consists of two files:

/src/main/resources/react4xp/components/content/Person.tsx (React component)
import React from 'react'
import styles from './Person.module.css';

export const Person = (props) => {
    const {parent, displayName, photo, restPhotos, bioHtml, birthDate} = props as any;
    return (
        <div className={styles.person}>
            <div className={"back"}>
                <a href={parent}>
                    <p>Back</p>
                </a>
            </div>
            <h1>{displayName}</h1>
            <p>{birthDate}</p>
            <div>
                {
                    photo ? (
                        <>
                            <div className={styles.photos}>
                                <img src={photo.imageUrl}
                                     title={photo.title}
                                     alt={photo.title}
                                     height={675}
                                     width={1200}
                                     loading="eager"
                                />
                            </div>
                        </>
                    ) : (
                        <p>No photo available</p>
                    )
                }
                {restPhotos && restPhotos.length > 0 && (
                    <>
                        <h2>Photos</h2>
                        <div className={styles.photoContainer}>
                            <div className={styles.photoGrid}>
                                <div className={styles.photoScroll}>
                                    {restPhotos.map((photo, index) => (
                                        <img key={index}
                                             src={photo.imageUrl}
                                             title={photo.title}
                                             alt={photo.title}
                                             height={175}
                                             width={175}
                                        />
                                    ))}
                                </div>
                            </div>
                        </div>
                    </>
                )}
            </div>
        </div>
    )
}

+ ./src/main/resources/react4xp/components/content/Person.module.css (CSS module)

.person .bio {
  font-family: sans-serif;
}

.person {
  display: flex;
  flex-direction: column;
  margin-inline: auto;
  min-height: 1600px;
}

.person h1 {
  margin-bottom: 0;
}

.person p {
  margin-top: 0;
}

.flexBlock {
  display: flex;
  justify-content: space-between;
}

.photoContainer {
  position: relative;
  display: flex;

  ::-webkit-scrollbar {
    -webkit-appearance: none;
    height: 6px;
  }

  ::-webkit-scrollbar-thumb {
    border-radius: 8px;
    background-color: ghostwhite;
  }
}

.photoGrid {
  overflow-x: auto;
}

.photoScroll {
  display: flex;
  gap: 25px;
  white-space: nowrap;
  padding: 0;
  margin: .5rem 0 .5rem 0;
}

.photoScroll img {
  border-radius: 5px;
}

.photos {
  margin-block: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  grid-gap: 50px;
  gap: 50px;
}

.restPhotosContainer {
  padding: 1rem 0;
  max-width: calc(90vw - 1rem);
}

.restPhotos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15%, 1fr));
  gap: 5%;
  margin-block: 1rem;
}

.photos img {
  width: 100%;
  height: auto;
  border-radius: 8px;
}

.restImg {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  object-position: 50% 50%;
  border-radius: 5px;
}


.richText {
  font-size: 24px;
}


.richText figure img {
  border-radius: 5px;
}

.photosHeader {
  margin-top: 0;
  color: #EF82F0;
}

Once they are created register the component in the componentRegistry, by adding the following lines:

componentRegistry
import {Person} from './components/content/Person';

componentRegistry.addContentType('com.enonic.app.hmdb:person', {View: Person});

Result

Back in Content Studio, select a random person to see the glorious result.

Next

You are on a roll, next - lets look into app layout.


Contents

Contents

AI-powered search

Juke AI