Skip to content

Commit

Permalink
Refactor templates into service/ref pattern (#3682)
Browse files Browse the repository at this point in the history
* Refactor templates into service/ref pattern

* docs

* fixed tests
  • Loading branch information
dvoytenko authored Jun 21, 2016
1 parent 82a52c8 commit de2fb5f
Show file tree
Hide file tree
Showing 5 changed files with 364 additions and 336 deletions.
4 changes: 2 additions & 2 deletions src/amp.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import './polyfills';
import {installPerformanceService} from './service/performance-impl';
import {installPullToRefreshBlocker} from './pull-to-refresh';
import {templatesFor} from './template';
import {installTemplatesService} from './service/template-impl';
import {installCoreServices} from './amp-core-service';
import {installGlobalClickListener} from './document-click';
import {installGlobalSubmitListener} from './document-submit';
Expand Down Expand Up @@ -52,7 +52,7 @@ try {
// We need the core services (viewer/resources) to start instrumenting
perf.coreServicesAvailable();
maybeTrackImpression(window);
templatesFor(window);
installTemplatesService(window);

installImg(window);
installPixel(window);
Expand Down
2 changes: 1 addition & 1 deletion src/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import {BaseElement} from './base-element';
import {BaseTemplate, registerExtendedTemplate} from './template';
import {BaseTemplate, registerExtendedTemplate} from './service/template-impl';
import {dev} from './log';
import {getMode} from './mode';
import {getService} from './service';
Expand Down
355 changes: 355 additions & 0 deletions src/service/template-impl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
/**
* Copyright 2016 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {childElementByTag} from '../dom';
import {getService} from '../service';
import {user} from '../log';


/**
* @fileoverview
* For the set of decisions made on templating see:
* {@link https://docs.google.com/document/d/1q-5MPQHnOHLF_uL7lQsGZdzuBgrPTkCy2PdRP-YCbOw/edit#}
*/


/**
* @typedef {function(new:BaseTemplate, !Element)}
*/
const TemplateClassDef = {};

/** @private @const {string} */
const PROP_ = '__AMP_IMPL_';

/** @private @const {string} */
const PROP_PROMISE_ = '__AMP_WAIT_';


/**
* The interface that is implemented by all templates.
*/
export class BaseTemplate {

/** @param {!Element} element */
constructor(element) {
/** @public @const */
this.element = element;

this.compileCallback();
}

/** @protected @return {!Window} */
getWin() {
return this.element.ownerDocument.defaultView;
}

/**
* Override in subclass if the element needs to compile the template.
* @protected
*/
compileCallback() {
// Subclasses may override.
}

/**
* To be implemented by subclasses.
* @param {!JSONType} unusedData
* @return {!Element}
*/
render(unusedData) {
throw new Error('Not implemented');
}

/**
* Helps the template implementation to unwrap the root element. The root
* element can be unwrapped only when it contains a single element or a
* single element surrounded by empty text nodes.
* @param {!Element} root
* @return {!Element}
* @protected @final
*/
unwrap(root) {
let singleElement = null;
for (let n = root.firstChild; n != null; n = n.nextSibling) {
if (n.nodeType == /* TEXT */ 3) {
if (n.textContent.trim()) {
// Non-empty text node - can't unwrap.
singleElement = null;
break;
}
} else if (n.nodeType == /* COMMENT */ 8) {
// Ignore comments.
} else if (n.nodeType == /* ELEMENT */ 1) {
if (!singleElement) {
singleElement = /** @type {!Element} */ (n);
} else {
// This is not the first element - can't unwrap.
singleElement = null;
break;
}
} else {
singleElement = null;
}
}
return singleElement || root;
}
}


/**
*/
export class Templates {
/** @param {!Window} win */
constructor(win) {
/** @private @const {!Window} */
this.win_ = win;

/**
* A map from template type to template's class promise.
* @private @const {!Object<string, !Promise<!TemplateClassDef>>}
*/
this.templateClassMap_ = {};

/**
* A map from template type to template's class promise. This is a transient
* storage. As soon as the template class loaded, the entry is removed.
* @private @const {!Object<string, function(!TemplateClassDef)>}
*/
this.templateClassResolvers_ = {};

/** @type {!Object<string, boolean>|undefined} */
this.declaredTemplates_ = undefined;
}

/**
* Renders the specified template element using the supplied data.
* @param {!Element} templateElement
* @param {!JSONType} data
* @return {!Promise<!Element>}
*/
renderTemplate(templateElement, data) {
return this.getImplementation_(templateElement).then(impl => {
return this.render_(impl, data);
});
}

/**
* Renders the specified template element using the supplied array of data
* and returns an array of resulting elements.
* @param {!Element} templateElement
* @param {!Array<!JSONType>} array
* @return {!Promise<!Array<!Element>>}
*/
renderTemplateArray(templateElement, array) {
if (array.length == 0) {
return Promise.resolve([]);
}
return this.getImplementation_(templateElement).then(impl => {
return array.map(item => {
return this.render_(impl, item);
});
});
}

/**
* Discovers the template for the specified parent and renders it using the
* supplied data. The template can be specified either via "template"
* attribute or as a child "template" element. When specified via "template"
* attribute, the value indicates the ID of the template element.
* @param {!Element} parent
* @param {!JSONType} data
* @return {!Promise<!Element>}
*/
findAndRenderTemplate(parent, data) {
return this.renderTemplate(this.findTemplate_(parent), data);
}

/**
* Discovers the template for the specified parent and renders it using the
* supplied array of data. The template can be specified either via "template"
* attribute or as a child "template" element. When specified via "template"
* attribute, the value indicates the ID of the template element. Returns
* the array of the rendered elements.
* @param {!Element} parent
* @param {!Array<!JSONType>} array
* @return {!Promise<!Array<!Element>>}
*/
findAndRenderTemplateArray(parent, array) {
return this.renderTemplateArray(this.findTemplate_(parent), array);
}

/**
* The template can be specified either via "template" attribute or as a
* child "template" element. When specified via "template" attribute,
* the value indicates the ID of the template element.
* @param {!Element} parent
* @return {!Element}
* @private
*/
findTemplate_(parent) {
let templateElement = null;
const templateId = parent.getAttribute('template');
if (templateId) {
templateElement = parent.ownerDocument.getElementById(templateId);
} else {
templateElement = childElementByTag(parent, 'template');
}
user.assert(templateElement, 'Template not found for %s', parent);
user.assert(templateElement.tagName == 'TEMPLATE',
'Template element must be a "template" tag %s', templateElement);
return templateElement;
}

/**
* Returns the promise that will eventually yield the template implementation
* for the specified template element.
* @param {!Element} element
* @return {!Promise<!BaseTemplate>}
* @private
*/
getImplementation_(element) {
const impl = element[PROP_];
if (impl) {
return Promise.resolve(impl);
}

const type = user.assert(element.getAttribute('type'),
'Type must be specified: %s', element);

let promise = element[PROP_PROMISE_];
if (promise) {
return promise;
}

promise = this.waitForTemplateClass_(element, type).then(templateClass => {
const impl = element[PROP_] = new templateClass(element);
delete element[PROP_PROMISE_];
return impl;
});
element[PROP_PROMISE_] = promise;
return promise;
}

/**
* Returns the promise that will eventually yield the template class. This will
* wait until the actual template script has been downloaded and parsed.
* @param {!Element} element
* @param {string} type
* @return {!Promise<!TemplateClassDef>}
* @private
*/
waitForTemplateClass_(element, type) {
if (this.templateClassMap_[type]) {
return this.templateClassMap_[type];
}

this.checkTemplateDeclared_(element, type);
let aResolve;
const promise = new Promise((resolve, unusedReject) => {
aResolve = resolve;
});
this.templateClassMap_[type] = promise;
this.templateClassResolvers_[type] = aResolve;
return promise;
}


/**
* Checks that the template type has actually been declared by a
* `<script custom-template=$type>` tag in the head.
* @param {!Element} element
* @param {string} type
* @private
*/
checkTemplateDeclared_(element, type) {
if (!this.declaredTemplates_) {
this.declaredTemplates_ = this.win_.Object.create(null);
const scriptTags = this.win_.document.querySelectorAll(
'script[custom-template]');
for (let i = 0; i < scriptTags.length; i++) {
this.declaredTemplates_[scriptTags[i].getAttribute(
'custom-template')] = true;
}
}
user.assert(this.declaredTemplates_[type],
'Template must be declared for %s as <script custom-template=%s>',
element, type);
}

/**
* Registers an extended template. This function should typically be called
* through the registerTemplate method on the AMP runtime.
* @param {string} type
* @param {!TemplateClassDef} templateClass
* @private
*/
registerTemplate_(type, templateClass) {
if (!this.templateClassMap_[type]) {
this.templateClassMap_[type] = Promise.resolve(templateClass);
} else {
const resolver = this.templateClassResolvers_[type];
user.assert(resolver, 'Duplicate template type: %s', type);
delete this.templateClassResolvers_[type];
resolver(templateClass);
}
}

/**
* @param {!BaseTemplate} impl
* @param {!JSONType} data
* @private
*/
render_(impl, data) {
const root = impl.render(data);
const anchors = root.getElementsByTagName('a');
for (let i = 0; i < anchors.length; i++) {
const anchor = anchors[i];
if (!anchor.hasAttribute('href')) {
// Ignore anchors without href.
continue;
}

// TODO(dvoytenko, #1572): This code should be unnecessary after
// sanitization issue has been addressed.
anchor.setAttribute('target', '_blank');
}
return root;
}
}


/**
* Registers an extended template. This function should typically be called
* through the registerTemplate method on the AMP runtime.
* @param {!Window} win
* @param {string} type
* @param {!TemplateClassDef} templateClass
* @package
*/
export function registerExtendedTemplate(win, type, templateClass) {
return installTemplatesService(win).registerTemplate_(type, templateClass);
}


/**
* @param {!Window} window
* @return {!Templates}
*/
export function installTemplatesService(window) {
return getService(window, 'templates', () => {
return new Templates(window);
});
};
Loading

0 comments on commit de2fb5f

Please sign in to comment.