Skip to content

Commit

Permalink
feat: use agreed component definition structure [SPA-1126] (#9)
Browse files Browse the repository at this point in the history
* chore: make the message source app origin configurable via an env var

* feat: define component definition types aligned with the proposal

* refactor: mvp component definition structure

* chore: prettify

* chore: update inline comment

* chore: clarify readme
  • Loading branch information
Spring3 authored May 5, 2023
1 parent 547a82c commit 836ca04
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 34 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,9 @@ const App = () => {
);
}
```

In case you expect the message to arrive from a different origin, define this environment variable with your desired value

```sh
REACT_APP_EXPERIENCE_BUILDER_ORIGIN=https://localhost:3001 # https://app.contentful.com by default
```
40 changes: 40 additions & 0 deletions src/Test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
import React from 'react'
import { useComponents } from './hooks'
import { ComponentDefinitionVariableArrayItemType, ComponentDefinitionVariableType } from './types'

const Com = () => {
return null
}

export const Test = () => {
const { defineComponent } = useComponents()

defineComponent(Com, {
id: 'Com',
name: 'Com',
variables: {
name: {
type: ComponentDefinitionVariableType.LINK,
linkType: 'Asset',
},
isChecked: {
type: ComponentDefinitionVariableType.BOOLEAN,
},
elements: {
type: ComponentDefinitionVariableType.ARRAY,
items: {
linkType: 'Entry',
type: ComponentDefinitionVariableArrayItemType.LINK,
},
},
elementsSymbol: {
type: ComponentDefinitionVariableType.ARRAY,
items: {
type: ComponentDefinitionVariableArrayItemType.SYMBOL,
},
},
elementsComponent: {
type: ComponentDefinitionVariableType.ARRAY,
items: {
type: ComponentDefinitionVariableArrayItemType.COMPONENT,
},
},
},
})
return <div data-test-id="test">Test</div>
}
37 changes: 19 additions & 18 deletions src/blocks/VisualEditorBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,13 @@ export const VisualEditorBlock = ({
boundData,
}: VisualEditorBlockProps) => {
const { sendMessage } = useCommunication()
const { getComponentDefinition } = useComponents()
const { getComponent } = useComponents()
const { onComponentDropped } = useInteraction()
const wasMousePressed = useRef(false)

const blockType = node.data.blockId.split(':')[0]

const blockConfiguration = useMemo(
() => getComponentDefinition(blockType),
[blockType, getComponentDefinition]
)
const definedComponent = useMemo(() => getComponent(blockType), [blockType, getComponent])

const { nodeBinding = {}, nodeBoundProps = {} } = useMemo(() => {
// plain node, not a child of a template
Expand Down Expand Up @@ -88,19 +85,23 @@ export const VisualEditorBlock = ({
}, [template, binding, node, boundData])

const props = useMemo(() => {
if (!blockConfiguration) {
if (!definedComponent) {
return {}
}

return blockConfiguration.componentDefinition.variables.reduce((acc, variable) => {
const boundValue = nodeBoundProps ? nodeBoundProps[variable.name]?.value : undefined
return Object.entries(definedComponent.componentDefinition.variables).reduce(
(acc, [variableName, variableDefinition]) => {
const boundValue = nodeBoundProps ? nodeBoundProps[variableName]?.value : undefined

return {
...acc,
[variable.name]: boundValue || node.data.props[variable.name] || variable.defaultValue,
}
}, {})
}, [blockConfiguration, node.data.props, nodeBoundProps])
return {
...acc,
[variableName]:
boundValue || node.data.props[variableName] || variableDefinition.defaultValue,
}
},
{}
)
}, [definedComponent, node.data.props, nodeBoundProps])

if (node.type === 'template') {
return (
Expand All @@ -113,11 +114,11 @@ export const VisualEditorBlock = ({
)
}

if (!blockConfiguration) {
if (!definedComponent) {
return null
}

const { component, componentDefinition } = blockConfiguration
const { component, componentDefinition } = definedComponent

const children = node.children.map((childNode: any) => {
if (childNode.type === 'string') {
Expand Down Expand Up @@ -168,8 +169,8 @@ export const VisualEditorBlock = ({
},
className: cx(
styles.hover,
componentDefinition.container && !children?.length ? styles.emptyContainer : undefined,
componentDefinition.container ? styles.container : undefined
componentDefinition.children && !children?.length ? styles.emptyContainer : undefined,
componentDefinition.children ? styles.container : undefined
),
...props,
},
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const CONTENTFUL_WEB_APP_ORIGIN = 'https://app.contentful.com'
4 changes: 2 additions & 2 deletions src/hooks/useComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ export const useComponents = () => {
[sendMessage]
)

const getComponentDefinition = useCallback((id: string) => {
const getComponent = useCallback((id: string) => {
return registeredComponentDefinitions.find(
(definition) => definition.componentDefinition.id === id
)
}, [])

return {
defineComponent,
getComponentDefinition,
getComponent,
}
}
16 changes: 12 additions & 4 deletions src/hooks/useExperienceBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import throttle from 'lodash.throttle'
import type { PlainClientAPI } from 'contentful-management'
import { BindingMapByBlockId, BoundData } from '../types'
import { useCommunication } from './useCommunication'
import { CONTENTFUL_WEB_APP_ORIGIN } from '../constants'

type VisualEditorMessagePayload = {
source: string
Expand All @@ -14,6 +15,15 @@ type UseExperienceBuilderProps = {
cma: PlainClientAPI
}

const getAppOrigins = () => {
if (typeof process.env !== 'undefined') {
if (process.env?.REACT_APP_EXPERIENCE_BUILDER_ORIGIN) {
return [process.env.REACT_APP_EXPERIENCE_BUILDER_ORIGIN]
}
}
return [CONTENTFUL_WEB_APP_ORIGIN]
}

export const useExperienceBuilder = ({ cma }: UseExperienceBuilderProps) => {
const [tree, setTree] = useState({})
const [binding, setBinding] = useState<BindingMapByBlockId>({})
Expand All @@ -23,8 +33,8 @@ export const useExperienceBuilder = ({ cma }: UseExperienceBuilderProps) => {

useEffect(() => {
const onMessage = (event: MessageEvent) => {
// where the app is contentful hosted when run locally
if (event.origin !== 'http://localhost:3001') {
// makes sure that the message originates from contentful web app
if (!getAppOrigins().includes(event.origin)) {
return
}

Expand All @@ -44,7 +54,6 @@ export const useExperienceBuilder = ({ cma }: UseExperienceBuilderProps) => {

switch (eventData.eventType) {
case 'componentDropped': {
console.log('component dropped', payload)
break
}
case 'componentTreeUpdated': {
Expand All @@ -56,7 +65,6 @@ export const useExperienceBuilder = ({ cma }: UseExperienceBuilderProps) => {
case 'valueChanged': {
const { boundData = {}, binding = {} } = payload
setBinding(binding)
console.log('setting stuff', boundData)
setBoundData(boundData)
break
}
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { VisualEditorRoot } from './blocks'
export { useExperienceBuilder, useComponents as useExperienceBuilderComponents } from './hooks'
export type { ComponentDefinition, ComponentDefinitionVariable } from './types'
export { ComponentDefinitionVariableType, ComponentDefinitionVariableArrayItemType } from './types'
83 changes: 73 additions & 10 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,80 @@
export type ComponentDefinitionVariables = {
name: string
dataType: string
defaultValue?: string | boolean
options?: string[]
export enum ComponentDefinitionVariableType {
TEXT = 'Text',
NUMBER = 'Number',
DATE = 'Date',
BOOLEAN = 'Boolean',
LOCATION = 'Location',
LINK = 'Link',
ARRAY = 'Array',
}

export enum ComponentDefinitionVariableArrayItemType {
LINK = 'Link',
SYMBOL = 'Symbol',
COMPONENT = 'Component',
}

export type ComponentDefinitionVariableValidation = {
required?: boolean
childNode?: boolean
}

export type ComponentDefinition = {
export interface ComponentDefinitionVariableBase<T extends ComponentDefinitionVariableType> {
type: T
validations?: ComponentDefinitionVariableValidation
isStyle?: boolean
description?: string
defaultValue?: string | boolean | number
}

export interface ComponentDefinitionVariableLink
extends ComponentDefinitionVariableBase<ComponentDefinitionVariableType.LINK> {
linkType: 'Entry' | 'Asset'
}

export interface ComponentDefinitionVariableArrayOfEntityLinks
extends ComponentDefinitionVariableBase<ComponentDefinitionVariableType.ARRAY> {
items: {
type: ComponentDefinitionVariableArrayItemType.LINK
linkType: 'Entry' | 'Asset'
}
}

export interface ComponentDefinitionVariableArrayOfPrimitives
extends ComponentDefinitionVariableBase<ComponentDefinitionVariableType.ARRAY> {
type: ComponentDefinitionVariableType.ARRAY
}

export interface ComponentDefinitionVariableArrayOfComponents {
type: ComponentDefinitionVariableType.ARRAY
items: {
type: ComponentDefinitionVariableArrayItemType.COMPONENT
}
}

export type ComponentDefinitionVariableArray<
K extends ComponentDefinitionVariableArrayItemType = ComponentDefinitionVariableArrayItemType
> = K extends ComponentDefinitionVariableArrayItemType.LINK
? ComponentDefinitionVariableArrayOfEntityLinks
: ComponentDefinitionVariableArrayOfPrimitives

export type ComponentDefinitionVariable<
T extends ComponentDefinitionVariableType,
K extends ComponentDefinitionVariableArrayItemType = ComponentDefinitionVariableArrayItemType
> = T extends ComponentDefinitionVariableType.LINK
? ComponentDefinitionVariableLink
: T extends ComponentDefinitionVariableType.ARRAY
? { items: { type: K } } & ComponentDefinitionVariableArray<K>
: ComponentDefinitionVariableBase<T>

export type ComponentDefinition<
T extends ComponentDefinitionVariableType = ComponentDefinitionVariableType
> = {
id: string
container: boolean
category: string
variables: ComponentDefinitionVariables[]
name: string
category?: string
thumbnailUrl?: string
variables: Record<string, { type: T } & ComponentDefinitionVariable<T>>
children?: boolean
}

export type Binding = {
Expand Down

0 comments on commit 836ca04

Please sign in to comment.