Implementing Web Features

Features can set up communication in both directions, Web to native and native to Web. We will approach the two in distinct examples, but a feature can support both at the same time.

Web to Native Communication

To overview Web to native communication, we will take a look at a simplified implementation of HandleLinkPressFeature.

A feature has two facets:

  • The Web Script, which embodies behavior in the Web environment;
  • The Feature Class, which tackles integration with the shell.
disclaimer

This implementation is naive and doesn't handle inner click events. If you need such a feature, use our official HandleLinkPressFeature.

Web Script

Let's start with the Web script, the code which will run in the WebView.

HandleLinkPressFeature.webjs

Every Web script top declaration must be a function taking one argument. The shape of this argument is depicted in WebjsContext definition. This context object is the central API to interact with the shell from the Web environment.

~ API Reference ~
WebjsContext

Below is the same snippet with highlighted lines each matching a different context usage:

HandleLinkPressFeature.webjs
  1. context.options is the result of shallow-merging defaultOptions with user-provided options during feature instantiation.
  2. context.postMessageToShell will send an event which will be intercepted by the shell. If an event handler has been registered for this feature and passed as a prop to the shell, it will be invoked with the message content.
  3. context.warn and context.info will send a log event to the shell when webshellDebug prop is set to true. The shell will log the message in the native environment for quick feedback during development.
  4. context.makeCallbackSafe guarantees that any error raised in the body of the callback will be intercepted by the context API and routed to the shell for quick feedback during development.

Building a Feature Class

~ API Reference ~
FeatureBuilder

Now let's dive into the native-side. As you can see, the definition is relatively terse:

HandleLinkPressFeature.ts

Below is a recap of all the symbols present in this snippet:

SymbolDescription
linkPressScriptBehavior in the Web environment (string).
defaultOptionsDefault options for this feature (object).
"onDOMLinkPress"The name of the handler prop which will be available in the shell (string).
"org.myorg/webshell.link-press"A unique identifier for this feature (string).
LinkPressOptions[Typescript] The shape of options the feature class can be instantiated with.
LinkPressTarget[Typescript] The type of payload the handler prop will receive.

It is also worth noting that invoking withShellHandler 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 the corresponding handler prop (onDOMLinkPress).
  2. When implemented in typescript, type generation will add the shell handler name and payload type to the the feature, which will be grabbed by the resulting shell prop type.
important

Be advised that linkPressScript must be a string. In the snippet, we are using babel-plugin-inline-import to import the Web script as string, see the tooling guide;

important

Don't forget to invoke build method, otherwise you'll end up with a FeatureBuilder instance.

Using the Feature

WebViewWithOnLinkPress.tsx

Native to Web Communication

Until now, we have previewed how to send messages from the Web environment to the shell. Let's say we now want to pass an apiToken prop to our Web script so that it execute some authentication logic when the controlling component prop changes.

To allow communication from the shell to the Web environment, we will need to do the following:

  • Declare a Web handler with the FeatureBuilder;
  • Register a handler in the Web script;
  • Use a WebHandle from the shell to send apiTokens.

Declaring Web Handler

We need to register a handler associated with the feature to allow the shell to convey messages to the appropriate destination. Let's use FeatureBuilder.withWebHandler to declare a Web handler. The handler will need an identifier, 'apiTokenChange', to disambiguate from multiple messages.

FeatureWithToken.ts

Registering the Web Handler

Now we need to do something with the token. We will use WebjsContext.onShellMessage to register a listener function.

important

The first argument, 'apiTokenChange', must match the first argument of withWebHandler.

FeatureWithToken.webjs

Using the Feature

Finally, we need to pass the token from the shell to the Web script. We will use WebshellInvariantProps.webHandleRef to imperatively send messages:

APIComponent.tsx

That's it. Our Web script is almost a React component now, it can react to prop changes!

tip

If your script relies on multiple props, it is best advised for performance and synchronization concerns to use one useEffect with a payload comprising all props.

caution

Make sure to follow the rules of hooks and declare dependencies accordingly in useEffect:

  • Declaring no dependencies will cause the effect callback to execute on every render cycle, which might jam the native to Web communication bus;
  • Missing dependencies could cause stale closures and inconsistencies.

Testing with Jest

Let's go back to our HandleLinkPressFeature and implement a simple test.

Install Ersatz and Testing Library

To test injected scripts, the best way is to use @formidable-webview/ersatz. It will mock the WebView and run a Web environment in jsdom.

yarn add --dev @testing-library/react-native \
@formidable-webview/ersatz \
@formidable-webview/ersatz-testing

Implementing Tests

HandleLinkPressFeature.test.tsx

Web-Engines Compatibility

For wide compatibility purposes of the Web script, it is recommended to:

  • enforce ECMAScript 5 syntax with ESlint;
  • lint the script with the eslint-plugin-compat to enforce backward compatibility with old engines;

See the tooling guide for more details on how to achieve this.

Last updated on by Jules Sam. Randolph