Skip to content

Commit

Permalink
feat: support for multiple handlers in a single feature
Browse files Browse the repository at this point in the history
Different handlers are disambiguated with an optional argument to
`FeatureBuilder.withHandlerProp` method.

BREAKING CHANGE: renamed `FeatureBuilder.withHandlerEventProp` to
`withHandlerProp` to prepare for bilateral communication.
  • Loading branch information
jsamr committed Sep 29, 2020
1 parent b9e0d14 commit 9313f07
Show file tree
Hide file tree
Showing 14 changed files with 89 additions and 25 deletions.
2 changes: 1 addition & 1 deletion apps/website/docs/implementing-features.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Below is a recap of all the symbols present in this snippet:
It is also worth noting that:

- `linkPressScript` is imported as a string thanks to babel-plugin-inline-import, see the [tooling guide](./tooling);
- invoking `withEventHandlerProp` is not an option:
- invoking `withHandlerProp` is not an option:
1. It register the handler to the shell, otherwise events fired by the below Web script will not be routed to their handler props.
2. When implemented in typescript, type generation will add the handler name and payload type to the the feature, which will be grabbed by the resulting shell type.

Expand Down
2 changes: 1 addition & 1 deletion packages/acceptance-tests/src/HandleLinkPressFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const HandleLinkPressFeature = new FeatureBuilder({
script: linkPressScript,
featureIdentifier: 'org.myorg/webshell.link-press'
})
.withEventHandlerProp<LinkPressTarget, 'onDOMLinkPress'>(
.withandlerProp<LinkPressTarget, 'onDOMLinkPress'>(
'onDOMLinkPress'
)
.build();
4 changes: 3 additions & 1 deletion packages/core/etc/webshell.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export abstract class Feature<O extends {}, S extends PropsSpecs<any> = []> impl
export class FeatureBuilder<O extends {}, S extends PropsSpecs<any> = []> {
constructor(config: FeatureBuilderConfig<O, S>);
build(): FeatureConstructor<O, S>;
withEventHandlerProp<P, H extends string>(eventHandlerName: H): FeatureBuilder<O, S[number] extends never ? [PropDefinition<{ [k in H]?: ((p: P) => void) | undefined; }>] : [PropDefinition<{ [k_1 in H]?: ((p: P) => void) | undefined; }>, ...S[number][]]>;
withandlerProp<P, H extends string>(eventHandlerName: H, handlerId?: string): FeatureBuilder<O, S[number] extends never ? [PropDefinition<{ [k in H]?: ((p: P) => void) | undefined; }>] : [PropDefinition<{ [k_1 in H]?: ((p: P) => void) | undefined; }>, ...S[number][]]>;
}

// @public
Expand Down Expand Up @@ -306,6 +306,7 @@ export interface MinimalWebViewProps {

// @public (undocumented)
export type PropDefinition<P extends Partial<Record<string, any>>> = {
handlerId: string;
type: 'handler' | 'inert';
featureIdentifier: string;
name: string;
Expand Down Expand Up @@ -342,6 +343,7 @@ export interface WebjsContext<O extends {}, P> {
numericFromPxString(style: string): number;
readonly options: O;
postShellMessage(payload: P): void;
postShellMessage(handlerId: string, payload: P): void;
warn(message: string): void;
}

Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/FeatureBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,16 @@ export class FeatureBuilder<O extends {}, S extends PropsSpecs<any> = []> {
* @param eventHandlerName - The name of the handler prop added to the shell.
* It is advised to follow the convention of prefixing all these handlers
* with `onDom` to avoid collisions with `WebView` own props.
*
* @param handlerId - The unique identifier of the handler that will be used by the Web
* script to post a message. If none is provided, fallback to `"default"`.
*/
withEventHandlerProp<P, H extends string>(eventHandlerName: H) {
withandlerProp<P, H extends string>(
eventHandlerName: H,
handlerId: string = 'default'
) {
const propDefinition: PropDefinition<{ [k in H]?: (p: P) => void }> = {
handlerId,
name: eventHandlerName,
featureIdentifier: this.config.featureIdentifier,
type: 'handler'
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/__tests__/feat/dummy-handlerid.webjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
function dummyHandlerIdFeature(arg) {
var postShellMessage = arg.postShellMessage;
postShellMessage('hi', 'Hello world!');
}
28 changes: 25 additions & 3 deletions packages/core/src/__tests__/make-webshell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { View } from 'react-native';
import dummyHelloScript from './feat/dummy-hello.webjs';
import dummyFailingScript from './feat/dummy-failing.webjs';
import dummyOptionScript from './feat/dummy-option.webjs';
import dummyHandleridScript from './feat/dummy-handlerid.webjs';
import { makeWebshell } from '../make-webshell';
import { FeatureBuilder } from '../FeatureBuilder';
import { MinimalWebViewProps } from '../types';
Expand All @@ -19,23 +20,31 @@ const HelloFeature = new FeatureBuilder({
featureIdentifier: 'test.hello',
defaultOptions: {}
})
.withEventHandlerProp('onDOMDummyHello')
.withandlerProp('onDOMDummyHello')
.build();

const FailingFeature = new FeatureBuilder({
script: dummyFailingScript,
featureIdentifier: 'test.fail',
defaultOptions: {}
})
.withEventHandlerProp('onDOMDummyFailure')
.withandlerProp('onDOMDummyFailure')
.build();

const OptionFeature = new FeatureBuilder({
script: dummyOptionScript,
featureIdentifier: 'test.option',
defaultOptions: {}
})
.withEventHandlerProp<{ foo: string }, 'onDOMDummyOption'>('onDOMDummyOption')
.withandlerProp<{ foo: string }, 'onDOMDummyOption'>('onDOMDummyOption')
.build();

const HandlerIdFeature = new FeatureBuilder({
script: dummyHandleridScript,
featureIdentifier: 'test.handlerid',
defaultOptions: {}
})
.withandlerProp('onDOMDummyOption', 'hi')
.build();

describe('Webshell component', () => {
Expand Down Expand Up @@ -83,6 +92,19 @@ describe('Webshell component', () => {
);
expect(onDOMDummyOption).toHaveBeenCalledWith({ foo: 'bar' });
});
it('should disambiguate between handlerIds', async () => {
const onHandlerIdDummyOption = jest.fn();
const Webshell = makeWebshell(Ersatz, new HandlerIdFeature());
await waitForErsatz(
render(
<Webshell
webshellDebug={false}
onDOMDummyOption={onHandlerIdDummyOption}
/>
)
);
expect(onHandlerIdDummyOption).toHaveBeenCalledWith('Hello world!');
});
it('should keep support for onMessage and injectedJavaScript', async () => {
const onDOMDummyOption = jest.fn();
const onMessage = jest.fn();
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/features-loader.webjs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ function registerFeature(specs) {
},
numericFromPxString: numericFromPxString,
makeCallbackSafe: makeCallbackSafe.bind(null, sendErrorMessage),
postShellMessage: function (message) {
postShellMessage: function () {
var message = arguments.length > 1 ? arguments[1] : arguments[0];
var handlerId = arguments.length > 1 ? arguments[0] : 'default';
if (handlerName == null) {
sendErrorMessage(
new TypeError(
Expand All @@ -124,6 +126,7 @@ function registerFeature(specs) {
identifier: snippetIdentifier,
body: message,
handlerName: handlerName,
handlerId: handlerId,
__isWebshellPostMessage: true
})
);
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/features/HandleElementCSSBoxFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,7 @@ export const HandleElementCSSBoxFeature: FeatureConstructor<
featureIdentifier:
'org.formidable-webview/webshell.handle-element-cssbox-dimensions'
})
.withEventHandlerProp<
ElementCSSBoxDimensions,
.withandlerProp<ElementCSSBoxDimensions, 'onDOMElementCSSBoxDimensions'>(
'onDOMElementCSSBoxDimensions'
>('onDOMElementCSSBoxDimensions')
)
.build();
4 changes: 1 addition & 3 deletions packages/core/src/features/HandleHTMLDimensionsFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,5 @@ export const HandleHTMLDimensionsFeature: FeatureConstructor<
defaultOptions,
featureIdentifier: 'org.formidable-webview/webshell.handle-html-dimensions'
})
.withEventHandlerProp<HTMLDimensions, 'onDOMHTMLDimensions'>(
'onDOMHTMLDimensions'
)
.withandlerProp<HTMLDimensions, 'onDOMHTMLDimensions'>('onDOMHTMLDimensions')
.build();
2 changes: 1 addition & 1 deletion packages/core/src/features/HandleHashChangeFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@ export const HandleHashChangeFeature: FeatureConstructor<
defaultOptions,
featureIdentifier: 'org.formidable-webview/webshell.handle-hash-change'
})
.withEventHandlerProp<HashChangeEvent, 'onDOMHashChange'>('onDOMHashChange')
.withandlerProp<HashChangeEvent, 'onDOMHashChange'>('onDOMHashChange')
.build();
2 changes: 1 addition & 1 deletion packages/core/src/features/HandleLinkPressFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,5 @@ export const HandleLinkPressFeature: FeatureConstructor<
defaultOptions,
featureIdentifier: 'org.formidable-webview/webshell.link-press'
})
.withEventHandlerProp<LinkPressTarget, 'onDOMLinkPress'>('onDOMLinkPress')
.withandlerProp<LinkPressTarget, 'onDOMLinkPress'>('onDOMLinkPress')
.build();
2 changes: 1 addition & 1 deletion packages/core/src/features/HandleVisualViewportFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const HandleVisualViewportFeature: FeatureConstructor<
defaultOptions: {},
featureIdentifier: 'org.formidable-webview/webshell.handle-visual-viewport'
})
.withEventHandlerProp<VisualViewportDimensions, 'onDOMVisualViewport'>(
.withandlerProp<VisualViewportDimensions, 'onDOMVisualViewport'>(
'onDOMVisualViewport'
)
.build();
31 changes: 25 additions & 6 deletions packages/core/src/make-webshell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface WebViewMessage {

interface PostMessage {
identifier: string;
handlerName: string;
handlerId: string;
type: 'feature' | 'error' | 'log';
severity: 'warn' | 'info';
body: any;
Expand Down Expand Up @@ -92,6 +92,23 @@ function filterWebViewProps<W>(
}, {} as W);
}

function getHandlerUUID(def: PropDefinition<any>) {
return `${def.featureIdentifier}:${def.handlerId}`;
}

function extractHandlersMap(features: Feature<any, PropsSpecs<any>>[]) {
return features
.map((f: Feature<any, PropsSpecs<any>>) => f.propSpecs)
.reduce((p, c) => [...p, ...c], [])
.reduce(
(map, spec: PropDefinition<any>) => ({
...map,
[getHandlerUUID(spec)]: spec
}),
{}
) as Record<string, PropDefinition<any>>;
}

function extractPropsSpecsMap(features: Feature<any, PropsSpecs<any>>[]) {
return features
.map((f: Feature<any, PropsSpecs<any>>) => f.propSpecs)
Expand Down Expand Up @@ -129,6 +146,7 @@ export function makeWebshell<
> {
const filteredFeatures = features.filter((f) => !!f);
const propsMap = extractPropsSpecsMap(filteredFeatures);
const handlersMap = extractHandlersMap(filteredFeatures);
const serializedFeatureScripts = serializeFeatureList(filteredFeatures);
const injectableScript = assembleScript(serializedFeatureScripts);
const Webshell = (
Expand All @@ -145,25 +163,26 @@ export function makeWebshell<
({ nativeEvent }: NativeSyntheticEvent<WebViewMessage>) => {
const parsedJSON = parseJSONSafe(nativeEvent.data);
if (isPostMessageObject(parsedJSON)) {
const { type, identifier, body, handlerName, severity } = parsedJSON;
const { type, identifier, body, handlerId, severity } = parsedJSON;
if (type === 'feature') {
const handlerName = handlersMap[`${identifier}:${handlerId}`].name;
const handler =
typeof handlerName === 'string' ? domHandlers[handlerName] : null;
typeof handlerId === 'string' ? domHandlers[handlerName] : null;
if (propsMap[handlerName]) {
if (typeof handler === 'function') {
handler(body);
} else {
webshellDebug &&
console.info(
`[Webshell]: script from feature "${identifier}" sent an event, but there ` +
`[Webshell]: script from feature "${identifier}" sent an event towards ${handlerId} handler, but there ` +
`is no handler prop named "${handlerName}" attached to the shell.`
);
}
} else {
console.warn(
`[Webshell]: script from feature "${identifier}" sent an event, but there is ` +
`[Webshell]: script from feature "${identifier}" sent an event towards ${handlerId} handler, but there is ` +
`no handler named "${handlerName}" defined for this feature. ` +
'Use FeatureBuilder.withEventHandlerProp to register that handler, or make ' +
'Use FeatureBuilder.withHandlerProp to register that handler, or make ' +
'sure its name is not misspell in the DOM script.'
);
}
Expand Down
14 changes: 12 additions & 2 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export type FeatureDefinition<O extends {}> = {
* @public
*/
export type PropDefinition<P extends Partial<Record<string, any>>> = {
handlerId: string;
type: 'handler' | 'inert';
featureIdentifier: string;
name: string;
Expand Down Expand Up @@ -168,12 +169,21 @@ export interface WebjsContext<O extends {}, P> {
*/
readonly options: O;
/**
* When invoked, the shell will call the handler associated with this
* script, if any.
* Instruct the shell to call **the default handler** associated with
* this feature, if any.
*
* @param payload - The value which will be passed to the handler.
*/
postShellMessage(payload: P): void;
/**
* Instruct the shell to call the handler associated with this
* feature and `eventId`, if any.
*
* @param handlerId - A unique string to disambiguate between different handlers.
* You can omit this param if you are sending to `"default"` handler.
* @param payload - The value which will be passed to the handler.
*/
postShellMessage(handlerId: string, payload: P): void;
/**
* Create a function which execute a callback in a try-catch block that will
* grab errors en send them to the `Webshell` component.
Expand Down

0 comments on commit 9313f07

Please sign in to comment.