Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Editing Integration] Expose raw placeholder name not a computed one when dynamic placeholder is rendered #1816

Merged
merged 5 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Our versioning strategy is as follows:

### 🛠 Breaking Change

* Editing Integration Support: ([#1776](https://github.com/Sitecore/jss/pull/1776))([#1792](https://github.com/Sitecore/jss/pull/1792))([#1773](https://github.com/Sitecore/jss/pull/1773))([#1797](https://github.com/Sitecore/jss/pull/1797))([#1800](https://github.com/Sitecore/jss/pull/1800))([#1803](https://github.com/Sitecore/jss/pull/1803))([#1806](https://github.com/Sitecore/jss/pull/1806))([#1809](https://github.com/Sitecore/jss/pull/1809))
* Editing Integration Support: ([#1776](https://github.com/Sitecore/jss/pull/1776))([#1792](https://github.com/Sitecore/jss/pull/1792))([#1773](https://github.com/Sitecore/jss/pull/1773))([#1797](https://github.com/Sitecore/jss/pull/1797))([#1800](https://github.com/Sitecore/jss/pull/1800))([#1803](https://github.com/Sitecore/jss/pull/1803))([#1806](https://github.com/Sitecore/jss/pull/1806))([#1809](https://github.com/Sitecore/jss/pull/1809))([#1816](https://github.com/Sitecore/jss/pull/1816))
* `[sitecore-jss-react]` Introduces `PlaceholderMetadata` component which supports the hydration of chromes on Pages by rendering the components and placeholders with required metadata.
* `[sitecore-jss]` Chromes are hydrated based on the basis of new `editMode` property derived from LayoutData, which is defined as an enum consisting of `metadata` and `chromes`.
* `ComponentConsumerProps` is removed. You might need to reuse `WithSitecoreContextProps` type.
Expand Down
106 changes: 105 additions & 1 deletion packages/sitecore-jss-react/src/components/Placeholder.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ import * as FEAASWrapper from './FEaaSWrapper';
import { HiddenRendering } from './HiddenRendering';
import { MissingComponent, MissingComponentProps } from './MissingComponent';
import { Placeholder } from './Placeholder';
import { ComponentProps } from './PlaceholderCommon';
import {
ComponentProps,
getDynamicPlaceholderPattern,
isDynamicPlaceholder,
} from './PlaceholderCommon';
import { SitecoreContext } from './SitecoreContext';
import { ComponentFactory } from './sharedTypes';
import { PlaceholderMetadata } from './PlaceholderMetadata';
Expand Down Expand Up @@ -814,6 +818,35 @@ describe('PlaceholderMetadata', () => {
},
};

const layoutDataForNestedDynamicPlaceholder = (rootPhKey: string) => ({
sitecore: {
context: {
pageEditing: true,
editMode: EditMode.Metadata,
},
route: {
name: 'main',
uid: 'root123',
placeholders: {
[rootPhKey]: [
{
uid: 'nested123',
componentName: 'Header',
placeholders: {
logo: [
{
uid: 'deep123',
componentName: 'Logo',
},
],
},
},
],
},
},
},
});

const componentFactory: ComponentFactory = (componentName: string) => {
const components = new Map<string, React.FC>();

Expand Down Expand Up @@ -930,7 +963,78 @@ describe('PlaceholderMetadata', () => {
].join('')
);
});

it('should render dynamic placeholder', () => {
const phKey = 'container-1';
const layoutData = layoutDataForNestedDynamicPlaceholder('container-{*}');
const wrapper = mount(
<SitecoreContext componentFactory={componentFactory} layoutData={layoutData}>
<Placeholder name={phKey} rendering={layoutData.sitecore.route} />
</SitecoreContext>
);

expect(wrapper.html()).to.equal(
[
'<code type="text/sitecore" chrometype="placeholder" class="scpm" kind="open" id="container-{*}"></code>',
'<code type="text/sitecore" chrometype="rendering" class="scpm" kind="open" id="nested123"></code>',
'<div class="header-wrapper">',
'<code type="text/sitecore" chrometype="placeholder" class="scpm" kind="open" id="logo_nested123"></code>',
'<code type="text/sitecore" chrometype="rendering" class="scpm" kind="open" id="deep123"></code>',
'<div class="Logo-mock"></div><code type="text/sitecore" chrometype="rendering" class="scpm" kind="close"></code>',
'<code type="text/sitecore" chrometype="placeholder" class="scpm" kind="close"></code>',
'</div>',
'<code type="text/sitecore" chrometype="rendering" class="scpm" kind="close"></code>',
'<code type="text/sitecore" chrometype="placeholder" class="scpm" kind="close"></code>',
].join('')
);

expect(wrapper.find(PlaceholderMetadata).length).to.equal(4);
});

it('should render double digit dynamic placeholder', () => {
const phKey = 'container-1-2';
const layoutData = layoutDataForNestedDynamicPlaceholder('container-1-{*}');
const wrapper = mount(
<SitecoreContext componentFactory={componentFactory} layoutData={layoutData}>
<Placeholder name={phKey} rendering={layoutData.sitecore.route} />
</SitecoreContext>
);

expect(wrapper.html()).to.equal(
[
'<code type="text/sitecore" chrometype="placeholder" class="scpm" kind="open" id="container-1-{*}"></code>',
'<code type="text/sitecore" chrometype="rendering" class="scpm" kind="open" id="nested123"></code>',
'<div class="header-wrapper">',
'<code type="text/sitecore" chrometype="placeholder" class="scpm" kind="open" id="logo_nested123"></code>',
'<code type="text/sitecore" chrometype="rendering" class="scpm" kind="open" id="deep123"></code>',
'<div class="Logo-mock"></div><code type="text/sitecore" chrometype="rendering" class="scpm" kind="close"></code>',
'<code type="text/sitecore" chrometype="placeholder" class="scpm" kind="close"></code>',
'</div>',
'<code type="text/sitecore" chrometype="rendering" class="scpm" kind="close"></code>',
'<code type="text/sitecore" chrometype="placeholder" class="scpm" kind="close"></code>',
].join('')
);

expect(wrapper.find(PlaceholderMetadata).length).to.equal(4);
});
});

it('isDynamicPlaceholder', () => {
expect(isDynamicPlaceholder('container-{*}')).to.be.true;
expect(isDynamicPlaceholder('container-1-{*}')).to.be.true;
expect(isDynamicPlaceholder('container-1-2')).to.be.false;
expect(isDynamicPlaceholder('container-1')).to.be.false;
expect(isDynamicPlaceholder('container-1-2-3')).to.be.false;
expect(isDynamicPlaceholder('container-1-{*}-3')).to.be.true;
});

it('getDynamicPlaceholderPattern', () => {
expect(getDynamicPlaceholderPattern('container-{*}').test('container-1')).to.be.true;
expect(getDynamicPlaceholderPattern('container-{*}').test('container-1-2')).to.be.false;
expect(getDynamicPlaceholderPattern('container-1-{*}').test('container-1-2')).to.be.true;
expect(getDynamicPlaceholderPattern('container-1-{*}').test('container-1-2-3')).to.be.false;
});

after(() => {
(global as any).window.close();
});
3 changes: 2 additions & 1 deletion packages/sitecore-jss-react/src/components/Placeholder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ class PlaceholderComponent extends PlaceholderCommon<PlaceholderComponentProps>

const placeholderData = PlaceholderCommon.getPlaceholderDataFromRenderingData(
renderingData,
this.props.name
this.props.name,
this.props.sitecoreContext?.editMode
);

this.isEmpty = placeholderData.every((rendering: ComponentRendering | HtmlElementRendering) =>
Expand Down
49 changes: 36 additions & 13 deletions packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@ export type ComponentProps = {
rendering: ComponentRendering;
};

/**
* Returns a regular expression pattern for a dynamic placeholder name.
* @param {string} placeholder Placeholder name with a dynamic segment (e.g. 'main-{*}')
* @returns Regular expression pattern for the dynamic segment
*/
export const getDynamicPlaceholderPattern = (placeholder: string) => {
return new RegExp(`^${placeholder.replace(/\{\*\}+/i, '\\d+')}$`);
};

/**
* Checks if the placeholder name is dynamic.
* @param {string} placeholder Placeholder name
* @returns True if the placeholder name is dynamic
*/
export const isDynamicPlaceholder = (placeholder: string) => placeholder.indexOf('{*}') !== -1;

export interface PlaceholderProps {
[key: string]: unknown;
/** Name of the placeholder to render. */
Expand Down Expand Up @@ -131,34 +147,41 @@ export class PlaceholderCommon<T extends PlaceholderProps> extends React.Compone

static getPlaceholderDataFromRenderingData(
rendering: ComponentRendering | RouteData,
name: string
name: string,
editMode?: EditMode
) {
let result;
let phName = name.slice();

/** [SXA] it needs for deleting dynamics placeholder when we set him number(props.name) of container.
from backend side we get common name of placeholder is called 'nameOfContainer-{*}' where '{*}' marker for replacing **/
if (rendering?.placeholders) {
Object.keys(rendering.placeholders).forEach((placeholder) => {
const patternPlaceholder =
placeholder.indexOf('{*}') !== -1
? new RegExp(`^${placeholder.replace(/\{\*\}+/i, '\\d+')}$`)
: null;

if (patternPlaceholder && patternPlaceholder.test(name)) {
rendering.placeholders[name] = rendering.placeholders[placeholder];
delete rendering.placeholders[placeholder];
const patternPlaceholder = isDynamicPlaceholder(placeholder)
? getDynamicPlaceholderPattern(placeholder)
: null;

if (patternPlaceholder && patternPlaceholder.test(phName)) {
// When working in Edit Mode Metadata we need to keep the raw placeholder name
if (editMode === EditMode.Metadata) {
phName = placeholder;
} else {
rendering.placeholders[phName] = rendering.placeholders[placeholder];
delete rendering.placeholders[placeholder];
}
}
});
}

if (rendering && rendering.placeholders && Object.keys(rendering.placeholders).length > 0) {
result = rendering.placeholders[name];
result = rendering.placeholders[phName];
} else {
result = null;
}

if (!result) {
console.warn(
`Placeholder '${name}' was not found in the current rendering data`,
`Placeholder '${phName}' was not found in the current rendering data`,
JSON.stringify(rendering, null, 2)
);

Expand Down Expand Up @@ -294,7 +317,7 @@ export class PlaceholderCommon<T extends PlaceholderProps> extends React.Compone
// if editMode is equal to 'metadata' then emit shallow chromes for hydration in Pages
if (this.props.sitecoreContext?.editMode === EditMode.Metadata) {
return (
<PlaceholderMetadata key={key} uid={(rendering as ComponentRendering).uid}>
<PlaceholderMetadata key={key} rendering={rendering as ComponentRendering}>
{rendered}
</PlaceholderMetadata>
);
Expand All @@ -308,8 +331,8 @@ export class PlaceholderCommon<T extends PlaceholderProps> extends React.Compone
return [
<PlaceholderMetadata
key={(this.props.rendering as ComponentRendering).uid}
uid={(this.props.rendering as ComponentRendering).uid}
placeholderName={name}
rendering={this.props.rendering as ComponentRendering}
>
{transformedComponents}
</PlaceholderMetadata>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ describe('PlaceholderMetadata', () => {
it('renders rendering code blocks for metadataType rendering', () => {
const children = <div className="richtext-class"></div>;

const wrapper = shallow(<PlaceholderMetadata uid={'123'}>{children}</PlaceholderMetadata>);
const wrapper = shallow(
<PlaceholderMetadata rendering={{ uid: '123', componentName: 'RichText' }}>
{children}
</PlaceholderMetadata>
);

expect(wrapper.html()).to.equal(
[
Expand All @@ -21,7 +25,14 @@ describe('PlaceholderMetadata', () => {
it('renders placeholder code blocks when metadataType is placeholder', () => {
const children = <div className="richtext-mock"></div>;
const wrapper = shallow(
<PlaceholderMetadata uid={'123'} placeholderName={'main'}>
<PlaceholderMetadata
rendering={{
uid: '123',
componentName: 'RichText',
placeholders: { main: [] },
}}
placeholderName={'main'}
>
{children}
</PlaceholderMetadata>
);
Expand All @@ -34,4 +45,52 @@ describe('PlaceholderMetadata', () => {
].join('')
);
});

it('renders placeholder code blocks when metadataType is dynamic placeholder', () => {
const children = <div className="richtext-mock"></div>;
const wrapper = shallow(
<PlaceholderMetadata
rendering={{
uid: '123',
componentName: 'RichText',
placeholders: { 'main-{*}': [] },
}}
placeholderName={'main-1'}
>
{children}
</PlaceholderMetadata>
);

expect(wrapper.html()).to.equal(
[
'<code type="text/sitecore" chrometype="placeholder" class="scpm" kind="open" id="main-{*}"></code>',
'<div class="richtext-mock"></div>',
'<code type="text/sitecore" chrometype="placeholder" class="scpm" kind="close"></code>',
].join('')
);
});

it('renders placeholder code blocks when metadataType is double digit dynamic placeholder', () => {
const children = <div className="richtext-mock"></div>;
const wrapper = shallow(
<PlaceholderMetadata
rendering={{
uid: '123',
componentName: 'RichText',
placeholders: { 'main-1-{*}': [] },
}}
placeholderName={'main-1-1'}
>
{children}
</PlaceholderMetadata>
);

expect(wrapper.html()).to.equal(
[
'<code type="text/sitecore" chrometype="placeholder" class="scpm" kind="open" id="main-1-{*}"></code>',
'<div class="richtext-mock"></div>',
'<code type="text/sitecore" chrometype="placeholder" class="scpm" kind="close"></code>',
].join('')
);
});
});
38 changes: 32 additions & 6 deletions packages/sitecore-jss-react/src/components/PlaceholderMetadata.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { ReactNode } from 'react';
import { ComponentRendering } from '@sitecore-jss/sitecore-jss/layout';
import { getDynamicPlaceholderPattern, isDynamicPlaceholder } from './PlaceholderCommon';

/**
* Props containing the component data to render.
*/
export interface PlaceholderMetadataProps {
uid: string;
rendering: ComponentRendering;
placeholderName?: string;
children?: ReactNode;
}
Expand All @@ -23,13 +25,13 @@ export type CodeBlockAttributes = {
* or as a rendering to properly render the surrounding code blocks.
*
* @param {object} props The properties passed to the component.
* @param {string} props.uid A unique identifier for the component instance.
* @param {ComponentRendering} props.rendering The rendering data.
* @param {string} [props.placeholderName] The name of the placeholder.
* @param {JSX.Element} props.children The child components or elements to be wrapped by the metadata code blocks.
* @returns {JSX.Element} A React fragment containing open and close code blocks surrounding the children elements.
*/
export const PlaceholderMetadata = ({
uid,
rendering,
placeholderName,
children,
}: PlaceholderMetadataProps): JSX.Element => {
Expand All @@ -48,9 +50,33 @@ export const PlaceholderMetadata = ({
};

if (kind === 'open') {
attributes.id =
chrometype === 'placeholder' && placeholderName ? `${placeholderName}_${id}` : id;
if (chrometype === 'placeholder' && placeholderName) {
let phId = '';

for (const placeholder of Object.keys(rendering.placeholders)) {
if (placeholderName === placeholder) {
phId = `${placeholderName}_${id}`;
break;
}

// Check if the placeholder is a dynamic placeholder
if (isDynamicPlaceholder(placeholder)) {
const pattern = getDynamicPlaceholderPattern(placeholder);

// Check if the placeholder matches the dynamic placeholder pattern
if (pattern.test(placeholderName)) {
phId = placeholder;
break;
}
}
}

attributes.id = phId;
} else {
attributes.id = id;
}
}

return attributes;
};

Expand All @@ -62,5 +88,5 @@ export const PlaceholderMetadata = ({
</>
);

return <>{renderComponent(uid, placeholderName)}</>;
return <>{renderComponent(rendering.uid, placeholderName)}</>;
};