Skip to content

Commit 9313f07

Browse files
committed
feat: support for multiple handlers in a single feature
Different handlers are disambiguated with an optional argument to `FeatureBuilder.withHandlerProp` method. BREAKING CHANGE: renamed `FeatureBuilder.withHandlerEventProp` to `withHandlerProp` to prepare for bilateral communication.
1 parent b9e0d14 commit 9313f07

14 files changed

+89
-25
lines changed

apps/website/docs/implementing-features.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Below is a recap of all the symbols present in this snippet:
7979
It is also worth noting that:
8080

8181
- `linkPressScript` is imported as a string thanks to babel-plugin-inline-import, see the [tooling guide](./tooling);
82-
- invoking `withEventHandlerProp` is not an option:
82+
- invoking `withHandlerProp` is not an option:
8383
1. It register the handler to the shell, otherwise events fired by the below Web script will not be routed to their handler props.
8484
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.
8585

packages/acceptance-tests/src/HandleLinkPressFeature.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const HandleLinkPressFeature = new FeatureBuilder({
1919
script: linkPressScript,
2020
featureIdentifier: 'org.myorg/webshell.link-press'
2121
})
22-
.withEventHandlerProp<LinkPressTarget, 'onDOMLinkPress'>(
22+
.withandlerProp<LinkPressTarget, 'onDOMLinkPress'>(
2323
'onDOMLinkPress'
2424
)
2525
.build();

packages/core/etc/webshell.api.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export abstract class Feature<O extends {}, S extends PropsSpecs<any> = []> impl
146146
export class FeatureBuilder<O extends {}, S extends PropsSpecs<any> = []> {
147147
constructor(config: FeatureBuilderConfig<O, S>);
148148
build(): FeatureConstructor<O, S>;
149-
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][]]>;
149+
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][]]>;
150150
}
151151

152152
// @public
@@ -306,6 +306,7 @@ export interface MinimalWebViewProps {
306306

307307
// @public (undocumented)
308308
export type PropDefinition<P extends Partial<Record<string, any>>> = {
309+
handlerId: string;
309310
type: 'handler' | 'inert';
310311
featureIdentifier: string;
311312
name: string;
@@ -342,6 +343,7 @@ export interface WebjsContext<O extends {}, P> {
342343
numericFromPxString(style: string): number;
343344
readonly options: O;
344345
postShellMessage(payload: P): void;
346+
postShellMessage(handlerId: string, payload: P): void;
345347
warn(message: string): void;
346348
}
347349

packages/core/src/FeatureBuilder.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,16 @@ export class FeatureBuilder<O extends {}, S extends PropsSpecs<any> = []> {
4040
* @param eventHandlerName - The name of the handler prop added to the shell.
4141
* It is advised to follow the convention of prefixing all these handlers
4242
* with `onDom` to avoid collisions with `WebView` own props.
43+
*
44+
* @param handlerId - The unique identifier of the handler that will be used by the Web
45+
* script to post a message. If none is provided, fallback to `"default"`.
4346
*/
44-
withEventHandlerProp<P, H extends string>(eventHandlerName: H) {
47+
withandlerProp<P, H extends string>(
48+
eventHandlerName: H,
49+
handlerId: string = 'default'
50+
) {
4551
const propDefinition: PropDefinition<{ [k in H]?: (p: P) => void }> = {
52+
handlerId,
4653
name: eventHandlerName,
4754
featureIdentifier: this.config.featureIdentifier,
4855
type: 'handler'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
function dummyHandlerIdFeature(arg) {
2+
var postShellMessage = arg.postShellMessage;
3+
postShellMessage('hi', 'Hello world!');
4+
}

packages/core/src/__tests__/make-webshell.test.tsx

+25-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { View } from 'react-native';
66
import dummyHelloScript from './feat/dummy-hello.webjs';
77
import dummyFailingScript from './feat/dummy-failing.webjs';
88
import dummyOptionScript from './feat/dummy-option.webjs';
9+
import dummyHandleridScript from './feat/dummy-handlerid.webjs';
910
import { makeWebshell } from '../make-webshell';
1011
import { FeatureBuilder } from '../FeatureBuilder';
1112
import { MinimalWebViewProps } from '../types';
@@ -19,23 +20,31 @@ const HelloFeature = new FeatureBuilder({
1920
featureIdentifier: 'test.hello',
2021
defaultOptions: {}
2122
})
22-
.withEventHandlerProp('onDOMDummyHello')
23+
.withandlerProp('onDOMDummyHello')
2324
.build();
2425

2526
const FailingFeature = new FeatureBuilder({
2627
script: dummyFailingScript,
2728
featureIdentifier: 'test.fail',
2829
defaultOptions: {}
2930
})
30-
.withEventHandlerProp('onDOMDummyFailure')
31+
.withandlerProp('onDOMDummyFailure')
3132
.build();
3233

3334
const OptionFeature = new FeatureBuilder({
3435
script: dummyOptionScript,
3536
featureIdentifier: 'test.option',
3637
defaultOptions: {}
3738
})
38-
.withEventHandlerProp<{ foo: string }, 'onDOMDummyOption'>('onDOMDummyOption')
39+
.withandlerProp<{ foo: string }, 'onDOMDummyOption'>('onDOMDummyOption')
40+
.build();
41+
42+
const HandlerIdFeature = new FeatureBuilder({
43+
script: dummyHandleridScript,
44+
featureIdentifier: 'test.handlerid',
45+
defaultOptions: {}
46+
})
47+
.withandlerProp('onDOMDummyOption', 'hi')
3948
.build();
4049

4150
describe('Webshell component', () => {
@@ -83,6 +92,19 @@ describe('Webshell component', () => {
8392
);
8493
expect(onDOMDummyOption).toHaveBeenCalledWith({ foo: 'bar' });
8594
});
95+
it('should disambiguate between handlerIds', async () => {
96+
const onHandlerIdDummyOption = jest.fn();
97+
const Webshell = makeWebshell(Ersatz, new HandlerIdFeature());
98+
await waitForErsatz(
99+
render(
100+
<Webshell
101+
webshellDebug={false}
102+
onDOMDummyOption={onHandlerIdDummyOption}
103+
/>
104+
)
105+
);
106+
expect(onHandlerIdDummyOption).toHaveBeenCalledWith('Hello world!');
107+
});
86108
it('should keep support for onMessage and injectedJavaScript', async () => {
87109
const onDOMDummyOption = jest.fn();
88110
const onMessage = jest.fn();

packages/core/src/features-loader.webjs

+4-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ function registerFeature(specs) {
106106
},
107107
numericFromPxString: numericFromPxString,
108108
makeCallbackSafe: makeCallbackSafe.bind(null, sendErrorMessage),
109-
postShellMessage: function (message) {
109+
postShellMessage: function () {
110+
var message = arguments.length > 1 ? arguments[1] : arguments[0];
111+
var handlerId = arguments.length > 1 ? arguments[0] : 'default';
110112
if (handlerName == null) {
111113
sendErrorMessage(
112114
new TypeError(
@@ -124,6 +126,7 @@ function registerFeature(specs) {
124126
identifier: snippetIdentifier,
125127
body: message,
126128
handlerName: handlerName,
129+
handlerId: handlerId,
127130
__isWebshellPostMessage: true
128131
})
129132
);

packages/core/src/features/HandleElementCSSBoxFeature.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,7 @@ export const HandleElementCSSBoxFeature: FeatureConstructor<
135135
featureIdentifier:
136136
'org.formidable-webview/webshell.handle-element-cssbox-dimensions'
137137
})
138-
.withEventHandlerProp<
139-
ElementCSSBoxDimensions,
138+
.withandlerProp<ElementCSSBoxDimensions, 'onDOMElementCSSBoxDimensions'>(
140139
'onDOMElementCSSBoxDimensions'
141-
>('onDOMElementCSSBoxDimensions')
140+
)
142141
.build();

packages/core/src/features/HandleHTMLDimensionsFeature.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,5 @@ export const HandleHTMLDimensionsFeature: FeatureConstructor<
106106
defaultOptions,
107107
featureIdentifier: 'org.formidable-webview/webshell.handle-html-dimensions'
108108
})
109-
.withEventHandlerProp<HTMLDimensions, 'onDOMHTMLDimensions'>(
110-
'onDOMHTMLDimensions'
111-
)
109+
.withandlerProp<HTMLDimensions, 'onDOMHTMLDimensions'>('onDOMHTMLDimensions')
112110
.build();

packages/core/src/features/HandleHashChangeFeature.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,5 @@ export const HandleHashChangeFeature: FeatureConstructor<
6161
defaultOptions,
6262
featureIdentifier: 'org.formidable-webview/webshell.handle-hash-change'
6363
})
64-
.withEventHandlerProp<HashChangeEvent, 'onDOMHashChange'>('onDOMHashChange')
64+
.withandlerProp<HashChangeEvent, 'onDOMHashChange'>('onDOMHashChange')
6565
.build();

packages/core/src/features/HandleLinkPressFeature.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,5 @@ export const HandleLinkPressFeature: FeatureConstructor<
101101
defaultOptions,
102102
featureIdentifier: 'org.formidable-webview/webshell.link-press'
103103
})
104-
.withEventHandlerProp<LinkPressTarget, 'onDOMLinkPress'>('onDOMLinkPress')
104+
.withandlerProp<LinkPressTarget, 'onDOMLinkPress'>('onDOMLinkPress')
105105
.build();

packages/core/src/features/HandleVisualViewportFeature.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const HandleVisualViewportFeature: FeatureConstructor<
5050
defaultOptions: {},
5151
featureIdentifier: 'org.formidable-webview/webshell.handle-visual-viewport'
5252
})
53-
.withEventHandlerProp<VisualViewportDimensions, 'onDOMVisualViewport'>(
53+
.withandlerProp<VisualViewportDimensions, 'onDOMVisualViewport'>(
5454
'onDOMVisualViewport'
5555
)
5656
.build();

packages/core/src/make-webshell.tsx

+25-6
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ interface WebViewMessage {
2323

2424
interface PostMessage {
2525
identifier: string;
26-
handlerName: string;
26+
handlerId: string;
2727
type: 'feature' | 'error' | 'log';
2828
severity: 'warn' | 'info';
2929
body: any;
@@ -92,6 +92,23 @@ function filterWebViewProps<W>(
9292
}, {} as W);
9393
}
9494

95+
function getHandlerUUID(def: PropDefinition<any>) {
96+
return `${def.featureIdentifier}:${def.handlerId}`;
97+
}
98+
99+
function extractHandlersMap(features: Feature<any, PropsSpecs<any>>[]) {
100+
return features
101+
.map((f: Feature<any, PropsSpecs<any>>) => f.propSpecs)
102+
.reduce((p, c) => [...p, ...c], [])
103+
.reduce(
104+
(map, spec: PropDefinition<any>) => ({
105+
...map,
106+
[getHandlerUUID(spec)]: spec
107+
}),
108+
{}
109+
) as Record<string, PropDefinition<any>>;
110+
}
111+
95112
function extractPropsSpecsMap(features: Feature<any, PropsSpecs<any>>[]) {
96113
return features
97114
.map((f: Feature<any, PropsSpecs<any>>) => f.propSpecs)
@@ -129,6 +146,7 @@ export function makeWebshell<
129146
> {
130147
const filteredFeatures = features.filter((f) => !!f);
131148
const propsMap = extractPropsSpecsMap(filteredFeatures);
149+
const handlersMap = extractHandlersMap(filteredFeatures);
132150
const serializedFeatureScripts = serializeFeatureList(filteredFeatures);
133151
const injectableScript = assembleScript(serializedFeatureScripts);
134152
const Webshell = (
@@ -145,25 +163,26 @@ export function makeWebshell<
145163
({ nativeEvent }: NativeSyntheticEvent<WebViewMessage>) => {
146164
const parsedJSON = parseJSONSafe(nativeEvent.data);
147165
if (isPostMessageObject(parsedJSON)) {
148-
const { type, identifier, body, handlerName, severity } = parsedJSON;
166+
const { type, identifier, body, handlerId, severity } = parsedJSON;
149167
if (type === 'feature') {
168+
const handlerName = handlersMap[`${identifier}:${handlerId}`].name;
150169
const handler =
151-
typeof handlerName === 'string' ? domHandlers[handlerName] : null;
170+
typeof handlerId === 'string' ? domHandlers[handlerName] : null;
152171
if (propsMap[handlerName]) {
153172
if (typeof handler === 'function') {
154173
handler(body);
155174
} else {
156175
webshellDebug &&
157176
console.info(
158-
`[Webshell]: script from feature "${identifier}" sent an event, but there ` +
177+
`[Webshell]: script from feature "${identifier}" sent an event towards ${handlerId} handler, but there ` +
159178
`is no handler prop named "${handlerName}" attached to the shell.`
160179
);
161180
}
162181
} else {
163182
console.warn(
164-
`[Webshell]: script from feature "${identifier}" sent an event, but there is ` +
183+
`[Webshell]: script from feature "${identifier}" sent an event towards ${handlerId} handler, but there is ` +
165184
`no handler named "${handlerName}" defined for this feature. ` +
166-
'Use FeatureBuilder.withEventHandlerProp to register that handler, or make ' +
185+
'Use FeatureBuilder.withHandlerProp to register that handler, or make ' +
167186
'sure its name is not misspell in the DOM script.'
168187
);
169188
}

packages/core/src/types.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export type FeatureDefinition<O extends {}> = {
7272
* @public
7373
*/
7474
export type PropDefinition<P extends Partial<Record<string, any>>> = {
75+
handlerId: string;
7576
type: 'handler' | 'inert';
7677
featureIdentifier: string;
7778
name: string;
@@ -168,12 +169,21 @@ export interface WebjsContext<O extends {}, P> {
168169
*/
169170
readonly options: O;
170171
/**
171-
* When invoked, the shell will call the handler associated with this
172-
* script, if any.
172+
* Instruct the shell to call **the default handler** associated with
173+
* this feature, if any.
173174
*
174175
* @param payload - The value which will be passed to the handler.
175176
*/
176177
postShellMessage(payload: P): void;
178+
/**
179+
* Instruct the shell to call the handler associated with this
180+
* feature and `eventId`, if any.
181+
*
182+
* @param handlerId - A unique string to disambiguate between different handlers.
183+
* You can omit this param if you are sending to `"default"` handler.
184+
* @param payload - The value which will be passed to the handler.
185+
*/
186+
postShellMessage(handlerId: string, payload: P): void;
177187
/**
178188
* Create a function which execute a callback in a try-catch block that will
179189
* grab errors en send them to the `Webshell` component.

0 commit comments

Comments
 (0)