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

[nextjs] Bring your own code (BYOC) feature #1568

Merged
merged 16 commits into from
Jul 26, 2023
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
* 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))
* `[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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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 styles = props.params?.styles?.trimEnd();
const id = props.params?.RenderingIdentifier;
const rendererProps = {
components: FEAAS.External.registered,
...props,
};
return (
<div className={styles ? styles : undefined} id={id ? id : undefined}>
<div className="component-content">
<BYOCRenderer {...rendererProps} />
</div>
</div>
);
};
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 @@ -170,6 +170,10 @@ export {
FEaaSComponentProps,
FEaaSComponentParams,
fetchFEaaSComponentServerProps,
BYOCProps,
BYOCRenderingParams,
BYOCRenderer,
BYOCRendererProps,
File,
FileField,
RichTextField,
Expand Down
225 changes: 225 additions & 0 deletions packages/sitecore-jss-react/src/components/BYOCRenderer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import React from 'react';
import { expect } from 'chai';
import { mount } from 'enzyme';
import { BYOCRenderer } from './BYOCRenderer';
import { MissingComponent, MissingComponentProps } from './MissingComponent';
import { ComponentFields } from '@sitecore-jss/sitecore-jss/layout';

describe('<BYOCRenderer />', () => {
type PropType = {
text: string;
};

const ComponentWithProps = (props: PropType) => (
<div className="byoc">I display this: {props.text || 'nothing'}</div>
);

const ThrowingComponent = () => {
throw Error('error thrown');
};

const getBaseByocProps = (
registeredComponent: React.ComponentType<any>,
componentProps?: string,
fields?: ComponentFields
) => {
const registeredComponents = {
RegisteredComponent: {
name: 'RegisteredComponent',
component: registeredComponent,
},
ThrowingComponent: {
name: 'ThrowingComponent',
component: ThrowingComponent,
},
};

return {
params: {
ComponentName: 'RegisteredComponent',
ComponentProps: componentProps,
},
fields: fields,
components: registeredComponents,
};
};

it('should render the registered component with provided props', () => {
const noPropComponent = () => <div className="byoc">Registered Component</div>;
const props = getBaseByocProps(noPropComponent);

const wrapper = mount(<BYOCRenderer {...props} />);

expect(wrapper.find('div.byoc').text()).to.equal('Registered Component');
});

it('should use props from rendering params when present', () => {
const props = getBaseByocProps(ComponentWithProps, JSON.stringify({ text: 'this is text' }));

const wrapper = mount(<BYOCRenderer {...props} />);

expect(wrapper.find('div.byoc').text()).to.contain('I display this');
expect(wrapper.find('div.byoc').text()).to.contain('this is text');
});

it('should prioritize props from rendering params', () => {
const dataSourceFields: ComponentFields = {
text: {
value: 'this is data source text',
},
};

const props = getBaseByocProps(
ComponentWithProps,
JSON.stringify({ text: 'this is param text' }),
dataSourceFields
);

const wrapper = mount(<BYOCRenderer {...props} />);

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 dataSourceFields: ComponentFields = {
text: {
value: 'this is data source text',
},
};
const props = getBaseByocProps(ComponentWithProps, '', dataSourceFields);

const wrapper = mount(<BYOCRenderer {...props} />);

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, '', {});

const wrapper = mount(<BYOCRenderer {...props} />);

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(<BYOCRenderer {...props} />);

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 = () => <div>custom error</div>;
const props = {
errorComponent: customErrorComponent,
...getBaseByocProps(ComponentWithProps, 'this is not a JSON', dataSourceFields),
};

const wrapper = mount(<BYOCRenderer {...props} />);

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(<BYOCRenderer {...props} />);

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) => <div>custom error: {props?.error?.message}</div>;
const props = {
...getBaseByocProps(ComponentWithProps),
errorComponent: customErrorComponent,
params: {
ComponentName: 'ThrowingComponent',
},
};

const wrapper = mount(<BYOCRenderer {...props} />);

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(<BYOCRenderer {...props} />);

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) => (
<div>
Custom missive for {props.rendering?.componentName}: {props.errorOverride}
</div>
);

const props = {
missingComponentComponent: missingComponent,
params: { ComponentName: 'NonExistentComponent' },
components: {},
};
const wrapper = mount(<BYOCRenderer {...props} />);

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(<BYOCRenderer {...props} />);

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) => (
<div>
Custom missive for {props.rendering?.componentName}: {props.errorOverride}
</div>
);

const props = {
missingComponentComponent: missingComponent,
params: { ComponentName: '' },
components: {},
};

const wrapper = mount(<BYOCRenderer {...props} />);

expect(wrapper.find('div').text()).to.contain('Custom missive');
expect(wrapper.find('div').text()).to.contain(
'The ComponentName for this rendering is missing'
);
});
});
});
Loading