From f83b34058303ec33e69aeec8191a90e8740820ef Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Wed, 19 Jul 2023 16:29:32 -0400 Subject: [PATCH 01/15] initial implementation (wip) --- .../src/components/BYOCRenderer.test.tsx | 50 ++++++++++++++++ .../src/components/BYOCRenderer.tsx | 60 +++++++++++++++++++ .../src/components/FEaaSComponent.tsx | 16 +---- .../src/components/MissingComponent.tsx | 10 ++-- packages/sitecore-jss-react/src/utils.ts | 15 +++++ 5 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx create mode 100644 packages/sitecore-jss-react/src/components/BYOCRenderer.tsx diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx new file mode 100644 index 0000000000..8d984b0d09 --- /dev/null +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx @@ -0,0 +1,50 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow, mount } from 'enzyme'; +import sinon from 'sinon'; + +import { BYOCProps, BYOCRenderer } from './BYOCRenderer'; +import * as FEAAS from '@sitecore-feaas/clientside/react'; +import { afterEach } from 'node:test'; + +describe('', () => { + const ExternalComponent = () => { +
I was rendered, woo!
; + }; + + type TestProps = { + message: string; + }; + + const ExternalWithPropsComponent = (props: TestProps) => { +
My message to you is {props.message}
; + }; + + const sandbox = sinon.createSandbox(); + + afterEach(() => { + sandbox.restore(); + }); + + it('should render component', () => { + const components: Record = {}; + components['ExternalComponent'] = { name: 'ExternalComponent', component: ExternalComponent }; + sandbox.stub(FEAAS.External, 'registered').value(components); + const props: BYOCProps = { + params: { + ComponentName: 'ExternalComponent', + }, + }; + const rendered = mount(); + // const wrapper = shallow(); + expect(rendered).to.have.length(1); + expect(rendered.text()).to.contain('I was rendered, woo!'); + }); + + xit('should render missing component frame when component isnt registered', () => {}); + xit('should use props from rendering params when present', () => {}); + xit('should use props from data source as fallback', () => {}); + xit('should use props from data source if params have invalid JSON', () => {}); + xit('should fallback to empty props when other sources fail', () => {}); +}); diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx new file mode 100644 index 0000000000..c10277c053 --- /dev/null +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import React from 'react'; +import * as FEAAS from '@sitecore-feaas/clientside/react'; +import { ComponentFields } from '@sitecore-jss/sitecore-jss/layout'; +import { getDataFromFields } from '../utils'; +import { MissingComponent } from './MissingComponent'; + +type BYOCRenderingParams = { + ComponentName: string; + ComonentProps?: string; +}; + +export type BYOCProps = { + params?: BYOCRenderingParams; + fields?: ComponentFields; +}; + +/** + * BYOCRenderer helps us + * @param props + * @returns + */ +export const BYOCRenderer = (props: BYOCProps) => { + const { ComponentName: componentName } = props.params || {}; + if (!componentName) return ; + + const Component = Object.keys(FEAAS.External.registered).length + ? Object.values(FEAAS.External.registered).find((component) => component.name === componentName) + ?.component + : null; + + if (!Component) { + const missingProps = { + rendering: { + componentName: componentName, + }, + errorOverride: 'BYOC: The component you requested is not registered', + }; + return ; + } + + let componentProps: { [key: string]: unknown } = {}; + + if (props.params?.ComonentProps) { + try { + componentProps = JSON.parse(props.params.ComonentProps) ?? {}; + } catch (e) { + console.warn( + `Parsing props for ${componentName} component from rendering params failed. Attempting to parse from data source` + ); + } + } + if (!componentProps && props.fields) { + componentProps = getDataFromFields(props.fields) ?? {}; + } + + return <>{Component && }; +}; +// this will be in initializer/nextjs app +export const BYOCWrapper = () => ; diff --git a/packages/sitecore-jss-react/src/components/FEaaSComponent.tsx b/packages/sitecore-jss-react/src/components/FEaaSComponent.tsx index 1478f7aed7..bff84211fc 100644 --- a/packages/sitecore-jss-react/src/components/FEaaSComponent.tsx +++ b/packages/sitecore-jss-react/src/components/FEaaSComponent.tsx @@ -1,10 +1,7 @@ import React from 'react'; import * as FEAAS from '@sitecore-feaas/clientside/react'; -import { - ComponentFields, - LayoutServicePageState, - getFieldValue, -} from '@sitecore-jss/sitecore-jss/layout'; +import { ComponentFields, LayoutServicePageState } from '@sitecore-jss/sitecore-jss/layout'; +import { getDataFromFields } from '../utils'; export const FEAAS_COMPONENT_RENDERING_NAME = 'FEaaSComponent'; @@ -139,15 +136,6 @@ export async function fetchFEaaSComponentServerProps( } } -const getDataFromFields = (fields: ComponentFields): { [key: string]: unknown } => { - let data: { [key: string]: unknown } = {}; - data = Object.entries(fields).reduce((acc, [key]) => { - acc[key] = getFieldValue(fields, key); - return acc; - }, data); - return data; -}; - /** * Build component endpoint URL from component's params * @param {FEaaSComponentParams} params rendering parameters for FEAAS component diff --git a/packages/sitecore-jss-react/src/components/MissingComponent.tsx b/packages/sitecore-jss-react/src/components/MissingComponent.tsx index 24fa2fc4a4..d328a5e83a 100644 --- a/packages/sitecore-jss-react/src/components/MissingComponent.tsx +++ b/packages/sitecore-jss-react/src/components/MissingComponent.tsx @@ -5,6 +5,7 @@ export interface MissingComponentProps { rendering?: { componentName?: string; }; + errorOverride?: string; } export const MissingComponent: React.FC = (props) => { @@ -14,7 +15,9 @@ export const MissingComponent: React.FC = (props) => { : 'Unnamed Component'; console.log(`Component props for unimplemented '${componentName}' component`, props); - + const errorMessage = + props.errorOverride || + 'JSS component is missing React implementation. See the developer console for more information.'; return (
= (props) => { }} >

{componentName}

-

- JSS component is missing React implementation. See the developer console for more - information. -

+

{errorMessage}

); }; diff --git a/packages/sitecore-jss-react/src/utils.ts b/packages/sitecore-jss-react/src/utils.ts index d4dbf622f2..68a81fb2a3 100644 --- a/packages/sitecore-jss-react/src/utils.ts +++ b/packages/sitecore-jss-react/src/utils.ts @@ -1,3 +1,4 @@ +import { ComponentFields, getFieldValue } from '@sitecore-jss/sitecore-jss/layout'; import { parse as styleParse } from 'style-attr'; // https://stackoverflow.com/a/10426674/9324 @@ -98,3 +99,17 @@ export const getAttributesString = (attributes: { [key: string]: unknown }): str return attributesEntries.join(' '); }; + +/** + * Used in FEAAS and BYOC implementations to convert datasource item field values into component props + * @param fields field collection from Sitecore + * @returns JSON object that can be used as props + */ +export const getDataFromFields = (fields: ComponentFields): { [key: string]: unknown } => { + let data: { [key: string]: unknown } = {}; + data = Object.entries(fields).reduce((acc, [key]) => { + acc[key] = getFieldValue(fields, key); + return acc; + }, data); + return data; +}; From 9f977feaa94f1ae82c5a5032cfc9f16472e823f6 Mon Sep 17 00:00:00 2001 From: Addy Pathania Date: Thu, 20 Jul 2023 01:23:58 -0400 Subject: [PATCH 02/15] add unit tests --- .../src/components/BYOCRenderer.test.tsx | 84 +++++++++++++------ 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx index 8d984b0d09..5d95ab656c 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx @@ -1,48 +1,82 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import React from 'react'; import { expect } from 'chai'; -import { shallow, mount } from 'enzyme'; +import { mount } from 'enzyme'; import sinon from 'sinon'; import { BYOCProps, BYOCRenderer } from './BYOCRenderer'; +import { MissingComponent } from './MissingComponent'; import * as FEAAS from '@sitecore-feaas/clientside/react'; -import { afterEach } from 'node:test'; describe('', () => { - const ExternalComponent = () => { -
I was rendered, woo!
; - }; + // const ExternalComponent = () => { + //
I was rendered, woo!
; + // }; - type TestProps = { - message: string; - }; + // type TestProps = { + // message: string; + // }; - const ExternalWithPropsComponent = (props: TestProps) => { -
My message to you is {props.message}
; - }; + // const ExternalWithPropsComponent = (props: TestProps) => { + //
My message to you is {props.message}
; + // }; - const sandbox = sinon.createSandbox(); + // const sandbox = sinon.createSandbox(); - afterEach(() => { - sandbox.restore(); - }); + // afterEach(() => { + // sandbox.restore(); + // }); + + // it('should render component', () => { + // const components: Record = {}; + // components['ExternalComponent'] = { name: 'ExternalComponent', component: ExternalComponent }; + // sandbox.stub(FEAAS.External, 'registered').value(components); + // const props: BYOCProps = { + // params: { + // ComponentName: 'ExternalComponent', + // }, + // }; + // const rendered = mount(); + // // const wrapper = shallow(); + // expect(rendered).to.have.length(1); + // expect(rendered.text()).to.contain('I was rendered, woo!'); + // }); + + it('should render the registered component with provided props', () => { + const registeredComponents = { + RegisteredComponent: { + name: 'RegisteredComponent', + component: () =>
Registered Component
, + }, + }; + + // Mocking the FEAAS.External.registered + const registeredStub = sinon.stub(FEAAS.External, 'registered').value(registeredComponents); - it('should render component', () => { - const components: Record = {}; - components['ExternalComponent'] = { name: 'ExternalComponent', component: ExternalComponent }; - sandbox.stub(FEAAS.External, 'registered').value(components); const props: BYOCProps = { params: { - ComponentName: 'ExternalComponent', + ComponentName: 'RegisteredComponent', + ComonentProps: JSON.stringify({ text: 'Hello, World!' }), }, }; - const rendered = mount(); - // const wrapper = shallow(); - expect(rendered).to.have.length(1); - expect(rendered.text()).to.contain('I was rendered, woo!'); + + const wrapper = mount(); + expect(wrapper.find('div').text()).to.equal('Registered Component'); + + registeredStub.restore(); + }); + + it('should render missing component frame when component isnt registered', () => { + const registeredStub = sinon.stub(FEAAS.External, 'registered').value({}); + + const props = { params: { ComponentName: 'NonExistentComponent' } }; + + const wrapper = mount(); + expect(wrapper.find(MissingComponent)).to.have.lengthOf(1); + + registeredStub.restore(); }); - xit('should render missing component frame when component isnt registered', () => {}); xit('should use props from rendering params when present', () => {}); xit('should use props from data source as fallback', () => {}); xit('should use props from data source if params have invalid JSON', () => {}); From 81e75471a7488969020e58a4cbb5416475aaeb3d Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Thu, 20 Jul 2023 10:29:43 -0400 Subject: [PATCH 03/15] finalize byoc unit tests --- .../src/components/BYOCRenderer.test.tsx | 186 +++++++++++++----- .../src/components/BYOCRenderer.tsx | 8 +- 2 files changed, 143 insertions(+), 51 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx index 5d95ab656c..9195db5de2 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx @@ -7,78 +7,170 @@ import sinon from 'sinon'; import { BYOCProps, BYOCRenderer } from './BYOCRenderer'; import { MissingComponent } from './MissingComponent'; import * as FEAAS from '@sitecore-feaas/clientside/react'; +import { afterEach } from 'node:test'; +import { ComponentFields } from '@sitecore-jss/sitecore-jss/layout'; describe('', () => { - // const ExternalComponent = () => { - //
I was rendered, woo!
; - // }; - - // type TestProps = { - // message: string; - // }; - - // const ExternalWithPropsComponent = (props: TestProps) => { - //
My message to you is {props.message}
; - // }; - - // const sandbox = sinon.createSandbox(); - - // afterEach(() => { - // sandbox.restore(); - // }); - - // it('should render component', () => { - // const components: Record = {}; - // components['ExternalComponent'] = { name: 'ExternalComponent', component: ExternalComponent }; - // sandbox.stub(FEAAS.External, 'registered').value(components); - // const props: BYOCProps = { - // params: { - // ComponentName: 'ExternalComponent', - // }, - // }; - // const rendered = mount(); - // // const wrapper = shallow(); - // expect(rendered).to.have.length(1); - // expect(rendered.text()).to.contain('I was rendered, woo!'); - // }); + const sandbox = sinon.createSandbox(); + + type PropType = { + text: string; + }; + const ComponentWithProps = (props: PropType) => ( +
I display this: {props.text || 'nothing'}
+ ); + + afterEach(() => { + sandbox.restore(); + }); it('should render the registered component with provided props', () => { const registeredComponents = { RegisteredComponent: { name: 'RegisteredComponent', - component: () =>
Registered Component
, + component: () =>
Registered Component
, }, }; - - // Mocking the FEAAS.External.registered - const registeredStub = sinon.stub(FEAAS.External, 'registered').value(registeredComponents); - + sandbox.stub(FEAAS.External, 'registered').value(registeredComponents); const props: BYOCProps = { params: { ComponentName: 'RegisteredComponent', - ComonentProps: JSON.stringify({ text: 'Hello, World!' }), }, }; const wrapper = mount(); - expect(wrapper.find('div').text()).to.equal('Registered Component'); - registeredStub.restore(); + expect(wrapper.find('div.byoc').text()).to.equal('Registered Component'); }); it('should render missing component frame when component isnt registered', () => { - const registeredStub = sinon.stub(FEAAS.External, 'registered').value({}); - + sandbox.stub(FEAAS.External, 'registered').value({}); const props = { params: { ComponentName: 'NonExistentComponent' } }; const wrapper = mount(); + expect(wrapper.find(MissingComponent)).to.have.lengthOf(1); + }); + + it('should use props from rendering params when present', () => { + const registeredComponents = { + RegisteredComponent: { + name: 'RegisteredComponent', + component: ComponentWithProps, + }, + }; + sandbox.stub(FEAAS.External, 'registered').value(registeredComponents); + const props: BYOCProps = { + params: { + ComponentName: 'RegisteredComponent', + ComonentProps: JSON.stringify({ text: 'this is text' }), + }, + }; - registeredStub.restore(); + const wrapper = mount(); + + expect(wrapper.find('div.byoc').text()).to.contain('I display this'); + expect(wrapper.find('div.byoc').text()).to.contain('this is text'); }); - xit('should use props from rendering params when present', () => {}); - xit('should use props from data source as fallback', () => {}); - xit('should use props from data source if params have invalid JSON', () => {}); - xit('should fallback to empty props when other sources fail', () => {}); + it('should prioritize props from rendering params', () => { + const registeredComponents = { + RegisteredComponent: { + name: 'RegisteredComponent', + component: ComponentWithProps, + }, + }; + const dataSourceFields: ComponentFields = { + text: { + value: 'this is data source text', + }, + }; + sandbox.stub(FEAAS.External, 'registered').value(registeredComponents); + const props: BYOCProps = { + params: { + ComponentName: 'RegisteredComponent', + ComonentProps: JSON.stringify({ text: 'this is param text' }), + }, + fields: dataSourceFields, + }; + + const wrapper = mount(); + + expect(wrapper.find('div.byoc').text()).to.contain('I display this'); + expect(wrapper.find('div.byoc').text()).to.contain('this is param text'); + }); + + it('should use props from data source as fallback', () => { + const registeredComponents = { + RegisteredComponent: { + name: 'RegisteredComponent', + component: ComponentWithProps, + }, + }; + const dataSourceFields: ComponentFields = { + text: { + value: 'this is data source text', + }, + }; + sandbox.stub(FEAAS.External, 'registered').value(registeredComponents); + const props: BYOCProps = { + params: { + ComponentName: 'RegisteredComponent', + ComonentProps: '', + }, + fields: dataSourceFields, + }; + + const wrapper = mount(); + + expect(wrapper.find('div.byoc').text()).to.contain('I display this'); + expect(wrapper.find('div.byoc').text()).to.contain('this is data source text'); + }); + + it('should use props from data source if params have invalid JSON', () => { + const registeredComponents = { + RegisteredComponent: { + name: 'RegisteredComponent', + component: ComponentWithProps, + }, + }; + const dataSourceFields: ComponentFields = { + text: { + value: 'this is data source text', + }, + }; + sandbox.stub(FEAAS.External, 'registered').value(registeredComponents); + const props: BYOCProps = { + params: { + ComponentName: 'RegisteredComponent', + ComonentProps: 'this is not a JSON', + }, + fields: dataSourceFields, + }; + + const wrapper = mount(); + + expect(wrapper.find('div.byoc').text()).to.contain('I display this'); + expect(wrapper.find('div.byoc').text()).to.contain('this is data source text'); + }); + it('should fallback to empty props when other sources fail', () => { + const registeredComponents = { + RegisteredComponent: { + name: 'RegisteredComponent', + component: ComponentWithProps, + }, + }; + sandbox.stub(FEAAS.External, 'registered').value(registeredComponents); + const props: BYOCProps = { + params: { + ComponentName: 'RegisteredComponent', + ComonentProps: '', + }, + fields: {}, + }; + + const wrapper = mount(); + + expect(wrapper.find('div.byoc').text()).to.equal('I display this: nothing'); + }); }); diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx index c10277c053..688d9617fa 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx @@ -39,7 +39,7 @@ export const BYOCRenderer = (props: BYOCProps) => { return ; } - let componentProps: { [key: string]: unknown } = {}; + let componentProps: { [key: string]: unknown } = undefined; if (props.params?.ComonentProps) { try { @@ -50,11 +50,11 @@ export const BYOCRenderer = (props: BYOCProps) => { ); } } - if (!componentProps && props.fields) { - componentProps = getDataFromFields(props.fields) ?? {}; + if (!componentProps) { + componentProps = props.fields ? getDataFromFields(props.fields) : {}; } - return <>{Component && }; + return ; }; // this will be in initializer/nextjs app export const BYOCWrapper = () => ; From de30ba46f4a0829dd8505a7d83e01d296245467a Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Thu, 20 Jul 2023 16:15:10 -0400 Subject: [PATCH 04/15] cleanup and template component --- .../nextjs-sxa/src/components/BYOCWrapper.tsx | 15 +++ packages/sitecore-jss-nextjs/src/index.ts | 2 + .../src/components/BYOCRenderer.test.tsx | 117 +++++------------- .../src/components/BYOCRenderer.tsx | 50 ++++++-- packages/sitecore-jss-react/src/index.ts | 1 + packages/sitecore-jss-react/src/utils.ts | 2 +- 6 files changed, 87 insertions(+), 100 deletions(-) create mode 100644 packages/create-sitecore-jss/src/templates/nextjs-sxa/src/components/BYOCWrapper.tsx diff --git a/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/components/BYOCWrapper.tsx b/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/components/BYOCWrapper.tsx new file mode 100644 index 0000000000..26be17c990 --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/components/BYOCWrapper.tsx @@ -0,0 +1,15 @@ +import { BYOCProps, BYOCRenderer } from '@sitecore-jss/sitecore-jss-nextjs'; +import React from 'react'; +import * as FEAAS from '@sitecore-feaas/clientside/react'; + +export const Default = (props: BYOCProps): JSX.Element => { + const rendererProps = { + components: FEAAS.External.registered, + ...props, + }; + return ( +
+ +
+ ); +}; diff --git a/packages/sitecore-jss-nextjs/src/index.ts b/packages/sitecore-jss-nextjs/src/index.ts index 5a84e831ff..698889100a 100644 --- a/packages/sitecore-jss-nextjs/src/index.ts +++ b/packages/sitecore-jss-nextjs/src/index.ts @@ -170,6 +170,8 @@ export { FEaaSComponentProps, FEaaSComponentParams, fetchFEaaSComponentServerProps, + BYOCProps, + BYOCRenderer, File, FileField, RichTextField, diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx index 9195db5de2..be4e7de29e 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx @@ -1,42 +1,44 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ import React from 'react'; import { expect } from 'chai'; import { mount } from 'enzyme'; -import sinon from 'sinon'; - -import { BYOCProps, BYOCRenderer } from './BYOCRenderer'; +import { BYOCRenderer } from './BYOCRenderer'; import { MissingComponent } from './MissingComponent'; -import * as FEAAS from '@sitecore-feaas/clientside/react'; -import { afterEach } from 'node:test'; import { ComponentFields } from '@sitecore-jss/sitecore-jss/layout'; describe('', () => { - const sandbox = sinon.createSandbox(); - type PropType = { text: string; }; + const ComponentWithProps = (props: PropType) => (
I display this: {props.text || 'nothing'}
); - afterEach(() => { - sandbox.restore(); - }); - - it('should render the registered component with provided props', () => { + const getBaseByocProps = ( + registeredComponent: React.ComponentType, + componentProps?: string, + fields?: ComponentFields + ) => { const registeredComponents = { RegisteredComponent: { name: 'RegisteredComponent', - component: () =>
Registered Component
, + component: registeredComponent, }, }; - sandbox.stub(FEAAS.External, 'registered').value(registeredComponents); - const props: BYOCProps = { + + return { params: { ComponentName: 'RegisteredComponent', + ComponentProps: componentProps, }, + fields: fields, + components: registeredComponents, }; + }; + + it('should render the registered component with provided props', () => { + const noPropComponent = () =>
Registered Component
; + const props = getBaseByocProps(noPropComponent); const wrapper = mount(); @@ -44,8 +46,7 @@ describe('', () => { }); it('should render missing component frame when component isnt registered', () => { - sandbox.stub(FEAAS.External, 'registered').value({}); - const props = { params: { ComponentName: 'NonExistentComponent' } }; + const props = { params: { ComponentName: 'NonExistentComponent' }, components: {} }; const wrapper = mount(); @@ -53,19 +54,7 @@ describe('', () => { }); it('should use props from rendering params when present', () => { - const registeredComponents = { - RegisteredComponent: { - name: 'RegisteredComponent', - component: ComponentWithProps, - }, - }; - sandbox.stub(FEAAS.External, 'registered').value(registeredComponents); - const props: BYOCProps = { - params: { - ComponentName: 'RegisteredComponent', - ComonentProps: JSON.stringify({ text: 'this is text' }), - }, - }; + const props = getBaseByocProps(ComponentWithProps, JSON.stringify({ text: 'this is text' })); const wrapper = mount(); @@ -74,25 +63,17 @@ describe('', () => { }); it('should prioritize props from rendering params', () => { - const registeredComponents = { - RegisteredComponent: { - name: 'RegisteredComponent', - component: ComponentWithProps, - }, - }; const dataSourceFields: ComponentFields = { text: { value: 'this is data source text', }, }; - sandbox.stub(FEAAS.External, 'registered').value(registeredComponents); - const props: BYOCProps = { - params: { - ComponentName: 'RegisteredComponent', - ComonentProps: JSON.stringify({ text: 'this is param text' }), - }, - fields: dataSourceFields, - }; + + const props = getBaseByocProps( + ComponentWithProps, + JSON.stringify({ text: 'this is param text' }), + dataSourceFields + ); const wrapper = mount(); @@ -101,25 +82,12 @@ describe('', () => { }); it('should use props from data source as fallback', () => { - const registeredComponents = { - RegisteredComponent: { - name: 'RegisteredComponent', - component: ComponentWithProps, - }, - }; const dataSourceFields: ComponentFields = { text: { value: 'this is data source text', }, }; - sandbox.stub(FEAAS.External, 'registered').value(registeredComponents); - const props: BYOCProps = { - params: { - ComponentName: 'RegisteredComponent', - ComonentProps: '', - }, - fields: dataSourceFields, - }; + const props = getBaseByocProps(ComponentWithProps, '', dataSourceFields); const wrapper = mount(); @@ -128,46 +96,21 @@ describe('', () => { }); it('should use props from data source if params have invalid JSON', () => { - const registeredComponents = { - RegisteredComponent: { - name: 'RegisteredComponent', - component: ComponentWithProps, - }, - }; const dataSourceFields: ComponentFields = { text: { value: 'this is data source text', }, }; - sandbox.stub(FEAAS.External, 'registered').value(registeredComponents); - const props: BYOCProps = { - params: { - ComponentName: 'RegisteredComponent', - ComonentProps: 'this is not a JSON', - }, - fields: dataSourceFields, - }; + const props = getBaseByocProps(ComponentWithProps, 'this is not a JSON', dataSourceFields); const wrapper = mount(); expect(wrapper.find('div.byoc').text()).to.contain('I display this'); expect(wrapper.find('div.byoc').text()).to.contain('this is data source text'); }); + it('should fallback to empty props when other sources fail', () => { - const registeredComponents = { - RegisteredComponent: { - name: 'RegisteredComponent', - component: ComponentWithProps, - }, - }; - sandbox.stub(FEAAS.External, 'registered').value(registeredComponents); - const props: BYOCProps = { - params: { - ComponentName: 'RegisteredComponent', - ComonentProps: '', - }, - fields: {}, - }; + const props = getBaseByocProps(ComponentWithProps, '', {}); const wrapper = mount(); diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx index 688d9617fa..532b67f3f2 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx @@ -1,31 +1,59 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import React from 'react'; -import * as FEAAS from '@sitecore-feaas/clientside/react'; import { ComponentFields } from '@sitecore-jss/sitecore-jss/layout'; import { getDataFromFields } from '../utils'; import { MissingComponent } from './MissingComponent'; +import { RegisteredComponents } from '@sitecore-feaas/clientside/types/ui/FEAASExternal'; +/** + * Data from rendering params on Sitecore's BYOC rendering + */ type BYOCRenderingParams = { + /** + * Name of the component to render + */ ComponentName: string; - ComonentProps?: string; + /** + * JSON props to pass into rendered component + */ + ComponentProps?: string; }; +/** + * Props for BYOC wrapper component + */ export type BYOCProps = { + /** + * rendering params + */ params?: BYOCRenderingParams; + /** + * fields from datasource items to be passed as rendered child component props + */ fields?: ComponentFields; }; /** - * BYOCRenderer helps us - * @param props - * @returns + * Props for BYOCRenderer component. Includes components list to load external components from. + */ +type ByocRendererProps = BYOCProps & { + components: RegisteredComponents; +}; + +/** + * BYOCRenderer helps rendering BYOC components - that can be taken from anywhere + * and registered without being deployed as Sitecore renderings + * @param {ByocRendererProps} props component props + * @returns dynamicly rendered component or Missing Component error frame */ -export const BYOCRenderer = (props: BYOCProps) => { +export const BYOCRenderer = (props: ByocRendererProps) => { const { ComponentName: componentName } = props.params || {}; if (!componentName) return ; - const Component = Object.keys(FEAAS.External.registered).length - ? Object.values(FEAAS.External.registered).find((component) => component.name === componentName) + // props.components would contain component from internal FEAAS regsitered component collection (registered in app) + // we can't access this collection here directly, as the collection from packages's dependency would be different from the one in app + const Component = Object.keys(props.components).length + ? Object.values(props.components).find((component) => component.name === componentName) ?.component : null; @@ -41,9 +69,9 @@ export const BYOCRenderer = (props: BYOCProps) => { let componentProps: { [key: string]: unknown } = undefined; - if (props.params?.ComonentProps) { + if (props.params?.ComponentProps) { try { - componentProps = JSON.parse(props.params.ComonentProps) ?? {}; + componentProps = JSON.parse(props.params.ComponentProps) ?? {}; } catch (e) { console.warn( `Parsing props for ${componentName} component from rendering params failed. Attempting to parse from data source` @@ -56,5 +84,3 @@ export const BYOCRenderer = (props: BYOCProps) => { return ; }; -// this will be in initializer/nextjs app -export const BYOCWrapper = () => ; diff --git a/packages/sitecore-jss-react/src/index.ts b/packages/sitecore-jss-react/src/index.ts index ea9de0821c..bd214351d6 100644 --- a/packages/sitecore-jss-react/src/index.ts +++ b/packages/sitecore-jss-react/src/index.ts @@ -62,6 +62,7 @@ export { FEaaSComponentParams, fetchFEaaSComponentServerProps, } from './components/FEaaSComponent'; +export { BYOCProps, BYOCRenderer } from './components/BYOCRenderer'; export { Link, LinkField, LinkFieldValue, LinkProps, LinkPropTypes } from './components/Link'; export { File, FileField } from './components/File'; export { VisitorIdentification } from './components/VisitorIdentification'; diff --git a/packages/sitecore-jss-react/src/utils.ts b/packages/sitecore-jss-react/src/utils.ts index 68a81fb2a3..bc931a2516 100644 --- a/packages/sitecore-jss-react/src/utils.ts +++ b/packages/sitecore-jss-react/src/utils.ts @@ -102,7 +102,7 @@ export const getAttributesString = (attributes: { [key: string]: unknown }): str /** * Used in FEAAS and BYOC implementations to convert datasource item field values into component props - * @param fields field collection from Sitecore + * @param {ComponentFields} fields field collection from Sitecore * @returns JSON object that can be used as props */ export const getDataFromFields = (fields: ComponentFields): { [key: string]: unknown } => { From cd37526179348c2502b2eeff8db760ec1dae8f36 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Thu, 20 Jul 2023 16:36:52 -0400 Subject: [PATCH 05/15] better error reporting for MissingComponent --- .../src/components/BYOCRenderer.tsx | 5 ++++- .../src/components/MissingComponent.test.tsx | 20 +++++++++++++++++++ .../src/components/MissingComponent.tsx | 4 +++- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 packages/sitecore-jss-react/src/components/MissingComponent.test.tsx diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx index 532b67f3f2..e9bb7ec743 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx @@ -58,11 +58,14 @@ export const BYOCRenderer = (props: ByocRendererProps) => { : null; if (!Component) { + console.warn( + `Component "${componentName}" was not registered, please ensure the FEEAS.External.registerComponent call is made.` + ); const missingProps = { rendering: { componentName: componentName, }, - errorOverride: 'BYOC: The component you requested is not registered', + errorOverride: 'BYOC: This component was not registered.', }; return ; } diff --git a/packages/sitecore-jss-react/src/components/MissingComponent.test.tsx b/packages/sitecore-jss-react/src/components/MissingComponent.test.tsx new file mode 100644 index 0000000000..6cdc344b02 --- /dev/null +++ b/packages/sitecore-jss-react/src/components/MissingComponent.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import { MissingComponent } from './MissingComponent'; + +describe('', () => { + it('should accept and display custom error', () => { + const errorMsg = 'Oops, I errored again'; + const props = { + rendering: { + componentName: 'test', + }, + errorOverride: errorMsg, + }; + + const wrapper = mount(); + + expect(wrapper.find('div p').text()).to.contain(errorMsg); + }); +}); diff --git a/packages/sitecore-jss-react/src/components/MissingComponent.tsx b/packages/sitecore-jss-react/src/components/MissingComponent.tsx index d328a5e83a..c2635cb0a6 100644 --- a/packages/sitecore-jss-react/src/components/MissingComponent.tsx +++ b/packages/sitecore-jss-react/src/components/MissingComponent.tsx @@ -14,7 +14,9 @@ export const MissingComponent: React.FC = (props) => { ? props.rendering.componentName : 'Unnamed Component'; - console.log(`Component props for unimplemented '${componentName}' component`, props); + // error override would mean component is not unimplemented + !props.errorOverride && + console.log(`Component props for unimplemented '${componentName}' component`, props); const errorMessage = props.errorOverride || 'JSS component is missing React implementation. See the developer console for more information.'; From 75ec132cf1316d3f749ba219d71abb3e0bbf9289 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Fri, 21 Jul 2023 14:36:21 -0400 Subject: [PATCH 06/15] small refactor, extra test --- .../src/components/BYOCRenderer.test.tsx | 10 ++++++++++ .../src/components/BYOCRenderer.tsx | 13 +++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx index be4e7de29e..83a57c7840 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx @@ -51,6 +51,16 @@ describe('', () => { const wrapper = mount(); expect(wrapper.find(MissingComponent)).to.have.lengthOf(1); + expect(wrapper.find('div p').text()).to.contain('This component was not registered'); + }); + + it('should render missing component frame when component name is not provided', () => { + const props = { params: { ComponentName: '' }, components: {} }; + + const wrapper = mount(); + + expect(wrapper.find(MissingComponent)).to.have.lengthOf(1); + expect(wrapper.find('div p').text()).to.contain('The ComponentName for this rendering is missing'); }); it('should use props from rendering params when present', () => { diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx index e9bb7ec743..1ef4a0a204 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx @@ -48,14 +48,15 @@ type ByocRendererProps = BYOCProps & { */ export const BYOCRenderer = (props: ByocRendererProps) => { const { ComponentName: componentName } = props.params || {}; - if (!componentName) return ; - + if (!componentName) { + const noNameProps = { + errorOverride: 'BYOC: The ComponentName for this rendering is missing', + } + return ; + } // props.components would contain component from internal FEAAS regsitered component collection (registered in app) // we can't access this collection here directly, as the collection from packages's dependency would be different from the one in app - const Component = Object.keys(props.components).length - ? Object.values(props.components).find((component) => component.name === componentName) - ?.component - : null; + const Component = props.components[componentName]?.component; if (!Component) { console.warn( From 269e2a02dd860e2c80d460c4ad9c570ab04d1ed1 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Fri, 21 Jul 2023 14:42:55 -0400 Subject: [PATCH 07/15] lint --- .../sitecore-jss-react/src/components/BYOCRenderer.test.tsx | 4 +++- packages/sitecore-jss-react/src/components/BYOCRenderer.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx index 83a57c7840..075b800d72 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx @@ -60,7 +60,9 @@ describe('', () => { const wrapper = mount(); expect(wrapper.find(MissingComponent)).to.have.lengthOf(1); - expect(wrapper.find('div p').text()).to.contain('The ComponentName for this rendering is missing'); + expect(wrapper.find('div p').text()).to.contain( + 'The ComponentName for this rendering is missing' + ); }); it('should use props from rendering params when present', () => { diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx index 1ef4a0a204..d4c216c077 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx @@ -51,7 +51,7 @@ export const BYOCRenderer = (props: ByocRendererProps) => { if (!componentName) { const noNameProps = { errorOverride: 'BYOC: The ComponentName for this rendering is missing', - } + }; return ; } // props.components would contain component from internal FEAAS regsitered component collection (registered in app) From 0d816a920e6ac06f3b6d70a857a1bbcd4603b9d0 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 24 Jul 2023 12:00:52 -0400 Subject: [PATCH 08/15] Add client side error boundary in BYOC Renderer --- .../src/components/BYOCRenderer.tsx | 92 ++++++++++++------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx index d4c216c077..10e33225a2 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import React from 'react'; import { ComponentFields } from '@sitecore-jss/sitecore-jss/layout'; import { getDataFromFields } from '../utils'; @@ -17,6 +16,8 @@ type BYOCRenderingParams = { * JSON props to pass into rendered component */ ComponentProps?: string; + styles?: string; + RenderingIdentifier?: string; }; /** @@ -36,7 +37,7 @@ export type BYOCProps = { /** * Props for BYOCRenderer component. Includes components list to load external components from. */ -type ByocRendererProps = BYOCProps & { +type BYOCRendererProps = BYOCProps & { components: RegisteredComponents; }; @@ -46,45 +47,66 @@ type ByocRendererProps = BYOCProps & { * @param {ByocRendererProps} props component props * @returns dynamicly rendered component or Missing Component error frame */ -export const BYOCRenderer = (props: ByocRendererProps) => { - const { ComponentName: componentName } = props.params || {}; - if (!componentName) { - const noNameProps = { - errorOverride: 'BYOC: The ComponentName for this rendering is missing', - }; - return ; +export class BYOCRenderer extends React.Component { + state: Readonly<{ error?: Error }>; + + constructor(props: BYOCRendererProps) { + super(props); + this.state = {}; + } + + static getDerivedStateFromError(error: Error) { + // Update state so the next render will show the fallback UI. + return { error: error }; } - // props.components would contain component from internal FEAAS regsitered component collection (registered in app) - // we can't access this collection here directly, as the collection from packages's dependency would be different from the one in app - const Component = props.components[componentName]?.component; - if (!Component) { - console.warn( - `Component "${componentName}" was not registered, please ensure the FEEAS.External.registerComponent call is made.` - ); - const missingProps = { - rendering: { - componentName: componentName, - }, - errorOverride: 'BYOC: This component was not registered.', - }; - return ; + componentDidCatch(error: Error) { + this.setState({ error }); } - let componentProps: { [key: string]: unknown } = undefined; + render() { + const props: BYOCRendererProps = this.props; + if (this.state.error) { + return
A rendering error occurred: {this.state.error.message}.
; + } + const { ComponentName: componentName } = props.params || {}; + if (!componentName) { + const noNameProps = { + errorOverride: 'BYOC: The ComponentName for this rendering is missing', + }; + return ; + } + // props.components would contain component from internal FEAAS regsitered component collection (registered in app) + // we can't access this collection here directly, as the collection from packages's dependency would be different from the one in app + const Component = props.components[componentName]?.component; - if (props.params?.ComponentProps) { - try { - componentProps = JSON.parse(props.params.ComponentProps) ?? {}; - } catch (e) { + if (!Component) { console.warn( - `Parsing props for ${componentName} component from rendering params failed. Attempting to parse from data source` + `Component "${componentName}" was not registered, please ensure the FEEAS.External.registerComponent call is made.` ); + const missingProps = { + rendering: { + componentName: componentName, + }, + errorOverride: 'BYOC: This component was not registered.', + }; + return ; } - } - if (!componentProps) { - componentProps = props.fields ? getDataFromFields(props.fields) : {}; - } - return ; -}; + let componentProps: { [key: string]: unknown } = undefined; + + if (props.params?.ComponentProps) { + try { + componentProps = JSON.parse(props.params.ComponentProps) ?? {}; + } catch (e) { + console.warn( + `Parsing props for ${componentName} component from rendering params failed. Attempting to parse from data source` + ); + } + } + if (!componentProps) { + componentProps = props.fields ? getDataFromFields(props.fields) : {}; + } + return ; + } +} From eb44b3be0c0616954739865267490387b9316320 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 24 Jul 2023 12:01:38 -0400 Subject: [PATCH 09/15] SXA themes support, misc --- .../nextjs-sxa/src/components/BYOCWrapper.tsx | 8 ++++-- packages/sitecore-jss-react/src/utils.test.ts | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/components/BYOCWrapper.tsx b/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/components/BYOCWrapper.tsx index 26be17c990..6209592766 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/components/BYOCWrapper.tsx +++ b/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/components/BYOCWrapper.tsx @@ -3,13 +3,17 @@ import React from 'react'; import * as FEAAS from '@sitecore-feaas/clientside/react'; export const Default = (props: BYOCProps): JSX.Element => { + const styles = props.params?.styles?.trimEnd(); + const id = props.params?.RenderingIdentifier; const rendererProps = { components: FEAAS.External.registered, ...props, }; return ( -
- +
+
+ +
); }; diff --git a/packages/sitecore-jss-react/src/utils.test.ts b/packages/sitecore-jss-react/src/utils.test.ts index 5b3ccd8582..41084c5621 100644 --- a/packages/sitecore-jss-react/src/utils.test.ts +++ b/packages/sitecore-jss-react/src/utils.test.ts @@ -4,7 +4,9 @@ import { convertAttributesToReactProps, convertStyleAttribute, getAttributesString, + getDataFromFields, } from './utils'; +import { ComponentFields } from '@sitecore-jss/sitecore-jss/layout'; describe('jss-react utils', () => { describe('convertStyleAttribute', () => { @@ -92,4 +94,27 @@ describe('jss-react utils', () => { expect(result).to.eql(''); }); }); + + describe('getDataFromFields', () => { + it('should parse fields into JSON', () => { + const fields: ComponentFields = { + text: { + value: 'we count to', + }, + number: { + value: 10 + }, + message: { + value: 'well done counting', + } + }; + const expectedResult = { + text: 'we count to', + number: 10, + message: 'well done counting', + }; + + expect(getDataFromFields(fields)).to.deep.equal(expectedResult); + }); + }); }); From 4d6d5e9d22f674d7646ba52c4082efaa082fa520 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 24 Jul 2023 16:24:32 -0400 Subject: [PATCH 10/15] export BYOCRenderingParams --- packages/sitecore-jss-nextjs/src/index.ts | 1 + packages/sitecore-jss-react/src/components/BYOCRenderer.tsx | 5 ++++- packages/sitecore-jss-react/src/index.ts | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/index.ts b/packages/sitecore-jss-nextjs/src/index.ts index 698889100a..054408383e 100644 --- a/packages/sitecore-jss-nextjs/src/index.ts +++ b/packages/sitecore-jss-nextjs/src/index.ts @@ -171,6 +171,7 @@ export { FEaaSComponentParams, fetchFEaaSComponentServerProps, BYOCProps, + BYOCRenderingParams, BYOCRenderer, File, FileField, diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx index 10e33225a2..0ec3b7650a 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx @@ -7,7 +7,7 @@ import { RegisteredComponents } from '@sitecore-feaas/clientside/types/ui/FEAASE /** * Data from rendering params on Sitecore's BYOC rendering */ -type BYOCRenderingParams = { +export type BYOCRenderingParams = { /** * Name of the component to render */ @@ -16,6 +16,9 @@ type BYOCRenderingParams = { * JSON props to pass into rendered component */ ComponentProps?: string; + /** + * A string with classes that can be used to apply themes, via SXA functionality + */ styles?: string; RenderingIdentifier?: string; }; diff --git a/packages/sitecore-jss-react/src/index.ts b/packages/sitecore-jss-react/src/index.ts index bd214351d6..8767fb9ccb 100644 --- a/packages/sitecore-jss-react/src/index.ts +++ b/packages/sitecore-jss-react/src/index.ts @@ -62,7 +62,7 @@ export { FEaaSComponentParams, fetchFEaaSComponentServerProps, } from './components/FEaaSComponent'; -export { BYOCProps, BYOCRenderer } from './components/BYOCRenderer'; +export { BYOCProps, BYOCRenderer, BYOCRenderingParams } from './components/BYOCRenderer'; export { Link, LinkField, LinkFieldValue, LinkProps, LinkPropTypes } from './components/Link'; export { File, FileField } from './components/File'; export { VisitorIdentification } from './components/VisitorIdentification'; From 26c561a7953e782540861147e1bde7dd61642312 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 24 Jul 2023 16:24:47 -0400 Subject: [PATCH 11/15] lint util tests --- packages/sitecore-jss-react/src/utils.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sitecore-jss-react/src/utils.test.ts b/packages/sitecore-jss-react/src/utils.test.ts index 41084c5621..5669eb3532 100644 --- a/packages/sitecore-jss-react/src/utils.test.ts +++ b/packages/sitecore-jss-react/src/utils.test.ts @@ -102,11 +102,11 @@ describe('jss-react utils', () => { value: 'we count to', }, number: { - value: 10 + value: 10, }, message: { value: 'well done counting', - } + }, }; const expectedResult = { text: 'we count to', From 829f0fa8132a029e382a6231b2fd5bb464c2d20e Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Tue, 25 Jul 2023 17:34:55 -0400 Subject: [PATCH 12/15] improve error handling, add optional error components to props --- .../src/components/BYOCRenderer.test.tsx | 164 ++++++++++++++---- .../src/components/BYOCRenderer.tsx | 40 ++++- 2 files changed, 164 insertions(+), 40 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx index 075b800d72..de314771ab 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { expect } from 'chai'; import { mount } from 'enzyme'; import { BYOCRenderer } from './BYOCRenderer'; -import { MissingComponent } from './MissingComponent'; +import { MissingComponent, MissingComponentProps } from './MissingComponent'; import { ComponentFields } from '@sitecore-jss/sitecore-jss/layout'; describe('', () => { @@ -14,6 +14,10 @@ describe('', () => {
I display this: {props.text || 'nothing'}
); + const ThrowingComponent = () => { + throw Error('error thrown'); + }; + const getBaseByocProps = ( registeredComponent: React.ComponentType, componentProps?: string, @@ -24,6 +28,10 @@ describe('', () => { name: 'RegisteredComponent', component: registeredComponent, }, + ThrowingComponent: { + name: 'ThrowingComponent', + component: ThrowingComponent, + }, }; return { @@ -45,26 +53,6 @@ describe('', () => { expect(wrapper.find('div.byoc').text()).to.equal('Registered Component'); }); - it('should render missing component frame when component isnt registered', () => { - const props = { params: { ComponentName: 'NonExistentComponent' }, components: {} }; - - const wrapper = mount(); - - expect(wrapper.find(MissingComponent)).to.have.lengthOf(1); - expect(wrapper.find('div p').text()).to.contain('This component was not registered'); - }); - - it('should render missing component frame when component name is not provided', () => { - const props = { params: { ComponentName: '' }, components: {} }; - - const wrapper = mount(); - - expect(wrapper.find(MissingComponent)).to.have.lengthOf(1); - expect(wrapper.find('div p').text()).to.contain( - 'The ComponentName for this rendering is missing' - ); - }); - it('should use props from rendering params when present', () => { const props = getBaseByocProps(ComponentWithProps, JSON.stringify({ text: 'this is text' })); @@ -107,20 +95,6 @@ describe('', () => { expect(wrapper.find('div.byoc').text()).to.contain('this is data source text'); }); - it('should use props from data source if params have invalid JSON', () => { - const dataSourceFields: ComponentFields = { - text: { - value: 'this is data source text', - }, - }; - const props = getBaseByocProps(ComponentWithProps, 'this is not a JSON', dataSourceFields); - - const wrapper = mount(); - - expect(wrapper.find('div.byoc').text()).to.contain('I display this'); - expect(wrapper.find('div.byoc').text()).to.contain('this is data source text'); - }); - it('should fallback to empty props when other sources fail', () => { const props = getBaseByocProps(ComponentWithProps, '', {}); @@ -128,4 +102,124 @@ describe('', () => { expect(wrapper.find('div.byoc').text()).to.equal('I display this: nothing'); }); + + describe('error handling', () => { + it('should render error if params have invalid JSON', () => { + const dataSourceFields: ComponentFields = { + text: { + value: 'this is data source text', + }, + }; + const props = getBaseByocProps(ComponentWithProps, 'this is not a JSON', dataSourceFields); + + const wrapper = mount(); + + expect(wrapper.find('div').text()).to.contain('A rendering error occurred:'); + expect(wrapper.find('div').text()).to.contain('Unexpected token'); + }); + + it('should render custom error component when provided, when params have invalid JSON', () => { + const dataSourceFields: ComponentFields = { + text: { + value: 'this is data source text', + }, + }; + const customErrorComponent = () =>
custom error
; + const props = { + errorComponent: customErrorComponent, + ...getBaseByocProps(ComponentWithProps, 'this is not a JSON', dataSourceFields), + }; + + const wrapper = mount(); + + expect(wrapper.find('div').text()).to.contain('custom error'); + }); + + it('should render error if underlying component throws', () => { + const props = { + ...getBaseByocProps(ComponentWithProps), + params: { + ComponentName: 'ThrowingComponent', + }, + }; + + const wrapper = mount(); + + expect(wrapper.find('div').text()).to.contain('A rendering error occurred: error thrown'); + }); + + it('should render custom error component when provided, when underlying component throws', () => { + const customErrorComponent = (props) =>
custom error: {props?.error?.message}
; + const props = { + ...getBaseByocProps(ComponentWithProps), + errorComponent: customErrorComponent, + params: { + ComponentName: 'ThrowingComponent', + }, + }; + + const wrapper = mount(); + + expect(wrapper.find('div').text()).to.contain('custom error: error thrown'); + }); + + it('should render missing component frame when component isnt registered', () => { + const props = { params: { ComponentName: 'NonExistentComponent' }, components: {} }; + + const wrapper = mount(); + + expect(wrapper.find(MissingComponent)).to.have.lengthOf(1); + expect(wrapper.find('div p').text()).to.contain('This component was not registered'); + }); + + it('should render custom missing component when provided, when component isnt registered', () => { + const missingComponent = (props: MissingComponentProps) => ( +
+ Custom missive for {props.rendering?.componentName}: {props.errorOverride} +
+ ); + + const props = { + missingComponentComponent: missingComponent, + params: { ComponentName: 'NonExistentComponent' }, + components: {}, + }; + const wrapper = mount(); + + expect(wrapper.find('div').text()).to.contain('Custom missive for NonExistentComponent'); + expect(wrapper.find('div').text()).to.contain('This component was not registered'); + }); + + it('should render missing component frame when component name is not provided', () => { + const props = { params: { ComponentName: '' }, components: {} }; + + const wrapper = mount(); + + expect(wrapper.find(MissingComponent)).to.have.lengthOf(1); + expect(wrapper.find('div p').text()).to.contain( + 'The ComponentName for this rendering is missing' + ); + }); + + it('should render custom missing component when provided, when component name is not provided', () => { + const missingComponent = (props: MissingComponentProps) => ( +
+ Custom missive for {props.rendering?.componentName}: {props.errorOverride} +
+ ); + + const props = { + missingComponentComponent: missingComponent, + params: { ComponentName: '' }, + components: {}, + }; + + const wrapper = mount(); + + expect(wrapper.find('div').text()).to.contain('Custom missive'); + expect(wrapper.find('div').text()).to.contain( + 'The ComponentName for this rendering is missing' + ); + }); + }); }); diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx index 0ec3b7650a..254236aa05 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { ComponentFields } from '@sitecore-jss/sitecore-jss/layout'; import { getDataFromFields } from '../utils'; -import { MissingComponent } from './MissingComponent'; +import { MissingComponent, MissingComponentProps } from './MissingComponent'; import { RegisteredComponents } from '@sitecore-feaas/clientside/types/ui/FEAASExternal'; /** @@ -42,8 +42,21 @@ export type BYOCProps = { */ type BYOCRendererProps = BYOCProps & { components: RegisteredComponents; + errorComponent?: React.ComponentClass | React.FC; + missingComponentComponent?: + | React.ComponentClass + | React.FC; }; +type ErrorComponentProps = { + [prop: string]: unknown; + error?: Error; +}; + +const DefaultErrorComponent = (props: ErrorComponentProps) => ( +
A rendering error occurred: {props.error.message}.
+); + /** * BYOCRenderer helps rendering BYOC components - that can be taken from anywhere * and registered without being deployed as Sitecore renderings @@ -70,14 +83,22 @@ export class BYOCRenderer extends React.Component { render() { const props: BYOCRendererProps = this.props; if (this.state.error) { - return
A rendering error occurred: {this.state.error.message}.
; + return this.props.errorComponent ? ( + + ) : ( + + ); } const { ComponentName: componentName } = props.params || {}; if (!componentName) { const noNameProps = { errorOverride: 'BYOC: The ComponentName for this rendering is missing', }; - return ; + return props.missingComponentComponent ? ( + + ) : ( + + ); } // props.components would contain component from internal FEAAS regsitered component collection (registered in app) // we can't access this collection here directly, as the collection from packages's dependency would be different from the one in app @@ -93,7 +114,11 @@ export class BYOCRenderer extends React.Component { }, errorOverride: 'BYOC: This component was not registered.', }; - return ; + return props.missingComponentComponent ? ( + + ) : ( + + ); } let componentProps: { [key: string]: unknown } = undefined; @@ -103,7 +128,12 @@ export class BYOCRenderer extends React.Component { componentProps = JSON.parse(props.params.ComponentProps) ?? {}; } catch (e) { console.warn( - `Parsing props for ${componentName} component from rendering params failed. Attempting to parse from data source` + `Parsing props for ${componentName} component from rendering params failed. Error: ${e}` + ); + return this.props.errorComponent ? ( + + ) : ( + ); } } From fb406babd01ba8b3be45f9f58ae39a0f00d816b5 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Wed, 26 Jul 2023 09:27:23 -0400 Subject: [PATCH 13/15] extra type exports, comments --- packages/sitecore-jss-nextjs/src/index.ts | 1 + .../src/components/BYOCRenderer.tsx | 14 ++++++++++++-- packages/sitecore-jss-react/src/index.ts | 7 ++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/index.ts b/packages/sitecore-jss-nextjs/src/index.ts index 054408383e..2ce900fd4d 100644 --- a/packages/sitecore-jss-nextjs/src/index.ts +++ b/packages/sitecore-jss-nextjs/src/index.ts @@ -173,6 +173,7 @@ export { BYOCProps, BYOCRenderingParams, BYOCRenderer, + BYOCRendererProps, File, FileField, RichTextField, diff --git a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx index 254236aa05..f035a3fa75 100644 --- a/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx +++ b/packages/sitecore-jss-react/src/components/BYOCRenderer.tsx @@ -40,9 +40,19 @@ export type BYOCProps = { /** * Props for BYOCRenderer component. Includes components list to load external components from. */ -type BYOCRendererProps = BYOCProps & { +export type BYOCRendererProps = BYOCProps & { + /** + * Registered component collection. Would be taken from FEAAS.External.registered + */ components: RegisteredComponents; + /** + * Error component override. To be shown when Renderer or underlying component throws + */ errorComponent?: React.ComponentClass | React.FC; + /** + * Override to indicate missing component situations. Would be shown when BYOC component is not registered + * or ComponentName is missing + */ missingComponentComponent?: | React.ComponentClass | React.FC; @@ -54,7 +64,7 @@ type ErrorComponentProps = { }; const DefaultErrorComponent = (props: ErrorComponentProps) => ( -
A rendering error occurred: {props.error.message}.
+
A rendering error occurred: {props.error?.message}.
); /** diff --git a/packages/sitecore-jss-react/src/index.ts b/packages/sitecore-jss-react/src/index.ts index 8767fb9ccb..3fde5eccfc 100644 --- a/packages/sitecore-jss-react/src/index.ts +++ b/packages/sitecore-jss-react/src/index.ts @@ -62,7 +62,12 @@ export { FEaaSComponentParams, fetchFEaaSComponentServerProps, } from './components/FEaaSComponent'; -export { BYOCProps, BYOCRenderer, BYOCRenderingParams } from './components/BYOCRenderer'; +export { + BYOCProps, + BYOCRenderer, + BYOCRenderingParams, + BYOCRendererProps, +} from './components/BYOCRenderer'; export { Link, LinkField, LinkFieldValue, LinkProps, LinkPropTypes } from './components/Link'; export { File, FileField } from './components/File'; export { VisitorIdentification } from './components/VisitorIdentification'; From 12954b47d3288140a628a2f7ff1cf57775610ef8 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Wed, 26 Jul 2023 09:41:20 -0400 Subject: [PATCH 14/15] update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00354896f1..df37cc3ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,16 @@ Our versioning strategy is as follows: ### ๐ŸŽ‰ New Features & Improvements +* `[templates/nextjs-sxa]` `[sitecore-jss-nextjs]` "Bring Your Own Code" (BYOC) feature is introduced. This allows developers and editors more flexibility when developing and working with new components, i.e.: + * Avoid the jss deploy process for components, and use FEAAS registration instead + * Put components anywhere in the project, with any prop type that is not dependent on Layout Service data + * (As soon as the feature is fully integrated for editing) Set component props on the fly when editing a page +Check the BYOC documentation for more info. ([#1568](https://github.com/Sitecore/jss/pull/1568)) * `[templates]` Add JSS_APP_NAME to .env files ([#1571](https://github.com/Sitecore/jss/pull/1571)) * `[sitecore-jss]` `[sitecore-jss-nextjs]` `[templates/nextjs]` Introduce performance metrics for debug logging ([#1555](https://github.com/Sitecore/jss/pull/1555)) * `[templates/nextjs]` `[templates/react]` `[templates/vue]` `[templates/angular]` Introduce layout service REST configuration name environment variable ([#1543](https://github.com/Sitecore/jss/pull/1543)) * `[templates/nextjs]` `[sitecore-jss-nextjs]` Support for out-of-process editing data caches was added. Vercel KV or a custom Redis cache can be used to improve editing in Pages and Experience Editor when using Vercel deployment as editing/rendering host ([#1530](https://github.com/Sitecore/jss/pull/1530)) +* `[sitecore-jss-react]` Built-in MissingComponent component can now accept "errorOverride" text in props - to be displayed in the yellow frame as a custom error message. ([#1568](https://github.com/Sitecore/jss/pull/1568)) ### ๐Ÿงน Chores From 38bccf535680eaecb791a7643590b6bf50be585a Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Wed, 26 Jul 2023 10:29:49 -0400 Subject: [PATCH 15/15] changelog entry tweak --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df37cc3ae1..157fef7098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,8 @@ Our versioning strategy is as follows: * `[templates/nextjs-sxa]` `[sitecore-jss-nextjs]` "Bring Your Own Code" (BYOC) feature is introduced. This allows developers and editors more flexibility when developing and working with new components, i.e.: * Avoid the jss deploy process for components, and use FEAAS registration instead - * Put components anywhere in the project, with any prop type that is not dependent on Layout Service data - * (As soon as the feature is fully integrated for editing) Set component props on the fly when editing a page + * Put components anywhere in the project, + * Use any prop type, without dependence on Layout Service data Check the BYOC documentation for more info. ([#1568](https://github.com/Sitecore/jss/pull/1568)) * `[templates]` Add JSS_APP_NAME to .env files ([#1571](https://github.com/Sitecore/jss/pull/1571)) * `[sitecore-jss]` `[sitecore-jss-nextjs]` `[templates/nextjs]` Introduce performance metrics for debug logging ([#1555](https://github.com/Sitecore/jss/pull/1555))