Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@formidable-webview/webshell

Package Overview
Dependencies
Maintainers
1
Versions
22
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@formidable-webview/webshell

🚀 A Higher-order component to handle WebView DOM events in React Native

  • 1.0.0
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
9K
increased by16.32%
Maintainers
1
Weekly downloads
 
Created
Source

npm semver codecov CI

:warning: This library is in early development.

@formidable-webview/webshell

@formidable-webview/webshell exports a simple builder-function (HOC) which creates an augmented WebView with additional DOM-based features. The returned component behaves exactly like a WebView, and given a ref prop, you'll get the underlying WebView instance and be able to access its imperative methods.

The features provide an abstraction layer of the message system. Each DOM feature augments the WebView with a DOM message handler prop, for example omDOMLinkPress. Here are examples of features you can assemble in the shell:

  • Intercept click events on anchors and prevent navigation (linkPressFeature);
  • Get notified when an element size changes in the view (elementDimensionsFeature);

And you can, of course, implement any DOM feature you'd like. This library also presents a set of good practices to test features in isolation and, by extension, test any injected scripts. See the tooling section.

How does it work?

It uses WebView.injectedJavascript prop to inject javascript “features” (snippets) responsible for communicating information to the Webshell controller through DOM handlers. Each DOM feature is try-catched in siloed error-flows.

Install

npm install @formidable-webview/webshell

Basic usage

This library is fairly easy to use. Just create a shell by invoking makeShell function with a WebView component and the assembled features you wish the shell to implement. Each DOM feature can be assembled with options, which alter their behavior. For example, the linkPressFeature default behavior prevents click events on anchors to propagate, effectively disallowing redirects. You can, however, change this behavior to let redirects happen while still being notified by setting preventDefault option to false.

// integration/basic.tsx

import React, { useCallback } from 'react';
import { Linking } from 'react-native';
import makeWebshell, {
  linkPressFeature,
  elementDimensionsFeature,
  ElementDimensionsObject
} from '@formidable-webview/webshell';
import WebView from 'react-native-webview';

const Webshell = makeWebshell(
  WebView,
  linkPressFeature.assemble({ preventDefault: true }),
  elementDimensionsFeature.assemble({ tagName: 'body' })
);

export default function EnhancedWebView(webViewProps) {
  const onLinkPress = useCallback((url: string) => Linking.openURL(url), []);
  const onBodyDimensions = useCallback(
    ({ width, height }: ElementDimensionsObject) => console.info(width, height),
    []
  );
  const onError = useCallback((featureIdentifier, errorMessage) => {
    if (featureIdentifier === linkPressFeature.identifier) {
      // Handle linkPress error
      console.error(errorMessage);
    } else if (featureIdentifier === elementDimensionsFeature.identifier) {
      // Handle dimensions error
      console.error(errorMessage);
    }
  }, []);
  return (
    <Webshell
      onDOMLinkPress={onLinkPress}
      onDOMElementDimensions={onBodyDimensions}
      onDOMError={onError}
      {...webViewProps}
    />
  );
}

A classic use-case: implementing AutoheightWebView

This component height automatically and dynamically adapts to the page height, even after DOM is mounted.

// integration/autoheight.tsx

import React, { useCallback, useState } from 'react';
import makeWebshell, {
  elementDimensionsFeature,
  ElementDimensionsObject
} from '@formidable-webview/webshell';
import WebView from 'react-native-webview';

const Webshell = makeWebshell(
  WebView,
  elementDimensionsFeature.assemble({ tagName: 'body' })
);

export default function AutoheightWebView(webViewProps) {
  const [height, setHeight] = useState<number | undefined>(undefined);
  const onBodyDimensions = useCallback(
    ({ height: bodyHeight }: ElementDimensionsObject) => setHeight(bodyHeight),
    []
  );
  return (
    <Webshell
      onDOMElementDimensions={onBodyDimensions}
      {...webViewProps}
      style={[webViewProps.style, { height }]}
    />
  );
}

Implementing features

To have a good sense on how to make new features, we will take a look at the linkPressFeature implementation. The implementation is in typescript, which is convenient to communicate the different types implied in a DOM feature. There are four important areas to specify in such feature:

  1. How does it behave in the DOM? → See the content of linkPressScript.
  2. What options can it be assembled with? → LinkPressOptions
  3. What is the name of the DOM message handler prop which will be available in the Webshell? → onDOMLinkPress
  4. What type of payload does it ship with events? → string

:warning: Note that you will need to replace relative imports with direct imports from the library,

import {...} from "@formidable-webview/webshell";

// src/features/link-press.ts

import linkPressScript from './link-press.webjs';
import { makeFeature } from '../make-feature';
import type { Feature } from '../types';

/**
 * An object describing customization for the linkPress feature.
 *
 * @public
 */
export interface LinkPressOptions {
  /**
   * Prevent click events on anchors to propagate.
   *
   * @defaultValue true
   */
  preventDefault?: boolean;
}

/**
 * This feature allows to intercept clicks on anchors (`<a>`). By default, it
 * will prevent the click from propagating. But you can disable this option.
 *
 * @public
 */
export const linkPressFeature: Feature<
  LinkPressOptions,
  'onDOMLinkPress',
  string
> = makeFeature({
  script: linkPressScript,
  eventHandlerName: 'onDOMLinkPress',
  identifier: 'org.webshell.linkPress'
});

The behavior in the DOM is implemented in the following file (please note that the extension is arbitrary, see the tooling section):

// src/features/link-press.webjs

function linkPressFeature(arg) {
  var postMessage = arg.postMessage;
  var options = arg.options || {};
  var preventDefault = options.preventDefault !== false;

  function findParent(tagname, el) {
    while (el) {
      if ((el.nodeName || el.tagName).toLowerCase() === tagname.toLowerCase()) {
        return el;
      }
      el = el.parentNode;
    }
    return null;
  }

  function interceptClickEvent(e) {
    var href;
    var target = e.target || e.srcElement;
    var anchor = findParent('a', target);
    if (anchor) {
      href = anchor.getAttribute('href');
      preventDefault && e.preventDefault();
      postMessage(href);
    }
  }
  document.addEventListener('click', interceptClickEvent);
}

Every DOM script top declaration must be a function taking one argument. The shape of this argument is depicted in WebjsContext definition. For wide compatibility purposes, 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;
  • unit-test with jest jsdom environment;

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

API Reference

The API reference is build with the amazing @microsoft/api-extractor utility. Read the API here: docs/webshell.md.

Landmark exports:

Tooling

The objectives of the setup are:

  • Import DOM scripts (.webjs) as strings;
  • Statically check DOM scripts for syntax errors;
  • Statically check DOM scripts compatibility given targeted WebViews versions;
  • Test the DOM scripts behaviors.

babel

To import .webjs files as strings, we will use babel-plugin-inline-import with webjs or whichever extension you are using for your WebView scripts, see babel.config.js file. This plugin will allow you to import scripts as strings instead of compiling the module!

eslint

You can use @formidable-webview/eslint-config-webjs to target .webjs files with specific config:

  • Enforce ECMAScript 5 to make sure it runs on reasonably old WebView backends.
  • Enforce a list of supported web engines with the outstanding eslint-plugin-compat. We make sure we don't use recent web APIs without a fallback or polyfill.

Jest

To test injected scripts, the easiest way is to use @formidable-webview/ersatz. Best to show with an example of our link-press DOM feature testing:

:warning: Note that you will need to replace relative imports with direct imports from the library,

import {...} from "@formidable-webview/webshell";

// src/features/__tests__/link-press.test.tsx

import * as React from 'react';
import Ersatz from '@formidable-webview/ersatz';
import makeErsatzTesting from '@formidable-webview/ersatz-testing';
import { render } from '@testing-library/react-native';
import { makeWebshell } from '../../make-webshell';
import { linkPressFeature } from '../link-press';

const { waitForDocument } = makeErsatzTesting(Ersatz);

describe('Webshell with linkPressFeature', () => {
  it('should invoke onDOMLinkPress prop when a link is pressed', async () => {
    const onDOMLinkPress = jest.fn();
    const Webshell = makeWebshell(Ersatz, linkPressFeature.assemble());
    const document = await waitForDocument(
      render(
        <Webshell
          onDOMLinkPress={onDOMLinkPress}
          source={{ html: '<a id="anchor0" href="https://foo.org">bar</a>' }}
        />
      )
    );
    document.getElementById('anchor0').click();
    expect(onDOMLinkPress).toHaveBeenCalledWith('https://foo.org');
  });
});

Editor

The last thing you need to do is associate Javascript syntax with the custom extension you have chosen in your text editor. From now-on, you will have a full featured QC in those scripts!

Finally, if you want Github to provide syntax highlight to you webjs files, see our .gitattributes file.

Keywords

FAQs

Package last updated on 19 Aug 2020

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc