Skip to content

Commit

Permalink
[sitecore-jss-react] [sitecore-jss-nextjs] Add support for chrome's h…
Browse files Browse the repository at this point in the history
…ydration for fields (#1773)

* render field metadata for Text field component; introduce field metadata component - wip

* rename FieldMetadata module, add unit test for Text component, add comments

* add field metadata component to Date, Image and File field components; include unit tests

* add field metadata component to link and richtext field components, include unit tests

* update FieldMetadata interfaces to prevent build errors in sitecore-jss-nextjs; component update

* export fieldmetadata component and interfaces from sitecore-jss-react

* add metadata component for nextjs link field component; include unit test

* add field metadata component to nextimage component; small fix in link field component

* unit tests for FieldMetadata

* update unit test

* introduce getFieldMetadataMarkup function and used in the field components; add unit test

* update changelog

* react - use higher order component to wrap metadata around field components

* update nextjs components to use metadata wrapper hoc; aadjust unit tests

* adjust unit tests and fix File component

* adjust image field tests; include check for media property in metadata wrapper

* some types updates

* some unit tests adjustments and metadata wrapper component update

* some FieldMetadata related renamings

* add unit test for RichText nextjs component

* update changelog

* update changelog pull request

* some type updates

* reenable file tests

* update function description

Co-authored-by: Illia Kovalenko <[email protected]>

* minor variable renaming

Co-authored-by: Illia Kovalenko <[email protected]>

* remove unnecessary commented line

* remove unnecessary undefined check

* move FieldMetada interfaces to base package; extract metadata proptypes

* move FieldMetadata under enchancments

* added some descriptions

* move and rename FieldMetadata to layout submodule of base package

* rename FieldMetadata component

* add tsdoc description for fieldmetadata component

* conditionally forwardRef in fieldMetadata

* two separate withFieldMetadata functions based on if used with forwardRef

* single withFieldMetadata function with forwardref parameter

* update with metadata unit test to test the whole structure of markup

* withMetadata refactoring wip

* Adjusted withFieldMetadata generic type

* update unit test

* wip - refactor field metadata hoc

* Updates

* Updated unit tests, simplified types

* Update

* Expose withFieldMetadata as a part of nextjs sdk

* Updated PropTypes

* Removed extra asserts

* remove media property from propTypes

---------

Co-authored-by: Illia Kovalenko <[email protected]>
Co-authored-by: illiakovalenko <[email protected]>
  • Loading branch information
3 people authored Apr 24, 2024
1 parent 0d84c70 commit cdb899a
Show file tree
Hide file tree
Showing 22 changed files with 950 additions and 320 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Our versioning strategy is as follows:

## Unreleased

### 🎉 New Features & Improvements
* `[sitecore-jss-react]` `[sitecore-jss-nextjs]` Introduce FieldMetadata component and functionality to render it when metadata field property is provided in the field's layout data. In such case the field component is wrapped with metadata markup to enable chromes hydration when editing in pages. Ability to render metadata has been added to the field rendering components for react and nextjs. ([#1773](https://github.com/Sitecore/jss/pull/1773))

### 🛠 Breaking Changes

* `[sitecore-jss]` Switch to edge site query for XP and gets config sites + sxa sites (ignoring website)
Expand Down
41 changes: 40 additions & 1 deletion packages/sitecore-jss-nextjs/src/components/Link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,48 @@ describe('<Link />', () => {
expect(rendered).to.have.length(0);
});

it('should render nothing with missing editable and value', () => {
it('should render nothing with missing field', () => {
const field = {};
const rendered = mount(<Link field={field} />).children();
expect(rendered).to.have.length(0);
});

it('should render field metadata component when metadata property is present', () => {
const testMetadata = {
contextItem: {
id: '{09A07660-6834-476C-B93B-584248D3003B}',
language: 'en',
revision: 'a0b36ce0a7db49418edf90eb9621e145',
version: 1,
},
fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}',
fieldType: 'single-line',
rawValue: 'Test1',
};

const field = {
value: {
href: '/lorem',
text: 'ipsum',
class: 'my-link',
},
metadata: testMetadata,
};

const rendered = mount(
<Page>
<Link field={field} />
</Page>
);

expect(rendered.html()).to.equal(
[
`<code type="text/sitecore" chrometype="field" class="scpm" kind="open">${JSON.stringify(
testMetadata
)}</code>`,
'<a href="/lorem" class="my-link">ipsum</a>',
'<code type="text/sitecore" chrometype="field" class="scpm" kind="close"></code>',
].join('')
);
});
});
4 changes: 3 additions & 1 deletion packages/sitecore-jss-nextjs/src/components/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
? field
: (field as LinkField).value) as LinkFieldValue;
const { href, querystring, anchor } = value;
const isEditing = editable && (field as LinkFieldValue).editable;

const isEditing =
editable && ((field as LinkFieldValue).editable || (field as LinkFieldValue).metadata);

if (href && !isEditing) {
const text = showLinkTextWithChildrenPresent || !children ? value.text || value.href : null;
Expand Down
36 changes: 35 additions & 1 deletion packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,10 @@ describe('<NextImage />', () => {
describe('error cases', () => {
const src = '/assets/img/test0.png';
it('should throw an error if src is present', () => {
expect(() => mount(<NextImage src={src} />)).to.throw(
const field = {
src: '/assets/img/test0.png',
};
expect(() => mount(<NextImage src={src} field={field} />)).to.throw(
'Detected src prop. If you wish to use src, use next/image directly.'
);
});
Expand Down Expand Up @@ -283,4 +286,35 @@ describe('<NextImage />', () => {
);
});
});

it('should render field metadata component when metadata property is present', () => {
const testMetadata = {
contextItem: {
id: '{09A07660-6834-476C-B93B-584248D3003B}',
language: 'en',
revision: 'a0b36ce0a7db49418edf90eb9621e145',
version: 1,
},
fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}',
fieldType: 'image',
rawValue: 'Test1',
};

const field = {
value: { src: '/assets/img/test0.png', alt: 'my image' },
metadata: testMetadata,
};

const rendered = mount(<NextImage field={field} fill={true} />);

expect(rendered.html()).to.equal(
[
`<code type="text/sitecore" chrometype="field" class="scpm" kind="open">${JSON.stringify(
testMetadata
)}</code>`,
'<img alt="my image" loading="lazy" decoding="async" data-nimg="fill" style="position: absolute; height: 100%; width: 100%; left: 0px; top: 0px; right: 0px; bottom: 0px; color: transparent;" sizes="100vw" srcset="/_next/image?url=%2Fassets%2Fimg%2Ftest0.png&amp;w=640&amp;q=75 640w, /_next/image?url=%2Fassets%2Fimg%2Ftest0.png&amp;w=750&amp;q=75 750w, /_next/image?url=%2Fassets%2Fimg%2Ftest0.png&amp;w=828&amp;q=75 828w, /_next/image?url=%2Fassets%2Fimg%2Ftest0.png&amp;w=1080&amp;q=75 1080w, /_next/image?url=%2Fassets%2Fimg%2Ftest0.png&amp;w=1200&amp;q=75 1200w, /_next/image?url=%2Fassets%2Fimg%2Ftest0.png&amp;w=1920&amp;q=75 1920w, /_next/image?url=%2Fassets%2Fimg%2Ftest0.png&amp;w=2048&amp;q=75 2048w, /_next/image?url=%2Fassets%2Fimg%2Ftest0.png&amp;w=3840&amp;q=75 3840w" src="/_next/image?url=%2Fassets%2Fimg%2Ftest0.png&amp;w=3840&amp;q=75">',
'<code type="text/sitecore" chrometype="field" class="scpm" kind="close"></code>',
].join('')
);
});
});
144 changes: 69 additions & 75 deletions packages/sitecore-jss-nextjs/src/components/NextImage.tsx
Original file line number Diff line number Diff line change
@@ -1,92 +1,86 @@
import { mediaApi } from '@sitecore-jss/sitecore-jss/media';
import PropTypes from 'prop-types';
import React from 'react';

import {
getEEMarkup,
ImageProps,
ImageField,
ImageFieldValue,
withFieldMetadata,
} from '@sitecore-jss/sitecore-jss-react';
import Image, { ImageProps as NextImageProperties } from 'next/image';

type NextImageProps = ImageProps & Partial<NextImageProperties>;

export const NextImage: React.FC<NextImageProps> = ({
editable,
imageParams,
field,
mediaUrlPrefix,
fill,
priority,
...otherProps
}) => {
// next handles src and we use a custom loader,
// throw error if these are present
if (otherProps.src) {
throw new Error('Detected src prop. If you wish to use src, use next/image directly.');
}

const dynamicMedia = field as ImageField | ImageFieldValue;

if (
!field ||
(!dynamicMedia.editable && !dynamicMedia.value && !(dynamicMedia as ImageFieldValue).src)
) {
return null;
}

const imageField = dynamicMedia as ImageField;

// we likely have an experience editor value, should be a string
if (editable && imageField.editable) {
return getEEMarkup(
imageField,
imageParams as { [paramName: string]: string | number },
mediaUrlPrefix as RegExp,
otherProps as { src: string }
);
}

// some wise-guy/gal is passing in a 'raw' image object value
const img: ImageFieldValue = (dynamicMedia as ImageFieldValue).src
? (field as ImageFieldValue)
: (dynamicMedia.value as ImageFieldValue);
if (!img) {
return null;
export const NextImage: React.FC<NextImageProps> = withFieldMetadata<NextImageProps>(
({ editable, imageParams, field, mediaUrlPrefix, fill, priority, ...otherProps }) => {
// next handles src and we use a custom loader,
// throw error if these are present
if (otherProps.src) {
throw new Error('Detected src prop. If you wish to use src, use next/image directly.');
}

const dynamicMedia = field as ImageField | ImageFieldValue;

if (
!field ||
(!dynamicMedia.editable && !dynamicMedia.value && !(dynamicMedia as ImageFieldValue).src)
) {
return null;
}

const imageField = dynamicMedia as ImageField;

// we likely have an experience editor value, should be a string
if (editable && imageField.editable) {
return getEEMarkup(
imageField,
imageParams as { [paramName: string]: string | number },
mediaUrlPrefix as RegExp,
otherProps as { src: string }
);
}

// some wise-guy/gal is passing in a 'raw' image object value
const img: ImageFieldValue = (dynamicMedia as ImageFieldValue).src
? (field as ImageFieldValue)
: (dynamicMedia.value as ImageFieldValue);
if (!img) {
return null;
}

const attrs = {
...img,
...otherProps,
fill,
priority,
src: mediaApi.updateImageUrl(
img.src as string,
imageParams as { [paramName: string]: string | number },
mediaUrlPrefix as RegExp
),
};

const imageProps = {
...attrs,
// force replace /media with /jssmedia in src since we _know_ we will be adding a 'mw' query string parameter
// this is required for Sitecore media API resizing to work properly
src: mediaApi.replaceMediaUrlPrefix(attrs.src, mediaUrlPrefix as RegExp),
};

// Exclude `width`, `height` in case image is responsive, `fill` is used
if (imageProps.fill) {
delete imageProps.width;
delete imageProps.height;
}

if (attrs) {
return <Image alt="" {...imageProps} />;
}

return null; // we can't handle the truth
}

const attrs = {
...img,
...otherProps,
fill,
priority,
src: mediaApi.updateImageUrl(
img.src as string,
imageParams as { [paramName: string]: string | number },
mediaUrlPrefix as RegExp
),
};

const imageProps = {
...attrs,
// force replace /media with /jssmedia in src since we _know_ we will be adding a 'mw' query string parameter
// this is required for Sitecore media API resizing to work properly
src: mediaApi.replaceMediaUrlPrefix(attrs.src, mediaUrlPrefix as RegExp),
};

// Exclude `width`, `height` in case image is responsive, `fill` is used
if (imageProps.fill) {
delete imageProps.width;
delete imageProps.height;
}

if (attrs) {
return <Image alt="" {...imageProps} />;
}

return null; // we can't handle the truth
};
);

NextImage.propTypes = {
field: PropTypes.oneOfType([
Expand Down
55 changes: 55 additions & 0 deletions packages/sitecore-jss-nextjs/src/components/RichText.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,4 +379,59 @@ describe('RichText', () => {

expect(router.prefetch).callCount(0);
});

it('should render field metadata component when metadata property is present', () => {
const app = document.createElement('main');

document.body.appendChild(app);

const router = Router();

const testMetadata = {
contextItem: {
id: '{09A07660-6834-476C-B93B-584248D3003B}',
language: 'en',
revision: 'a0b36ce0a7db49418edf90eb9621e145',
version: 1,
},
fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}',
fieldType: 'image',
rawValue: 'Test1',
};

const props = {
field: {
value: `
<div id="test">
<h1>Hello!</h1>
<a href="/t10">1</a>
<a href="/t10">2</a>
<a href="/contains-children"><span id="child">Title</span></a>
</div>`,
metadata: testMetadata,
},
};

const rendered = mount(
<Page value={router}>
<RichText {...props} prefetchLinks={false} />
</Page>,
{ attachTo: app }
);

expect(rendered.html()).to.equal(
[
`<code type="text/sitecore" chrometype="field" class="scpm" kind="open">${JSON.stringify(
testMetadata
)}</code><div>
`,
`<div id="test">
<h1>Hello!</h1>
<a href="/t10">1</a>
<a href="/t10">2</a>
<a href="/contains-children"><span id="child">Title</span></a>
</div></div><code type="text/sitecore" chrometype="field" class="scpm" kind="close"></code>`,
].join('')
);
});
});
1 change: 1 addition & 0 deletions packages/sitecore-jss-nextjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,5 @@ export {
ComponentConsumerProps,
WithSitecoreContextOptions,
WithSitecoreContextProps,
withFieldMetadata,
} from '@sitecore-jss/sitecore-jss-react';
Loading

0 comments on commit cdb899a

Please sign in to comment.