Skip to content

Commit

Permalink
Implement support for the amp-bind premutate message (ampproject#12906)
Browse files Browse the repository at this point in the history
* Implement support for the amp-bind premutate message

* fix unit tests

* Fix lint

* Revert changes to onMessage and instead add a new onMessageRespond method

* remove eventType and awaitResponse from RequestResponder parameters
  • Loading branch information
josh313 authored and RanAbram committed Mar 12, 2018
1 parent 7824b8f commit c9a66ff
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 0 deletions.
6 changes: 6 additions & 0 deletions extensions/amp-bind/0.1/amp-state.js
Expand Up @@ -80,6 +80,12 @@ export class AmpState extends AMP.BaseElement {

/** @private */
initialize_() {
if (this.element.hasAttribute('overridable')) {
Services.bindForDocOrNull(this.element).then(bind => {
dev().assert(bind, 'Bind service can not be found.');
bind.makeStateKeyOverridable(this.element.getAttribute('id'));
});
}
// Parse child script tag and/or fetch JSON from endpoint at `src`
// attribute, with the latter taking priority.
const children = this.element.children;
Expand Down
37 changes: 37 additions & 0 deletions extensions/amp-bind/0.1/bind-impl.js
Expand Up @@ -124,6 +124,9 @@ export class Bind {
/** @private {!../../../src/service/history-impl.History} */
this.history_ = Services.historyForDoc(ampdoc);

/** @private {!Array<string>} */
this.overridableKeys_ = [];

/**
* Upper limit on number of bindings for performance.
* @private {number}
Expand All @@ -147,6 +150,7 @@ export class Bind {

/** @const @private {!../../../src/service/viewer-impl.Viewer} */
this.viewer_ = Services.viewerForDoc(this.ampdoc);
this.viewer_.onMessageRespond('premutate', this.premutate_.bind(this));

const bodyPromise = (opt_win)
? waitForBodyPromise(opt_win.document)
Expand Down Expand Up @@ -333,6 +337,39 @@ export class Bind {
return this.history_;
}

/**
* Calls setState(s), where s is data.state with the non-overridable keys removed.
* @param {*} data
* @return {!Promise}
* @private
*/
premutate_(data) {
const ignoredKeys = [];
return this.initializePromise_.then(() => {
Object.keys(data.state).forEach(key => {
if (!this.overridableKeys_.includes(key)) {
delete data.state[key];
ignoredKeys.push(key);
}
});
if (ignoredKeys.length > 0) {
user().warn(TAG, 'Some state keys could not be premutated ' +
'because they are missing the overridable attribute: ' +
ignoredKeys.join(', '));
}
return this.setState(data.state);
});
}

/**
* Marks the given key as overridable so that it can be overriden by
* a premutate message from the viewer.
* @param {string} key
*/
makeStateKeyOverridable(key) {
this.overridableKeys_.push(key);
}

/**
* Scans the document for <amp-bind-macro> elements, and adds them to the
* bind-evaluator.
Expand Down
30 changes: 30 additions & 0 deletions extensions/amp-bind/0.1/test/integration/test-bind-impl.js
Expand Up @@ -23,6 +23,7 @@ import * as sinon from 'sinon';
import {AmpEvents} from '../../../../../src/amp-events';
import {Bind} from '../../bind-impl';
import {BindEvents} from '../../bind-events';
import {Services} from '../../../../../src/services';
import {chunkInstanceForTesting} from '../../../../../src/chunk';
import {toArray} from '../../../../../src/types';
import {user} from '../../../../../src/log';
Expand Down Expand Up @@ -210,11 +211,13 @@ describe.configure().ifNewChrome().run('Bind', function() {
}, env => {
let bind;
let container;
let viewer;

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

viewer = Services.viewerForDoc(env.ampdoc);
bind = new Bind(env.ampdoc);
// Connected <div> element created by describes.js.
container = env.win.document.getElementById('parent');
Expand Down Expand Up @@ -583,5 +586,32 @@ describe.configure().ifNewChrome().run('Bind', function() {
sinon.match(/Maximum number of bindings reached/));
});
});

it('should update premutate keys that are overridable', () => {
bind.makeStateKeyOverridable('foo');
bind.makeStateKeyOverridable('bar');
const foo = createElement(env, container, '[text]="foo"');
const bar = createElement(env, container, '[text]="bar"');
const baz = createElement(env, container, '[text]="baz"');
const qux = createElement(env, container, '[text]="qux"');

return onBindReadyAndSetState(env, bind, {
foo: 1, bar: 2, baz: 3, qux: 4,
}).then(() => {
return viewer.receiveMessage('premutate', {
state: {
foo: 'foo',
bar: 'bar',
baz: 'baz',
qux: 'qux',
},
}).then(() => {
expect(foo.textContent).to.equal('foo');
expect(bar.textContent).to.equal('bar');
expect(baz.textContent).to.be.equal('3');
expect(qux.textContent).to.be.equal('4');
});
});
});
}); // in single ampdoc
});
3 changes: 3 additions & 0 deletions extensions/amp-bind/validator-amp-bind.protoascii
Expand Up @@ -63,6 +63,9 @@ tags: { # <amp-state>
name: "id"
mandatory: true
}
attrs: {
name: "overridable"
}
attrs: {
name: "src"
value_url: {
Expand Down
29 changes: 29 additions & 0 deletions src/service/viewer-impl.js
Expand Up @@ -87,6 +87,12 @@ const TRUSTED_REFERRER_HOSTS = [
/^t.co$/,
];

/**
* @typedef {function(*):(!Promise<*>|undefined)}
*/
export let RequestResponder;


/**
* An AMP representation of the Viewer. This class doesn't do any work itself
* but instead delegates everything to the actual viewer. This class and the
Expand Down Expand Up @@ -131,6 +137,9 @@ export class Viewer {
/** @private {!Object<string, !Observable<!JsonObject>>} */
this.messageObservables_ = map();

/** @private {!Object<string, !RequestResponder>} */
this.messageResponders_ = map();

/** @private {!Observable<boolean>} */
this.runtimeOnObservable_ = new Observable();

Expand Down Expand Up @@ -803,6 +812,21 @@ export class Viewer {
return observable.add(handler);
}

/**
* Adds a eventType listener for viewer events.
* @param {string} eventType
* @param {!RequestResponder} responder
* @return {!UnlistenDef}
*/
onMessageRespond(eventType, responder) {
this.messageResponders_[eventType] = responder;
return () => {
if (this.messageResponders_[eventType] === responder) {
delete this.messageResponders_[eventType];
}
};
}

/**
* Requests AMP document to receive a message from Viewer.
* @param {string} eventType
Expand All @@ -828,6 +852,11 @@ export class Viewer {
const observable = this.messageObservables_[eventType];
if (observable) {
observable.fire(data);
}
const responder = this.messageResponders_[eventType];
if (responder) {
return responder(data);
} else if (observable) {
return Promise.resolve();
}
dev().fine(TAG_, 'unknown message:', eventType);
Expand Down

0 comments on commit c9a66ff

Please sign in to comment.