Skip to content

Commit cdb899a

Browse files
[sitecore-jss-react] [sitecore-jss-nextjs] Add support for chrome's hydration 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]>
1 parent 0d84c70 commit cdb899a

22 files changed

+950
-320
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ Our versioning strategy is as follows:
1111

1212
## Unreleased
1313

14+
### 🎉 New Features & Improvements
15+
* `[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))
16+
1417
### 🛠 Breaking Changes
1518

1619
* `[sitecore-jss]` Switch to edge site query for XP and gets config sites + sxa sites (ignoring website)

packages/sitecore-jss-nextjs/src/components/Link.test.tsx

+40-1
Original file line numberDiff line numberDiff line change
@@ -355,9 +355,48 @@ describe('<Link />', () => {
355355
expect(rendered).to.have.length(0);
356356
});
357357

358-
it('should render nothing with missing editable and value', () => {
358+
it('should render nothing with missing field', () => {
359359
const field = {};
360360
const rendered = mount(<Link field={field} />).children();
361361
expect(rendered).to.have.length(0);
362362
});
363+
364+
it('should render field metadata component when metadata property is present', () => {
365+
const testMetadata = {
366+
contextItem: {
367+
id: '{09A07660-6834-476C-B93B-584248D3003B}',
368+
language: 'en',
369+
revision: 'a0b36ce0a7db49418edf90eb9621e145',
370+
version: 1,
371+
},
372+
fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}',
373+
fieldType: 'single-line',
374+
rawValue: 'Test1',
375+
};
376+
377+
const field = {
378+
value: {
379+
href: '/lorem',
380+
text: 'ipsum',
381+
class: 'my-link',
382+
},
383+
metadata: testMetadata,
384+
};
385+
386+
const rendered = mount(
387+
<Page>
388+
<Link field={field} />
389+
</Page>
390+
);
391+
392+
expect(rendered.html()).to.equal(
393+
[
394+
`<code type="text/sitecore" chrometype="field" class="scpm" kind="open">${JSON.stringify(
395+
testMetadata
396+
)}</code>`,
397+
'<a href="/lorem" class="my-link">ipsum</a>',
398+
'<code type="text/sitecore" chrometype="field" class="scpm" kind="close"></code>',
399+
].join('')
400+
);
401+
});
363402
});

packages/sitecore-jss-nextjs/src/components/Link.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
3939
? field
4040
: (field as LinkField).value) as LinkFieldValue;
4141
const { href, querystring, anchor } = value;
42-
const isEditing = editable && (field as LinkFieldValue).editable;
42+
43+
const isEditing =
44+
editable && ((field as LinkFieldValue).editable || (field as LinkFieldValue).metadata);
4345

4446
if (href && !isEditing) {
4547
const text = showLinkTextWithChildrenPresent || !children ? value.text || value.href : null;

packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx

+35-1
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,10 @@ describe('<NextImage />', () => {
252252
describe('error cases', () => {
253253
const src = '/assets/img/test0.png';
254254
it('should throw an error if src is present', () => {
255-
expect(() => mount(<NextImage src={src} />)).to.throw(
255+
const field = {
256+
src: '/assets/img/test0.png',
257+
};
258+
expect(() => mount(<NextImage src={src} field={field} />)).to.throw(
256259
'Detected src prop. If you wish to use src, use next/image directly.'
257260
);
258261
});
@@ -283,4 +286,35 @@ describe('<NextImage />', () => {
283286
);
284287
});
285288
});
289+
290+
it('should render field metadata component when metadata property is present', () => {
291+
const testMetadata = {
292+
contextItem: {
293+
id: '{09A07660-6834-476C-B93B-584248D3003B}',
294+
language: 'en',
295+
revision: 'a0b36ce0a7db49418edf90eb9621e145',
296+
version: 1,
297+
},
298+
fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}',
299+
fieldType: 'image',
300+
rawValue: 'Test1',
301+
};
302+
303+
const field = {
304+
value: { src: '/assets/img/test0.png', alt: 'my image' },
305+
metadata: testMetadata,
306+
};
307+
308+
const rendered = mount(<NextImage field={field} fill={true} />);
309+
310+
expect(rendered.html()).to.equal(
311+
[
312+
`<code type="text/sitecore" chrometype="field" class="scpm" kind="open">${JSON.stringify(
313+
testMetadata
314+
)}</code>`,
315+
'<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">',
316+
'<code type="text/sitecore" chrometype="field" class="scpm" kind="close"></code>',
317+
].join('')
318+
);
319+
});
286320
});

packages/sitecore-jss-nextjs/src/components/NextImage.tsx

+69-75
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,86 @@
11
import { mediaApi } from '@sitecore-jss/sitecore-jss/media';
22
import PropTypes from 'prop-types';
33
import React from 'react';
4-
54
import {
65
getEEMarkup,
76
ImageProps,
87
ImageField,
98
ImageFieldValue,
9+
withFieldMetadata,
1010
} from '@sitecore-jss/sitecore-jss-react';
1111
import Image, { ImageProps as NextImageProperties } from 'next/image';
1212

1313
type NextImageProps = ImageProps & Partial<NextImageProperties>;
1414

15-
export const NextImage: React.FC<NextImageProps> = ({
16-
editable,
17-
imageParams,
18-
field,
19-
mediaUrlPrefix,
20-
fill,
21-
priority,
22-
...otherProps
23-
}) => {
24-
// next handles src and we use a custom loader,
25-
// throw error if these are present
26-
if (otherProps.src) {
27-
throw new Error('Detected src prop. If you wish to use src, use next/image directly.');
28-
}
29-
30-
const dynamicMedia = field as ImageField | ImageFieldValue;
31-
32-
if (
33-
!field ||
34-
(!dynamicMedia.editable && !dynamicMedia.value && !(dynamicMedia as ImageFieldValue).src)
35-
) {
36-
return null;
37-
}
38-
39-
const imageField = dynamicMedia as ImageField;
40-
41-
// we likely have an experience editor value, should be a string
42-
if (editable && imageField.editable) {
43-
return getEEMarkup(
44-
imageField,
45-
imageParams as { [paramName: string]: string | number },
46-
mediaUrlPrefix as RegExp,
47-
otherProps as { src: string }
48-
);
49-
}
50-
51-
// some wise-guy/gal is passing in a 'raw' image object value
52-
const img: ImageFieldValue = (dynamicMedia as ImageFieldValue).src
53-
? (field as ImageFieldValue)
54-
: (dynamicMedia.value as ImageFieldValue);
55-
if (!img) {
56-
return null;
15+
export const NextImage: React.FC<NextImageProps> = withFieldMetadata<NextImageProps>(
16+
({ editable, imageParams, field, mediaUrlPrefix, fill, priority, ...otherProps }) => {
17+
// next handles src and we use a custom loader,
18+
// throw error if these are present
19+
if (otherProps.src) {
20+
throw new Error('Detected src prop. If you wish to use src, use next/image directly.');
21+
}
22+
23+
const dynamicMedia = field as ImageField | ImageFieldValue;
24+
25+
if (
26+
!field ||
27+
(!dynamicMedia.editable && !dynamicMedia.value && !(dynamicMedia as ImageFieldValue).src)
28+
) {
29+
return null;
30+
}
31+
32+
const imageField = dynamicMedia as ImageField;
33+
34+
// we likely have an experience editor value, should be a string
35+
if (editable && imageField.editable) {
36+
return getEEMarkup(
37+
imageField,
38+
imageParams as { [paramName: string]: string | number },
39+
mediaUrlPrefix as RegExp,
40+
otherProps as { src: string }
41+
);
42+
}
43+
44+
// some wise-guy/gal is passing in a 'raw' image object value
45+
const img: ImageFieldValue = (dynamicMedia as ImageFieldValue).src
46+
? (field as ImageFieldValue)
47+
: (dynamicMedia.value as ImageFieldValue);
48+
if (!img) {
49+
return null;
50+
}
51+
52+
const attrs = {
53+
...img,
54+
...otherProps,
55+
fill,
56+
priority,
57+
src: mediaApi.updateImageUrl(
58+
img.src as string,
59+
imageParams as { [paramName: string]: string | number },
60+
mediaUrlPrefix as RegExp
61+
),
62+
};
63+
64+
const imageProps = {
65+
...attrs,
66+
// force replace /media with /jssmedia in src since we _know_ we will be adding a 'mw' query string parameter
67+
// this is required for Sitecore media API resizing to work properly
68+
src: mediaApi.replaceMediaUrlPrefix(attrs.src, mediaUrlPrefix as RegExp),
69+
};
70+
71+
// Exclude `width`, `height` in case image is responsive, `fill` is used
72+
if (imageProps.fill) {
73+
delete imageProps.width;
74+
delete imageProps.height;
75+
}
76+
77+
if (attrs) {
78+
return <Image alt="" {...imageProps} />;
79+
}
80+
81+
return null; // we can't handle the truth
5782
}
58-
59-
const attrs = {
60-
...img,
61-
...otherProps,
62-
fill,
63-
priority,
64-
src: mediaApi.updateImageUrl(
65-
img.src as string,
66-
imageParams as { [paramName: string]: string | number },
67-
mediaUrlPrefix as RegExp
68-
),
69-
};
70-
71-
const imageProps = {
72-
...attrs,
73-
// force replace /media with /jssmedia in src since we _know_ we will be adding a 'mw' query string parameter
74-
// this is required for Sitecore media API resizing to work properly
75-
src: mediaApi.replaceMediaUrlPrefix(attrs.src, mediaUrlPrefix as RegExp),
76-
};
77-
78-
// Exclude `width`, `height` in case image is responsive, `fill` is used
79-
if (imageProps.fill) {
80-
delete imageProps.width;
81-
delete imageProps.height;
82-
}
83-
84-
if (attrs) {
85-
return <Image alt="" {...imageProps} />;
86-
}
87-
88-
return null; // we can't handle the truth
89-
};
83+
);
9084

9185
NextImage.propTypes = {
9286
field: PropTypes.oneOfType([

packages/sitecore-jss-nextjs/src/components/RichText.test.tsx

+55
Original file line numberDiff line numberDiff line change
@@ -379,4 +379,59 @@ describe('RichText', () => {
379379

380380
expect(router.prefetch).callCount(0);
381381
});
382+
383+
it('should render field metadata component when metadata property is present', () => {
384+
const app = document.createElement('main');
385+
386+
document.body.appendChild(app);
387+
388+
const router = Router();
389+
390+
const testMetadata = {
391+
contextItem: {
392+
id: '{09A07660-6834-476C-B93B-584248D3003B}',
393+
language: 'en',
394+
revision: 'a0b36ce0a7db49418edf90eb9621e145',
395+
version: 1,
396+
},
397+
fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}',
398+
fieldType: 'image',
399+
rawValue: 'Test1',
400+
};
401+
402+
const props = {
403+
field: {
404+
value: `
405+
<div id="test">
406+
<h1>Hello!</h1>
407+
<a href="/t10">1</a>
408+
<a href="/t10">2</a>
409+
<a href="/contains-children"><span id="child">Title</span></a>
410+
</div>`,
411+
metadata: testMetadata,
412+
},
413+
};
414+
415+
const rendered = mount(
416+
<Page value={router}>
417+
<RichText {...props} prefetchLinks={false} />
418+
</Page>,
419+
{ attachTo: app }
420+
);
421+
422+
expect(rendered.html()).to.equal(
423+
[
424+
`<code type="text/sitecore" chrometype="field" class="scpm" kind="open">${JSON.stringify(
425+
testMetadata
426+
)}</code><div>
427+
`,
428+
`<div id="test">
429+
<h1>Hello!</h1>
430+
<a href="/t10">1</a>
431+
<a href="/t10">2</a>
432+
<a href="/contains-children"><span id="child">Title</span></a>
433+
</div></div><code type="text/sitecore" chrometype="field" class="scpm" kind="close"></code>`,
434+
].join('')
435+
);
436+
});
382437
});

packages/sitecore-jss-nextjs/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,5 @@ export {
167167
ComponentConsumerProps,
168168
WithSitecoreContextOptions,
169169
WithSitecoreContextProps,
170+
withFieldMetadata,
170171
} from '@sitecore-jss/sitecore-jss-react';

0 commit comments

Comments
 (0)