docs/building-an-amp-extension.md
AMP can be extended to allow more functionality and components through
building open source extensions (aka custom elements). For example, AMP
provides amp-carousel, amp-sidebar and amp-access as
extensions. If you'd like to add an extension to support your company
video player, rich embed or just a general UI component like a star
rating viewer, you'd do this by building an extension.
This document describes how to create a new AMP extension, which is one of the most common ways of adding a new feature to AMP.
Before diving into the details on creating a new AMP extension, please familiarize yourself with the general process for contributing code and features to AMP. Since you are adding a new extension you will likely need to follow the process for making a significant change, including filing an "Intent to Implement" issue and finding a guide before you start significant development.
To bootstrap the creation of a new component, the following command will create the directory structure and boilerplate code for you:
$ amp make-extension --name=amp-my-element
All AMP extensions (and built-in elements) have their tag names prefixed
with amp-. Make sure to choose an accurate and clear name for your
extension.
Extensions that embed a third-party service must follow the guidelines for naming a third-party component.
You create your extension's files inside the extensions/ directory.
The directory structure is below:
/extensions/amp-my-element/
├── 0.1/
| ├── test/
| | ├── test-amp-my-element.js # Element's unit test suite (req'd)
| | └── More test JS files (optional)
| ├── amp-my-element.js # Element's implementation (req'd)
| ├── amp-my-element.css # Custom CSS (optional)
| └── More JS files (optional)
├── validator-amp-my-element.protoascii # Validator rules (req'd)
├── amp-my-element.md # Element's main documentation (req'd)
└── More documentation in .md files (optional)
└── OWNERS # Owners file. Primary contact(s) for the extension. (req'd)
In most cases you'll only create the required (req'd) files. If your element does not need custom CSS, you don't need to create the CSS file.
Almost all AMP extensions extend AMP.BaseElement, which provides some hookups and callbacks for you to override in order to implement and customize your element behavior. These callbacks are explained below in the BaseElement Callbacks section, and are also explained inline in the BaseElement class.
The following shows the overall structure of your element implementation file (extensions/amp-my-element/0.1/amp-my-element.js).
import {func1, func2} from '../src/module';
import {CSS} from '../../../build/amp-my-element-0.1.css';
// more ES2015-style import statements.
/** @const */
const EXPERIMENT = 'amp-my-element';
/** @const */
const TAG = 'amp-my-element';
class AmpMyElement extends AMP.BaseElement {
/** @param {!AmpElement} element */
constructor(element) {
super(element);
// Declare instance variables with type annotations.
}
/** @override */
isLayoutSupported(layout) {
return layout == LAYOUT.FIXED;
}
/** @override */
buildCallback() {
// Get attributes, assertions of values, assign instance variables.
// Build lightweight DOM and append to this.element.
}
/** @override */
layoutCallback() {
// Actually load your resource or render more expensive resources.
}
}
AMP.extension('amp-my-element', '0.1', (AMP) => {
AMP.registerElement('amp-my-element', AmpMyElement, CSS);
});
You can write a stylesheet to style your element to provide a minimal visual appeal. Your element structure should account for whether you want users (publishers and developers using your element) to customize the default styling you're providing and allow for easy CSS classes and/or well-structure DOM elements.
Element styles are loaded when the element script itself is included in an AMP doc. You tell AMP which CSS belongs to this element when registering the element (see below).
Class names prefixed with i-amphtml are considered private. Publishers
are not allowed to use them for customization (enforced by AMP validator).
Class names prefixed with amp- are public css classes that can be customized
by publishers. All such classes should be documented in the component-specific
.md file. All CSS classes in component stylesheets should be prefixed with
either i-amphtml- or amp-.
Once you have implemented your AMP element, you need to register it with
AMP; all AMP extensions are prefixed with amp-. This is where you
tell AMP which class to use for this tag name and which CSS to load.
AMP.extension('amp-carousel', '0.1', (AMP) => {
AMP.registerElement('amp-carousel', CarouselSelector, CSS);
});
AMP provides a framework for elements to fire their own
events
to allow users of that element to listen and react to the events. For
example, amp-form extension fires a few events on <form> elements
like submit-success. This allow publishers to listen to that event
and react to it, for example, by launching a lightbox to display a
message.
The other part of the event-system in AMP is actions. When listening to
an event on an element usually you'd like to trigger an action (possibly
on other elements). For example, in the example above, the publisher is
executing the open action on lightbox.
The syntax for using this on elements is as follow:
<form
on="submit-success:my-success-lightbox.open;submit-error:my-error-lightbox.open"
></form>
To fire events on your element use AMP's action service and the
.trigger method.
actionServiceForDoc(doc.documentElement).trigger(
this.form_,
'submit-success',
null
);
And to expose actions use registerAction method that your element
inherits from BaseElement.
this.registerAction('close', this.close.bind(this));
Your element could also choose to override the activate method
inherited from BaseElement that would define the default action for your
element. For example amp-lightbox overrides activate to define the open
default case.
Make sure your element documentation documents the events and actions it exposes.
AMP elements are usually discovered and scheduled by the AMP runtime automatically and managed through Resources. In some cases an AMP element might want to control and own when its sub-elements get scheduled and not leave that to the AMP runtime. An example to this is the <amp-carousel> component, where it wants to schedule preloading/pre-rendering or layouting of its cells based on the window the user is in.
AMP provides a way for an element to control this by setting the owner on the element you want to control. In the carousel example, the component loops over all its elements and sets itself as the owner of these elements. The AMP runtime will not manage scheduling layouting for elements that have owners.
this.cells_ = realChildElements(this.element);
this.cells_.forEach((cell) => {
Services.ownersForDoc(this.element).setOwner(cell, this.element);
cell.style.display = 'inline-block';
this.container_.appendChild(cell);
});
An element can then later call schedulePreload or scheduleLayout to
schedule preload or layout respectively. For example, <amp-carousel
type=slider> (Slider instance of amp-carousel) calls
schedulePreload for the next/previous slide when the user moves
forward/backward in the slides and then calls scheduleLayout for the
current slide when the user moves to it.
const owners = Services.ownersForDoc(this.element);
owners.scheduleLayout(this.element, newSlide);
this.setControlsState();
owners.schedulePause(this.element, oldSlide);
owners.schedulePreload(this.element, nextSlide);
It's important to understand that the parent/owner element is responsible for managing all of its children (except for placeholders, see below). This means you need to make sure your element updates whether the child is in viewport and when to schedule different phases for the element.
Your element should anticipate its sub-elements to nest some more amp-elements and schedule preload or layout for these as well, otherwise the element will never be preloaded or laid out. This is true to all nested amp-elements that are not placeholders. AMP runtime will schedule nested amp-elements that are placeholders.
<!-- prettier-ignore-start --><amp-carousel> ← Parent element
<amp-figure> ← Parent needs to schedule this element
<amp-img placeholder></amp-img> ← AMP will schedule this when amp-figure is scheduled
<amp-img></amp-img> ← Parent needs to schedule this element
<amp-fit-text></amp-fit-text> ← Parent needs to schedule this element
</amp-figure>
</amp-carousel>
One of AMP's features is that a document can be checked against validation rules to confirm it's valid AMP. When you implement your element, the AMP Validator needs to be updated to add rules for your element to keep documents using your element valid. Create your own rules by following the directions at Contributing Component Validator Rules.
Another enabling feature of instant-web in AMP is support for prerendering in a way that does not consume loads of data and does not waste too much of the user's device resources. AMP does this by strictly controlling resource loading and rendering.
If your extension is lightweight, it might be worth enabling pre-rendering of your elements so that users will be able to see it appear instantly when they click on an article.
Sometimes fully pre-rendering the element isn't an option because it is heavyweight. Your element might want to opt into creating a dynamic placeholder for itself (in case a placeholder wasn't provided by the developer/publisher who is using your element). This allows elements to display content as fast as possible and allow prerendering that placeholder. Learn more about placeholder elements.
NOTE: Make sure not to request external resources in the pre-render phase. Requests to the publisher's origin itself are OK. If in doubt, please flag this in review.
AMP will automatically call your element's
createPlaceholderCallback during
build step if it didn't detect a placeholder was provided. This allows
you to create your own placeholder. Here's an example of how
amp-instagram element used this callback to create a dynamic
placeholder of an amp-img element to avoid loading the heavyweight
instagram iframe embed during pre-rendering and instead loads just the
image directly from instagram media endpoint.
class AmpInstagram extends AMP.BaseElement {
// ...
/** @override */
createPlaceholderCallback() {
const placeholder = this.win.document.createElement('div');
placeholder.setAttribute('placeholder', '');
const image = this.win.document.createElement('amp-img');
// This is always the same URL that is actually used inside of the embed.
// This lets us avoid loading the image twice and make use of browser cache.
image.setAttribute(
'src',
'https://www.instagram.com/p/' +
encodeURIComponent(this.shortcode_) +
'/media/?size=l'
);
image.setAttribute('width', this.element.getAttribute('width'));
image.setAttribute('height', this.element.getAttribute('height'));
image.setAttribute('layout', 'responsive');
setStyles(image, {
'object-fit': 'cover',
});
const wrapper = this.element.ownerDocument.createElement('wrapper');
// This makes the non-iframe image appear in the exact same spot
// where it will be inside of the iframe.
setStyles(wrapper, {
'position': 'absolute',
'top': '48px',
'bottom': '48px',
'left': '8px',
'right': '8px',
});
wrapper.appendChild(image);
placeholder.appendChild(wrapper);
this.applyFillContent(image);
return placeholder;
}
// …
}
Important: One thing to keep in mind is that when you create a
placeholder, use the amp- provided elements when loading external
resources. This is most likely going to be amp-img like in the
instagram case above. This still allows AMP resource manager to control
when these resources get loaded and rendered as oppose to using the
HTML-native img tag which will be out of AMP resource management.
Consider showing a loading indicator if your element is expected to take
a long time to load (for example, loading a GIF, video or iframe). AMP
has a built-in mechanism to show a loading indicator simply by
listing your element so it's allowed to show it. You can do that inside the layout.js
file in the LOADING_ELEMENTS_ENUM object.
export const LOADING_ELEMENTS_ENUM = {
...
AMP_YOUTUBE: 'AMP-YOUTUBE',
AMP_MY_ELEMENT: 'AMP-MY-ELEMENT',
}
To stay good to our promise of lowering resources usage especially on mobile, elements that create and load heavyweight resources (e.g. iframes, video, very large images, an expensive timer or computation...) need to be destroyed when they are no longer needed.
AMP signals to your element that it needs to do that with unlayoutCallback. AMP calls this when the document becomes inactive; like when the user swipes away from the document to another one or when they switch tabs.
This might be also be called in special cases like if your element is
used as an amp-carousel cell and it was swiped away to become outside
the viewport. This will only happen if your element sets
unlayoutOnPause. Carousel by default only pauses the elements that
are outside its viewport.
Here's an example of how amp-instagram destroys the iframe it has
embedded when unlayoutCallback is called.
/** @override */
unlayoutCallback() {
if (this.iframe_) {
removeElement(this.iframe_);
this.iframe_ = null;
this.iframePromise_ = null;
setStyles(this.placeholderWrapper_, {
'display': '',
});
}
return true; // Call layoutCallback again.
}
Note if your element unlayoutCallback destroys the resources, it
probably wants to return true in order to signal to AMP the need to call
layoutCallback again once the document is active. Otherwise your
element will never be re-laid out.
AMP provides multiple utilities to optimize many mutations and measuring for better performance. These include vsync service with a mutate and measure utility method that will synchronize all measuring happening in short period of time together and then do all the mutating in a requestAnimationFrame or similar cycles.
If your extension needs to load external resources (like an sdk) then you might need to add proper third party integration for it to work and use the proper third party iframe. Loading external resources is only allowed inside a 3p iframe which AMP serves on a different domain for security and performance reasons. Take a look at adding <amp-facebook> extension PR for examples of 3p integration.
Read about Inclusion of third party software, embeds and services into AMP.
For contrast, take a look at amp-instagram which does NOT require an SDK to be loaded in order to embed a post, instead it provides an iframe-based embedding allowing amp-instagram extension to use a normal iframe with no 3p integration needed, similarly, amp-youtube and others.
AMP defines different layouts that elements can choose whether or not to
support Your element needs to announce which layouts it supports through
overriding the isLayoutSupported(layout) callback and returning true
if the element supports that layout. Read more about AMP Layout
System
and Layout
Types.
After understanding each layout type, if it makes sense, support all of
them. Otherwise choose what makes sense to your element. A popular
support choice is to support size-defined layouts (Fixed, Fixed Height,
Responsive and Fill) through using the utility isLayoutSizeDefined
in layout.js.
For example, amp-pixel only supports fixed layout.
class AmpPixel extends BaseElement {
/** @override */
isLayoutSupported(layout) {
return layout == Layout.FIXED;
}
}
While amp-carousel supports all size-defined layouts.
class AmpSlides extends AMP.BaseElement {
/** @override */
isLayoutSupported(layout) {
return isLayoutSizeDefined(layout);
}
}
Most newly created elements are initially launched as experiments. This allows people to experiment with using the new element and provide the author(s) with feedback. It also provides the AMP Team with the opportunity to monitor for any potential errors. This is especially required if the validator hasn't been updated yet to allow your newly created extension, otherwise people using it in production will invalidate all their AMP documents.
Add your extension as an experiment in the
amphtml/tools/experiments file by adding a record for your extension
in EXPERIMENTS variable.
/** @const {!Array<!ExperimentDef>} */
const EXPERIMENTS = [
// ...
{
id: 'amp-my-element',
name: 'AMP My Element',
spec:
'https://github.com/ampproject/amphtml/blob/main/extensions/' +
'amp-my-element/amp-my-element.md',
cleanupIssue: 'https://github.com/ampproject/amphtml/issues/XXXYYY',
},
// ...
];
And then protecting your code with a check isExperimentOn(win, 'amp-my-element') and only execute your code when it is on.
import {isExperimentOn} from '../../../src/experiments';
import {userAssert} from '../../../src/log';
/** @const */
const EXPERIMENT = 'amp-my-element';
/** @const */
const TAG = 'amp-my-element';
Class AmpMyElement extends AMP.BaseElement {
/** @param {!AmpElement} element */
constructor(element) {
super(element);
// declare instance variables with type annotations.
}
/** @override */
isLayoutSupported(layout) {
return layout == LAYOUT.FIXED;
}
/** @override */
buildCallback() {
userAssert(isExperimentOn(this.win, 'amp-my-element'),
`Experiment ${EXPERIMENT} is not turned on.`);
// get attributes, assertions of values, assign instance variables.
// build lightweight dom and append to this.element.
}
/** @override */
layoutCallback() {
userAssert(isExperimentOn(this.win, 'amp-my-element'),
`Experiment ${EXPERIMENT} is not turned on.`);
// actually load your resource or render more expensive resources.
}
}
AMP.extension('amp-my-element', '0.1', AMP => {
AMP.registerElement('amp-my-element', AmpMyElement, CSS);
});
Users wanting to experiment with your element can then go to the experiments page and enable your experiment.
If you are testing on your localhost, use the command AMP.toggleExperiment(id, true/false) to enable the experiment.
File a github issue to cleanup your experiment. Assign it to yourself as a reminder to remove your experiment and code checks. Removal of your experiment happens after the extension has been thoroughly tested and all issues have been addressed.
Create a .md file that serves as the main documentation for your element. This document should include:
For samples of element documentation, see: amp-list, amp-instagram, amp-carousel
This greatly helps users to understand and demonstrate how
your element works, and provides an easy start-point for them to
experiment with it. This is basically where you actually build an AMP
HTML document and use your element in it by creating a file in the
examples/ directory, usually with the my-element.amp.html file
name. Browse that directory to see examples for other elements and
extensions.
Also consider contributing an example to amp.dev on GitHub.
In order for your element to build correctly you would need to make few
changes to build-system/compile/bundles.config.extensions.json to tell it about your
extension, its files and its examples. You will need to add an entry in the top-level array.
exports.extensionBundles = [
...
{name: 'amp-kaltura-player', version: '0.1'},
{name: 'amp-carousel', version: '0.1', options: {hasCss: true}},
...
];
AMP runtime is currently in v0 major version. Extensions versions are maintained separately. If your changes to your non-experimental extension makes breaking changes that are not backward compatible you should version your extension. This would usually be by creating a 0.2 directory next to your 0.1.
If your extension is still in experiments breaking changes usually are fine so you can just update the same version.
Make sure you write good coverage for your code. We require unit tests for all checked in code. We use the following frameworks for testing:
For faster testing during development, consider using --files argument to only run your extensions' tests.
$ amp unit --files=extensions/amp-my-element/0.1/test/test-amp-my-element.js --watch
We use Closure Compiler to perform type checking. Please see Annotating JavaScript for the Closure Compiler and existing AMP code for examples of how to add type annotations to your code. The following command should be run to ensure no type violations are introduced by your extension.
$ amp check-types