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 #1773

Merged
merged 51 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
51 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
0a5c3d5
update changelog pull request
yavorsk Apr 17, 2024
77ccdcd
some type updates
yavorsk Apr 17, 2024
6c74f77
reenable file tests
yavorsk Apr 17, 2024
d5dcd8d
update function description
yavorsk Apr 17, 2024
69fe14b
minor variable renaming
yavorsk Apr 17, 2024
fcb38b9
remove unnecessary commented line
yavorsk Apr 17, 2024
8c47f76
remove unnecessary undefined check
yavorsk Apr 17, 2024
19333b2
move FieldMetada interfaces to base package; extract metadata proptypes
yavorsk Apr 17, 2024
04afea6
move FieldMetadata under enchancments
yavorsk Apr 17, 2024
6c63e83
Merge branch 'feature/JSS-1827' of https://github.com/Sitecore/jss in…
yavorsk Apr 17, 2024
581527e
added some descriptions
yavorsk Apr 17, 2024
32deed2
move and rename FieldMetadata to layout submodule of base package
yavorsk Apr 18, 2024
a0115fd
rename FieldMetadata component
yavorsk Apr 18, 2024
8095600
add tsdoc description for fieldmetadata component
yavorsk Apr 18, 2024
d749ad9
conditionally forwardRef in fieldMetadata
yavorsk Apr 18, 2024
dde6878
two separate withFieldMetadata functions based on if used with forwar…
yavorsk Apr 18, 2024
5cd3c75
single withFieldMetadata function with forwardref parameter
yavorsk Apr 19, 2024
633e322
update with metadata unit test to test the whole structure of markup
yavorsk Apr 19, 2024
c12dbb8
withMetadata refactoring wip
yavorsk Apr 19, 2024
13473fd
Adjusted withFieldMetadata generic type
illiakovalenko Apr 19, 2024
9120a7b
update unit test
yavorsk Apr 22, 2024
e6252f6
wip - refactor field metadata hoc
yavorsk Apr 22, 2024
d5f49e9
Updates
illiakovalenko Apr 23, 2024
6842fc8
Merge branch 'dev' of https://github.com/Sitecore/jss into feature/JS…
illiakovalenko Apr 23, 2024
8277ee6
Updated unit tests, simplified types
illiakovalenko Apr 23, 2024
f8e8232
Update
illiakovalenko Apr 23, 2024
15c9f40
Expose withFieldMetadata as a part of nextjs sdk
illiakovalenko Apr 23, 2024
bdbddd8
Updated PropTypes
illiakovalenko Apr 23, 2024
c86c0d2
Removed extra asserts
illiakovalenko Apr 23, 2024
59cf116
remove media property from propTypes
yavorsk Apr 24, 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. ([#1773](https://github.com/Sitecore/jss/pull/1773))

## 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,
withFieldMetadataForwardRef,
} 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 = withFieldMetadataForwardRef(
// 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,
withFieldMetadata,
} 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> = withFieldMetadata(
({ 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
46 changes: 46 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,50 @@ 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.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