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

[Next.js][Editing] Partial rendering implementation #1169

Merged
merged 18 commits into from
Oct 5, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import { GetServerSideProps } from 'next';
import NotFound from 'src/NotFound';
import Layout from 'src/Layout';
import {
RenderingType,
SitecoreContext,
ComponentPropsContext,
handleEditorFastRefresh,
EditingComponentPlaceholder,
<% if (prerender === 'SSG') { -%>
StaticPath,
<% } -%>
Expand All @@ -35,14 +37,24 @@ const SitecorePage = ({ notFound, componentProps, layoutData }: SitecorePageProp
}

const isEditing = layoutData.sitecore.context.pageEditing;
const isComponentRendering =
layoutData.sitecore.context.renderingType === RenderingType.Component;

return (
<ComponentPropsContext value={componentProps}>
<SitecoreContext
componentFactory={isEditing ? editingComponentFactory : componentFactory}
layoutData={layoutData}
>
<Layout layoutData={layoutData} />
{/*
Sitecore Pages supports component rendering to avoid refreshing the entire page during component editing.
If you are using Experience Editor only, this logic can be removed, Layout can be left.
*/}
{isComponentRendering ? (
<EditingComponentPlaceholder rendering={layoutData.sitecore.route} />
) : (
<Layout layoutData={layoutData} />
)}
</SitecoreContext>
</ComponentPropsContext>
);
Expand Down
1 change: 1 addition & 0 deletions packages/sitecore-jss-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@sitecore-jss/sitecore-jss": "^21.0.0-canary.185",
"@sitecore-jss/sitecore-jss-dev-tools": "^21.0.0-canary.185",
"@sitecore-jss/sitecore-jss-react": "^21.0.0-canary.185",
"node-html-parser": "^6.0.0",
"prop-types": "^15.7.2",
"regex-parser": "^2.2.11",
"sync-disk-cache": "^2.1.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import {
EDITING_COMPONENT_ID,
EDITING_COMPONENT_PLACEHOLDER,
RouteData,
} from '@sitecore-jss/sitecore-jss/layout';
import { EditingComponentPlaceholder } from './EditingComponentPlaceholder';
import * as PlaceholderModule from './Placeholder';
import { expect } from 'chai';
import { mount } from 'enzyme';
import sinon from 'sinon';

describe('<EditingComponentPlaceholder />', () => {
it('should render component', () => {
sinon.stub(PlaceholderModule, 'Placeholder').returns(<div className="test"></div>);
const rendering: RouteData = {
name: 'ComponentRendering',
placeholders: {
[EDITING_COMPONENT_PLACEHOLDER]: [
{
componentName: 'Home',
},
],
},
};

const c = mount(<EditingComponentPlaceholder rendering={rendering} />);

const component = c.find(`#${EDITING_COMPONENT_ID}`);

expect(component.length).to.equal(1);

expect(component.find(PlaceholderModule.Placeholder).length).to.equal(1);
expect(component.find('.test').length).to.equal(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
illiakovalenko marked this conversation as resolved.
Show resolved Hide resolved
import {
EDITING_COMPONENT_ID,
EDITING_COMPONENT_PLACEHOLDER,
RouteData,
} from '@sitecore-jss/sitecore-jss/layout';
import { Placeholder } from './Placeholder';

export const EditingComponentPlaceholder = ({
rendering,
}: {
rendering: RouteData;
}): JSX.Element => (
<div id={EDITING_COMPONENT_ID}>
<Placeholder name={EDITING_COMPONENT_PLACEHOLDER} rendering={rendering} />
</div>
);
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ import {
EditingPreviewData,
QUERY_PARAM_EDITING_SECRET,
} from './editing-data-service';
import { EE_PATH, EE_LANGUAGE, EE_LAYOUT, EE_DICTIONARY, EE_BODY } from '../test-data/ee-data';
import {
EE_PATH,
EE_LANGUAGE,
EE_LAYOUT,
EE_DICTIONARY,
EE_BODY,
EE_COMPONENT_BODY,
} from '../test-data/ee-data';
import { EditingRenderMiddleware, extractEditingData } from './editing-render-middleware';
import { spy, match } from 'sinon';
import sinonChai from 'sinon-chai';
Expand Down Expand Up @@ -123,6 +130,86 @@ describe('EditingRenderMiddleware', () => {
});
});

it('should handle component rendering request', async () => {
const html =
'<html phkey="test1"><body phkey="test2"><div id="editing-component"><h1>Hello world</h1><p>Something amazing</p></div></body></html>';
const query = {} as Query;
query[QUERY_PARAM_EDITING_SECRET] = secret;
const previewData = { key: 'key1234' } as EditingPreviewData;

const fetcher = mockFetcher(html);
const dataService = mockDataService(previewData);
const req = mockRequest(EE_COMPONENT_BODY, query);
const res = mockResponse();

const middleware = new EditingRenderMiddleware({
dataFetcher: fetcher,
editingDataService: dataService,
});
const handler = middleware.getHandler();

await handler(req, res);

expect(dataService.setEditingData, 'stash editing data').to.have.been.called;
expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith(previewData);
expect(res.getHeader, 'get preview cookies').to.have.been.calledWith('Set-Cookie');
expect(fetcher.get).to.have.been.calledOnce;
expect(fetcher.get, 'pass along preview cookies').to.have.been.calledWith(
match('http://localhost:3000/test/path?timestamp'),
{
headers: {
Cookie: mockNextJsPreviewCookies.join(';'),
},
}
);
expect(res.status).to.have.been.calledOnce;
expect(res.status).to.have.been.calledWith(200);
expect(res.json).to.have.been.calledOnce;
expect(res.json).to.have.been.calledWith({
html: '<h1>Hello world</h1><p>Something amazing</p>',
});
});

it('should throw error when component rendering markup is missing', async () => {
const html = '<html phkey="test1"><body phkey="test2"><div></div></body></html>';
const query = {} as Query;
query[QUERY_PARAM_EDITING_SECRET] = secret;
const previewData = { key: 'key1234' } as EditingPreviewData;

const fetcher = mockFetcher(html);
const dataService = mockDataService(previewData);
const req = mockRequest(EE_COMPONENT_BODY, query);
const res = mockResponse();

const middleware = new EditingRenderMiddleware({
dataFetcher: fetcher,
editingDataService: dataService,
});
const handler = middleware.getHandler();

await handler(req, res);

expect(dataService.setEditingData, 'stash editing data').to.have.been.called;
expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith(previewData);
expect(res.getHeader, 'get preview cookies').to.have.been.calledWith('Set-Cookie');
expect(fetcher.get).to.have.been.calledOnce;
expect(fetcher.get, 'pass along preview cookies').to.have.been.calledWith(
match('http://localhost:3000/test/path?timestamp'),
{
headers: {
Cookie: mockNextJsPreviewCookies.join(';'),
},
}
);
expect(res.status).to.have.been.calledOnce;
expect(res.status).to.have.been.calledWith(500);
expect(res.json).to.have.been.calledOnce;
expect(res.json).to.have.been.calledWith({
html:
'<html><body>Error: Failed to render component for http://localhost:3000/test/path</body></html>',
});
});

it('should handle 404 for route data request', async () => {
const html = '<html phkey="test1"><body phkey="test2">Page not found</body></html>';
const query = {} as Query;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { STATIC_PROPS_ID, SERVER_PROPS_ID } from 'next/constants';
import { AxiosDataFetcher, debug } from '@sitecore-jss/sitecore-jss';
import { EDITING_COMPONENT_ID, RenderingType } from '@sitecore-jss/sitecore-jss/layout';
import { parse } from 'node-html-parser';
import { EditingData } from './editing-data';
import {
EditingDataService,
Expand Down Expand Up @@ -160,6 +162,13 @@ export class EditingRenderMiddleware {
// The following line will trick it into thinking we're SSR, thus avoiding any router.replace.
html = html.replace(STATIC_PROPS_ID, SERVER_PROPS_ID);

if (editingData.layoutData.sitecore.context.renderingType === RenderingType.Component) {
// Handle component rendering. Extract component markup only
html = parse(html).getElementById(EDITING_COMPONENT_ID)?.innerHTML;
illiakovalenko marked this conversation as resolved.
Show resolved Hide resolved

if (!html) throw new Error(`Failed to render component for ${requestUrl}`);
}

const body = { html };

// Return expected JSON result
Expand Down
4 changes: 4 additions & 0 deletions packages/sitecore-jss-nextjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export {
ComponentRendering,
ComponentFields,
ComponentParams,
RenderingType,
EDITING_COMPONENT_PLACEHOLDER,
EDITING_COMPONENT_ID,
} from '@sitecore-jss/sitecore-jss/layout';
export { mediaApi } from '@sitecore-jss/sitecore-jss/media';
export {
Expand Down Expand Up @@ -103,6 +106,7 @@ export { handleEditorFastRefresh, getPublicUrl } from './utils';
export { Link, LinkProps } from './components/Link';
export { RichText, RichTextProps } from './components/RichText';
export { Placeholder } from './components/Placeholder';
export { EditingComponentPlaceholder } from './components/EditingComponentPlaceholder';
export { NextImage } from './components/NextImage';

export {
Expand Down
12 changes: 12 additions & 0 deletions packages/sitecore-jss-nextjs/src/test-data/ee-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const EE_PATH = '/test/path';
export const EE_LANGUAGE = 'en';
export const EE_LAYOUT = `{"sitecore":{"context":{"pageEditing":true,"site":{"name":"JssNext"},"pageState":"normal","language":"en","itemPath":"${EE_PATH}"},"route":{"name":"home","displayName":"home","fields":{"pageTitle":{"value":"Welcome to Sitecore JSS"}},"databaseName":"master","deviceId":"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3","itemId":"d6ac9d26-9474-51cf-982d-4f8d44951229","itemLanguage":"en","itemVersion":1,"layoutId":"4092f843-b14e-5f7a-9ae6-3ed9f5c2b919","templateId":"ca5a5aeb-55ae-501b-bb10-d37d009a97e1","templateName":"App Route","placeholders":{"jss-main":[{"uid":"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d","componentName":"ContentBlock","dataSource":"{FF0E7D28-D8EF-539C-9CEC-28E1175F8C1D}","params":{},"fields":{"heading":{"value":"Welcome to Sitecore JSS"},"content":{"value":"<p>Thanks for using JSS!! Here are some resources to get you started:</p>"}}}]}}}}`;
export const EE_DICTIONARY = '{"entry1":"Entry One","entry2":"Entry Two"}';
export const EE_COMPONENT_LAYOUT = `{"sitecore":{"context":{"pageEditing":true,"renderingType":"component","site":{"name":"JssNext"},"pageState":"normal","language":"en","itemPath":"${EE_PATH}"},"route":{"name":"home","displayName":"home","fields":{"pageTitle":{"value":"Welcome to Sitecore JSS"}},"databaseName":"master","deviceId":"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3","itemId":"d6ac9d26-9474-51cf-982d-4f8d44951229","itemLanguage":"en","itemVersion":1,"layoutId":"4092f843-b14e-5f7a-9ae6-3ed9f5c2b919","templateId":"ca5a5aeb-55ae-501b-bb10-d37d009a97e1","templateName":"App Route","placeholders":{"editing-componentmode-placeholder":[{"uid":"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d","componentName":"ContentBlock","dataSource":"{FF0E7D28-D8EF-539C-9CEC-28E1175F8C1D}","params":{},"fields":{"heading":{"value":"Welcome to Sitecore JSS"},"content":{"value":"<p>Thanks for using JSS!! Here are some resources to get you started:</p>"}}}]}}}}`;

export const EE_BODY = {
id: 'JssApp',
Expand All @@ -27,5 +28,16 @@ export const EE_BODY = {
moduleName: 'server.bundle',
};

export const EE_COMPONENT_BODY = {
id: 'JssApp',
args: [
'/',
EE_COMPONENT_LAYOUT,
`{\"language\":\"${EE_LANGUAGE}\",\"dictionary\":${EE_DICTIONARY}}`,
],
functionName: 'renderView',
moduleName: 'server.bundle',
};

export const imageField =
'<input id=\'fld_F5201E35767444EBB903E52488A0EB5A_B7F425624A1F4F3F925C4A4381197239_en_1_0f581df6173e468f9c0b36bd730739e4_13\' class=\'scFieldValue\' name=\'fld_F5201E35767444EBB903E52488A0EB5A_B7F425624A1F4F3F925C4A4381197239_en_1_0f581df6173e468f9c0b36bd730739e4_13\' type=\'hidden\' value="&lt;image mediaid=&quot;{B013777F-C6CA-4880-9562-B9B7688AF63A}&quot; /&gt;" /><code id="fld_F5201E35767444EBB903E52488A0EB5A_B7F425624A1F4F3F925C4A4381197239_en_1_0f581df6173e468f9c0b36bd730739e4_13_edit" type="text/sitecore" chromeType="field" scFieldType="image" class="scpm" kind="open">{"commands":[{"click":"chrome:field:editcontrol({command:\\"webedit: chooseimage\\"})","header":"Choose Image","icon":"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2.png","disabledIcon":"/temp/photo_landscape2_disabled16x16.png","isDivider":false,"tooltip":"Choose an image.","type":""},{"click":"chrome:field:editcontrol({command:\\"webedit: editimage\\"})","header":"Properties","icon":"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_edit.png","disabledIcon":"/temp/photo_landscape2_edit_disabled16x16.png","isDivider":false,"tooltip":"Modify image appearance.","type":""},{"click":"chrome:field:editcontrol({command:\\"webedit: clearimage\\"})","header":"Clear","icon":"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_delete.png","disabledIcon":"/temp/photo_landscape2_delete_disabled16x16.png","isDivider":false,"tooltip":"Remove the image.","type":""},{"click":"chrome:common:edititem({command:\\"webedit: open\\"})","header":"Edit the related item","icon":"/temp/iconcache/office/16x16/cubes.png","disabledIcon":"/temp/cubes_disabled16x16.png","isDivider":false,"tooltip":"Edit the related item in the Content Editor.","type":"common"},{"click":"chrome:rendering:personalize({command:\\"webedit: personalize\\"})","header":"Personalize","icon":"/temp/iconcache/office/16x16/users_family.png","disabledIcon":"/temp/users_family_disabled16x16.png","isDivider":false,"tooltip":"Create or edit personalization for this component.","type":"sticky"},{"click":"chrome:rendering:editvariations({command:\\"webedit: editvariations\\"})","header":"Edit variations","icon":"/temp/iconcache/office/16x16/windows.png","disabledIcon":"/temp/windows_disabled16x16.png","isDivider":false,"tooltip":"Edit the variations.","type":"sticky"}],"contextItemUri":"sitecore://master/{F5201E35-7674-44EB-B903-E52488A0EB5A}?lang=en&ver=1","custom":{},"displayName":"Image","expandedDisplayName":null}</code><img src="http://jssadvancedapp/sitecore/shell/-/media/JssAdvancedApp/assets/img/portfolio/1.ashx?h=350&amp;la=en&amp;w=650&amp;hash=B973470AA333773341C62A76511361C88897E2D4" alt="" width="650" height="350" /><code class="scpm" type="text/sitecore" chromeType="field" kind="close"></code>';
3 changes: 3 additions & 0 deletions packages/sitecore-jss/src/layout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export {
PlaceholdersData,
ComponentFields,
ComponentParams,
RenderingType,
EDITING_COMPONENT_PLACEHOLDER,
EDITING_COMPONENT_ID,
} from './models';

export { getFieldValue, getChildPlaceholder } from './utils';
Expand Down
18 changes: 18 additions & 0 deletions packages/sitecore-jss/src/layout/models.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
/**
* Static placeholder name used for component rendering
*/
export const EDITING_COMPONENT_PLACEHOLDER = 'editing-componentmode-placeholder';

/**
* Id of wrapper for component rendering
*/
export const EDITING_COMPONENT_ID = 'editing-component';

/**
* A reply from the Sitecore Layout Service
*/
Expand All @@ -16,11 +26,19 @@ export enum LayoutServicePageState {
Normal = 'normal',
}

/**
* Editing rendering type
*/
export enum RenderingType {
Component = 'component',
}

/**
* Shape of context data from the Sitecore Layout Service
*/
export interface LayoutServiceContext {
[key: string]: unknown;
renderingType?: RenderingType;
pageEditing?: boolean;
language?: string;
pageState?: LayoutServicePageState;
Expand Down
Loading