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.

What are we going to be building?

In this codelab, you'll create a bare bones Hello World component, run tests against it, and add validation rules.

What you'll need

Get the code

Clone the amphtml repository from GitHub:

git clone git://github.com/ampproject/amphtml.git
cd amphtml
git checkout -b amp-hello-world
yarn install

Generate the scaffolding

Run the following gulp command to generate the basic scaffolding:

gulp make-extension --name amp-hello-world 

This will generate the following files:

We will get into the details of these files in the next section.

Declare the extension

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 line (preferably alphabetically sorted):

{name: 'amp-hello-world', version: '0.1', type: TYPES.MISC}, 

This object defines the compile options:

Generate the JavaScript binary and serving

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

What does the code do?

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.registerElement('amp-hello-world', AmpHelloWorld);

Run single extension test

To run our test against our source code, execute the following command:

gulp test --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".

What does this test code do?

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';

describes.realWin('amp-hello-world', {
  amp: {
    extensions: ['amp-hello-world'],
  }
}, env => {

  let win;
  let element;

  beforeEach(() => {
    win = env.win;
    element = win.document.createElement('amp-hello-world');
    win.document.body.appendChild(element);
  });

  it('should have hello world when built', () => {
    element.build();
    expect(element.querySelector('div').textContent).to.equal('hello world');
  });
});

Understanding the simple validation specification

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

This is the validation spec that gets generated by the make-extension command, but we'll expand it later.

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
  }
}

Running the validator

gulp validator

This will run all validator tests. At the moment, all tests should pass since we haven't modified any.

Modifying the validation specification

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

Resource loading

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_ = this.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://s.codepen.io/aaoo/debug/wExmMK/mWAoNxzKKpar?';
    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.registerElement('amp-hello-world', AmpHelloWorld);

layoutCallback

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.

isLayoutSupported

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.

LifeCycle hooks

Read about all of AMP's life cycle hooks with a more detailed explanation at https://docs.google.com/document/d/19o7eDta6oqPGF4RQ17LvZ9CHVQN53whN-mCIeIMM8Qk.

Developing and Contributing

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

Closure Annotations

To get our code to correctly type check: https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler