Skip to content

Commit

Permalink
Add documentation for contributing a Bento iframe component (ampproje…
Browse files Browse the repository at this point in the history
…ct#34911)

* Add documentation

* Add note about direct vs. proxy iframe

* Fix dead links

* Remove .* from GH allowlist

* Fix pattern to ignore GitHub PRs and issues

Co-authored-by: Raghu Simha <[email protected]>
  • Loading branch information
caroqliu and rsimha authored Jun 24, 2021
1 parent f96f46b commit 5e85bd1
Show file tree
Hide file tree
Showing 4 changed files with 319 additions and 6 deletions.
5 changes: 1 addition & 4 deletions build-system/tasks/check-links.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,7 @@ function checkLinksInFile(file) {
// codepen returns a 503 for these link checks
{pattern: /https:\/\/codepen.*/},
// GitHub PRs and Issues can be assumed to exist
{
pattern:
/https:\/\/github.com\/ampproject\/amphtml\/(pull|issue)\/d+.*/,
},
{pattern: /https:\/\/github.com\/ampproject\/amphtml\/(pull|issue)\/.*/},
// Templated links are merely used to generate other markdown files.
{pattern: /\$\{[a-z]*\}/},
{pattern: /https:.*?__component_name\w*__/},
Expand Down
2 changes: 1 addition & 1 deletion docs/building-a-bento-amp-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ $ amp check-types
- [amp-video](https://github.com/ampproject/amphtml/pull/30280)
- [amp-vimeo](https://github.com/ampproject/amphtml/pull/33971)
- [amp-youtube](https://github.com/ampproject/amphtml/pull/30444)
- Adding iframe based embeds
- Adding iframe based embeds [You may also follow the guide to Building a Bento Video Component.](./building-a-bento-iframe-component.md)
- [amp-instagram](https://github.com/ampproject/amphtml/pull/30230)
- Adding third party iframe based embeds
- [amp-facebook](https://github.com/ampproject/amphtml/pull/34585)
Expand Down
316 changes: 316 additions & 0 deletions docs/building-a-bento-iframe-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
# Building a Bento Iframe Component

> **You should first read through the [guide to Building a Bento AMP Extension](./building-a-bento-amp-extension.md).** 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.
<!--
(Do not remove or edit this comment.)
This table-of-contents is automatically generated. To generate it, run:
amp markdown-toc --fix
-->

<!-- {"maxdepth": 3} -->

- [How Iframe Components Work](#how-iframe-components-work)
- [Getting Started](#getting-started)
- [Directory Structure](#directory-structure)
- [Define a Preact component](#define-a-preact-component)
- [Loading an iframe with `IframeEmbed`](#loading-an-iframe-with-iframeembed)
- [`src`](#src)
- [Handling events with `messageHandler`](#handling-events-with-messagehandler)
- [Resizing AMP components](#handling-events-with-messagehandler)
- [Use `ProxyIframeEmbed` directly](#use-proxyiframeembed-directly)
- [Passing or overriding props](#passing-or-overriding-props)
- [Completing your extension](#completing-your-extension)
- [Example Pull Requests](#example-pull-requests)

## How Iframe Components Work

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:

```js
return <IframeEmbed frameborder="no" scrolling="no" title="My iframe" {...props} />
```

Some components may additionally [load external resources](./building-a-bento-amp-extension.md#loading-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:

```js
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`](./building-a-bento-video-player.md#loading-an-iframe-with-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](./contributing-code.md#find-a-guide) can help identify the best one to use.

## Getting Started

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](./spec/amp-3p-naming.md).

```console
amp make-extension --bento --nojss --name=amp-fantastic-embed
```

## Directory Structure

A [full directory for a Bento component](./building-a-bento-amp-extension.md#directory-structure) 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
```

## Define a Preact component

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>`.

### Loading an iframe with `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`**:

```diff
- 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>`:

```js
// 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`.

#### `src`

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:

```js
// component.js
// ...
function FantasticEmbedWithRef(
{appId, ...rest},
ref
) {
// ...
const src = useMemo(
() =>
`https://example.com/fantastic/${encodeURIComponent(appId)}/`,
[appId]
);
// ...
return (
<IframeEmbed
{...rest}
src={src}
...
/>
);
}
```

#### Handling events with `messageHandler`

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.

```js
// 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`.

### Use `ProxyIframeEmbed` directly

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`**.

```diff
- 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>`:

```js
// component.js
// ...
+ import {ProxyIframeEmbed} from '#preact/component/3p-frame';

// ...
function FantasticEmbedWithRef({...rest}, ref) {
return <ProxyIframeEmbed ref={ref} {...rest}/>;
}
```

#### Resizing components in AMP

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 `BaseElement` class:

```javascript
// amp-fantastic-embed.js
// ...
class AmpFantasticEmbed extends BaseElement {
/** @override */
init() {
return dict({
'requestResize': (height) => {
this.attemptChangeHeight(height);
},
});
}
}
```

#### Passing or overriding props

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.

```js
// component.js
// ...
function FantasticEmbedInternalWithRef(
{
allow,
allowFullScreen,
allowTransparency,
frameborder,
loading,
name,
sandbox,
scrolling,
src,
title,
},
ref
) {
return (
<div ref={ref} style={style}>
<iframe
allow={allow}
allowFullScreen={allowFullScreen}
allowTransparency={allowTransparency}
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` or `ref` to the underlying iframe, these are exceptional in that they are propagated to the outer `ContainWrapper` which parents the `iframe` element. You should use `iframeStyle` or `iframeRef` 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`](../src/preact/component/component.type.js)

## Completing your extension

Follow the [guide to Building a Bento AMP Component](./building-a-bento-amp-extension.md) 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](https://amp.dev/)

## Example Pull Requests

- Iframe embed:
- [amp-instagram](https://github.com/ampproject/amphtml/pull/30230)
- [amp-soundcloud](https://github.com/ampproject/amphtml/pull/34828)
- Third party iframe:
- [amp-facebook](https://github.com/ampproject/amphtml/pull/34585)
- [amp-twitter](https://github.com/ampproject/amphtml/pull/33335)
2 changes: 1 addition & 1 deletion docs/building-a-bento-video-player.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ If you need to directly insert nodes to the document, like a `<video>` element,

However, it's more likely that you load a third-party iframe and you communicate with the host via `postMessage`. In this case you should use a `<VideoIframe>` as opposed to a `<VideoWrapper>`.

> ⚠️ Components may **not** embed scripts from a third-party location into host documents. If a third-party script is absolutely required, like on `<amp-ima-video>`, it must be inserted in an intermediate iframe, which we call a **proxy frame**.
> ⚠️ Components may **not** embed scripts from a third-party location into host documents. If a third-party script is absolutely required, like on `<amp-ima-video>`, it must be inserted in an intermediate iframe, which we call a [**proxy frame**](./building-a-bento-iframe-component.md).
>
> Proxy frames on Bento have not yet been tested as video player components, so they're not covered in this guide. If you wish to use one, please get in touch with `@alanorozco` via a Github issue or on on [Slack](https://bit.ly/amp-slack-signup) in the [`#contributing` channel](https://amphtml.slack.com/messages/C9HRJ1GPN/).
Expand Down

0 comments on commit 5e85bd1

Please sign in to comment.