Skip to content

Commit

Permalink
Add the ability for consumers to render custom components (#86)
Browse files Browse the repository at this point in the history
* Add the ability for consumers to render custom components
  • Loading branch information
James Woo authored Jul 19, 2021
1 parent 526f863 commit 34c4142
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 77 deletions.
63 changes: 33 additions & 30 deletions packages/react/src/host/RemoteComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,41 @@
import {memo, useMemo} from 'react';
import type {ComponentType} from 'react';
import {
KIND_COMPONENT,
KIND_TEXT,
isRemoteReceiverAttachableFragment,
} from '@remote-ui/core';
import type {
RemoteReceiver,
RemoteReceiverAttachableComponent,
RemoteReceiverAttachableFragment,
RemoteReceiverAttachableChild,
} from '@remote-ui/core';

import type {Controller} from './controller';
import {RemoteText} from './RemoteText';
import {useAttached} from './hooks';
import type {
Controller,
RemoteComponentProps,
RemoteFragmentProps,
} from './types';

interface RemoteFragmentProps {
receiver: RemoteReceiver;
fragment: RemoteReceiverAttachableFragment;
controller: Controller;
}
const emptyObject = {};

interface Props {
receiver: RemoteReceiver;
component: RemoteReceiverAttachableComponent;
controller: Controller;
// Type override allows components to bypass default wrapping behavior, specifically in Argo Admin which uses Polaris to render on the host. Ex: Stack, ResourceList...
// See https://github.com/Shopify/app-extension-libs/issues/996#issuecomment-710437088
__type__?: ComponentType;
export function renderComponent({
component,
controller,
receiver,
key,
}: RemoteComponentProps) {
return (
<RemoteComponent
receiver={receiver}
component={component}
controller={controller}
key={key}
/>
);
}

const emptyObject = {};

export const RemoteComponent = memo(
({receiver, component, controller}: Props) => {
({receiver, component, controller}: RemoteComponentProps) => {
const Implementation = controller.get(component.type)!;

const attached = useAttached(receiver, component);
Expand Down Expand Up @@ -84,20 +85,22 @@ function renderChildren(
receiver: RemoteReceiver,
controller: Controller,
) {
const {renderComponent, renderText} = controller.renderer;
return [...children].map((child) => {
switch (child.kind) {
case KIND_COMPONENT:
return (
<RemoteComponent
key={child.id}
receiver={receiver}
component={child}
controller={controller}
__type__={(controller.get(child.type) as any)?.__type__}
/>
);
return renderComponent({
component: child,
receiver,
controller,
key: child.id,
});
case KIND_TEXT:
return <RemoteText key={child.id} text={child} receiver={receiver} />;
return renderText({
text: child,
receiver,
key: child.id,
});
default:
return null;
}
Expand Down
62 changes: 31 additions & 31 deletions packages/react/src/host/RemoteRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
import {memo} from 'react';
import {KIND_COMPONENT, KIND_TEXT, RemoteReceiver} from '@remote-ui/core';

import type {Controller} from './controller';
import type {Controller} from './types';
import {useAttached} from './hooks';
import {RemoteText} from './RemoteText';
import {RemoteComponent} from './RemoteComponent';

interface Props {
export interface RemoteRendererProps {
receiver: RemoteReceiver;
controller: Controller;
}

export const RemoteRenderer = memo(({controller, receiver}: Props) => {
const {children} = useAttached(receiver, receiver.attached.root)!;
export const RemoteRenderer = memo(
({controller, receiver}: RemoteRendererProps) => {
const {children} = useAttached(receiver, receiver.attached.root)!;
const {renderComponent, renderText} = controller.renderer;

return (
<>
{children.map((child) => {
switch (child.kind) {
case KIND_COMPONENT:
return (
<RemoteComponent
key={child.id}
component={child}
receiver={receiver}
controller={controller}
__type__={(controller.get(child.type) as any)?.__type__}
/>
);
case KIND_TEXT:
return (
<RemoteText key={child.id} text={child} receiver={receiver} />
);
default:
return null;
}
})}
</>
);
});
return (
<>
{children.map((child) => {
switch (child.kind) {
case KIND_COMPONENT:
return renderComponent({
component: child,
receiver,
controller,
key: child.id,
});
case KIND_TEXT:
return renderText({
text: child,
receiver,
key: child.id,
});
default:
return null;
}
})}
</>
);
},
);
12 changes: 4 additions & 8 deletions packages/react/src/host/RemoteText.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import {memo} from 'react';
import type {
RemoteReceiver,
RemoteReceiverAttachableText,
} from '@remote-ui/core';

import type {RemoteTextProps} from './types';
import {useAttached} from './hooks';

interface Props {
text: RemoteReceiverAttachableText;
receiver: RemoteReceiver;
export function renderText({text, receiver, key}: RemoteTextProps) {
return <RemoteText key={key} text={text} receiver={receiver} />;
}

export const RemoteText = memo(({text, receiver}: Props) => {
export const RemoteText = memo(({text, receiver}: RemoteTextProps) => {
const attached = useAttached(receiver, text);
return attached ? <>{attached.text}</> : null;
});
2 changes: 1 addition & 1 deletion packages/react/src/host/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {createContext} from 'react';

export const ControllerContext = createContext<
import('./controller').Controller | null
import('./types').Controller | null
>(null);

export const RemoteReceiverContext = createContext<
Expand Down
50 changes: 45 additions & 5 deletions packages/react/src/host/controller.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,52 @@
import type {ComponentType} from 'react';
import type {RemoteComponentType} from '@remote-ui/core';
import type {ComponentType, ReactNode} from 'react';
import type {
Controller,
RemoteComponentProps,
RemoteTextProps,
RenderComponentOptions,
Renderer,
RenderTextOptions,
} from './types';

import {renderComponent as defaultRenderComponent} from './RemoteComponent';
import {renderText as defaultRenderText} from './RemoteText';

export interface ComponentMapping {
[key: string]: ComponentType<any>;
}

export interface Controller {
get(type: string | RemoteComponentType<string, any, any>): ComponentType<any>;
interface RendererFactory {
renderComponent(
props: RemoteComponentProps,
options: RenderComponentOptions,
): ReactNode;
renderText(props: RemoteTextProps, options: RenderTextOptions): ReactNode;
}

export function createController(components: ComponentMapping): Controller {
export function createController(
components: ComponentMapping,
{
renderComponent: externalRenderComponent,
renderText: externalRenderText,
}: Partial<RendererFactory> = {},
): Controller {
const registry = new Map(Object.entries(components));
const renderComponent: Renderer['renderComponent'] = externalRenderComponent
? (componentProps) =>
externalRenderComponent(componentProps, {
renderDefault() {
return defaultRenderComponent(componentProps);
},
})
: defaultRenderComponent;
const renderText: Renderer['renderText'] = externalRenderText
? (textProps) =>
externalRenderText(textProps, {
renderDefault() {
return defaultRenderText(textProps);
},
})
: defaultRenderText;

return {
get(type) {
Expand All @@ -20,5 +56,9 @@ export function createController(components: ComponentMapping): Controller {
}
return value;
},
renderer: {
renderComponent,
renderText,
},
};
}
8 changes: 6 additions & 2 deletions packages/react/src/host/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ export type {RemoteReceiver} from '@remote-ui/core';
export {createRemoteReceiver} from '@remote-ui/core';

export {RemoteRenderer} from './RemoteRenderer';
export type {RemoteRendererProps} from './RemoteRenderer';
export {RemoteComponent} from './RemoteComponent';
export {RemoteText} from './RemoteText';
export {createController} from './controller';
export type {Controller, ComponentMapping} from './controller';
export type {ComponentMapping} from './controller';
export type {
ReactPropsFromRemoteComponentType,
ReactComponentTypeFromRemoteComponentType,
} from '../types';
export {RemoteReceiverContext, ControllerContext} from './context';
export {useRemoteReceiver} from './hooks';
export {useRemoteReceiver, useAttached} from './hooks';
export {useWorker} from './workers';
export type {Controller, RemoteComponentProps, RemoteTextProps} from './types';
45 changes: 45 additions & 0 deletions packages/react/src/host/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type {ComponentType, ReactNode} from 'react';
import type {
RemoteReceiver,
RemoteReceiverAttachableComponent,
RemoteReceiverAttachableText,
RemoteComponentType,
RemoteReceiverAttachableFragment,
} from '@remote-ui/core';

export interface RemoteTextProps {
text: RemoteReceiverAttachableText;
receiver: RemoteReceiver;
key: string | number;
}

export interface RemoteComponentProps {
receiver: RemoteReceiver;
component: RemoteReceiverAttachableComponent;
controller: Controller;
key: string | number;
}

export interface RemoteFragmentProps {
receiver: RemoteReceiver;
fragment: RemoteReceiverAttachableFragment;
controller: Controller;
}

export interface Controller {
get(type: string | RemoteComponentType<string, any, any>): ComponentType<any>;
renderer: Renderer;
}

export interface Renderer {
renderComponent(props: RemoteComponentProps): ReactNode;
renderText(props: RemoteTextProps): ReactNode;
}

export interface RenderComponentOptions {
renderDefault(): ReactNode;
}

export interface RenderTextOptions {
renderDefault(): ReactNode;
}

0 comments on commit 34c4142

Please sign in to comment.