AMP is a way to build pages for static content 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
npm 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 gulp build pipeline about it. We'll do this by declaring the extension in the gulpfile.js.

Look for a large block of declareExtensions and add the following line (preferably alphabetically sorted):

declareExtension('amp-hello-world', '0.1', false);

The arguments are the compile options--the name of the extension "amp-hello-world", the extension's version "0.1", and whether it has CSS bundled with it (it doesn't, for now).

Generate the JavaScript binary and serving

We need to generate the development version of the compiled binary and also spin up a localhost web server 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.max.html to see our AMP extension in action.

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_ = this.win.document.createElement('div');
  }

  /** @override */
  buildCallback() {
    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');
  });
});

Add simple validation specification

We add 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

tags: {  # amp-hello-world
  html_format: AMP
  tag_name: "SCRIPT"
  satisfies: "amp-hello-world extension .js script"
  requires: "amp-hello-world"
  extension_spec: {
    name: "hello-world"
    allowed_versions: "0.1"
    allowed_versions: "latest"
  }
  attr_lists: "common-extension-attrs"
}
tags: {  # <amp-hello-world>
  html_format: AMP
  tag_name: "AMP-HELLO-WORLD"
  satisfies: "amp-hello-world"
  requires: "amp-hello-world extension .js script"
  attr_lists: "extended-amp-global"
  spec_url: "https://www.ampproject.org/docs/reference/components/amp-hello-world"
  amp_layout: {
    supported_layouts: CONTAINER
    supported_layouts: RESPONSIVE
  }
}

Running the validator

gulp validator

Viewport loading

Let's actually participate in the resource management by adding the text when the element is in viewport and removing the text when it is not in viewport.

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_ = this.win.document.createElement('div');
  }

  /** @override */
  buildCallback() {
    this.container_.textContent = this.myText_;
    this.element.appendChild(this.container_);
    this.applyFillContent(this.container_, /* replacedContent */ true);
  }

  /** @override */
  isLayoutSupported(layout) {
    return layout == Layout.RESPONSIVE;
  }

  /** @override */
  viewportCallback() {
    if (this.isInViewport()) {
      this.container_.textContent = this.myText_;
    } else {
      this.container_.textContent = '';
    }
  }

  /** @override */
  isRelayoutNeeded() {
    return true;
  }
}

AMP.registerElement('amp-hello-world', AmpHelloWorld);

isRelayoutNeeded

We tell the component system that it can call the viewportCallback multiple times (when it goes in and out of viewport). We simply do return true.

viewportCallback

In the viewportCallback we call isInViewport and set the container's text if it is and empty it out if it isn't.

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