Skip to content

Commit

Permalink
registerComponent uses the component instance itself (#4865)
Browse files Browse the repository at this point in the history
* Better registerComponent

* Remove outdated test

* Fix circular dependency

* Added test for registerComponent

* Remove commented code

* Simplify test

* Add test for external component

* Remove unnecessary code line
  • Loading branch information
gbalint authored Feb 12, 2024
1 parent 3301c74 commit 8873ae6
Show file tree
Hide file tree
Showing 11 changed files with 250 additions and 170 deletions.
10 changes: 3 additions & 7 deletions editor/src/components/canvas/remix/remix-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import type { CurriedResolveFn, CurriedUtopiaRequireFn } from '../../custom-code
import type { MapLike } from 'typescript'
import type { UiJsxCanvasContextData } from '../ui-jsx-canvas'
import { attemptToResolveParsedComponents } from '../ui-jsx-canvas'
import type { ComponentRendererComponent } from '../ui-jsx-canvas-renderer/ui-jsx-canvas-component-renderer'
import type { ComponentRendererComponent } from '../ui-jsx-canvas-renderer/component-renderer-component'
import type { MutableUtopiaCtxRefData } from '../ui-jsx-canvas-renderer/ui-jsx-canvas-contexts'
import type { ElementPath, TextFile } from '../../../core/shared/project-file-types'
import type { ExecutionScope } from '../ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope'
import { createExecutionScope } from '../ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope'
import type { RemixRoutingTable } from '../../editor/store/remix-derived-data'
import { NO_OP, identity } from '../../../core/shared/utils'
import { NO_OP } from '../../../core/shared/utils'
import * as EP from '../../../core/shared/element-path'
import {
fileExportsFunctionWithName,
Expand All @@ -35,11 +35,7 @@ import { foldEither, forEachRight, left } from '../../../core/shared/either'
import type { CanvasBase64Blobs } from '../../editor/store/editor-state'
import { findPathToJSXElementChild } from '../../../core/model/element-template-utils'
import { MetadataUtils } from '../../../core/model/element-metadata-utils'
import type { ElementInstanceMetadata } from '../../../core/shared/element-template'
import {
getJSXAttribute,
type ElementInstanceMetadataMap,
} from '../../../core/shared/element-template'
import { type ElementInstanceMetadataMap } from '../../../core/shared/element-template'
import type { ElementPathTrees } from '../../../core/shared/element-path-tree'
import { getAllUniqueUids } from '../../../core/model/get-unique-ids'
import { safeIndex } from '../../../core/shared/array-utils'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { PropertyControls } from 'utopia-api/core'
import type { UTOPIA_INSTANCE_PATH, UTOPIA_PATH_KEY } from '../../../core/model/utopia-constants'
import type { ElementPath } from '../../../core/shared/project-file-types'

export type ComponentRendererComponent = React.ComponentType<
React.PropsWithChildren<{
[UTOPIA_INSTANCE_PATH]: ElementPath
[UTOPIA_PATH_KEY]?: string
}>
> & {
topLevelElementName: string | null
propertyControls?: PropertyControls
utopiaType: 'UTOPIA_COMPONENT_RENDERER_COMPONENT'
filePath: string
originalName: string | null
}

export function isComponentRendererComponent(
component:
| ComponentRendererComponent
| React.ComponentType<React.PropsWithChildren<unknown>>
| null
| undefined,
): component is ComponentRendererComponent {
return (
component != null &&
typeof component === 'function' &&
(component as ComponentRendererComponent).utopiaType === 'UTOPIA_COMPONENT_RENDERER_COMPONENT'
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react'
import type { MapLike } from 'typescript'
import type { PropertyControls } from 'utopia-api/core'
import type { JSXElementChild, UtopiaJSXComponent } from '../../../core/shared/element-template'
import {
isUtopiaJSXComponent,
Expand Down Expand Up @@ -28,41 +27,15 @@ import {
renderCoreElement,
utopiaCanvasJSXLookup,
} from './ui-jsx-canvas-element-renderer-utils'
import { useContextSelector } from 'use-context-selector'
import type { ElementPath } from '../../../core/shared/project-file-types'
import { UTOPIA_INSTANCE_PATH, UTOPIA_PATH_KEY } from '../../../core/model/utopia-constants'
import { getPathsFromString, getUtopiaID } from '../../../core/shared/uid-utils'
import { useGetTopLevelElementsAndImports } from './ui-jsx-canvas-top-level-elements'
import { useGetCodeAndHighlightBounds } from './ui-jsx-canvas-execution-scope'
import { usePubSubAtomReadOnly } from '../../../core/shared/atom-with-pub-sub'
import { JSX_CANVAS_LOOKUP_FUNCTION_NAME } from '../../../core/shared/dom-utils'
import { isFeatureEnabled } from '../../../utils/feature-switches'
import { objectMap } from '../../../core/shared/object-utils'

export type ComponentRendererComponent = React.ComponentType<
React.PropsWithChildren<{
[UTOPIA_INSTANCE_PATH]: ElementPath
[UTOPIA_PATH_KEY]?: string
}>
> & {
topLevelElementName: string | null
propertyControls?: PropertyControls
utopiaType: 'UTOPIA_COMPONENT_RENDERER_COMPONENT'
}

export function isComponentRendererComponent(
component:
| ComponentRendererComponent
| React.ComponentType<React.PropsWithChildren<unknown>>
| null
| undefined,
): component is ComponentRendererComponent {
return (
component != null &&
typeof component === 'function' &&
(component as ComponentRendererComponent).utopiaType === 'UTOPIA_COMPONENT_RENDERER_COMPONENT'
)
}
import type { ComponentRendererComponent } from './component-renderer-component'

function tryToGetInstancePath(
maybePath: ElementPath | null,
Expand Down Expand Up @@ -287,6 +260,8 @@ export function createComponentRendererComponent(params: {
Component.displayName = `ComponentRenderer(${params.topLevelElementName})`
Component.topLevelElementName = params.topLevelElementName
Component.utopiaType = 'UTOPIA_COMPONENT_RENDERER_COMPONENT' as const
Component.filePath = params.filePath
Component.originalName = params.topLevelElementName
return Component
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import {
clearOpposingConditionalSpyValues,
} from './ui-jsx-canvas-spy-wrapper'
import { getUtopiaID } from '../../../core/shared/uid-utils'
import { isComponentRendererComponent } from './ui-jsx-canvas-component-renderer'
import { isComponentRendererComponent } from './component-renderer-component'
import { optionalMap } from '../../../core/shared/optional-utils'
import { canvasMissingJSXElementError } from './canvas-render-errors'
import { importedFromWhere } from '../../editor/import-utils'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getProjectFileByFilePath, ProjectContentsTree } from '../../assets'
import { importResultFromImports } from '../../editor/npm-dependency/npm-dependency'
import type { CanvasBase64Blobs, UIFileBase64Blobs } from '../../editor/store/editor-state'
import { TransientFilesState, TransientFileState } from '../../editor/store/editor-state'
import type { ComponentRendererComponent } from './ui-jsx-canvas-component-renderer'
import type { ComponentRendererComponent } from './component-renderer-component'
import { createComponentRendererComponent } from './ui-jsx-canvas-component-renderer'
import type { MutableUtopiaCtxRefData } from './ui-jsx-canvas-contexts'
import {
Expand Down
2 changes: 1 addition & 1 deletion editor/src/components/canvas/ui-jsx-canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import { normalizeName } from '../custom-code/custom-code-utils'
import { getGeneratedExternalLinkText } from '../../printer-parsers/html/external-resources-parser'
import { Helmet } from 'react-helmet'
import parse from 'html-react-parser'
import type { ComponentRendererComponent } from './ui-jsx-canvas-renderer/ui-jsx-canvas-component-renderer'
import type { ComponentRendererComponent } from './ui-jsx-canvas-renderer/component-renderer-component'
import type { MutableUtopiaCtxRefData } from './ui-jsx-canvas-renderer/ui-jsx-canvas-contexts'
import {
RerenderUtopiaCtxAtom,
Expand Down
2 changes: 1 addition & 1 deletion editor/src/components/editor/store/remix-derived-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
} from '@remix-run/react'
import type { MutableUtopiaCtxRefData } from '../../canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-contexts'
import type { MapLike } from 'typescript'
import type { ComponentRendererComponent } from '../../canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-component-renderer'
import type { ComponentRendererComponent } from '../../canvas/ui-jsx-canvas-renderer/component-renderer-component'
import type { DataRouteObject } from 'react-router'
import {
getProjectFileByFilePath,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { within } from '@testing-library/react'
import * as EP from '../../../../core/shared/element-path'
import { selectComponentsForTest, wait } from '../../../../utils/utils.test-utils'
import { mouseClickAtPoint } from '../../../canvas/event-helpers.test-utils'
import { mouseClickAtPoint, pressKey } from '../../../canvas/event-helpers.test-utils'
import { renderTestEditorWithCode } from '../../../canvas/ui-jsx.test-utils'
import {
DataPickerPopupButtonTestId,
Expand Down Expand Up @@ -35,6 +35,44 @@ describe('Set element prop via the data picker', () => {
})
})

describe('Controls from registerComponent', () => {
it('registering internal component', async () => {
const editor = await renderTestEditorWithCode(
registerInternalComponentProject,
'await-first-dom-report',
)
await selectComponentsForTest(editor, [EP.fromString('sb/scene/pg:root/title')])

const dataPickerOpenerButton = editor.renderedDOM.getByTestId(
`text-string-input-property-control`,
)
dataPickerOpenerButton.focus()
document.execCommand('insertText', false, 'New title')
await pressKey('Enter', { targetElement: dataPickerOpenerButton })

const theScene = editor.renderedDOM.getByTestId('scene')
expect(within(theScene).queryByText('New title')).not.toBeNull()
})

it('registering external component', async () => {
const editor = await renderTestEditorWithCode(
registerExternalComponentProject,
'await-first-dom-report',
)
await selectComponentsForTest(editor, [EP.fromString('sb/scene/pg:root/title')])

const dataPickerOpenerButton = editor.renderedDOM.getByTestId(
`sampleprop-string-input-property-control`,
)
dataPickerOpenerButton.focus()
document.execCommand('insertText', false, 'New props value')
await pressKey('Enter', { targetElement: dataPickerOpenerButton })

const theView = editor.renderedDOM.getByTestId('view')
expect(theView.outerHTML).toContain('sampleprop="New props value"')
})
})

const project = `import * as React from 'react'
import { Storyboard, Scene } from 'utopia-api'
Expand Down Expand Up @@ -88,3 +126,142 @@ export var storyboard = (
</Scene>
</Storyboard>
)`

const registerInternalComponentProject = `import * as React from 'react'
import {
Storyboard,
Scene,
registerComponent,
} from 'utopia-api'
function Title({ text }) {
return <h2 data-uid='0cd'>{text}</h2>
}
var Playground = ({ style }) => {
return (
<div style={style} data-uid='root'>
<Title text='Hello Utopia' data-uid='title' />
</div>
)
}
export var storyboard = (
<Storyboard data-uid='sb'>
<Scene
style={{
width: 521,
height: 266,
position: 'absolute',
left: 554,
top: 247,
backgroundColor: 'white',
}}
data-uid='scene'
data-testid='scene'
commentId='120'
>
<Playground
style={{
width: 454,
height: 177,
position: 'absolute',
left: 34,
top: 44,
backgroundColor: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
title='Hello Utopia'
data-uid='pg'
/>
</Scene>
</Storyboard>
)
registerComponent(Title, {
supportsChildren: false,
properties: {
text: {
control: 'string-input',
},
},
variants: [
{
code: '<Title />',
},
],
})
`

const registerExternalComponentProject = `import * as React from 'react'
import {
Storyboard,
Scene,
View,
registerComponent,
} from 'utopia-api'
var Playground = ({ style }) => {
return (
<div style={style} data-uid='root' data-testid='view'>
<View sampleprop='Hello Utopia' data-uid='title'>
Hello Utopia
</View>
</div>
)
}
export var storyboard = (
<Storyboard data-uid='sb'>
<Scene
style={{
width: 521,
height: 266,
position: 'absolute',
left: 554,
top: 247,
backgroundColor: 'white',
}}
data-uid='scene'
data-testid='scene'
commentId='120'
>
<Playground
style={{
width: 454,
height: 177,
position: 'absolute',
left: 34,
top: 44,
backgroundColor: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
title='Hello Utopia'
data-uid='pg'
/>
</Scene>
</Storyboard>
)
registerComponent(
View,
{
supportsChildren: false,
properties: {
sampleprop: {
control: 'string-input',
},
},
variants: [
{
code: '<View />',
},
],
},
'utopia-api',
)`
Loading

0 comments on commit 8873ae6

Please sign in to comment.