diff --git a/CHANGELOG.md b/CHANGELOG.md
index 72f3e7fa29..609c19d569 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/packages/sitecore-jss-nextjs/src/components/Link.test.tsx b/packages/sitecore-jss-nextjs/src/components/Link.test.tsx
index 6a4acea4f4..921c7eb942 100644
--- a/packages/sitecore-jss-nextjs/src/components/Link.test.tsx
+++ b/packages/sitecore-jss-nextjs/src/components/Link.test.tsx
@@ -355,9 +355,48 @@ describe('', () => {
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().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(
+
+
+
+ );
+
+ expect(rendered.html()).to.equal(
+ [
+ `${JSON.stringify(
+ testMetadata
+ )}
`,
+ 'ipsum',
+ '
',
+ ].join('')
+ );
+ });
});
diff --git a/packages/sitecore-jss-nextjs/src/components/Link.tsx b/packages/sitecore-jss-nextjs/src/components/Link.tsx
index 4d8a1eb3c6..28a8bc96bf 100644
--- a/packages/sitecore-jss-nextjs/src/components/Link.tsx
+++ b/packages/sitecore-jss-nextjs/src/components/Link.tsx
@@ -39,7 +39,9 @@ export const Link = forwardRef(
? 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;
diff --git a/packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx b/packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx
index e3c4107916..cf99249d21 100644
--- a/packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx
+++ b/packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx
@@ -252,7 +252,10 @@ describe('', () => {
describe('error cases', () => {
const src = '/assets/img/test0.png';
it('should throw an error if src is present', () => {
- expect(() => mount()).to.throw(
+ const field = {
+ src: '/assets/img/test0.png',
+ };
+ expect(() => mount()).to.throw(
'Detected src prop. If you wish to use src, use next/image directly.'
);
});
@@ -283,4 +286,35 @@ describe('', () => {
);
});
});
+
+ 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();
+
+ expect(rendered.html()).to.equal(
+ [
+ `${JSON.stringify(
+ testMetadata
+ )}
`,
+ '',
+ '
',
+ ].join('')
+ );
+ });
});
diff --git a/packages/sitecore-jss-nextjs/src/components/NextImage.tsx b/packages/sitecore-jss-nextjs/src/components/NextImage.tsx
index 57a9795cd9..c3ffda9a3e 100644
--- a/packages/sitecore-jss-nextjs/src/components/NextImage.tsx
+++ b/packages/sitecore-jss-nextjs/src/components/NextImage.tsx
@@ -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;
-export const NextImage: React.FC = ({
- 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 = 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 ;
+ }
+
+ 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 ;
- }
-
- return null; // we can't handle the truth
-};
+);
NextImage.propTypes = {
field: PropTypes.oneOfType([
diff --git a/packages/sitecore-jss-nextjs/src/components/RichText.test.tsx b/packages/sitecore-jss-nextjs/src/components/RichText.test.tsx
index de0a4db97c..f8879a7dc0 100644
--- a/packages/sitecore-jss-nextjs/src/components/RichText.test.tsx
+++ b/packages/sitecore-jss-nextjs/src/components/RichText.test.tsx
@@ -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: `
+ `,
+ metadata: testMetadata,
+ },
+ };
+
+ const rendered = mount(
+
+
+ ,
+ { attachTo: app }
+ );
+
+ expect(rendered.html()).to.equal(
+ [
+ `${JSON.stringify(
+ testMetadata
+ )}
`,
+ ].join('')
+ );
+ });
});
diff --git a/packages/sitecore-jss-nextjs/src/index.ts b/packages/sitecore-jss-nextjs/src/index.ts
index c6fd7ddb35..ebebcc5a15 100644
--- a/packages/sitecore-jss-nextjs/src/index.ts
+++ b/packages/sitecore-jss-nextjs/src/index.ts
@@ -167,4 +167,5 @@ export {
ComponentConsumerProps,
WithSitecoreContextOptions,
WithSitecoreContextProps,
+ withFieldMetadata,
} from '@sitecore-jss/sitecore-jss-react';
diff --git a/packages/sitecore-jss-react/src/components/Date.test.tsx b/packages/sitecore-jss-react/src/components/Date.test.tsx
index 98b0b4d00b..b234517d5c 100644
--- a/packages/sitecore-jss-react/src/components/Date.test.tsx
+++ b/packages/sitecore-jss-react/src/components/Date.test.tsx
@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
import { expect } from 'chai';
-import { shallow } from 'enzyme';
+import { mount, shallow } from 'enzyme';
import React from 'react';
import { DateField } from './Date';
@@ -11,8 +11,7 @@ describe('', () => {
};
const c = shallow();
-
- expect(c.type()).to.be.null;
+ expect(c.html()).to.equal('');
});
it('should render value', () => {
@@ -81,4 +80,37 @@ describe('', () => {
expect(c.html()).equal('11-23-2001
');
});
+
+ 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: 'date',
+ rawValue: 'Test1',
+ };
+
+ const props = {
+ field: {
+ value: '23-11-2001',
+ metadata: testMetadata,
+ },
+ };
+
+ const rendered = mount();
+
+ expect(rendered.html()).to.equal(
+ [
+ `${JSON.stringify(
+ testMetadata
+ )}
`,
+ '23-11-2001',
+ '
',
+ ].join('')
+ );
+ });
});
diff --git a/packages/sitecore-jss-react/src/components/Date.tsx b/packages/sitecore-jss-react/src/components/Date.tsx
index 3ec447496a..41739729cd 100644
--- a/packages/sitecore-jss-react/src/components/Date.tsx
+++ b/packages/sitecore-jss-react/src/components/Date.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { withFieldMetadata } from '../enhancers/withFieldMetadata';
export interface DateFieldProps {
/** The date field data. */
@@ -7,6 +8,7 @@ export interface DateFieldProps {
field: {
value?: string;
editable?: string;
+ metadata?: { [key: string]: unknown };
};
/**
* The HTML element that will wrap the contents of the field.
@@ -21,47 +23,44 @@ export interface DateFieldProps {
render?: (date: Date | null) => React.ReactNode;
}
-export const DateField: React.FC = ({
- field,
- tag,
- editable,
- render,
- ...otherProps
-}) => {
- if (!field || (!field.editable && !field.value)) {
- return null;
- }
-
- let children: React.ReactNode;
+export const DateField: React.FC = withFieldMetadata(
+ ({ field, tag, editable, render, ...otherProps }) => {
+ if (!field || (!field.editable && !field.value)) {
+ return null;
+ }
- const htmlProps: {
- [htmlAttr: string]: unknown;
- children?: React.ReactNode;
- } = {
- ...otherProps,
- };
+ let children: React.ReactNode;
- if (field.editable && editable) {
- htmlProps.dangerouslySetInnerHTML = {
- __html: field.editable,
+ const htmlProps: {
+ [htmlAttr: string]: unknown;
+ children?: React.ReactNode;
+ } = {
+ ...otherProps,
};
- } else if (render) {
- children = render(field.value ? new Date(field.value) : null);
- } else {
- children = field.value;
- }
- if (tag || (field.editable && editable)) {
- return React.createElement(tag || 'span', htmlProps, children);
- } else {
- return {children};
+ if (field.editable && editable) {
+ htmlProps.dangerouslySetInnerHTML = {
+ __html: field.editable,
+ };
+ } else if (render) {
+ children = render(field.value ? new Date(field.value) : null);
+ } else {
+ children = field.value;
+ }
+
+ if (tag || (field.editable && editable)) {
+ return React.createElement(tag || 'span', htmlProps, children);
+ } else {
+ return {children};
+ }
}
-};
+);
DateField.propTypes = {
field: PropTypes.shape({
value: PropTypes.string,
editable: PropTypes.string,
+ metadata: PropTypes.objectOf(PropTypes.any),
}).isRequired,
tag: PropTypes.string,
editable: PropTypes.bool,
diff --git a/packages/sitecore-jss-react/src/components/FieldMetadata.test.tsx b/packages/sitecore-jss-react/src/components/FieldMetadata.test.tsx
new file mode 100644
index 0000000000..dc9b479e48
--- /dev/null
+++ b/packages/sitecore-jss-react/src/components/FieldMetadata.test.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { FieldMetadata } from './FieldMetadata';
+import { mount } from 'enzyme';
+import { expect } from 'chai';
+
+describe('', () => {
+ it('should render field metadata', () => {
+ const props = {
+ metadata: {
+ contextItem: {
+ id: '{09A07660-6834-476C-B93B-584248D3003B}',
+ language: 'en',
+ revision: 'a0b36ce0a7db49418edf90eb9621e145',
+ version: 1,
+ },
+ fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}',
+ fieldType: 'date',
+ rawValue: 'Test1',
+ },
+ };
+
+ const Foo = () => foo
;
+
+ const rendered = mount(
+
+
+
+ );
+
+ expect(rendered.html()).to.equal(
+ [
+ `${JSON.stringify(
+ props.metadata
+ )}
`,
+ 'foo
',
+ '
',
+ ].join('')
+ );
+ });
+});
diff --git a/packages/sitecore-jss-react/src/components/FieldMetadata.tsx b/packages/sitecore-jss-react/src/components/FieldMetadata.tsx
new file mode 100644
index 0000000000..0ec2974d38
--- /dev/null
+++ b/packages/sitecore-jss-react/src/components/FieldMetadata.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+interface FieldMetadataProps {
+ metadata: { [key: string]: unknown };
+ children: React.ReactNode;
+}
+
+/**
+ * The component which renders field metadata markup
+ * @param {FieldMetadataProps} props the props of the component
+ * @returns metadata markup wrapped around children
+ */
+export const FieldMetadata = (props: FieldMetadataProps): JSX.Element => {
+ const data = JSON.stringify(props.metadata);
+ const attributes = {
+ type: 'text/sitecore',
+ chrometype: 'field',
+ className: 'scpm',
+ };
+ const codeOpenAttributes = { ...attributes, kind: 'open' };
+ const codeCloseAttributes = { ...attributes, kind: 'close' };
+
+ return (
+ <>
+ {data}
+ {props.children}
+
+ >
+ );
+};
+
+FieldMetadata.displayName = 'FieldMetadata';
+
+FieldMetadata.propTypes = {
+ metadata: PropTypes.object.isRequired,
+ children: PropTypes.node.isRequired,
+};
diff --git a/packages/sitecore-jss-react/src/components/Image.test.tsx b/packages/sitecore-jss-react/src/components/Image.test.tsx
index b665ce594a..eb212bb02b 100644
--- a/packages/sitecore-jss-react/src/components/Image.test.tsx
+++ b/packages/sitecore-jss-react/src/components/Image.test.tsx
@@ -293,4 +293,36 @@ describe('', () => {
const rendered = mount();
expect(rendered.find('img')).to.have.length(1);
});
+
+ 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 imgField = {
+ src: '/assets/img/test0.png',
+ width: 8,
+ height: 10,
+ metadata: testMetadata,
+ };
+ const rendered = mount();
+
+ expect(rendered.html()).to.equal(
+ [
+ `${JSON.stringify(
+ testMetadata
+ )}
`,
+ '',
+ '
',
+ ].join('')
+ );
+ });
});
diff --git a/packages/sitecore-jss-react/src/components/Image.tsx b/packages/sitecore-jss-react/src/components/Image.tsx
index 2659d0394a..c00cb6257c 100644
--- a/packages/sitecore-jss-react/src/components/Image.tsx
+++ b/packages/sitecore-jss-react/src/components/Image.tsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { addClassName, convertAttributesToReactProps } from '../utils';
import { getAttributesString } from '../utils';
+import { withFieldMetadata } from '../enhancers/withFieldMetadata';
export interface ImageFieldValue {
[attributeName: string]: unknown;
@@ -36,7 +37,7 @@ export interface ImageSizeParameters {
export interface ImageProps {
[attributeName: string]: unknown;
/** Image field data (consistent with other field types) */
- field?: ImageField | ImageFieldValue;
+ field?: (ImageField | ImageFieldValue) & { metadata?: { [key: string]: unknown } };
/**
* Can be used to explicitly disable inline editing.
@@ -142,54 +143,46 @@ export const getEEMarkup = (
return getEditableWrapper(editableMarkup);
};
-export const Image: React.FC = ({
- editable,
- imageParams,
- field,
- mediaUrlPrefix,
- ...otherProps
-}) => {
- const dynamicMedia = field as ImageField | ImageFieldValue;
-
- if (
- !field ||
- (!dynamicMedia.editable && !dynamicMedia.value && !(dynamicMedia as ImageFieldValue).src)
- ) {
- return null;
- }
-
- const imageField = dynamicMedia as ImageField;
-
- if (editable && imageField.editable) {
- return getEEMarkup(imageField, imageParams, mediaUrlPrefix, otherProps);
- }
-
- // some wise-guy/gal is passing in a 'raw' image object value
- const img = (dynamicMedia as ImageFieldValue).src
- ? field
- : (dynamicMedia.value as ImageFieldValue);
- if (!img) {
- return null;
+export const Image: React.FC = withFieldMetadata(
+ ({ editable, imageParams, field, mediaUrlPrefix, ...otherProps }) => {
+ const dynamicMedia = field as ImageField | ImageFieldValue;
+
+ if (
+ !field ||
+ (!dynamicMedia.editable && !dynamicMedia.value && !(dynamicMedia as ImageFieldValue).src)
+ ) {
+ return null;
+ }
+
+ const imageField = dynamicMedia as ImageField;
+
+ if (editable && imageField.editable) {
+ return getEEMarkup(imageField, imageParams, mediaUrlPrefix, otherProps);
+ }
+
+ // some wise-guy/gal is passing in a 'raw' image object value
+ const img = (dynamicMedia as ImageFieldValue).src
+ ? field
+ : (dynamicMedia.value as ImageFieldValue);
+ if (!img) {
+ return null;
+ }
+
+ // prevent metadata from being passed to the img tag
+ if (img.metadata) {
+ delete img.metadata;
+ }
+
+ const attrs = getImageAttrs({ ...img, ...otherProps }, imageParams, mediaUrlPrefix);
+ if (attrs) {
+ return ;
+ }
+
+ return null; // we can't handle the truth
}
-
- const attrs = getImageAttrs({ ...img, ...otherProps }, imageParams, mediaUrlPrefix);
- if (attrs) {
- return ;
- }
-
- return null; // we can't handle the truth
-};
+);
Image.propTypes = {
- media: PropTypes.oneOfType([
- PropTypes.shape({
- src: PropTypes.string,
- }),
- PropTypes.shape({
- value: PropTypes.object,
- editable: PropTypes.string,
- }),
- ]),
field: PropTypes.oneOfType([
PropTypes.shape({
src: PropTypes.string,
diff --git a/packages/sitecore-jss-react/src/components/Link.test.tsx b/packages/sitecore-jss-react/src/components/Link.test.tsx
index 1b3737c82b..e9fc1942c4 100644
--- a/packages/sitecore-jss-react/src/components/Link.test.tsx
+++ b/packages/sitecore-jss-react/src/components/Link.test.tsx
@@ -8,14 +8,14 @@ import { generalLinkField as eeLinkData } from '../test-data/ee-data';
describe('', () => {
it('should render nothing with missing field', () => {
const field = (null as unknown) as LinkField;
- const rendered = mount().children();
- expect(rendered).to.have.length(0);
+ const rendered = mount();
+ expect(rendered.html()).to.equal('');
});
it('should render nothing with missing editable and value', () => {
const field = {};
- const rendered = mount().children();
- expect(rendered).to.have.length(0);
+ const rendered = mount();
+ expect(rendered.html()).to.equal('');
});
it('should render editable with an editable value', () => {
@@ -126,4 +126,35 @@ describe('', () => {
const link = c.find('a');
expect(ref.current?.id).to.equal(link.props().id);
});
+
+ 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 = {
+ href: '/lorem',
+ text: 'ipsum',
+ metadata: testMetadata,
+ };
+ const rendered = mount();
+
+ expect(rendered.html()).to.equal(
+ [
+ `${JSON.stringify(
+ testMetadata
+ )}
`,
+ 'ipsum',
+ '
',
+ ].join('')
+ );
+ });
});
diff --git a/packages/sitecore-jss-react/src/components/Link.tsx b/packages/sitecore-jss-react/src/components/Link.tsx
index a4906694a6..9eeeff30e7 100644
--- a/packages/sitecore-jss-react/src/components/Link.tsx
+++ b/packages/sitecore-jss-react/src/components/Link.tsx
@@ -1,5 +1,6 @@
-import React, { ReactElement, forwardRef } from 'react';
+import React, { ReactElement, RefAttributes, forwardRef } from 'react';
import PropTypes from 'prop-types';
+import { withFieldMetadata } from '../enhancers/withFieldMetadata';
export interface LinkFieldValue {
[attributeName: string]: unknown;
@@ -20,110 +21,112 @@ export interface LinkField {
editableLastPart?: string;
}
-export type LinkProps = React.DetailedHTMLProps<
- React.AnchorHTMLAttributes,
- HTMLAnchorElement
-> & {
- /** The link field data. */
- field: LinkField | LinkFieldValue;
- /**
- * Can be used to explicitly disable inline editing.
- * If true and `field.editable` has a value, then `field.editable` will be processed and rendered as component output. If false, `field.editable` value will be ignored and not rendered.
- * @default true
- */
- editable?: boolean;
-
- /**
- * Displays a link text ('description' in Sitecore) even when children exist
- * NOTE: when in Sitecore Experience Editor, this setting is ignored due to technical limitations, and the description is always rendered.
- */
- showLinkTextWithChildrenPresent?: boolean;
-};
+export type LinkProps = React.AnchorHTMLAttributes &
+ RefAttributes & {
+ /** The link field data. */
+ field: (LinkField | LinkFieldValue) & { metadata?: { [key: string]: unknown } };
+ /**
+ * Can be used to explicitly disable inline editing.
+ * If true and `field.editable` has a value, then `field.editable` will be processed and rendered as component output. If false, `field.editable` value will be ignored and not rendered.
+ * @default true
+ */
+ editable?: boolean;
+
+ /**
+ * Displays a link text ('description' in Sitecore) even when children exist
+ * NOTE: when in Sitecore Experience Editor, this setting is ignored due to technical limitations, and the description is always rendered.
+ */
+ showLinkTextWithChildrenPresent?: boolean;
+ };
+
+export const Link: React.FC = withFieldMetadata(
+ // eslint-disable-next-line react/display-name
+ forwardRef(
+ ({ field, editable, showLinkTextWithChildrenPresent, ...otherProps }, ref) => {
+ const children = otherProps.children as React.ReactNode;
+ const dynamicField: LinkField | LinkFieldValue = field;
+
+ if (
+ !field ||
+ (!dynamicField.editableFirstPart &&
+ !dynamicField.value &&
+ !(dynamicField as LinkFieldValue).href)
+ ) {
+ return null;
+ }
-export const Link = forwardRef(
- ({ field, editable, showLinkTextWithChildrenPresent, ...otherProps }, ref) => {
- const children = otherProps.children as React.ReactNode;
- const dynamicField: LinkField | LinkFieldValue = field;
-
- if (
- !field ||
- (!dynamicField.editableFirstPart &&
- !dynamicField.value &&
- !(dynamicField as LinkFieldValue).href)
- ) {
- return null;
- }
+ const resultTags: ReactElement[] = [];
+
+ // EXPERIENCE EDITOR RENDERING
+ if (editable && dynamicField.editableFirstPart) {
+ const markup = (dynamicField.editableFirstPart as string) + dynamicField.editableLastPart;
+
+ // in an ideal world, we'd pre-render React children here and inject them between editableFirstPart and editableLastPart.
+ // However, we cannot combine arbitrary unparsed HTML (innerHTML) based components with actual vDOM components (the children)
+ // because the innerHTML is not parsed - it'd make a discontinuous vDOM. So, we'll go for the next best compromise of rendering the link field and children separately
+ // as siblings. Should be "good enough" for most cases - and write your own helper if it isn't. Or bring xEditor out of 2006.
+
+ const htmlProps = {
+ className: 'sc-link-wrapper',
+ dangerouslySetInnerHTML: {
+ __html: markup,
+ },
+ ...otherProps,
+ key: 'editable',
+ };
+
+ // Exclude children, since 'dangerouslySetInnerHTML' and 'children' can't be set together
+ // and children will be added as a sibling
+ delete htmlProps.children;
+
+ resultTags.push();
+
+ // don't render normal link tag when editing, if no children exist
+ // this preserves normal-ish behavior if not using a link body (no hacks required)
+ if (!children) {
+ return resultTags[0];
+ }
+ }
- const resultTags: ReactElement[] = [];
+ // handle link directly on field for forgetful devs
+ const link = (dynamicField as LinkFieldValue).href
+ ? (field as LinkFieldValue)
+ : (dynamicField as LinkField).value;
- // EXPERIENCE EDITOR RENDERING
- if (editable && dynamicField.editableFirstPart) {
- const markup = (dynamicField.editableFirstPart as string) + dynamicField.editableLastPart;
+ if (!link) {
+ return null;
+ }
- // in an ideal world, we'd pre-render React children here and inject them between editableFirstPart and editableLastPart.
- // However, we cannot combine arbitrary unparsed HTML (innerHTML) based components with actual vDOM components (the children)
- // because the innerHTML is not parsed - it'd make a discontinuous vDOM. So, we'll go for the next best compromise of rendering the link field and children separately
- // as siblings. Should be "good enough" for most cases - and write your own helper if it isn't. Or bring xEditor out of 2006.
+ const anchor = link.linktype !== 'anchor' && link.anchor ? `#${link.anchor}` : '';
+ const querystring = link.querystring ? `?${link.querystring}` : '';
- const htmlProps = {
- className: 'sc-link-wrapper',
- dangerouslySetInnerHTML: {
- __html: markup,
- },
- ...otherProps,
- key: 'editable',
+ const anchorAttrs: { [attr: string]: unknown } = {
+ href: `${link.href}${querystring}${anchor}`,
+ className: link.class,
+ title: link.title,
+ target: link.target,
};
- // Exclude children, since 'dangerouslySetInnerHTML' and 'children' can't be set together
- // and children will be added as a sibling
- delete htmlProps.children;
-
- resultTags.push();
-
- // don't render normal link tag when editing, if no children exist
- // this preserves normal-ish behavior if not using a link body (no hacks required)
- if (!children) {
- return resultTags[0];
+ if (anchorAttrs.target === '_blank' && !anchorAttrs.rel) {
+ // information disclosure attack prevention keeps target blank site from getting ref to window.opener
+ anchorAttrs.rel = 'noopener noreferrer';
}
- }
-
- // handle link directly on field for forgetful devs
- const link = (dynamicField as LinkFieldValue).href
- ? (field as LinkFieldValue)
- : (dynamicField as LinkField).value;
-
- if (!link) {
- return null;
- }
- const anchor = link.linktype !== 'anchor' && link.anchor ? `#${link.anchor}` : '';
- const querystring = link.querystring ? `?${link.querystring}` : '';
+ const linkText = showLinkTextWithChildrenPresent || !children ? link.text || link.href : null;
- const anchorAttrs: { [attr: string]: unknown } = {
- href: `${link.href}${querystring}${anchor}`,
- className: link.class,
- title: link.title,
- target: link.target,
- };
+ resultTags.push(
+ React.createElement(
+ 'a',
+ { ...anchorAttrs, ...otherProps, key: 'link', ref },
+ linkText,
+ children
+ )
+ );
- if (anchorAttrs.target === '_blank' && !anchorAttrs.rel) {
- // information disclosure attack prevention keeps target blank site from getting ref to window.opener
- anchorAttrs.rel = 'noopener noreferrer';
+ return {resultTags};
}
-
- const linkText = showLinkTextWithChildrenPresent || !children ? link.text || link.href : null;
-
- resultTags.push(
- React.createElement(
- 'a',
- { ...anchorAttrs, ...otherProps, key: 'link', ref },
- linkText,
- children
- )
- );
-
- return {resultTags};
- }
+ ),
+ true
);
export const LinkPropTypes = {
diff --git a/packages/sitecore-jss-react/src/components/RichText.test.tsx b/packages/sitecore-jss-react/src/components/RichText.test.tsx
index 3aebdd0e45..1a741ca318 100644
--- a/packages/sitecore-jss-react/src/components/RichText.test.tsx
+++ b/packages/sitecore-jss-react/src/components/RichText.test.tsx
@@ -94,4 +94,35 @@ describe('', () => {
expect(rendered.html()).to.contain('');
expect(rendered.html()).to.contain('value');
});
+
+ 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: 'value',
+ metadata: testMetadata,
+ };
+
+ const rendered = mount();
+
+ expect(rendered.html()).to.equal(
+ [
+ `${JSON.stringify(
+ testMetadata
+ )}
`,
+ '
value
',
+ '
',
+ ].join('')
+ );
+ });
});
diff --git a/packages/sitecore-jss-react/src/components/RichText.tsx b/packages/sitecore-jss-react/src/components/RichText.tsx
index 6192998a7e..b522981a39 100644
--- a/packages/sitecore-jss-react/src/components/RichText.tsx
+++ b/packages/sitecore-jss-react/src/components/RichText.tsx
@@ -1,9 +1,11 @@
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
+import { withFieldMetadata } from '../enhancers/withFieldMetadata';
export interface RichTextField {
value?: string;
editable?: string;
+ metadata?: { [key: string]: unknown };
}
export interface RichTextProps {
@@ -23,8 +25,9 @@ export interface RichTextProps {
editable?: boolean;
}
-export const RichText: React.FC = forwardRef(
- ({ field, tag, editable, ...otherProps }, ref) => {
+export const RichText: React.FC = withFieldMetadata(
+ // eslint-disable-next-line react/display-name
+ forwardRef(({ field, tag, editable, ...otherProps }, ref) => {
if (!field || (!field.editable && !field.value)) {
return null;
}
@@ -38,13 +41,15 @@ export const RichText: React.FC = forwardRef(
};
return React.createElement(tag || 'div', htmlProps);
- }
+ }),
+ true
);
export const RichTextPropTypes = {
field: PropTypes.shape({
value: PropTypes.string,
editable: PropTypes.string,
+ metadata: PropTypes.objectOf(PropTypes.any),
}),
tag: PropTypes.string,
editable: PropTypes.bool,
diff --git a/packages/sitecore-jss-react/src/components/Text.test.tsx b/packages/sitecore-jss-react/src/components/Text.test.tsx
index 788b2f3bbc..633cd1fbb9 100644
--- a/packages/sitecore-jss-react/src/components/Text.test.tsx
+++ b/packages/sitecore-jss-react/src/components/Text.test.tsx
@@ -10,24 +10,23 @@ describe('', () => {
it('should render nothing with missing field', () => {
const field: TextField = null;
const rendered = mount();
- expect(rendered).to.have.length(1);
- expect(rendered.html()).to.be.null;
+ expect(rendered.html()).to.equal('');
});
- it('should render nothing with empty value', () => {
+ it('should render nothing with missing field', () => {
const field = {
value: '',
};
const rendered = mount();
expect(rendered).to.have.length(1);
- expect(rendered.html()).to.be.null;
+ expect(rendered.html()).to.equal('');
});
it('should render nothing with missing editable and value', () => {
const field = {};
const rendered = mount();
expect(rendered).to.have.length(1);
- expect(rendered.html()).to.be.null;
+ expect(rendered.html()).to.equal('');
});
it('should render editable with editable value', () => {
@@ -179,4 +178,39 @@ describe('', () => {
expect(rendered.html()).to.contain('');
expect(rendered.html()).to.contain('value');
});
+
+ 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: 'value',
+ metadata: testMetadata,
+ };
+
+ const rendered = mount(
+
+ test
+
+ );
+
+ expect(rendered.html()).to.equal(
+ [
+ `${JSON.stringify(
+ testMetadata
+ )}
`,
+ 'value',
+ '
',
+ ].join('')
+ );
+ });
});
diff --git a/packages/sitecore-jss-react/src/components/Text.tsx b/packages/sitecore-jss-react/src/components/Text.tsx
index a45d86b0a1..47f685f71c 100644
--- a/packages/sitecore-jss-react/src/components/Text.tsx
+++ b/packages/sitecore-jss-react/src/components/Text.tsx
@@ -1,9 +1,11 @@
-import React, { ReactElement, FunctionComponent } from 'react';
+import React, { ReactElement } from 'react';
+import { withFieldMetadata } from '../enhancers/withFieldMetadata';
import PropTypes from 'prop-types';
export interface TextField {
value?: string | number;
editable?: string;
+ metadata?: { [key: string]: unknown };
}
export interface TextProps {
@@ -26,87 +28,83 @@ export interface TextProps {
encode?: boolean;
}
-export const Text: FunctionComponent = ({
- field,
- tag,
- editable,
- encode,
- ...otherProps
-}) => {
- if (!field || (!field.editable && (field.value === undefined || field.value === ''))) {
- return null;
- }
+export const Text: React.FC = withFieldMetadata(
+ ({ field, tag, editable, encode, ...otherProps }) => {
+ if (!field || (!field.editable && (field.value === undefined || field.value === ''))) {
+ return null;
+ }
- // can't use editable value if we want to output unencoded
- if (!encode) {
- // eslint-disable-next-line no-param-reassign
- editable = false;
- }
+ // can't use editable value if we want to output unencoded
+ if (!encode) {
+ // eslint-disable-next-line no-param-reassign
+ editable = false;
+ }
- const isEditable = field.editable && editable;
+ const isEditable = field.editable && editable;
- let output: string | number | (ReactElement | string)[] = isEditable
- ? field.editable || ''
- : field.value === undefined
- ? ''
- : field.value;
+ let output: string | number | (ReactElement | string)[] = isEditable
+ ? field.editable || ''
+ : field.value === undefined
+ ? ''
+ : field.value;
- // when string value isn't formatted, we should format line breaks
- if (!field.editable && typeof output === 'string') {
- const splitted = String(output).split('\n');
+ // when string value isn't formatted, we should format line breaks
+ if (!field.editable && typeof output === 'string') {
+ const splitted = String(output).split('\n');
- if (splitted.length) {
- const formatted: (ReactElement | string)[] = [];
+ if (splitted.length) {
+ const formatted: (ReactElement | string)[] = [];
- splitted.forEach((str, i) => {
- const isLast = i === splitted.length - 1;
+ splitted.forEach((str, i) => {
+ const isLast = i === splitted.length - 1;
- formatted.push(str);
+ formatted.push(str);
- if (!isLast) {
- formatted.push(
);
- }
- });
+ if (!isLast) {
+ formatted.push(
);
+ }
+ });
- output = formatted;
+ output = formatted;
+ }
}
- }
-
- const setDangerously = isEditable || !encode;
- let children = null;
- const htmlProps: {
- [htmlAttributes: string]: unknown;
- children?: React.ReactNode;
- } = {
- ...otherProps,
- };
+ const setDangerously = isEditable || !encode;
- if (setDangerously) {
- htmlProps.dangerouslySetInnerHTML = {
- __html: output,
+ let children = null;
+ const htmlProps: {
+ [htmlAttributes: string]: unknown;
+ children?: React.ReactNode;
+ } = {
+ ...otherProps,
};
- } else {
- children = output;
- }
- if (tag || setDangerously) {
- return React.createElement(tag || 'span', htmlProps, children);
- } else {
- return {children};
+ if (setDangerously) {
+ htmlProps.dangerouslySetInnerHTML = {
+ __html: output,
+ };
+ } else {
+ children = output;
+ }
+
+ if (tag || setDangerously) {
+ return React.createElement(tag || 'span', htmlProps, children);
+ } else {
+ return {children};
+ }
}
-};
+);
Text.propTypes = {
field: PropTypes.shape({
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
editable: PropTypes.string,
+ metadata: PropTypes.objectOf(PropTypes.any),
}),
tag: PropTypes.string,
editable: PropTypes.bool,
encode: PropTypes.bool,
};
-
Text.defaultProps = {
editable: true,
encode: true,
diff --git a/packages/sitecore-jss-react/src/enhancers/withFieldMetadata.test.tsx b/packages/sitecore-jss-react/src/enhancers/withFieldMetadata.test.tsx
new file mode 100644
index 0000000000..52701c72dc
--- /dev/null
+++ b/packages/sitecore-jss-react/src/enhancers/withFieldMetadata.test.tsx
@@ -0,0 +1,212 @@
+/* eslint-disable no-unused-expressions */
+import React, { forwardRef } from 'react';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import { withFieldMetadata } from './withFieldMetadata';
+import { describe } from 'node:test';
+
+describe('withFieldMetadata', () => {
+ 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',
+ };
+
+ type TestComponentProps = {
+ field?: {
+ value?: string;
+ metadata?: { [key: string]: unknown };
+ };
+ editable?: boolean;
+ };
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const TestComponent = (props: TestComponentProps) => {
+ return (
+
+
{props.field?.value}
+
foo
+
bar
+
+ );
+ };
+
+ // eslint-disable-next-line react/display-name
+ const TestComponentWithRef = forwardRef(
+ (props: TestComponentProps, ref: React.ForwardedRef) => {
+ return (
+
+
{props.field?.value}
+
foo
+
bar
+
+ );
+ }
+ );
+
+ it('should return component if field is empty', () => {
+ const props = {
+ editable: true,
+ };
+
+ const WrappedComponent = withFieldMetadata(TestComponent);
+
+ const rendered = mount();
+
+ expect(rendered.html()).to.equal('');
+ });
+
+ it('should render unwrapped component if metadata field is not provided', () => {
+ const props = {
+ field: {
+ value: 'test',
+ },
+ editable: true,
+ };
+
+ const WrappedComponent = withFieldMetadata(TestComponent);
+
+ const rendered = mount();
+
+ expect(rendered.html()).to.equal('');
+ });
+
+ it('should render unwrapped component if metadata is provided but field is not editable', () => {
+ const props = {
+ field: {
+ value: 'test',
+ metadata: testMetadata,
+ },
+ editable: false,
+ };
+
+ const WrappedComponent = withFieldMetadata(TestComponent);
+
+ const rendered = mount();
+
+ expect(rendered.html()).to.equal('');
+ });
+
+ it('should wrap field with provided metadata', () => {
+ const props = {
+ field: {
+ value: 'car',
+ metadata: testMetadata,
+ },
+ editable: true,
+ };
+
+ const WrappedComponent = withFieldMetadata(TestComponent);
+
+ const rendered = mount();
+
+ expect(rendered.html()).to.equal(
+ [
+ `${JSON.stringify(
+ testMetadata
+ )}
`,
+ '',
+ '
car
',
+ '
foo
',
+ '
bar
',
+ '
',
+ '
',
+ ].join('')
+ );
+ });
+
+ describe('with forwardRef', () => {
+ it('should return component if field is empty', () => {
+ const props = {
+ editable: true,
+ };
+
+ const ref = React.createRef();
+
+ const WrappedComponent = withFieldMetadata(TestComponentWithRef, true);
+
+ const rendered = mount();
+
+ expect(ref.current?.outerHTML).to.equal('foo
');
+
+ expect(rendered.html()).to.equal('');
+ });
+
+ it('should render unwrapped component if metadata field is not provided', () => {
+ const props = {
+ field: {
+ value: 'test',
+ },
+ editable: true,
+ };
+
+ const ref = React.createRef();
+
+ const WrappedComponent = withFieldMetadata(TestComponentWithRef, true);
+
+ const rendered = mount();
+
+ expect(ref.current?.outerHTML).to.equal('foo
');
+
+ expect(rendered.html()).to.equal('');
+ });
+
+ it('should render unwrapped component if metadata is provided but field is not editable', () => {
+ const props = {
+ field: {
+ value: 'test',
+ metadata: testMetadata,
+ },
+ editable: false,
+ };
+
+ const ref = React.createRef();
+
+ const WrappedComponent = withFieldMetadata(TestComponentWithRef, true);
+
+ const rendered = mount();
+
+ expect(ref.current?.outerHTML).to.equal('foo
');
+
+ expect(rendered.html()).to.equal('');
+ });
+
+ it('should wrap field with provided metadata', () => {
+ const props = {
+ field: {
+ value: 'car',
+ metadata: testMetadata,
+ },
+ editable: true,
+ };
+
+ const ref = React.createRef();
+
+ const WrappedComponent = withFieldMetadata(TestComponentWithRef, true);
+
+ const rendered = mount();
+
+ expect(ref.current?.outerHTML).to.equal('foo
');
+
+ expect(rendered.html()).to.equal(
+ [
+ `${JSON.stringify(
+ testMetadata
+ )}
`,
+ '',
+ '
car
',
+ '
foo
',
+ '
bar
',
+ '
',
+ '
',
+ ].join('')
+ );
+ });
+ });
+});
diff --git a/packages/sitecore-jss-react/src/enhancers/withFieldMetadata.tsx b/packages/sitecore-jss-react/src/enhancers/withFieldMetadata.tsx
new file mode 100644
index 0000000000..5702c3d9d2
--- /dev/null
+++ b/packages/sitecore-jss-react/src/enhancers/withFieldMetadata.tsx
@@ -0,0 +1,53 @@
+import React, { ComponentType, forwardRef } from 'react';
+import { FieldMetadata } from '../components/FieldMetadata';
+
+interface WithMetadataProps {
+ field?: {
+ metadata?: { [key: string]: unknown };
+ };
+ editable?: boolean;
+}
+
+/**
+ * Wraps the field component with metadata markup intended to be used for chromes hydration in Pages
+ * @param {ComponentType} FieldComponent the field component
+ * @param {boolean} isForwardRef set to 'true' if forward reference is needed
+ */
+export function withFieldMetadata<
+ FieldComponentProps extends WithMetadataProps,
+ RefElementType = HTMLElement
+>(FieldComponent: ComponentType, isForwardRef = false) {
+ if (isForwardRef) {
+ // eslint-disable-next-line react/display-name
+ return forwardRef(
+ ({ ...props }: FieldComponentProps, ref: React.ForwardedRef) => {
+ const metadata = props.field?.metadata;
+
+ if (!metadata || !props.editable) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+ }
+ );
+ }
+
+ // eslint-disable-next-line react/display-name
+ return ({ ...props }: FieldComponentProps) => {
+ const metadata = props.field?.metadata;
+
+ if (!metadata || !props.editable) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+ };
+}
diff --git a/packages/sitecore-jss-react/src/index.ts b/packages/sitecore-jss-react/src/index.ts
index 057df7c5d5..4e07998a8b 100644
--- a/packages/sitecore-jss-react/src/index.ts
+++ b/packages/sitecore-jss-react/src/index.ts
@@ -99,3 +99,4 @@ export { withPlaceholder } from './enhancers/withPlaceholder';
export { withDatasourceCheck } from './enhancers/withDatasourceCheck';
export { EditFrameProps, EditFrame } from './components/EditFrame';
export { ComponentBuilder, ComponentBuilderConfig } from './ComponentBuilder';
+export { withFieldMetadata } from './enhancers/withFieldMetadata';