Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix amp-bind in PWA #10131

Merged
merged 8 commits into from
Jun 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/pwa/pwa.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ <h4>A4A</h4>
<div class="detail">Fusce pretium tempor justo, vitae consequat dolor maximus eget.</div>
</a>
</article>
<article class="card">
<a href="/pwa/examples/bind/basic.amp.html">
<h4>amp-bind</h4>
<div class="detail">Fusce pretium tempor justo, vitae consequat dolor maximus eget.</div>
</a>
</article>
</section>

<div id="doc-container" class="doc-container">
Expand Down
22 changes: 12 additions & 10 deletions extensions/amp-bind/0.1/bind-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ export class Bind {
/**
* The window containing the document to scan.
* May differ from the `ampdoc`'s window e.g. in FIE.
* @const @private {!Window} */
* @const @private {!Window}
*/
this.localWin_ = opt_win || ampdoc.win;

/** @private {!Array<BoundElementDef>} */
Expand Down Expand Up @@ -135,17 +136,19 @@ export class Bind {
/** @const @private {!../../../src/service/viewer-impl.Viewer} */
this.viewer_ = viewerForDoc(this.ampdoc);

/**
const bodyPromise = (opt_win)
? waitForBodyPromise(opt_win.document)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should make it resolve to a body. 😉

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😩 How about in fix-it next week?

.then(() => dev().assertElement(opt_win.document.body))
: ampdoc.whenBodyAvailable();

/**c.
* Resolved when the service finishes scanning the document for bindings.
* @const @private {Promise}
*/
this.initializePromise_ = Promise.all([
waitForBodyPromise(this.localWin_.document), // Wait for body.
this.viewer_.whenFirstVisible(), // Don't initialize in prerender mode.
]).then(() => {
const rootNode = dev().assertElement(this.localWin_.document.body);
return this.initialize_(rootNode);
});
this.initializePromise_ =
this.viewer_.whenFirstVisible().then(() => bodyPromise).then(body => {
return this.initialize_(body);
});

/** @const @private {!Function} */
this.boundOnTemplateRendered_ = this.onTemplateRendered_.bind(this);
Expand Down Expand Up @@ -241,7 +244,6 @@ export class Bind {
rootNode.addEventListener(
AmpEvents.TEMPLATE_RENDERED, this.boundOnTemplateRendered_);
});
// Check default values against initial expression results in development.
if (getMode().development) {
// Check default values against initial expression results.
promise = promise.then(() =>
Expand Down
38 changes: 34 additions & 4 deletions extensions/amp-bind/0.1/test/test-bind-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {AmpEvents} from '../../../../src/amp-events';
import {Bind} from '../bind-impl';
import {BindEvents} from '../bind-events';
import {chunkInstanceForTesting} from '../../../../src/chunk';
import {installTimerService} from '../../../../src/service/timer-impl';
import {toArray} from '../../../../src/types';
import {user} from '../../../../src/log';

Expand Down Expand Up @@ -109,8 +108,41 @@ function waitForEvent_(bind, env) {
// Unit tests
////////////////////////////////////////////////////////////////////////////////

describes.realWin('Bind', {
describes.realWin('Bind in shadow ampdoc', {
amp: {
ampdoc: 'shadow',
runtimeOn: false,
},
}, env => {
let bind;
let parent;

let createElementWithBinding;
let onBindReady;

beforeEach(() => {
// Make sure we have a chunk instance for testing.
chunkInstanceForTesting(env.ampdoc);

bind = new Bind(env.ampdoc);
parent = env.ampdoc.getBody();

createElementWithBinding = createElementWithBinding_(parent, env);
onBindReady = onBindReady_(bind, env);
});

it('should scan for bindings when ampdoc is ready', () => {
createElementWithBinding('[text]="1+1"');
expect(bind.boundElements_.length).to.equal(0);
return onBindReady().then(() => {
expect(bind.boundElements_.length).to.equal(1);
});
});
});

describes.realWin('Bind in single ampdoc', {
amp: {
ampdoc: 'single',
runtimeOn: false,
},
}, env => {
Expand All @@ -124,8 +156,6 @@ describes.realWin('Bind', {
let waitForEvent;

beforeEach(() => {
installTimerService(env.win);

// Make sure we have a chunk instance for testing.
chunkInstanceForTesting(env.ampdoc);

Expand Down
14 changes: 11 additions & 3 deletions src/element-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getAmpdoc,
getServicePromiseForDoc,
getServicePromiseOrNullForDoc,
getTopWindow,
} from './service';
import {user} from './log';
import * as dom from './dom';
Expand Down Expand Up @@ -61,7 +62,7 @@ export function getElementServiceIfAvailable(win, id, extension, opt_element) {
if (s) {
return /** @type {!Promise<?Object>} */ (s);
}
return elementServicePromiseOrNull(win, id, extension, opt_element);
return getElementServicePromiseOrNull(win, id, extension, opt_element);
}

/**
Expand Down Expand Up @@ -173,10 +174,17 @@ export function getElementServiceIfAvailableForDocInEmbedScope(
if (s) {
return /** @type {!Promise<?Object>} */ (Promise.resolve(s));
}
// Return embed-scope element service promise if scheduled.
if (nodeOrDoc.nodeType) {
const win = /** @type {!Document} */ (
nodeOrDoc.ownerDocument || nodeOrDoc).defaultView;
return elementServicePromiseOrNull(win, id, extension);
const topWin = getTopWindow(win);
// In embeds, doc-scope services are window-scope. But make sure to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't understand it, but ok.

// only do this for embeds (not the top window), otherwise we'd grab
// a promise from the wrong service holder which would never resolve.
if (win !== topWin) {
return getElementServicePromiseOrNull(win, id, extension);
}
}
return /** @type {!Promise<?Object>} */ (Promise.resolve(null));
}
Expand Down Expand Up @@ -207,7 +215,7 @@ function assertService(service, id, extension) {
* @return {!Promise<Object>}
* @private
*/
function elementServicePromiseOrNull(win, id, extension, opt_element) {
function getElementServicePromiseOrNull(win, id, extension, opt_element) {
// Microtask is necessary to ensure that window.ampExtendedElements has been
// initialized.
return Promise.resolve().then(() => {
Expand Down
110 changes: 62 additions & 48 deletions src/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
* limitations under the License.
*/

/**
* @fileoverview Registration and getter functions for AMP services.
*
* Invariant: Service getters never return null for registered services.
*/

// Requires polyfills in immediate side effect.
import './polyfills';
import {dev} from './log';
Expand Down Expand Up @@ -68,46 +74,37 @@ export class EmbeddableService {
adoptEmbedWindow(unusedEmbedWin) {}
}

/**
* Returns a service or null with the given id.
* @param {!Window} win
* @param {string} id
* @return {?Object} The service.
*/
export function getExistingServiceOrNull(win, id) {
win = getTopWindow(win);
if (isServiceRegistered(win, id)) {
return getServiceInternal(win, id);
} else {
return null;
}
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just moved below for consistent ordering.


/**
* Returns a service with the given id. Assumes that it has been registered
* already.
* @param {!Window} win
* @param {string} id
* @return {!Object} The service.
* @param {boolean=} opt_fallbackToTopWin
* @return {Object} The service.
*/
export function getExistingServiceInEmbedScope(win, id) {
export function getExistingServiceInEmbedScope(win, id, opt_fallbackToTopWin) {
// First, try to resolve via local (embed) window.
const local = getLocalExistingServiceForEmbedWinOrNull(win, id);
if (local) {
return local;
}
// Fallback to top-level window.
return getService(win, id);
if (opt_fallbackToTopWin) {
return getService(win, id);
}
return null;
}

/**
* Returns a service with the given id. Assumes that it has been constructed
* already.
* @param {!Node|!./service/ampdoc-impl.AmpDoc} nodeOrDoc
* @param {string} id
* @return {!Object} The service.
* @param {boolean=} opt_fallbackToTopWin
* @return {Object} The service.
*/
export function getExistingServiceForDocInEmbedScope(nodeOrDoc, id) {
export function getExistingServiceForDocInEmbedScope(
nodeOrDoc, id, opt_fallbackToTopWin) {
// First, try to resolve via local (embed) window.
if (nodeOrDoc.nodeType) {
// If a node is passed, try to resolve via this node.
Expand All @@ -118,8 +115,11 @@ export function getExistingServiceForDocInEmbedScope(nodeOrDoc, id) {
return local;
}
}
// Fallback to ampdoc.
return getServiceForDoc(nodeOrDoc, id);
// If an ampdoc is passed or fallback is allowed, continue resolving.
if (!nodeOrDoc.nodeType || opt_fallbackToTopWin) {
return getServiceForDoc(nodeOrDoc, id);
}
return null;
}

/**
Expand All @@ -134,13 +134,8 @@ export function installServiceInEmbedScope(embedWin, id, service) {
'Service override can only be installed in embed window: %s', id);
dev().assert(!getLocalExistingServiceForEmbedWinOrNull(embedWin, id),
'Service override has already been installed: %s', id);
registerServiceInternal(
embedWin,
embedWin,
id,
() => service);
// Force service to build
getServiceInternal(embedWin, id);
registerServiceInternal(embedWin, embedWin, id, () => service);
getServiceInternal(embedWin, id); // Force service to build.
}

/**
Expand All @@ -160,21 +155,6 @@ function getLocalExistingServiceForEmbedWinOrNull(embedWin, id) {
}
}

/**
* Returns a service for the given id and window (a per-window singleton).
* Users should typically wrap this as a special purpose function (e.g.
* `vsyncFor(win)`) for type safety and because the factory should not be
* passed around.
* @param {!Window} win
* @param {string} id of the service.
* @template T
* @return {T}
*/
export function getService(win, id) {
win = getTopWindow(win);
return getServiceInternal(win, id);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just moved below for consistent ordering.

}

/**
* Registers a service given a class to be used as implementation.
* @param {!Window} win
Expand All @@ -193,6 +173,7 @@ export function registerServiceBuilder(win,
}
}


/**
* Returns a service and registers it given a class to be used as
* implementation.
Expand All @@ -213,6 +194,23 @@ export function registerServiceBuilderForDoc(nodeOrDoc,
}
}


/**
* Returns a service for the given id and window (a per-window singleton).
* Users should typically wrap this as a special purpose function (e.g.
* `vsyncFor(win)`) for type safety and because the factory should not be
* passed around.
* @param {!Window} win
* @param {string} id of the service.
* @template T
* @return {T}
*/
export function getService(win, id) {
win = getTopWindow(win);
return getServiceInternal(win, id);
}


/**
* Returns a promise for a service for the given id and window. Also expects
* an element that has the actual implementation. The promise resolves when
Expand All @@ -229,6 +227,22 @@ export function getServicePromise(win, id) {
}


/**
* Returns a service or null with the given id.
* @param {!Window} win
* @param {string} id
* @return {?Object} The service.
*/
export function getExistingServiceOrNull(win, id) {
win = getTopWindow(win);
if (isServiceRegistered(win, id)) {
return getServiceInternal(win, id);
} else {
return null;
}
}


/**
* Like getServicePromise but returns null if the service was never registered.
* @param {!Window} win
Expand All @@ -239,6 +253,7 @@ export function getServicePromiseOrNull(win, id) {
return getServicePromiseOrNullInternal(win, id);
}


/**
* Returns a service for the given id and ampdoc (a per-ampdoc singleton).
* Expects service `id` to be registered.
Expand All @@ -253,6 +268,7 @@ export function getServiceForDoc(nodeOrDoc, id) {
return getServiceInternal(holder, id);
}


/**
* Returns a promise for a service for the given id and ampdoc. Also expects
* a service that has the actual implementation. The promise resolves when
Expand All @@ -263,8 +279,7 @@ export function getServiceForDoc(nodeOrDoc, id) {
*/
export function getServicePromiseForDoc(nodeOrDoc, id) {
return getServicePromiseInternal(
getAmpdocServiceHolder(nodeOrDoc),
id);
getAmpdocServiceHolder(nodeOrDoc), id);
}


Expand All @@ -277,8 +292,7 @@ export function getServicePromiseForDoc(nodeOrDoc, id) {
*/
export function getServicePromiseOrNullForDoc(nodeOrDoc, id) {
return getServicePromiseOrNullInternal(
getAmpdocServiceHolder(nodeOrDoc),
id);
getAmpdocServiceHolder(nodeOrDoc), id);
}

/**
Expand Down
6 changes: 4 additions & 2 deletions src/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export function accessServiceForDocOrNull(nodeOrDoc) {
*/
export function actionServiceForDoc(nodeOrDoc) {
return /** @type {!./service/action-impl.ActionService} */ (
getExistingServiceForDocInEmbedScope(nodeOrDoc, 'action'));
getExistingServiceForDocInEmbedScope(
nodeOrDoc, 'action', /* opt_fallbackToTopWin */ true));
}

/**
Expand Down Expand Up @@ -232,7 +233,8 @@ export function timerFor(window) {
*/
export function urlReplacementsForDoc(nodeOrDoc) {
return /** @type {!./service/url-replacements-impl.UrlReplacements} */ (
getExistingServiceForDocInEmbedScope(nodeOrDoc, 'url-replace'));
getExistingServiceForDocInEmbedScope(
nodeOrDoc, 'url-replace', /* opt_fallbackToTopWin */ true));
}

/**
Expand Down
Loading