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

[sitecore-jss-react] [sitecore-jss-nextjs] Add support for chrome's hydration for fields #1774

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4aea79e
render field metadata for Text field component; introduce field metad…
yavorsk Apr 11, 2024
5c4d4f9
rename FieldMetadata module, add unit test for Text component, add co…
yavorsk Apr 12, 2024
b23334a
add field metadata component to Date, Image and File field components…
yavorsk Apr 12, 2024
889b81e
add field metadata component to link and richtext field components, i…
yavorsk Apr 12, 2024
a8591e9
update FieldMetadata interfaces to prevent build errors in sitecore-j…
yavorsk Apr 12, 2024
f5641e4
export fieldmetadata component and interfaces from sitecore-jss-react
yavorsk Apr 12, 2024
5d16054
add metadata component for nextjs link field component; include unit …
yavorsk Apr 12, 2024
f8cfc3d
add field metadata component to nextimage component; small fix in lin…
yavorsk Apr 12, 2024
dcab12f
unit tests for FieldMetadata
yavorsk Apr 12, 2024
64431fb
update unit test
yavorsk Apr 12, 2024
6a05025
introduce getFieldMetadataMarkup function and used in the field compo…
yavorsk Apr 15, 2024
2e3362c
update changelog
yavorsk Apr 15, 2024
1141690
react - use higher order component to wrap metadata around field comp…
yavorsk Apr 16, 2024
08df912
update nextjs components to use metadata wrapper hoc; aadjust unit tests
yavorsk Apr 16, 2024
752506f
adjust unit tests and fix File component
yavorsk Apr 16, 2024
03b9de9
adjust image field tests; include check for media property in metadat…
yavorsk Apr 16, 2024
ecc5d82
some types updates
yavorsk Apr 16, 2024
4d23b39
some unit tests adjustments and metadata wrapper component update
yavorsk Apr 17, 2024
0b05351
some FieldMetadata related renamings
yavorsk Apr 17, 2024
46481cd
add unit test for RichText nextjs component
yavorsk Apr 17, 2024
cd24bb9
update changelog
yavorsk Apr 17, 2024
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
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. ([#1774](https://github.com/Sitecore/jss/pull/1774))

## 21.7.1

### 🐛 Bug Fixes
Expand Down
45 changes: 40 additions & 5 deletions packages/sitecore-jss-nextjs/src/components/Link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -351,13 +351,48 @@ describe('<Link />', () => {

it('should render nothing with missing field', () => {
const field = (null as unknown) as LinkField;
const rendered = mount(<Link field={field} />).children();
expect(rendered).to.have.length(0);
const rendered = mount(<Link field={field} />);
expect(rendered.html()).to.equal('');
});

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);
const rendered = mount(<Link field={field} />);
expect(rendered.children()).to.have.length(1);
expect(rendered.html()).to.equal('');
});

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.find('code')).to.have.length(2);
expect(rendered.html()).to.contain('kind="open"');
expect(rendered.html()).to.contain('kind="close"');
expect(rendered.html()).to.contain(JSON.stringify(testMetadata));
});
});
10 changes: 7 additions & 3 deletions packages/sitecore-jss-nextjs/src/components/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
LinkField,
LinkProps as ReactLinkProps,
LinkPropTypes,
withMetadata,
} from '@sitecore-jss/sitecore-jss-react';

export type LinkProps = ReactLinkProps & {
Expand All @@ -17,8 +18,9 @@ export type LinkProps = ReactLinkProps & {
internalLinkMatcher?: RegExp;
};

export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
(props: LinkProps, ref): JSX.Element | null => {
export const Link = withMetadata(
// eslint-disable-next-line react/display-name
forwardRef<HTMLAnchorElement, LinkProps>((props: LinkProps, ref): JSX.Element | null => {
const {
field,
editable,
Expand Down Expand Up @@ -68,8 +70,10 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
const reactLinkProps = { ...props };
delete reactLinkProps.internalLinkMatcher;

// we've already rendered the metadata wrapper - so set metadata to null to prevent duplicate wrapping
reactLinkProps.field.metadata = null;
return <ReactLink {...reactLinkProps} ref={ref} />;
}
})
);

Link.defaultProps = {
Expand Down
32 changes: 31 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,31 @@ 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.find('code')).to.have.length(2);
expect(rendered.find('img')).to.have.length(1);
expect(rendered.html()).to.contain('kind="open"');
expect(rendered.html()).to.contain('kind="close"');
expect(rendered.html()).to.contain(JSON.stringify(testMetadata));
});
});
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,
withMetadata,
} from '@sitecore-jss/sitecore-jss-react';
import Image, { ImageProps as NextImageProperties } from 'next/image';

type NextImageProps = Omit<ImageProps, 'media'> & 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> = withMetadata(
({ 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
48 changes: 48 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,52 @@ 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 }
);

// const rendered = mount(<RichText {...props} />);

expect(rendered.find('code')).to.have.length(2);
expect(rendered.html()).to.contain('<div id="test">');
expect(rendered.html()).to.contain('kind="open"');
expect(rendered.html()).to.contain('kind="close"');
expect(rendered.html()).to.contain(JSON.stringify(testMetadata));
});
});
Loading