You should first read through the guide to Building a Bento AMP Extension. Do not follow the steps to generate an extension, since they're specified here. Once you're familiar with the concepts related to AMP extensions and Bento components, follow this guide instead.
- How Iframe Components Work
- Getting Started
- Directory Structure
- Define a Preact component
- Completing your extension
- Example Pull Requests
A number of AMP components use iframes to load external resources while staying compliant to AMP's performance considerations, such as enforcing stable layouts whenever possible and pausing resources based on document state. For this reason, Bento provides a generic iframe component to handle many of these resource considerations so that component implementation can focus on the feature set being provided.
Preact components can get this behavior by using an IframeEmbed
that renders an iframe and propagates props accordingly:
return <IframeEmbed frameborder="no" scrolling="no" title="My iframe" {...props} />
Some components may additionally load external resources, such as an SDK, to enable a third party integration. AMP serves this on a different domain for security and performance reasons, and Bento provides ProxyIframeEmbed
to additionally wrap IframeEmbed
with an intermediary bootstrapping iframe.
Preact components can get this behavior by using an ProxyIframeEmbed
that renders an iframe and propagates props accordingly:
return <ProxyIframeEmbed frameborder="no" scrolling="no" title="My third party iframe" {...props} />
One important consideration is that direct iframes, such as those provided by IframeEmbed
and VideoIframe
, are not the same as a proxy iframe, which provides an additional layer of communication between an iframe and the document. If it is not clear which is the appropriate helper for your component, your guide can help identify the best one to use.
Start by generating an extension specifying --bento
and --nojss
. We name our extension amp-fantastic-embed
, according to our guidelines for naming a third-party component.
amp make-extension --bento --nojss --name=amp-fantastic-embed
A full directory for a Bento component is generated, but this guide will cover the following file in particular:
/extensions/amp-fantastic-embed/1.0/
├── amp-my-fantastic-player.js # Element's implementation
└── component.js # Preact implementation
If you need to directly insert nodes to the document, like a <iframe>
element, you need to use an <IframeEmbed>
. If you need to load a third-party iframe, you should use a <ProxyIframeEmbed>
as opposed to an <IframeEmbed>
.
Your FantasticEmbed
component should return an IframeEmbed
that's configured to a corresponding postMessage
API. To start, we update the implementation in component.js
:
- import {ContainWrapper} from '#preact/component';
+ import {IframeEmbed} from '#preact/iframe';
function FantasticEmbedWithRef({...rest}, ref) {
- ...
+ const src = 'https://example.com/fantastic';
+ const messageHandler = useCallback((e) => {
+ console.log(e);
+ }, []);
return (
- <ContainWrapper layout size paint {...rest} >
- ...
- </ContainWrapper>
+ <IframeEmbed
+ ref={ref}
+ {...rest}
+ src={src}
+ messageHandler={messageHandler}
+ />
);
}
So that our component returns an <IframeEmbed>
:
// component.js
// ...
import {IframeEmbed} from '#preact/component/iframe';
// ...
function FantasticPlayerWithRef({...rest}, ref) {
const src = 'https://example.com/fantastic';
const onMessage = useCallback((e) => {
console.log(e);
}, []);
return (
<IframeEmbed
ref={ref}
{...rest}
src={src}
messageHandler={messageHandler}
/>
);
}
We're rendering an iframe that always loads https://example.com/fantastic
, but we'll specify a dynamic URL later. Likewise, we'll need to define implementations for the communication function messageHandler
.
You may use props to construct the src
, like using a appId
to load https://example.com/fantastic/${appId}/
.
We employ the useMemo()
hook so that the src
is generated only when the appId
changes:
// component.js
// ...
function FantasticEmbedWithRef(
{appId, ...rest},
ref
) {
// ...
const src = useMemo(
() =>
`https://example.com/fantastic/${encodeURIComponent(appId)}/`,
[appId]
);
// ...
return (
<IframeEmbed
{...rest}
src={src}
...
/>
);
}
Upstream events originated by the iframe are received as messages. You should define a function that interprets these messages and responds accordingly.
Here we listen for measure events for an iframe that posts them as the following message structure:
{"event": {
"data" : {
"type": "MEASURE",
"details": {
"height": ___
}
}
}
}
The component, which may be instantiated with a static height, can then resize once it receives the message with a fresh height
value.
// component.js
// ...
function messageHandler(event) {
const {data} = event;
if (data['type'] == 'MEASURE' && data['details']) {
const height = data['details']['height'];
// use the height to resize.
}
}
function FantasticEmbedWithRef(
{appId, ...rest},
ref
) {
// ...
return (
<IframeEmbed
{...rest}
messageHandler={messageHandler}
...
/>
);
}
Your iframe's interface to post messages is likely different, but your component should always handle these events via the messageHandler
.
If you FantasticEmbed
component uses third party resources such as an SDK, then it should return a ProxyIframeEmbed
that's configured to a corresponding postMessage
API. To start, we update the implementation in component.js
.
- import {ContainWrapper} from '#preact/component';
+ import {ProxyIframeEmbed} from '#preact/component/3p-frame';
function FantasticEmbedWithRef({...rest}, ref) {
- ...
return (
- <ContainWrapper layout size paint {...rest} >
- ...
- </ContainWrapper>
+ <ProxyIframeEmbed ref={ref} {...rest} />
);
}
So that our component returns a <ProxyIframeEmbed>
:
// component.js
// ...
+ import {ProxyIframeEmbed} from '#preact/component/3p-frame';
// ...
function FantasticEmbedWithRef({...rest}, ref) {
return <ProxyIframeEmbed ref={ref} {...rest}/>;
}
AMP documents additionally guarantee layout stability to the degree that it manages when components may or may not resize on the page. Because of this, the IframeEmbed
component takes a requestResize
prop where a different flow of logic may be passed in by the publisher to respond to measure events.
In your AMP element implementation, you will use requestResize
to pass in the attemptChangeHeight
method that is extended from the PreactBaseElement
class:
// amp-fantastic-embed.js
// ...
class AmpFantasticEmbed extends BaseElement {
/** @override */
init() {
return {
'requestResize': (height) => this.attemptChangeHeight(height),
};
}
}
For components that request a resize that is denied by the AMP runtime, publishers are recommended to use an overflow
element to solicit user interaction in order to resize as a layout stability best-practice.
This information can be provided in the component documentation with an exemplary code sample:
<amp-fantastic-embed layout="fixed" width="400" height="200">
<button overflow>Click me to load the full iframed content!</button>
</amp-fantastic-embed>
<p>Content below the component.</p>
In the previous example, props received from the ProxyIframeEmbed
are implicitly set through ...rest
. If we set each explicitly, we see the HTMLIframeElement
attributes handled.
// component.js
// ...
function FantasticEmbedInternalWithRef(
{
allow,
allowFullScreen,
frameborder,
loading,
name,
sandbox,
scrolling,
src,
title,
},
ref
) {
return (
<div ref={ref} style={style}>
<iframe
allow={allow}
allowFullScreen={allowFullScreen}
frameborder="0"
loading={loading}
name={name}
part="iframe"
ref={iframeRef}
sandbox={sandbox}
scrolling="no"
src={src}
title={title}
/>
</div>
);
}
If you need to pass
style
orref
to the underlying iframe, these are exceptional in that they are propagated to the outerContainWrapper
which parents theiframe
element. You should useiframeStyle
oriframeRef
accordingly to pass inline styles and refs.
You may similarly choose to pass or override properties at the higher level, passed from FantasticEmbed
into the ProxyIframeEmbed
we instantiate. For a list of these properties see component.type.js
All Bento components implemented using iframes should utilize placeholders to mitigate the risk of poor perceived performance. Placeholders, Fallbacks, and Loaders can be toggled on/off using the hooks provided to the Preact Component. See here for detailed instructions on how to toggle placeholders/fallbacks/loaders.
It is encouraged to create a Storybook story that specifically demonstrates utilization of placeholders and fallbacks.
Follow the guide to Building a Bento AMP Component for other instructions that you should complete, including:
- Documentation that describes the component.
- Tests that verify the component's functionality.
- Validator rules to embed the component in an AMP document.
- An example to our Storybook or to be published on amp.dev
- Iframe embed:
- Third party iframe: