Skip to content

Commit

Permalink
Merge pull request #205 from contentful/feat/native-design-components-A…
Browse files Browse the repository at this point in the history
…LT-167

feat: design components on hybrid editor ALT-167
  • Loading branch information
chasepoirier authored Dec 21, 2023
2 parents 595aa03 + c1e2e05 commit 123e6f7
Show file tree
Hide file tree
Showing 96 changed files with 7,705 additions and 7,456 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ tmp/

# Necessary for pushing to alternative npm registry
.npmrc

# Ignore built styles.css file in packages/components
/packages/components/styles.css
11,573 changes: 5,448 additions & 6,125 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
#ContentfulContainer.defaultStyles {
overflow: scroll;
position: relative;
display: flex;
box-sizing: border-box;

-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}

#ContentfulContainer.defaultStyles::-webkit-scrollbar {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export const ContentfulContainer = (sectionProps: ContentfulContainerProps) => {
['data-cf-node-block-type']: node.type,
id: 'ContentfulContainer',
className: combineClasses(className, 'defaultStyles'),
zoneId: node.data.id,
WrapperComponent: Flex,
});
};
18 changes: 2 additions & 16 deletions packages/components/src/components/ContentfulContainer/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
import { builtInStyles } from '@contentful/experience-builder-core';
import { containerBuiltInStyles } from '@contentful/experience-builder-core';
import { ContentfulContainer } from './ContentfulContainer';
import {
ComponentDefinitionVariable,
ComponentDefinition,
} from '@contentful/experience-builder-core/types';
import { ComponentDefinition } from '@contentful/experience-builder-core/types';
import {
CONTENTFUL_COMPONENT_CATEGORY,
CONTENTFUL_CONTAINER_ID,
} from '@contentful/experience-builder-core/constants';

export { ContentfulContainer };

export const containerBuiltInStyles = {
...builtInStyles,
cfHeight: {
displayName: 'Height',
type: 'Text',
group: 'style',
description: 'The height of the section',
defaultValue: 'auto',
} as ComponentDefinitionVariable<'Text'>,
};

export const ContentfulContainerComponentDefinition: ComponentDefinition = {
id: CONTENTFUL_CONTAINER_ID,
name: 'Container',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
CompositionComponentNode,
ResolveDesignValueType,
StyleProps,
} from '@contentful/experience-builder-core/types';
import React from 'react';

export type DesignComponentProps<EditorMode = boolean> = EditorMode extends true
? {
children?: React.ReactNode;
className?: string;
cfHyperlink?: StyleProps['cfHyperlink'];
cfOpenInNewTab?: StyleProps['cfOpenInNewTab'];
editorMode?: EditorMode;
node: CompositionComponentNode;
resolveDesignValue?: ResolveDesignValueType;
renderDropZone: (
node: CompositionComponentNode,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props?: Record<string, any>
) => React.ReactNode;
}
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
Record<string, any>;

const designComponentStyle = { display: 'contents' };

// Feel free to do any magic as regards variable definitions for design components
// Or if this isn't necessary by the time we figure that part out, we can bid this part farewell
export const DesignComponent: React.FC<DesignComponentProps> = (props) => {
if (props.editorMode) {
const { node } = props;

return props.renderDropZone(node, {
['data-test-id']: 'contentful-container',
['data-cf-node-id']: node.data.id,
['data-cf-node-block-id']: node.data.blockId,
['data-cf-node-block-type']: node.type,
id: 'design-component',
className: props.className,
style: designComponentStyle,
});
}
// Using a display contents so design component content/children
// can appear as if they are direct children of the div wrapper's parent
return <div data-test-id="design-component" {...props} style={designComponentStyle} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './DesignComponent';
1 change: 1 addition & 0 deletions packages/components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './Text';
export * from './Image';
export * from './ContentfulContainer';
export * from './Columns';
export * from './DesignComponent';
4 changes: 0 additions & 4 deletions packages/core/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export default [
],
plugins: [nodeResolve(), typescript({ tsconfig: './tsconfig.json' })],
external: [/node_modules\/(?!tslib.*)/],
treeshake: false,
},
//specific exports in package.json
{
Expand All @@ -29,21 +28,18 @@ export default [
],
plugins: [nodeResolve(), typescript({ tsconfig: './tsconfig.json' })],
external: [/node_modules\/(?!tslib.*)/],
treeshake: false,
},
//typings
{
input: 'src/index.ts',
output: [{ dir: 'dist', format: 'esm', preserveModules: true }],
plugins: [dts({ tsconfig: './tsconfig.json' })],
external: [/.css/],
treeshake: false,
},
{
input: 'src/exports.ts',
output: [{ dir: 'dist', format: 'esm', preserveModules: true }],
plugins: [dts({ tsconfig: './tsconfig.json' })],
external: [/.css/],
treeshake: false,
},
];
2 changes: 2 additions & 0 deletions packages/core/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export const INCOMING_EVENTS = {
HoverComponent: 'hoverComponent',
UpdatedEntity: 'updatedEntity',
DesignComponentsAdded: 'designComponentsAdded',
DesignComponentsRegistered: 'designComponentsRegistered',
InitEditor: 'initEditor',
EntitiesResolved: 'entitiesResolved',
};

export const INTERNAL_EVENTS = {
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/definitions/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ComponentDefinition } from '@/types';

import { CONTENTFUL_COMPONENT_CATEGORY, CONTENTFUL_CONTAINER_ID } from '@/constants';
import { containerBuiltInStyles } from './styles';

export const containerDefinition: ComponentDefinition = {
id: CONTENTFUL_CONTAINER_ID,
name: 'Container',
category: CONTENTFUL_COMPONENT_CATEGORY,
children: true,
variables: containerBuiltInStyles,
};
2 changes: 2 additions & 0 deletions packages/core/src/definitions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './components';
export * from './styles';
48 changes: 38 additions & 10 deletions packages/core/src/entity/EditorModeEntityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,32 @@ import { EditorEntityStore, RequestedEntitiesMessage } from '@contentful/visual-
import type { Asset, AssetFile, Entry, UnresolvedLink } from 'contentful';
import { sendMessage } from '../communication/sendMessage';

// The default of 3s in the EditorEntityStore is sometimes timing out and
// leads to not rendering bound content and design components.
const REQUEST_TIMEOUT = 10000;

export class EditorModeEntityStore extends EditorEntityStore {
public locale: string;

constructor({ entities, locale }: { entities: Array<Entry | Asset>; locale: string }) {
console.debug(
`[exp-builder.sdk] Initializing editor entity store with ${entities.length} entities for locale ${locale}.`,
{ entities }
);

const subscribe = (method: unknown, cb: (payload: RequestedEntitiesMessage) => void) => {
const listeners = (event: MessageEvent) => {
if (typeof event.data !== 'string') {
return;
}
const data: {
source: 'composability-app';
eventType: string;
payload: RequestedEntitiesMessage;
} = JSON.parse(event.data);

if (typeof data !== 'object' || !data) return;

if (data.source !== 'composability-app') return;
if (data.eventType === method) {
cb(data.payload);
Expand All @@ -30,20 +45,33 @@ export class EditorModeEntityStore extends EditorEntityStore {
};
};

super({ entities, sendMessage, subscribe, locale });
super({ entities, sendMessage, subscribe, locale, timeoutDuration: REQUEST_TIMEOUT });
this.locale = locale;
}
/**
* This function collects and returns the list of requested entries and assets. Additionally, it checks
* upfront whether any async fetching logic is actually happening. If not, it returns a plain `false` value, so we
* can detect this early and avoid unnecessary re-renders.
* @param entityLinks
* @returns false if no async fetching is happening, otherwise a promise that resolves when all entities are fetched
*/
fetchEntities(entityLinks: UnresolvedLink<'Entry' | 'Asset'>[]): false | Promise<void> {
const entryLinks = entityLinks.filter((link) => link.sys?.linkType === 'Entry');
const assetLinks = entityLinks.filter((link) => link.sys?.linkType === 'Asset');

const uniqueEntryIds = [...new Set(entryLinks.map((link) => link.sys.id))];
const uniqueAssetIds = [...new Set(assetLinks.map((link) => link.sys.id))];

async fetchEntities(entityLinks: UnresolvedLink<'Entry' | 'Asset'>[]) {
const entryLinks = entityLinks.filter((link) => link.sys.linkType === 'Entry');
const assetLinks = entityLinks.filter((link) => link.sys.linkType === 'Asset');
const { missing: missingEntryIds } = this.getEntitiesFromMap('Entry', uniqueEntryIds);
const { missing: missingAssetIds } = this.getEntitiesFromMap('Asset', uniqueAssetIds);

const uniqueEntryLinks = new Set(entryLinks.map((link) => link.sys.id));
const uniqueAssetLinks = new Set(assetLinks.map((link) => link.sys.id));
// Return false to indicate that no async fetching is happening
if (!missingAssetIds.length && !missingEntryIds.length) return false;

return await Promise.allSettled([
this.fetchEntries([...uniqueEntryLinks]),
this.fetchAssets([...uniqueAssetLinks]),
]);
// Entries and assets will be stored in entryMap and assetMap
return Promise.all([this.fetchEntries(uniqueEntryIds), this.fetchAssets(uniqueAssetIds)]).then(
() => Promise.resolve()
);
}

getValue(
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './utils';
export * from './definitions/styles';
export * from './definitions';
export * from './entity';
export * from './communication';
export * from './enums';
export * from './registries';
45 changes: 45 additions & 0 deletions packages/core/src/registries/designTokenRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { DesignTokensDefinition } from '@/types';
import { OUTGOING_EVENTS } from '@/constants';
import { sendMessage } from '@/communication';

const designTokensRegistry = {} as DesignTokensDefinition;
const templateStringRegex = /\${(.+?)}/g;

/**
* Register design tokens styling
* @param designTokenDefinition - {[key:string]: Record<string, string>}
* @returns void
*/
export const defineDesignTokens = (designTokenDefinition: DesignTokensDefinition) => {
Object.assign(designTokensRegistry, designTokenDefinition);
sendMessage(OUTGOING_EVENTS.DesignTokens, {
designTokens: designTokenDefinition,
});
};

export const getDesignTokenRegistration = (breakpointValue: string) => {
if (!breakpointValue) return breakpointValue;

let resolvedValue = '';
const values = breakpointValue.split(' ');
values.forEach((value) => {
let tokenValue = value;
if (isTemplateStringFormat(value)) tokenValue = resolveSimpleDesignToken(value);
resolvedValue += `${tokenValue} `;
});

return resolvedValue.trim();
};

// Using this because export const StringTemplateRegex = /\${(.*?)\}/g doesn't work
const isTemplateStringFormat = (str: string) => {
return templateStringRegex.test(str);
};

const resolveSimpleDesignToken = (templateString: string) => {
const nonTemplateValue = templateString.replace(templateStringRegex, '$1');
const designKeys = nonTemplateValue.split('.');
const spacingValues = designTokensRegistry[designKeys[0]] as DesignTokensDefinition;
const resolvedValue = spacingValues[designKeys[1]] as string;
return resolvedValue || '0px';
};
1 change: 1 addition & 0 deletions packages/core/src/registries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './designTokenRegistry';
7 changes: 6 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ export type Composition = {
componentSettings?: ExperienceComponentSettings;
};

export type DesignTokensDefinition = { [key: string]: Record<string, string> };
export type DesignTokensDefinition = { [key: string]: string | DesignTokensDefinition };

export type ExperienceEntry = {
sys: Entry['sys'];
Expand Down Expand Up @@ -343,3 +343,8 @@ export interface DeprecatedExperience {
export type ValuesByBreakpoint =
| Record<string, CompositionVariableValueType>
| CompositionVariableValueType;

export type ResolveDesignValueType = (
valuesByBreakpoint: ValuesByBreakpoint,
variableName: string
) => CompositionVariableValueType;
Loading

0 comments on commit 123e6f7

Please sign in to comment.