AMP is a way to build pages that render fast. AMP extensions use the power of custom elements and allows us to create new components to enhance AMP HTML pages.
In this codelab, you'll create a bare bones Hello World component, run tests against it, and add validation rules.
yarn global add gulp
or npm install -g gulp
Clone the amphtml repository from GitHub:
git clone git://github.com/ampproject/amphtml.git cd amphtml git checkout -b amp-hello-world yarn install
Run the following gulp command to generate the basic scaffolding:
gulp make-extension --name amp-hello-world
This will generate the following files:
extensions/amp-hello-world/0.1/amp-hello-world.js
- Extension source codeextensions/amp-hello-world/0.1/test/test-amp-hello-world.js
- Test fileextensions/amp-hello-world/amp-hello-world.md
- Documentation fileextensions/amp-hello-world/0.1/validator-amp-hello-world.protoascii
- validation definitionextensions/amp-hello-world/0.1/test/validator-amp-hello-world.html
- Validation test inputextensions/amp-hello-world/0.1/test/validator-amp-hello-world.out
- Validation test outputexamples/amp-hello-world.amp.html
- Example fileWe will get into the details of these files in the next section.
To build our amp-hello-world
extension, we need to tell our build pipeline about it. We'll do this by declaring the extension in the bundles.config.js
file.
Look for the declaration of exports.extensionBundles
and insert the following object (preferably alphabetically by name
):
{
name: 'amp-hello-world',
version: '0.1',
latestVersion: '0.1',
type: TYPES.MISC,
}
This object defines the compile options:
amp-hello-world
)version
(0.1
),type
(TYPES.MISC
as a sane default)We need to generate the development version of the compiled binary and spin up a localhost webserver where we can preview our example file. Just run the default gulp
command like so:
gulp
Once you see a line that says "Finished 'default' after..."
that means that gulp has finished building the files and serving the example file. You can now open your web browser and head over to http://localhost:8000/examples/amp-hello-world.amp.html to see our AMP extension in action.
The gulp
command will compile all AMP extensions. For the purposes of this codelab you can choose to only compile our created extension to speed-up startup time. Use the following command:
gulp --extensions=amp-hello-world
Now this isn't really very exciting and it seems very complicated than just writing "hello world" in HTML, but AMP extensions gain a lot of power from Web Components, especially custom elements which gives AMP the power of resource management and resource discovery (just to name a few examples).
Looking at the code below, we inherit from AMP.BaseElement
which provides a lot of the resource management in AMP. You can think of resource management here as the mechanism that controls what should be visible to optimize resources (CPU cycles) on a page and what should be visible soon.
We initialize properties in the constructor
of the class, but always do element manipulation on the buildCallback
(one of AMP's lifecycle hooks). The isLayoutSupported
method here tells the component the valid layout types allowed for this component. See https://github.com/ampproject/amphtml/blob/master/spec/amp-html-layout.md for further reading on the different layouts allowed for AMP component and their differences.
Finally, we register the AMPHelloWorld
element with AMP.registerElement
as the backing class of the amp-hello-world
custom element.
import {Layout} from '../../../src/layout';
export class AmpHelloWorld extends AMP.BaseElement {
/** @param {!AmpElement} element */
constructor(element) {
super(element);
/** @private {string} */
this.myText_ = 'hello world';
/** @private {?Element} */
this.container_ = null;
}
/** @override */
buildCallback() {
this.container_ = this.element.ownerDocument.createElement('div');
this.container_.textContent = this.myText_;
this.element.appendChild(this.container_);
this.applyFillContent(this.container_, /* replacedContent */ true);
}
/** @override */
isLayoutSupported(layout) {
return layout == Layout.RESPONSIVE;
}
}
AMP.extension('amp-hello-world', '0.1', (AMP) => {
AMP.registerElement('amp-hello-world', AmpHelloWorld);
});
To run our test against our source code, execute the following command:
gulp unit --files extensions/amp-hello-world/0.1/test/test-amp-hello-world.js
If the test was successful, you should see "Executed 1 of 1 SUCCESS"
.
Let's explain what the code is doing. The code below imports our extension source code and creates an instance of it while manually triggering the buildCallback
to see if it displays "hello world".
AMP has a layer of testing infrastructure called "describes" (instead of describe) on top of mocha to provide some basic configuration AMP needs for test isolation and testing the code in different environments that AMP aims to support.
import {AmpHelloWorld} from '../amp-hello-world';
import {createElementWithAttributes} from '../../../../src/dom';
describes.realWin(
'amp-hello-world',
{
amp: {
extensions: ['amp-hello-world'],
},
},
(env) => {
let win;
let element;
beforeEach(() => {
win = env.win;
element = createElementWithAttributes(win.document, 'amp-hello-world', {
layout: 'responsive',
});
win.document.body.appendChild(element);
});
it('should have hello world when built', () => {
element.build();
expect(element.querySelector('div').textContent).to.equal('hello world');
});
}
);
The AMP Project infrastructure will run a set of checks against your pull request that should be passed in order for your code to be checked in. You can run these checks locally to fix issues:
gulp check-all --files extensions/amp-hello-world/0.1/amp-hello-world.js
You can also run each check individually:
gulp lint --files extensions/amp-hello-world/0.1/amp-hello-world.js gulp check-types --files extensions/amp-hello-world/0.1/amp-hello-world.js gulp presubmit --files extensions/amp-hello-world/0.1/amp-hello-world.js
We'll use a very simple validation spec that defines our new amp-hello-world
custom element.
It specifies that amp-hello-world
is a valid tag and that it needs the amp-hello-world
javascript in head and must have the async
attribute.
You can read the full specification and allowed definitions at https://github.com/ampproject/amphtml/blob/master/validator/validator.proto
Installation guide at
https://github.com/ampproject/amphtml/blob/master/validator/README.md#installation
This is the validation spec that gets generated by the make-extension
command, but we'll expand it later. It is found in validator-amp-hello-world.protoascii
.
tags: { # amp-hello-world
html_format: AMP
tag_name: "SCRIPT"
extension_spec: {
name: "amp-hello-world"
version: "0.1"
version: "latest"
}
attr_lists: "common-extension-attrs"
}
tags: { # <amp-hello-world>
html_format: AMP
tag_name: "AMP-HELLO-WORLD"
requires_extension: "amp-hello-world"
attr_lists: "extended-amp-global"
spec_url: "https://www.ampproject.org/docs/reference/components/amp-hello-world"
amp_layout: {
supported_layouts: RESPONSIVE
}
}
gulp validator
This will run all validator tests. At the moment, all tests should pass since we haven't modified any.
We'll extend the default validation spec by supporting the text
attribute, which we'll use later. We'll also allow all size-defined layouts to be used in the component.
tags: { # amp-hello-world
html_format: AMP
tag_name: "SCRIPT"
extension_spec: {
name: "amp-hello-world"
version: "0.1"
version: "latest"
}
attr_lists: "common-extension-attrs"
}
tags: { # <amp-hello-world>
html_format: AMP
tag_name: "AMP-HELLO-WORLD"
requires_extension: "amp-hello-world"
attr_lists: "extended-amp-global"
attrs: {
name: "text"
}
spec_url: "https://www.ampproject.org/docs/reference/components/amp-hello-world"
amp_layout: {
supported_layouts: FILL
supported_layouts: FIXED
supported_layouts: FIXED_HEIGHT
supported_layouts: FLEX_ITEM
supported_layouts: RESPONSIVE
}
}
Modify the validation test input in validator-amp-hello-world.html
so that the amp-hello-world
component takes a text
param.
<amp-hello-world layout="responsive" width="150" height="80" text="Hello World"> </amp-hello-world>
You don't need to update the validation test output manually. You can run the following command to update the validator-amp-hello-world.out
file.
gulp validator --update_tests
Let's actually participate in the resource management by loading an embed widget that will render an animation using the string provided in the text
attribute.
Note that we also need to change the value of myText_
on initialization so that it takes the value provided in the text
attribute.
import {isLayoutSizeDefined} from '../../../src/layout';
export class AmpHelloWorld extends AMP.BaseElement {
/** @param {!AmpElement} element */
constructor(element) {
super(element);
/** @private {string} */
this.myText_ = element.getAttribute('text');
/** @private {?Element} */
this.container_ = null;
}
/** @override */
buildCallback() {
this.container_ = this.element.ownerDocument.createElement('div');
this.container_.textContent = this.myText_;
this.element.appendChild(this.container_);
this.applyFillContent(this.container_, /* replacedContent */ true);
}
/** @override */
isLayoutSupported(layout) {
return isLayoutSizeDefined(layout);
}
/** @override */
layoutCallback() {
// Set frame URL to an embed endpoint.
const frameUrlPrefix = 'https://amp-hello-world-embed.netlify.app/?';
const frameUrl = frameUrlPrefix + this.myText_;
const iframe = this.element.ownerDocument.createElement('iframe');
iframe.src = frameUrl;
// Clear text content set on buildCallback.
this.container_.textContent = '';
// applyFillContent so that frame covers the entire component.
this.applyFillContent(iframe, /* replacedContent */ true);
this.container_.appendChild(iframe);
// Return a load promise for the frame so the runtime knows when the
// component is ready.
return this.loadPromise(iframe);
}
}
AMP.extension('amp-hello-world', '0.1', (AMP) => {
AMP.registerElement('amp-hello-world', AmpHelloWorld);
});
This is executed by the runtime once it can schedule the component's resources to load. This typically occurs as the user scrolls the document and the component gets closer to the viewport area or once the component becomes visible.
In this case, layoutCallback
constructs a frame that loads the embed's content by URL (the query string is the text to render). We return a loadPromise
that gets resolved once the frame loads. This is an important signal for the resource manager that is used for tasks like changing visual loading state or subsequent resource scheduling.
This is needed to validate component layout configuration on runtime. It's changed from the default extension code to use the isLayoutSizeDefined
helper to check validity. Note that the import statement at the beginning of our code now declares a dependency on this helper.
We've only really scratched the surface here of what you can do in an AMP components and you can find below more detailed documentation on lifecycle hooks and developing in AMP.
Read about all of AMP's life cycle hooks with a more detailed explanation at https://docs.google.com/document/d/19o7eDta6oqPGF4RQ17LvZ9CHVQN53whN-mCIeIMM8Qk.
Read more about how to develop and contribute in AMP at https://github.com/ampproject/amphtml/blob/master/contributing/DEVELOPING.md and https://github.com/ampproject/amphtml/blob/master/CONTRIBUTING.md
To get our code to correctly type check: https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler