{"id":21406686,"url":"https://github.com/ringcentral/web-apps","last_synced_at":"2025-06-14T13:03:38.430Z","repository":{"id":37913466,"uuid":"267772025","full_name":"ringcentral/web-apps","owner":"ringcentral","description":"RingCentral Web Apps Framework","archived":false,"fork":false,"pushed_at":"2022-12-14T17:35:37.000Z","size":3009,"stargazers_count":74,"open_issues_count":15,"forks_count":15,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-04-07T20:42:17.648Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://ringcentral-web-apps.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ringcentral.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-05-29T05:22:37.000Z","updated_at":"2025-03-30T00:40:52.000Z","dependencies_parsed_at":"2023-01-29T00:16:25.842Z","dependency_job_id":null,"html_url":"https://github.com/ringcentral/web-apps","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/ringcentral/web-apps","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ringcentral%2Fweb-apps","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ringcentral%2Fweb-apps/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ringcentral%2Fweb-apps/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ringcentral%2Fweb-apps/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ringcentral","download_url":"https://codeload.github.com/ringcentral/web-apps/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ringcentral%2Fweb-apps/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259820784,"owners_count":22916544,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-22T16:41:50.465Z","updated_at":"2025-06-14T13:03:38.395Z","avatar_url":"https://github.com/ringcentral.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"Web Apps\n========\n\nThis framework provides support for embeddable apps infrastructure aka Microfrontends. Host application can delegate the actual features to other apps and provide seamless navigation and UX between those apps. Applications can be implemented using any JS framework and can be deployed anywhere, can have own release cycle. Host can be a React application or any other JS framework thanks to Web Components support.\n\nCommon pitfall of all Microfrontends is inability to efficiently and seamlessly share dependencies between host and apps. Web Apps framework is written with built-in support of [Webpack Module Federation](https://webpack.js.org/concepts/module-federation), so apps can declare and share dependencies in a standard way.\n\n- Location synchronization between app and host\n- Ability to deep-link \"app to app\" or \"app to host\" or \"host to app\"\n- Consistent event-based interaction between apps and host\n- IFrame resize based on content of IFrame\n- IFrame popup support\n- Maximum adherence to Web Standards\n- 3-legged auth support\n- Written in TypeScript\n- React and Web Component host helpers\n- Unlimited nesting of apps within other apps, e.g. each app can become a host for more apps\n\nQuick remark. This framework is most useful when you have a system where apps can be written using different frameworks and you need a layer to orchestrate it. There's no need for this framework if you only deal with React host and React apps, Module Federation will work just fine for you. However, if you have to show `iframe`-based apps, or, say, Vue or Angular app inside React app, the Web Apps framework is a way to go.\n\n## TOC\n\n- [App Typs](#app-types)\n- [How It Works](#how-it-works)\n- [Host](#host)\n    - [React Host](#react-host)\n        - [HTML5 location sync and multiple instances of History object](#html5-location-sync-and-multiple-instances-of-history-object)\n        - [React Dev Tools](#react-dev-tools)\n    - [Hosts without React](#hosts-without-react)\n    - [Host-IFrame sync tracking modes](#host-iframe-sync-tracking-modes)\n    - [Authentication](#authentication)\n    - [App Registry (optional)](#apps-registry-optional)\n- [Apps](#apps)\n    - [Web Component Apps](#web-component-apps)\n        - [React-based Web Component Apps](#react-based-web-component-apps)\n    - [Global Apps](#global-apps)\n        - [Webpack Module Federation Apps](#webpack-module-federation-apps)\n        - [React-based Webpack Module Federation Apps](#react-based-webpack-module-federation-apps)\n        - [Global Apps JSONP](#global-apps)\n        - [React-based Global Apps JSONP](#react-based-global-apps)\n        - [Global Apps in Direct mode](#global-apps-in-direct-mode)\n    - [IFrame Apps](#iframe-apps)\n        - [React-based IFrame Apps](#react-based-iframe-apps)\n- [Demo](#demo)\n- [Upgrading](#upgrading)\n\n## App Types\n\nThere are 3 kind of embeddable applications: IFrame and Web Component based.\n\nAn IFrame application (type `iframe`) is rendered inside the `iframe` and can synchronize it's URL and size with the main application.\n\n`Web Component` based application (type `script`) is represented by a Custom Element (`HTMLElement`), a native technology available in modern browsers (for less modern browsers like Safari or IE11 we have a polyfill).\n\nGlobal application (type `global`) is just a `div` which acts as a mount point for an app. App lives in global JS and CSS scopes.\n\nLibrary loads scripts and styles for the App, manages the lifecycle of Custom Elements, Global Apps and IFrames and allows to interact with the host using consistent event-based system with same interface no matter what kind of app it is.\n\n### How To Choose An App Type\n\nYou can use the following table when choosing which app type better suits for your case:\n\n|  | IFrame | Web Components | Global |\n|-|-|-|-|\n| Type in config | `iframe` | `script` | `global` |\n| Isolation | Full: CSS, scripts | :warning: Partial: CSS when not polyfilled | :warning: No isolation |\n| Hot Module Replacement | Full support | :warning: Requires custom tailoring | :warning: Requires custom tailoring |\n| Popups | :warning: Limited to size of `iframe`, popup body must scroll | No limitations | No limitations |\n| Navigation | No limitations, `iframe` path will be synced as hosts's `hash` | No limitations | No limitations |\n| 3rd Party | Only choice | :warning: Forbidden to use for 3rd Parties | :warning: Forbidden to use for 3rd Parties |\n\nFramework provides ability to load apps developed by 3rd parties, which has to be used with caution. Best isolation is provided by `iframe` mode.\n\n## How It Works\n\nThe concept of this package is to load an application with certain type \u0026 URLs, render it on the page in any place and wire events between Host and App.\n\nEvents with like `eventTypes.message` from `@ringcentral/web-apps-*` packages will be transmitted to any type of app including IFrame.\n\nHere are the simplified flows of events:\n\n```\nHost App \u003c-\u003e IFrame Node \u003c-\u003e postMessage \u003c-\u003e Synthetic IFrame Node \u003c-\u003e IFrame App\nHost App \u003c-\u003e Custom Element Node \u003c-\u003e Web Component App\nHost App \u003c-\u003e Div Element \u003c-\u003e Global App\n```\n\n### Events\n\nEvents are instances of `CustomEvent` class and have `detail` property that carries the event value. Type of value depends on type of event.\n\n- `message` — anything\n- `popup` — special event that carries requested backdrop color as value\n- `authError` — special event to notify Host that App has authentication error, host should display login page in this case\n- `location` — special event that tells Host to open certain location, *handled automatically, no need to capture*\n- `state` — special event to sync location between Host and IFrame, *handled automatically, no need to capture*\n\n### IFrame retransmission flow from `iframe` to host\n\n1. IFrame app emits `CustomEvent` on synthetic `iframe` node\n2. IFrame SDK listens to event and retransmits it over `postMessage` to Host\n3. Host SDK receives `postMessage` and emits `RetransmittedEvent` on the real `iframe` node\n4. Host listens to `RetransmittedEvent` on the real `iframe` node\n\n## Polyfills\n\nHost must include following polyfills:\n\n```bash\nnpm install @webcomponents/webcomponentsjs @babel/polyfill --save-dev\n```\n\n```js\nimport \"@babel/polyfill\";\nimport \"@webcomponents/webcomponentsjs/custom-elements-es5-adapter\";\nimport \"@webcomponents/webcomponentsjs\";\n```\n\nWe have to use either https://github.com/github/babel-plugin-transform-custom-element-classes on app-level or  \n`@webcomponents/webcomponentsjs/custom-elements-es5-adapter` on host-level because app-level Babel-transpiled ES5\nclasses can't properly inherit browser's native ES6 classes.\n\nThere's no need to add polyfills to Web Component apps, IFrame apps has to manage their polyfills individually. Web Components polyfills are not needed if you are not using WC-based apps.\n\n## Host\n\nLet's review few things before we get started with configuring the host.\n\n### Host popup backdrop for IFrame apps\n\nIf you plan to display IFrame applications Host must import (or declare by itself) some CSS in order to display popup backdrop.\n\nInstall the package:\n\n```bash\n$ npm install @ringcentral/web-apps-host-css\n```\n\nThen import it:\n\n```js\nimport '@ringcentral/web-apps-host-css/styles.css';\n```\n\nThis assumes your Host will have this code around App that can be IFrame:\n\n```jsx\n\u003cdiv className={popup \u0026\u0026 'app-popup'}\u003e\n\u003cdiv\n    className=\"app-popup-bg\"\n    role=\"presentation\"\n    style={{backgroundColor: popup}}\n/\u003e\n```\n\nPopup variable has a color that was received in special popup event that we can capture on host. Color is needed to show properly colored backdrop because different IFrame apps may have different shade of backdrop. Empty variable means no popup.\n\n### React host\n\nInstall the `@ringcentral/web-apps-host-react` package by running following command:\n\n```bash\n$ npm install @ringcentral/web-apps-host-react\n```\n\n#### Hooks\n\nIn order to display an app on the host we will use the `useApplication` hook, it will load the source from the URL and provide a `Component` that you can insert in your Host application.\n\n```js\nimport {useApplication, eventType, useListenerEffect, dispatchEvent} from '@ringcentral/web-apps-host-react';\n\nconst Page = () =\u003e {\n    const {error, Component, node, loading} = useApplication({\n        id: 'xxx', // should be unique for each app\n        \n        type: 'script', // or global or iframe\n        \n        url: 'http://example.com/script.js', // one URL that will load all\n        \n        // or multiple URLs as an array, order matter\n        //url: [ \n        //    'http://example.com/styles.css',\n        //    'http://example.com/bundle.js',\n        //    'http://example.com/entry.js'\n        //]        \n    });\n    \n    // Messages\n    const [messages, setMessages] = useState([]);\n    const onMessage = event =\u003e setMessages(messages =\u003e [...messages, event.detail]);\n    useListenerEffect(node, eventType.message, onMessage);\n    \n    // Popups\n    const [popup, setPopup] = useState(false);\n    const onPopup = event =\u003e setPopup(popup =\u003e (popup !== event.detail ? event.detail : popup));\n    useListenerEffect(node, eventType.popup, onPopup);\n\n    if (error) return \u003cdiv\u003eApp cannot be rendered: {error.toString()}\u003c/div\u003e;\n    \n    return \u003cdiv className={popup \u0026\u0026 'app-popup'}\u003e\n        \u003cdiv\n            className=\"app-popup-bg\"\n            onClick={e =\u003e dispatchEvent(node, eventType.popup, false)}\n            style={{backgroundColor: popup}}\n            role=\"presentation\"\n        /\u003e\n\n        {loading \u0026\u0026 \u003cdiv\u003eApp is mounting\u003c/div\u003e}\n        {/* Component must be placed unconditionally, do not do !loading \u0026\u0026 Component */}\n        \u003cComponent/\u003e\n\n        \u003cdiv\u003e{JSON.stringify(messages)}\u003c/div\u003e\n        \u003cdiv\u003e\u003cbutton onClick={e =\u003e dispatchEvent(node, eventType.message, {foo: 'bar'})}\u003eSend Message\u003c/button\u003e\u003c/div\u003e\n    \u003c/\u003e;\n};\n```\n\nWhen `Component` is rendered a DOM `node` (either a Web Component's `HTMLElement` or an `iframe` or a `div`) is created \u0026 mounted. All props provided to `Component` will be spread on this DOM `node`.\n\nThis DOM `node` is used for communication with the App:\n\n```js\nuseListenerEffect(node, eventType.message, event =\u003e console.log(event.detail));\ndispatchEvent(node, eventType.message, {foo: 'bar'})\n```\n\n#### Render prop\n\n```js\nimport {Application} from '@ringcentral/web-apps-host-react';\n\nconst Page = () =\u003e (\n    \u003cApplication id=\"id\" url=\"http://example.com/script.js\" type=\"script\"\u003e{\n        ({error, loading, Component, node}) =\u003e {/* same stuff from hooks example */}}\n    \u003c/Application\u003e\n);\n``` \n\n#### HOC\n\n```js\nimport {withApplication} from '@ringcentral/web-apps-host-react';\n\n// you can pre-bind the app config\nconst OneAppComponent = withApplication({id: 'id', url: 'http://example.com/script.js', type: 'script'})(\n    ({error, loading, Component, node}) =\u003e (\n        /* same stuff from hooks example */\n        Component\n    )\n);\n\n// then you can place it anywhere \nconst Page1 = () =\u003e \u003cOneAppComponent /\u003e;\n\n// or app config should be provided as props\nconst MultipleAppComponent = withApplication()(\n    ({error, loading, Component, node}) =\u003e (\n        /* same stuff from hooks example */\n        Component\n    )\n);\n\n// and then\nconst Page2 = () =\u003e \u003cMultipleAppComponent id=\"id\" url=\"http://example.com/script.js\" type=\"script\" /\u003e; \n```\n\n#### HTML5 location sync and multiple instances of History object\n\nIf you're using hash location you may skip this part.\n\nSince `history` library and `react-router` do not support listening to global `window.history` object due to lack of `push` and `replace` events on the latter we need to use custom `LocationSync`.\n\nWe suggest putting it in the Router config at the very top of the application:\n\n```js\nimport React from 'react';\nimport {BrowserRouter} from 'react-router-dom';\nimport {LocationSync} from '@ringcentral/web-apps-host-react';\n\nexport default () =\u003e (\n    \u003cBrowserRouter\u003e\n        \u003cLocationSync /\u003e\n        {/* normal route config as usual */}\n    \u003c/BrowserRouter\u003e\n);\n```\n\nThis is a bulletproof solution because no matter what causes `window.history.push(...)` it will be captured and Host router will be synchronized. We suggest to use this solution when you don't control what is happening in apps and what framework they use, for example they are third party. **Unfortunately this has a drawback, when host will change location history block (`Prompt` component of `react-router`) on app level won't kick in.**\n\nHowever if you DO control apps and all of them are either React or IFrame, you can do the small trick to enable `Prompt`, `LocationSync` won't be needed since there's only one `history` object:\n\n```js\nimport React from 'react';\nimport {createBrowserHistory} from 'history';\nimport {Router} from 'react-router-dom';\n\n// This allows to block history in sub-apps, this is not required in general\nwindow.RCAppsDemoHistory = createBrowserHistory();\n\nexport default () =\u003e (\n    \u003cRouter history={window.RCAppsDemoHistory}\u003e\n        {/* normal route config as usual */}\n    \u003c/Router\u003e\n);\n```\n\nAnd then in React-based Apps routers as well:\n\n```js\nexport default () =\u003e (\n    \u003cRouter history={window.RCAppsDemoHistory}\u003e\n        {/* normal route config as usual */}\n    \u003c/Router\u003e\n);\n```\n\nThen `Prompt` will work as usual:\n\n```js\nimport React from 'react';\nimport {Prompt} from 'react-router-dom';\n\nexport default () =\u003e (\n    \u003cdiv\u003e\n        \u003cPrompt when={true} message={location =\u003e `Are you sure you want to go to ${location.pathname}`} /\u003e\n        Whatever\n    \u003c/div\u003e\n);\n```\n\n#### React Dev Tools\n\nDifferent guest application types are requiring different sets of actions to make devtools work.\n\n##### IFrame\n\nYou can use [react-devtools-inline](https://github.com/facebook/react/tree/master/packages/react-devtools-inline) if your host application is **not** built with React.\n\nYou can use standalone [react-devtools](https://github.com/facebook/react/tree/master/packages/react-devtools) version to access your guest application.\n\n##### Web Components\n\nYou can use standalone [react-devtools](https://github.com/facebook/react/tree/master/packages/react-devtools) version to access your guest application.\n\n##### Global\n\n:warning: [Module Federation](https://webpack.js.org/concepts/module-federation/) is a much better way to achieve the same. However you are using Webpack older than version 5 you can use this trick.\n\nDevtools will work perfectly if your host app is **not** build with React.\n\nOtherwise, you can try to share common libraries (like React, ReactDOM) between host and guest app.\n\nThe problem is that React declares `__REACT_DEVTOOLS_GLOBAL_HOOK__` on `window` [once](https://github.com/facebook/react/blob/baff5cc2f69d30589a5dc65b089e47765437294b/packages/react-dom/npm/index.js).\n\nThis means that only host application's hook will be registered and devtools will not be able to provide access to guest application.\n\nUse `expose-loader` for webpack inside your host application as an elegant way to place your common libraries onto `window`:\n\n```js\nconst exposedReactDependencies = [\n    {\n        test: require.resolve('react'),\n        use: [\n            {loader: 'expose-loader', options: 'React'},\n        ],\n    },\n    {\n        test: require.resolve('react-dom'),\n        use: [\n            {loader: 'expose-loader', options: 'ReactDOM'},\n        ],\n    },\n];\n\nconfig.module.rules.push(...exposedReactDependencies);\n```\n\nDeclare those libraries as external inside guest application webpack configuration:\n\n```js\nconfig.externals = {\n    ...config.externals,\n    react: 'React',\n    'react-dom': 'ReactDOM',\n};\n````\n\n### Hosts without React\n\nAlong with React version Web Apps also have Web Components versions. Don't forget [polyfills](#polyfills)!\n\nUsage is very simple:\n\n```js\nimport '@ringcentral/web-apps-host-web-component';\n```\n\nAnd then anywhere in the page:\n\n```html\n\u003cweb-app id='react' url='[\"http://example.com\"]' type=\"iframe\" style=\"...\" history=\"html5\" className=\"...\"/\u003e\n```\n\nYou may implement remote/local registry of apps the same way as in React demo. \n\nIn order to listen to events on the app you need to do following:\n\n```js\nimport {eventType} from '@ringcentral/web-apps-common';\n\nconst app = document.querySelector('web-app');\n\napp.addEventListener('load', () =\u003e {\n    const onMessage = event =\u003e console.log('React App got event', event.detail);\n    const node = app.getEventTarget();\n    node.addEventListener(eventType.message, onMessage);\n});\n```\n\nKeep in mind that `web-app` supports dynamic app switching, which means if `id` attribute changes then new app will be loaded, so `load` event may be emitted multiple times (depends on your setup).\n\n### Host-IFrame sync tracking modes\n\nSDK supports multiple sync tracking modes:\n\n- `hash` (default) — IFrame location will be placed in hash of host (for example IFrame has location `/foo/bar` then host will have it as `whatever#/foo/bar`), this mode is needed if you don't quite trust the contents of IFrame and to support completely different routing schemas in IFrame and App\n- `full` — IFrame and App will always have same location, useful to display a menu if an IFrame\n- `disabled` — No sync\n- `slave` — same as full, but IFrame will only follows location changes from Host\n\nYou can set mode via attribute on `Component` like so:\n\n- For React host:\n    ```html\n    \u003cComponent tracking=\"full\" /\u003e\n    ```\n\n- For non-React host:\n    ```html\n    \u003crc-app tracking=\"full\" /\u003e\n    ```\n\n### Authentication\n\nThe simplest way to provide authentication information to Web Component or Global app is to set it as an attribute on the `Component`:\n\n- For React host:\n    ```html\n    \u003cComponent authtoken={authtoken} /\u003e\n    ```\n\n- For non-React host:\n    ```html\n    \u003crc-app authtoken={authtoken} /\u003e\n    ```\n\nSee the host demos for more info.\n\n### Apps registry (optional)\n\nYou can hardcode all app configs if they never change, but if apps in the system can be dynamic, especially configured at backend, for example based on location main content area may show certain apps, then you'll need a registry.\n\nApplications configs (types \u0026 URLS) can be loaded from API or stored locally. This is not part of the SDK, just a recomendation, it could be anything, but in this demo it would be as follows:\n\n```js\nexport const appsRegistry = {\n    react: {\n        type: 'global',\n        getUrl: async overrideUrl =\u003e (overrideUrl || 'http://localhost:4001') + '/global.js'\n    },\n    vue: {\n        type: 'script',\n        getUrl: async overrideUrl =\u003e (overrideUrl || 'http://localhost:4002') + 'index.js'\n    },\n    iframe: {\n        type: 'iframe',\n        getUrl: overrideUrl =\u003e (overrideUrl || 'http://localhost:4003') + '/index.html?authToken=hardcoded'\n    }\n};\n```\n\nDemo host app support per-app URL overrides, so that you can set custom URL per app when you open deployed version, in\nthis case host will still run from CDN and overridden app will run from elsewhere (dev machine for example).\n\nTo do so simply open your browser's console and set:\n\n```js\nlocalStorage.appsOverrides = {\n    desiredAppId: {url: 'http://localhost:5000'}\n};\n```\n\nSo in order to load App config do this:\n\n```js\nawait appsRegistry[appId].getUrl(localStorage.appsOverrides \u0026\u0026 localStorage.appsOverrides[appId].url);\n```\n\n`appId` in this case can come from location of the Host app as a parameter `/apps/:appId` (needs extra setup, see the demo host).\n\n### Origins at Host\n\nIf you want to bring more security for IFrame apps you can specify origins for both Host and App endpoints like so:\n\nOn the host (for React host):\n\n```html\n\u003cComponent origin=\"http://example.com\" /\u003e\n```\n\nor for non-React host:\n\n```html\n\u003crc-app origin=\"http://example.com\" /\u003e\n```\n\nThis will check incoming origins and set target origin.\n\nKeep in mind that one app may appear in many Hosts (production, staging) so this might need extra configuration.\n\n## Apps\n\n### Web Component Apps\n\nFrom host standpoint app injection is as follows:\n\n```js\n    const {error, Component, node, loading} = useApplication({\n        id: 'xxx',\n        type: 'script',\n        url: 'http://example.com/script.js'\n    });\n```\n\nWeb Compoent's DOM node can be used to listen to Host events inside the React app, to do that we need to provide a node\nto React app which resides inside the Web Component.\n\nThe bare minimum what Web Component App must do is simply register the Custom Element following the pattern `web-app-ID` (ID should match the ID on Host):\n\n```js\nconst template = document.createElement('template');\n\ntemplate.innerHTML = `\n    \u003cstyle\u003e\n        /* shadow CSS */\n    \u003c/style\u003e\n`;\n\ncustomElements.define('web-app-react', class extends HTMLElement { // on the host ID will be react\n    constructor() {\n        super();\n        this.attachShadow({mode: 'open'});\n        this.shadowRoot.appendChild(document.importNode(template.content, true));\n    }\n});\n``` \n\n#### Events\n\n```js\nimport {dispatchEvent, eventType} from \"@ringcentral-web-apps/common\";\n\nconst template = document.createElement('template');\n\ntemplate.innerHTML = `\n    \u003cdiv\u003e\u003c/div\u003e\n    \u003cbutton\u003eSend Message\u003c/button\u003e\n`;\n\ncustomElements.define('web-app-react', class extends HTMLElement { // on the host ID will be react\n    div = null;\n    button = null;\n    messages = [];\n    constructor() {\n        super();\n        this.attachShadow({mode: 'open'});\n        this.shadowRoot.appendChild(document.importNode(template.content, true));\n\n        // get instances of elements in template\n        this.div = this.shadowRoot.querySelector('div');\n        this.button = this.shadowRoot.querySelector('button');\n    }\n    connectedCallback(){\n        \n        // send message on button click\n        this.button.addEventListener(e =\u003e dispatchEvent(this, eventType.message, {foo: 'bar'}));\n    \n        // capture message events emitted locally and from host\n        this.addEventListener(eventType.message, event =\u003e {\n            this.messages.push(event.detail);\n            this.div.innerText = JSON.stringify(this.messages);        \n        });\n    }\n});\n``` \n\n#### Shadow CSS \u0026 Polyfills\n\nWeb Components can be shipped with Shadow CSS as in example above, which will not be visible outside of Shadow DOM. All host styles are ignored. Make sure your bundler places styles correctly.\n\n:warning: **Keep in mind that if you target IE browsers then a polyfill will be used which cannot isolate CSS properly, so host styles will be affecting polyfilled Shadow DOM.** \n\nYou may also mount directly into Custom Element, without Shadow DOM, in this case styles \u0026 DOM will be consistent in modern and polyfilled browsers:\n\n\n```js\ncustomElements.define('web-app-react', class extends HTMLElement { // on the host ID will be react\n    div = null;\n    button = null;\n    messages = [];\n    constructor() {\n        super();\n    }\n    connectedCallback(){\n        this.div = document.createElement('div');\n        this.appendChild(this.div);\n        // and so on\n    }\n});\n```\n\n#### React-based Web Component Apps\n\nReact apps inside Web Components must have `react-shadow-dom-retarget-events` imported due to the bug: https://github.com/spring-media/react-shadow-dom-retarget-events.\n\n```js\n// index.js\nimport React from \"react\";\nimport {render, unmountComponentAtNode} from \"react-dom\";\nimport retargetEvents from 'react-shadow-dom-retarget-events';\nimport {App} from './app';\n\nconst template = document.createElement('template');\n\ntemplate.innerHTML = `\n    \u003cstyle\u003e\n        /* shadow CSS */\n    \u003c/style\u003e\n    \u003cdiv class=\"container\"\u003e\u003c/div\u003e\n`;\n\ncustomElements.define('web-app-react', class extends HTMLElement {\n    \n    mount = null;\n\n    constructor() {\n        super();\n        this.attachShadow({mode: 'open'});\n        this.shadowRoot.appendChild(document.importNode(template.content, true));\n        this.mount = this.shadowRoot.querySelector('.container');\n        retargetEvents(this.mount);\n    }\n\n    static get observedAttributes() {\n        return ['authtoken'];\n    }\n\n    render() {\n        // as you see we re-render every time when authtoken changes\n        render(\u003cApp authtoken={this.getAttribute('authtoken')} node={this}/\u003e, this.mount);\n    }\n\n    attributeChangedCallback(name, oldValue, newValue) {\n        this.render();\n    }\n\n    connectedCallback() {\n        this.render();\n    }\n\n    disconnectedCallback() {\n        unmountComponentAtNode(this);    \n    }\n    \n});\n``` \n\nAnd then inside the actual React application we wire events the same way as in the [example above](#events), but for React-base apps we provide an SDK to make things easier:\n\n```js\n// App.js\nimport React from \"react\";\nimport {dispatchEvent, useListenerEffect, eventType} from \"@ringcentral/web-apps-react\";\n\n// node and authtoken props are provided by Custom Component wrapper and will be automatically updated if host will change\nexport default ({node, authtoken}) =\u003e {\n\n    // set up local state\n    const [messages, setMessages] = useState([]);\n\n    // set up event listener for local \u0026 host events\n    useListenerEffect(node, eventType.message, event =\u003e setMessages(messages =\u003e [...messages, event.detail]));\n\n    // set up event dispatcher\n    const sendMessage = () =\u003e dispatchEvent(node, eventType.message, {toHost: 'message to host'});\n\n    return (\u003c\u003e\n        \u003cdiv\u003e{authtoken}\u003c/div\u003e\n        \u003cdiv\u003e{JSON.stringify(messages)}\u003c/div\u003e\n        \u003cbutton onClick={sendMessage}\u003eSend message\u003c/button\u003e\n    \u003c/\u003e);\n\n}\n```\n\nAs you see the code is identical to the React-based Host code.\n\nYou may use React Router inside such apps, it will track same location as Host app, for instance one of your Apps can be a Menu and another App can be Content area and Host will render both separately.\n\n### Global Apps JSONP\n\nIf you don't need the isolation of the Web Components and you are OK to interfere with global scopes of JS and CSS (hence the name Global Apps) you can use this approach as it's simpler and more direct.\n\n#### Webpack Module Federation Apps\n\nFrom host standpoint app injection is as follows:\n\n```js\n    const {error, Component, node, loading} = useApplication({\n        id: 'appId',\n        type: 'global',\n        url: 'http://example.com/script.js',\n        options: {\n            federation: true,\n            defaultScope: 'default', // scope to store shared modules, optional\n            scope: 'web_app_appId', // scope for app modules, optional\n            module: './index', // whis file to import modules from, optional\n            exportName: 'default', // which export will be taken\n        }\n    });\n```\n\nIf messing with Web Components is too much, you can use a simpler way, but it would have less isolation due to complete lack of Shadow DOM and Shadow CSS.\n\nUsing [Webpack Module Federation](https://webpack.js.org/concepts/module-federation/) we `export default` (or other if configured) callback from the federated module (defaults to `./index`), this callback can do something with the mounted node.\n\nIn this mode app's `webpack-config.js` has to be configured in a following way:\n\n```js\nconst {ModuleFederationPlugin} = require('webpack').container;\nconst path = require('path');\n\nmodule.exports = {\n    ...,\n    plugins: [\n        new ModuleFederationPlugin({\n            name: 'web_app_federated', // ID on host must match: federated\n            library: {type: 'var', name: 'web_app_federated'}, // ID on host must match: federated\n            filename: 'remoteEntry.js',\n            exposes: {\n                // note that host will pick up './index', this is public\n                // './src/index' is your internal detail\n                './index': './src/index',\n            },\n            shared: {\n                'react-dom': 'react-dom',\n                moment: '^2.24.0',\n                react: {\n                    import: 'react',\n                    shareKey: 'react',\n                    shareScope: 'default',\n                    singleton: true,\n                },\n            },\n        }),\n    ],\n    ...,\n};\n```\n\nNow in `src/index.js` may we only need to export default function that will be used as callback to mount the app:\n\n```js\nexport default (node) =\u003e {\n    // do something with the provided node\n    node.innerText = Date.now();\n    return () =\u003e {\n        // unmount handler\n    };\n};\n```\n\n#### React-based Webpack Module Federation Apps\n\nApp code is almost the same as in [React-based Web Component example](#react-based-web-component-apps), but skip the `customElement.define` part.\n\n```js\nimport App from './App';\nimport React from 'react';\nimport ReactDOM from 'react-dom';\n\nconst MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;\n\nexport default (node) =\u003e { // ID on host must match: global\n\n    const onChange = () =\u003e render(\u003cApp authtoken={node.getAttribute('authtoken')} node={node}/\u003e, node);\n\n    const observer = new MutationObserver(mutations =\u003e\n        mutations.forEach(\n            // re-render on changes\n            mutation =\u003e mutation.type === 'attributes' \u0026\u0026 onChange(), // you may also accumulate this instead of calling every time\n        ),\n    );\n\n    node.addEventListener('remove', () =\u003e {\n        unmountComponentAtNode(node);\n        observer.disconnect();\n    });\n\n    observer.observe(node, {attributes: true});\n\n    // initial render\n    onChange();\n\n    // unmount handler\n    return () =\u003e ReactDOM.unmountComponentAtNode(node);\n\n};\n```\n\n#### Global Apps JSONP\n\nFrom host standpoint app injection is as follows:\n\n```js\n    const {error, Component, node, loading} = useApplication({\n        id: 'xxx',\n        type: 'global',\n        url: 'http://example.com/script.js'\n    });\n```\n\nThis kind of apps is very similar to [Webpack Module Federation Apps](#webpack-module-federation-apps) but the registration is a bit different, it uses a JSONP-style function: \n\n```js\nimport {registerAppCallback} from \"@ringcentral/web-apps-common\";\n\nregisterAppCallback('global', (node) =\u003e { // ID on host must match: global\n    // do something with the provided node\n    node.innerText = Date.now();\n    return () =\u003e {\n        // unmount handler\n    };\n});\n```\n\n:warning: **If you're using Webpack to build Global apps make sure you set `output.jsonpFunction` to something unique to your app so that it will not clash with host's or other apps JSONP function.**\n\n#### React-based Global Apps JSONP\n\n```js\nimport React from \"react\";\nimport {render, unmountComponentAtNode} from \"react-dom\";\nimport {registerAppCallback} from \"@ringcentral/web-apps-react\";\nimport App from \"./App\";\n\nregisterAppCallback('global', (node) =\u003e { // ID on host must match: global\n    ReactDOM.render(\u003cApp foo={node.getAttribute('foo')} /\u003e, node);\n    return () =\u003e ReactDOM.unmountComponentAtNode(node);\n});\n```\n\n#### Global Apps in Direct mode\n\nGlobal apps support a shortcut, if you know that both Host and App are written using the same framework, you can omit the usage of events and interact with `Component` directly.\n\n```js\n    const {error, Component, node, loading} = useApplication({\n        id: 'xxx',\n        type: 'global',\n        url: 'http://example.com/script.js',\n        options: {\n            federation: true, // optional\n            direct: true\n        }\n    });\n\n    return \u003cComponent foo=\"bar\" /\u003e; // here you can use component as you normally would\n```\n\nIn Webpack Module Federation mode should simply export the component:\n\n```js\nconst Cmp = ({node}) =\u003e (\u003cdiv\u003e...\u003c/div\u003e); // node will still be provided as prop\nexport default Cmp;\n```\n\nIn this case the `registerAppCallback` can be called with React component for example:\n\n```js\nconst Cmp = ({node}) =\u003e (\u003cdiv\u003e...\u003c/div\u003e); // node will still be provided as prop\nregisterAppCallback('global', Cmp);\n``` \n\n### IFrame Apps\n\nFrom host standpoint app injection is as follows:\n\n```js\n    const {error, Component, node, loading} = useApplication({\n        id: 'xxx',\n        type: 'iframe',\n        url: 'http://example.com/script.js'\n    });\n```\n\n#### Location Sync\n\nIn order to enable location sync we need to create a special synchronization object:\n\n```js\nimport {IFrameSync} from \"@ringcentral/web-apps-sync-iframe\";\n\nconst iFrameSync = new IFrameSync({history: 'html5', id: 'id-as-registered-on-host'}); // or 'hash' or custom implementation\n```\n\nIf you have hash history then the URL of the app should end with `#`: `http://localhost:3000#`.\n\nIf you'd like to force application to report it's location (for example if you use true HTTP redirects) you may\nprovide a `sendInitialLocation` flag.\n\n#### Messages\n\nFrom now on we may use the sync object to send/receive events from the Host application by using `eventTarget` property:\n\n```js\nimport {dispatchEvent, eventType} from \"@ringcentral/web-apps-common\";\n\niFrameSync.getEventTarget().addEventListener(eventType.message, message =\u003e {});\ndispatchEvent(iFrameSync.getEventTarget(), eventType.message, {foo: 'bar'});\n```\n\n#### Popups\n\n```js\ndispatchEvent(iFrameSync.getEventTarget(), eventType.popup, 'rgba(0,0,0,0.5)');\n```\n\n#### Navigation\n\n```js\ndispatchEvent(iFrameSync.getEventTarget(), eventType.location, '/path/on/host?query=string');\n```\n\n#### Props\n\n:warning: **Props set at `\u003cComponent/\u003e` or `\u003crc-app/\u003e` are NOT synchronized to IFRame apps at the moment. This feature will be implemented in future**.\n\n#### React-based IFrame Apps\n\nApp code is almost the same as in [React-based Web Component example](#react-based-web-component-apps), but the acquisition of `node` to dispatch events and listenen to events is different as it's IFrame app:\n\n```js\n// App.js\nimport React from \"react\";\nimport {IFrameSync} from \"@ringcentral/web-apps-sync-iframe\";\nimport {dispatchEvent, useListenerEffect, eventType} from \"@ringcentral/web-apps-react\";\n\nconst iFrameSync = new IFrameSync({history: 'html5', id: 'id-as-registered-on-host'}); // or 'hash' or custom implementation\nconst node = iFrameSync.getEventTarget();\n\nconst Page = () =\u003e {\n\n    // set up local state\n    const [messages, setMessages] = useState([]);\n\n    // set up event listener for local \u0026 host events\n    useListenerEffect(node, eventType.message, event =\u003e setMessages(messages =\u003e [...messages, event.detail]));\n\n    // set up event dispatcher\n    const sendMessage = () =\u003e dispatchEvent(node, eventType.message, {toHost: 'message to host'});\n\n    return (\u003c\u003e\n        \u003cdiv\u003e{JSON.stringify(messages)}\u003c/div\u003e\n        \u003cbutton onClick={sendMessage}\u003eSend message\u003c/button\u003e\n    \u003c/\u003e);\n\n}\n```\n\nIn the example above the history will be synchronized auto-magically, but if you want full control you can supply your instance of `react-router` history like so:\n\n```js\nimport {IFrameSync} from '@ringcentral/web-apps-sync-iframe';\nimport {createBrowserHistory} from 'history';\nimport {Router} from 'react-router-dom';\n\nconst history = createBrowserHistory();\nconst iFrameSync = new IFrameSync({history, id: 'id-as-registered-on-host'});\n\nexport default () =\u003e (\n    \u003cRouter history={history}\u003e\n        {/* normal route config as usual */}\n    \u003c/Router\u003e\n);\n```\n\n#### Origins in Apps\n\nOn app-level:\n\n```js\nexport const sync = new IFrameSync({\n    history: 'html5',\n    id: 'iframe', // must match host config\n    origin: `http://example.com`, // strict mode, remove if you don't know which host is used or add dynamic host determination\n});\n```\n\nKeep in mind that one app may appear in many Hosts (production, staging) so this might need extra configuration.\n\n#### Non-browserified IFrame applications\n\nFor non-browserified applications a pre-built UMD bundle may be used:\n\n```html\n\u003cscript type=\"text/javascript\" src=\"node_modules/@ringcentral/web-apps-sync-iframe/dist/ringcentral-web-apps-iframe.js\"\u003e\u003c/script\u003e\n```\n\nAnd then global object `RCApps.IFrameSDK` can be utilized to get all needed utils:\n\n```js\nconst {eventType, dispatchEvent, IFrameSync} = RCApps.IFrameSDK; // and so on\n\nconst sync = new IFrameSync({\n    history: 'html5', \n    id: 'id-as-registered-on-host',\n    sendInitialLocation: true // useful in apps that does not use HTML5 history and reload on navigation\n});\n```\n\n## Repo Structure\n\n- `demo`\n    - `admin` \u0026mdash; simple demo with full page transitions\n    - `host` \u0026mdash; Create React App Host application\n    - `iframe` \u0026mdash; Create React App IFrame application\n    - `react` \u0026mdash; Webpack React-based Web Component or Global application\n    - `vue` \u0026mdash; Webpack Vue-based JS Web Component application\n- `packages`\n    - `common` \u0026mdash; common application SDK\n    - `host` \u0026mdash; SDK for Hosts\n    - `host-css` \u0026mdash; common CSS for hosts\n    - `host-react` \u0026mdash; React SDK for Hosts\n    - `host-web-component` \u0026mdash; Web Component SDK for Hosts\n    - `react` \u0026mdash; fix for React Router\n    - `sync` \u0026mdash; synchronization SDK\n    - `sync-host` \u0026mdash; synchronization SDK for Host\n    - `sync-iframe` \u0026mdash; synchronization SDK for IFrame\n    - `sync-react` \u0026mdash; React wrapper for IFrame\n    - `sync-web-component` \u0026mdash; Web Component for IFrame \n\n## Demo\n\n```bash\nnpm install\n```\n\nThis will install Lerna and all monorepo dependencies.\n\n\nPut `.env` file in the repo root in order to launch the demo:\n\n```\nBROWSER=false\nSKIP_PREFLIGHT_CHECK=true\n\nREACT_APP_VERSION=1.0.0\n\nREACT_APP_HOST_PORT=3000\nREACT_APP_HOST_WC_PORT=3001\nREACT_APP_REACT_PORT=4001\nREACT_APP_VUE_PORT=4002\nREACT_APP_IFRAME_PORT=4003\nREACT_APP_ADMIN_PORT=4005\nREACT_APP_REACT_MENU_PORT=4006\nREACT_APP_ANGULAR_PORT=4007\n\nREACT_APP_PRODUCTION_HOST=http://localhost\n```\n\nThen you can start the watchers/servers:\n\n```bash\nnpm run start\n```\n\nKeep in mind that this will also run watchers in SDKs so it can take a number of rebuilds of demo apps, just wait until\nno more messages will pop in terminal.\n\n## Upgrading\n\n### From `0.6.x` to `0.7.x`\n\n1. `\u003cApplication nodeRef={xxx}/\u003e` will not work, use `\u003cApplication\u003e{({node}) =\u003e { ... }}\u003c/Application\u003e`\n\n### From `0.4.x` to `0.5.x`\n\n1. Remove `makeHistoryFromRouter` or anything else that normalizes `history` on host, lib now does it internally\n2. Rename `registerApp` has been renamed: `import {registerAppCallback} from '@ringcentral/web-apps-common';`\n3. Remove `isRetransmittedEvent`, rely on state changes:\n    ```diff\n    - if (isRetransmittedEvent(event)) this.setState({popup: event.detail});\n    + if (this.state.popup !== event.detail) this.setState({popup: event.detail});\n    ```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fringcentral%2Fweb-apps","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fringcentral%2Fweb-apps","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fringcentral%2Fweb-apps/lists"}