This library enables cross-framework embedding of a part of one application into a page of another application at run-time using iframes.
An application part that can be embedded is called a widget. The app which publishes the widget is called the publisher. The app which embeds the published widget into a page is called the container.
Widgets can declare API's that are available in the container's page, even if container and publisher are implemented in different front-end frameworks.
If you are migrating from v1.x of this framework to v2.x be aware that there are breaking changes, see the migration notes as well as the changelog.
A first step for both publishing and embedding is including the widgets library into the page.
<script src="https://cdn.antwerpen.be/embeddable-widgets_module_js/2.0.9/aui-embeddable-widgets.min.js"></script>
If you don't want to load from CDN, you can also npm install @acpaas-ui/embeddable-widgets
and you will find the library in the node_modules/@acpaas-ui/embeddable-widgets/lib
folder.
You can publish any webpage as an embeddable widget.
First you need to publish the definition of the widget on a web-accessible URL as JSON.
{
"tag": "my-foo-widget",
"url": "/foo-widget",
"dimensions": {
"width": "100%",
"height": "500px"
},
"props": {
"fooData": {
"type": "array",
"required": true
},
"onFoo": {
"type": "function",
"required": false
}
}
}
What is going on here:
-
The
tag
is a unique identifier for the widget in the page (it is not automatically mapped to a HTML tag). -
The
url
points to where the widget's page is hosted. It can be relative (to the JSON's URL) or absolute.It is not required that the definition is hosted on the same server as the widget.
-
The
dimensions
specify the initial rendering dimensions, applies as style attributes to the iframe. -
The
props
specify the properties that the widget can be initialized withfooData
is an array which will be passed from container to widgetonFoo
is an event handler which will be defined in the container and called by the widget
If you want to render the same component multiple times on the same page but with different definition values.
You will have to define()
the component with different tags.
See the API section below for more details.
CORS headers need to be set on this JSON (e.g.
Access-Control-Allow-Origin: *
). CORS headers do not need to be set on the widget page itself.
To publish an Angular 6+ app, use the Angular wrapper ngx-embeddable-widgets. It includes an example app.
Initialize the widget:
window.auiEmbeddableWidgets.load('//example.com/path/to/definition.json');
Access the properties passed from the container in window.xprops
.
function doFoo() {
if (window.xprops && window.xprops.fooData) {
const result = window.xprops.fooData.map(...);
if (window.xprops.onFoo) {
window.xprops.onFoo(result);
}
}
}
The widget's page does not need cross-origin headers. It communicates to the container via
window.postMessage
. However, it should allow framing by setting appropriateX-Frame-Options
orContent-Security-Policy
headers if necessary.
To embed into an Angular 6+ app, use the Angular wrapper ngx-embeddable-widgets.
import React from 'react';
import ReactDOM from 'react-dom';
const MyWidget = window.auiEmbeddableWidgets.reactComponent(
// url to the definition
"//example.com/path/to/defintion.json",
{ React, ReactDOM }
)
class App extends Component {
onFoo(result) { ... }
render() {
return (
<MyWidget
fooData={ ['one', 'two'] }
onFoo={ result => this.onFoo(result) }
className="my-widget"
/>
);
}
}
This renders a
<div>
with the (optional)className
applied to it.
Provide a div to render the widget in:
<div id="my-container"></div>
Render the widget into the div, passing it the necessary properties:
window.auiEmbeddableWidgets.renderUrl(
'//example.com/path/to/definition.json',
{ fooData: ['one', 'two'],
onFoo: function(result) { ... } },
document.getElementById('my-container')
);
If you're currently using v1.x of this library in an app or in a widget, care must be taken to upgrade properly. Both app and widget must be running the same major version of the embeddable widgets library.
This library appends the _aui_api_version query parameter to the URL it loads into the iframe. Based on this the appropriate library version should be loaded inside of the widget's page. v1.x
for _aui_api_version=1
, and v2.x
for _aui_api_version=2
.
To load multiple versions of this library inside the widget's app, you can use the npm feature to have multiple versions of the same library.
A suggested upgrade strategy in case widget and app are separately hosted:
- Upgrade the widget to interpret
_aui_api_version
and load the appropriate library version. - Upgrade the app to use the new major version of the library.
-
define(definition: object, ?overrides: object): object
Defines a widget from the specified definition (same as the JSON described above) and definition overrides and returns a composed object with everything required to instantiate a component. Object has:
- options: the definition with processed default values
- overrides: the overrides you passed down
- component: a function to pass the properties to and instantiate
Each widget has a unique tag. Each tag can only be defined once in the page, but can be rendered multiple times. However if you want to change the dimensions on the same widget, you will have to redefine one with a new tag.
-
isDefined(tag: string): boolean
Returns true if the widget is already defined in the page.
-
load(url: string, ?overrides: object): Promise<object>
Loads a widget definition from a URL, applies the optional overrides to the loaded definition, then returns a handle to the widget for instantiating.
-
render(tag: string|object, props: object, elem: HTMLElement): object
Renders a previously defined widget with the specified props parameters into the specified element and returns a handle to the instance.
tag
can be the widget instance returned from the load operation, or its tag string. -
renderUrl(url: string, props: object, elem: HTMLElement, ?overrides: object): Promise<object>
Loads a widget definition from URL (if not yet loaded), applies the optional overrides to the loaded definition, then renders it to the specified element with the given props parameters. Returns a promise for the rendered instance.
-
reactComponent(url: string, deps: object, ?overrides: object): object
Creates a React component for the widget with definition hosted at
url
, with the optional overrides applied to that definition. The deps object must contain theReact
andReactDOM
objects provided by React.
The possible attributes for the widget definition:
A tag-name for the component, used for:
- Loading the correct component in the child window or frame
- Generating framework drivers
- Logging
tag: 'my-component-tag'
The URL that will be loaded when the widget is rendered. Can be relative to the JSON's URL or absolute.
url: 'https://example.com/foo-widget'
url: '/foo-widget'
The initial dimensions for the widget, in css-style units, with support for px
or %
.
dimensions: {
width: '300px',
height: '200px'
}
dimensions: {
width: '80%',
height: '90%'
}
Dimensions are set at
define()
time, not atrender()
time. To override dimensions from the defaults they can be passed as a property of theoverrides
argument torenderUrl
. However, this will be ignored on successive calls torenderUrl()
for the same widget. To render the same widget multiple times with different dimensions it is suggested to use width and height 100% and put each widget in a container div that is appropriately sized.
Props that can be passed to the widget when rendering (data or functions).
props: {
onLogin: {
type: 'function'
},
prefilledEmail: {
type: 'string',
required: false
}
}
Sometimes you need to scroll the page outside the iframe to match an element inside the iframe. In order to do this, from inside the widget the scrollTo prop can be called:
this.props.scrollTo(elementOffset, this.props.tag);
There is a default handler for scrollTo
that is provided by the library. You can override it by passing your own implementation as props.scrollTo
, for example, to compensate for header elements. This is the default implementation:
const scrollTo = (elementOffset, tag) => {
const containerElement = document.querySelector(`[id^='zoid-${tag}-']`);
const newTopOffset = containerElement.offsetParent.offsetTop + elementOffset;
window.scrollTo({
top: newTopOffset,
behavior: 'smooth',
});
};
NOTE:
window.scrollTo
is polyfilled by this library.
-
type
string
The data-type expected for the prop
'string'
'number'
'boolean'
'object'
'function'
'array'
-
required
boolean
Whether or not the prop is mandatory. Defaults to
true
.onLogin: { type: 'function', required: false }
-
defaultValue
The default value for the prop if not passed at render time.
required
must be false.fooData: { type: "array", required: false, defaultValue: ["one", "two"] }
This can be any type of value. However if you pass a function, it will be called with
props
as the first argument. So if you want to have a function asdefaultValue
, make sure you wrap it. -
queryParam
boolean | string
Should a prop be passed in the url (so it can influence the routing)?
email: { type: 'string', queryParam: true // ?email=foo@bar.com }
If a string is set, this specifies the url param name which will be used.
email: { type: 'string', queryParam: 'user-email' // ?user-email=foo@bar.com }
-
serialization
string
If
json
, the prop will be JSON stringified before being inserted into the urluser: { type: 'object', serialization: 'json' // ?user={"name":"Zippy","age":34} }
If
dotify
the prop will be converted to dot-notation.user: { type: 'object', serialization: 'dotify' // ?user.name=Zippy&user.age=34 }
If
base64
, the prop will be JSON stringified then base64 encoded before being inserted into the urluser: { type: 'object', serialization: 'base64' // ?user=eyJuYW1lIjoiWmlwcHkiLCJhZ2UiOjM0fQ== }
Makes the container's iframe resize automatically when the child widget window size changes.
autoResize: {
width: false,
height: true,
}
Note that by default it matches the body
element of your content.
You can override this setting by specifying a custom selector as an element
property.
autoResize: {
width: false,
height: true,
element: '.my-selector',
}
Recommended to only use autoResize for height. Width has some strange effects, especially when scroll bars are present.
The default logging level for the widget's internals, helpful for debugging. Options are:
'debug'
'info'
'warn'
(default)'error'
defaultLogLevel: 'info'
Note that this value can be overriden by passing
logLevel
as a prop when rendering the component.
Check the Zoid API documentation for additional properties. The function-based properties can only be specified as overrides, not in the JSON.
-
Run a server publishing the embeddable widgets framework
npm install npm start
-
Point your publisher and container apps to this locally hosted version.
See the contribution guide for additional details.
This framework makes use of the Zoid framework, which implements the boilerplate for embedding apps into other apps using iframes and the postMessage API.
The wrapper is necessary to allow for a different developer experience which is more suited to the needs of Digipolis development projects.
- Zoid loads widget definitions synchronously from a script tag. This framework loads them asynchronously from JSON
- no foreign code needs to execute inside the container app, low-risk
- changes to the JSON's schema are easy to support with framework upgrades
- no globals aside from the widgets framework itself
- Zoid requires the widget to know its own URL's, this doesn't
- The widget does not need to known its own absolute URL, because the JSON can have a relative
url
- The widget framework uses the absolute URL of the JSON passed to
renderUrl
to determine the URL for the widget page itself
- The widget does not need to known its own absolute URL, because the JSON can have a relative
- Zoid supports popup windows, this doesn't
- It adds a lot of code, and it still is very tricky in IE (see zoid documentation)
- All zoid API's are wrapped to allow replacing zoid later on and to support additional logic
- defaultLogLevel = warn, whereas zoid has defaultLogLevel = info (which is spammy)
- Can still be overridden by the widget's JSON
- The framework itself is purely client-side, to allow hosting on a CDN.
Pull requests are always welcome, however keep the following things in mind:
- New features (both breaking and non-breaking) should always be discussed with the repo's owner. If possible, please open an issue first to discuss what you would like to change.
- Fork this repo and issue your fix or new feature via a pull request.
- Please make sure to update tests as appropriate. Also check possible linting errors and update the CHANGELOG if applicable.
Joeri Sebrechts (joeri.sebrechts@digipolis.be)
Copyright (c) 2019-present, Digipolis