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

Bind service and state component #6449

Merged
merged 43 commits into from
Dec 14, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
aa35ab3
initial commit for bind component/service
Nov 16, 2016
7bc1806
make default value error more descriptive, only fire in dev mode
Nov 16, 2016
e201862
rename some methods/vars
Nov 16, 2016
521790a
don't strict equality check for default verification
Nov 16, 2016
6e3366f
hack support for on='tap:setState'
Nov 17, 2016
350836c
remove null result check for expr eval
Nov 17, 2016
41fd24f
use vsync for digest mutation, add class and attr toggle
Nov 18, 2016
1c3ea61
first pass on amp-bind-state element
Nov 27, 2016
0bb8352
add class change example
Nov 27, 2016
09e6276
add amp-img ex, fix dumb sanitize attr bug
Nov 27, 2016
530da99
move state to separate element per design doc
Nov 28, 2016
d2efb01
document methods in bind-impl.js
Nov 28, 2016
7cca3a9
properly handle boolean attributes
Nov 28, 2016
16746d9
add framework for reacting to attr changes, support size change, fix …
Nov 28, 2016
49ade02
propagate attrs in amp-img
Nov 29, 2016
505615b
move bind service to /extensions, skip digest for amp-bind-state
Nov 29, 2016
4397029
clean up action-impl
Nov 30, 2016
40074ae
fix lint errors
Nov 30, 2016
d9f872c
handle amp-bind not installed
Nov 30, 2016
446d4ca
fix closure errors in compiled bind expr
Nov 30, 2016
c1ca7aa
fix type annotations
Nov 30, 2016
808c283
add amp-video support
Dec 1, 2016
d1120dd
add amp-bind validator proto
Dec 2, 2016
1c5df14
PR comments, more types, TODOs
Dec 5, 2016
3fefcab
fix long line
Dec 5, 2016
f54de34
remove local assets in bind example
Dec 5, 2016
ce4ea22
fix typedef
Dec 5, 2016
80678ef
more type fixes
Dec 5, 2016
9b751b3
fix lint error
Dec 5, 2016
0660cfe
revert amp-video changes
Dec 6, 2016
491c8e0
rename bind-state to state
Dec 6, 2016
1b04da1
PR comments
Dec 6, 2016
b247b8e
add unit tests
Dec 6, 2016
651c241
check shallow equality during mutate, more unit tests
Dec 7, 2016
5c637aa
PR comments
Dec 9, 2016
7fe4c19
nit fix
Dec 9, 2016
0cf2c8a
call attr changed callback for bool attrs
Dec 9, 2016
5b3d71d
more PR comments
Dec 9, 2016
6c98f3a
lint and style fixes
Dec 9, 2016
7875beb
PR comments, class-ify bind-expr
Dec 12, 2016
344dc19
move expr eval to separate file
Dec 12, 2016
6be79b1
fix lint and type errors
Dec 12, 2016
14b1d3d
PR comments
Dec 14, 2016
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
2 changes: 1 addition & 1 deletion builtins/amp-img.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class AmpImg extends BaseElement {
this.srcset_ = srcsetFromElement(this.element);
this.updateImageSrc_();
} else if (this.img_ && ATTRIBUTES_TO_PROPAGATE_.indexOf(name) >= 0) {
this.propagateAttributes(name, this.img_);
this.propagateAttributes(name, this.img_, /* opt_removeMissingAttrs */ true);
}
}

Expand Down
6 changes: 3 additions & 3 deletions examples/bind.amp.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@
background-color: red;
}
</style>
</head>

<body>
<amp-state id="myState">
<script type="application/json">
{
"myStateKey1": "myStateValue1"
}
</script>
</amp-state>
</head>
</amp-state>

<body>
<button onclick="AMP.toggleExperiment('amp-bind');window.location.href=window.location.href;">Toggle Experiment</button>

<p [text]="foo">After clicking the button below, this will read 'foo'<p>
Expand Down
13 changes: 5 additions & 8 deletions extensions/amp-bind/0.1/amp-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ export class AmpState extends AMP.BaseElement {
buildCallback() {
const TAG = this.getName_();

toggle(this.element, false);
this.element.setAttribute('aria-hidden', 'true');
Copy link
Contributor

Choose a reason for hiding this comment

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

apply nodisplay layout to it instead? (otherwise it will create a line break as it is a block element)

Copy link
Author

@dreamofabear dreamofabear Dec 9, 2016

Choose a reason for hiding this comment

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

Can you modify layout_ within the element subclass? I don't see any examples of this. It shouldn't cause a newline since we toggle it off before layout completes.


const id = this.element.id;
user().assert(id, '%s element must have an id.', TAG);
const id = user().assert(this.element.id, '%s element must have an id.', TAG);

let json;
const children = this.element.children;
Expand Down Expand Up @@ -73,12 +73,9 @@ export class AmpState extends AMP.BaseElement {
}

/** @override */
layoutCallback() {
// Now that we are rendered, stop rendering the element to reduce
// resource consumption.
toggle(this.element, false);

return Promise.resolve();
renderOutsideViewport() {
// We want the state data to be available wherever it is in the document.
return true;
}

/**
Expand Down
45 changes: 26 additions & 19 deletions extensions/amp-bind/0.1/bind-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,20 @@
import {evaluateBindExpr} from './bind-expr';
import {getMode} from '../../../src/mode';
import {isExperimentOn} from '../../../src/experiments';
import {isFiniteNumber} from '../../../src/types';
import {dev, user} from '../../../src/log';
import {vsyncFor} from '../../../src/vsync';

const TAG = 'AMP-BIND';

/**
* A single binding, e.g. <element [property]="expression"></element>.
* `previousResult` is the result of this expression during the last digest.
* @typedef {{
* property: !string,
* expression: !string,
* element: !Element
* element: !Element,
* previousResult: (BindExpressionResultDef|undefined)
* }}
*/
let BindingDef;
Expand Down Expand Up @@ -67,13 +70,6 @@ export class Bind {
/** {!Array<BindingDef>} */
this.bindings_ = [];

/**
* Expression results during the previous digest cycle, where the i'th
* element is the last result of `this.bindings_[i].expression`.
* @type {!Array<BindExpressionResultDef>}
*/
this.previousResults_ = [];

/** @const {!Object} */
this.scope_ = Object.create(null);

Expand All @@ -86,6 +82,16 @@ export class Bind {
/** @const {!Function} */
this.boundMutate_ = this.mutate_.bind(this);

/**
* Keys correspond to valid attribute value types.
* @const {!Object<string,boolean>}
*/
this.attributeValueTypes_ = {
'string': true,
'boolean': true,
'number': true,
};

this.ampdoc.whenBodyAvailable().then(body => {
this.bindings_ = this.scanForBindings_(body);
Copy link
Contributor

Choose a reason for hiding this comment

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

This alone is not enough and can't handle dynamic content (e.g. rendered via amp-mustache templates). Ideally we could use MutationObserver to run this on newly added DOM but aside from not being supported everywhere, I am not sure of its performance. Maybe a good compromise it to have the template service either run this on render of new templates or it triggers an event we can listen to here to scan again (I like the latter for looser coupling)

Copy link
Author

@dreamofabear dreamofabear Dec 9, 2016

Choose a reason for hiding this comment

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

Agreed. Current approach is to add support for other AMP extensions one-by-one and make adjustments as necessary.

it triggers an event we can listen to here to scan again

SGTM.


Expand Down Expand Up @@ -172,7 +178,7 @@ export class Bind {
const task = {measure: this.boundMeasure_, mutate: this.boundMutate_};
this.vsync_.run(task, {
results: [],
verifyOnly: opt_verifyOnly,
verifyOnly: !!opt_verifyOnly,
});
}

Expand All @@ -199,10 +205,10 @@ export class Bind {
const result = state.results[i];

// Don't apply mutation if the result hasn't changed.
if (this.shallowEquals_(result, this.previousResults_[i])) {
if (this.shallowEquals_(result, binding.previousResult)) {
continue;
} else {
this.previousResults_[i] = result;
binding.previousResult = result;
}

if (state.verifyOnly) {
Expand Down Expand Up @@ -269,11 +275,11 @@ export class Bind {
if (element.classList.contains('-amp-element')) {
const resources = element.getResources();
if (property === 'width') {
user().assert(typeof attributeValue === 'number',
user().assert(isFiniteNumber(attributeValue),
'Invalid result for [width]: %s', attributeValue);
resources./*OK*/changeSize(element, undefined, attributeValue);
} else if (property === 'height') {
user().assert(typeof attributeValue === 'number',
user().assert(isFiniteNumber(attributeValue),
'Invalid result for [height]: %s', attributeValue);
resources./*OK*/changeSize(element, attributeValue, undefined);
}
Expand Down Expand Up @@ -319,13 +325,14 @@ export class Bind {
match = this.compareStringArrays_(initialValue, classes);
} else {
const attribute = element.getAttribute(property);
initialValue = attribute;
if (typeof expectedValue === 'boolean') {
initialValue = !!attribute;
// Boolean attributes return values of either '' or null.
match = (expectedValue && initialValue === '')
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should allow "true" and "false" as well, else we'd disallow expressions like [attr]="true".

Copy link
Author

Choose a reason for hiding this comment

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

IMO this is probably not a common use case and muddles the API a bit. Also, the expression would have to be [attr]="'true'", which evaluates to a string.

Copy link
Contributor

Choose a reason for hiding this comment

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

We don't allow toplevel boolean attributes? Never mind.

|| (!expectedValue && initialValue === null);
} else {
initialValue = attribute;
match = (initialValue === expectedValue);
}
// Use abstract equality since boolean attributes return value of ''.
match = (initialValue == expectedValue);
}

if (!match) {
Expand Down Expand Up @@ -363,8 +370,8 @@ export class Bind {
* @return {(string|boolean|number|null)}
*/
attributeValueOf_(value) {
if (typeof value === 'string' || typeof value === 'boolean'
|| typeof value === 'number') {
const type = typeof(value);
if (this.attributeValueTypes_[type] !== undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

No need for the !== undefined?

Copy link
Author

Choose a reason for hiding this comment

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

Done.

return value;
} else {
return null;
Expand Down
13 changes: 12 additions & 1 deletion extensions/amp-bind/0.1/test/test-bind-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describes.realWin('amp-bind', {
});
});

it('should verify initial values of bindings in dev mode', () => {
it('should verify initial values of string attribute bindings in dev mode', () => {
env.sandbox.stub(window, 'AMP_MODE', {development: true});
// Only the initial value for [a] binding does not match.
createElementWithBinding('[a]="a" [b]="b" b="b"');
Expand All @@ -87,6 +87,17 @@ describes.realWin('amp-bind', {
});
});

it('should verify initial values of boolean attribute bindings in dev mode', () => {
env.sandbox.stub(window, 'AMP_MODE', {development: true});
// Only the initial value for [c] binding does not match.
createElementWithBinding(`a [a]="true" [b]="false" c="false" [c]="false"`);
const errorStub = env.sandbox.stub(user(), 'error').withArgs('AMP-BIND');
return bodyReady(unusedBody => {
env.flushVsync();
expect(errorStub.callCount).to.equal(1);
});
});

it('should skip digest if specified in setState()', () => {
const element = createElementWithBinding('[onePlusOne]="1+1"');
expect(element.getAttribute('onePlusOne')).to.equal(null);
Expand Down
16 changes: 10 additions & 6 deletions src/base-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -511,18 +511,22 @@ export class BaseElement {
/**
* Utility method that propagates attributes from this element
* to the given element.
* @param {string|!Array<string>} attributes
* @param {!Element} element
* If `opt_removeMissingAttrs` is true, then also removes any specified
* attributes that are missing on this element from the target element.
* @param {string|!Array<string>} attributes
* @param {!Element} element
* @param {boolean=} opt_removeMissingAttrs
* @public @final
*/
propagateAttributes(attributes, element) {
propagateAttributes(attributes, element, opt_removeMissingAttrs) {
attributes = isArray(attributes) ? attributes : [attributes];
for (let i = 0; i < attributes.length; i++) {
const attr = attributes[i];
if (!this.element.hasAttribute(attr)) {
continue;
if (this.element.hasAttribute(attr)) {
element.setAttribute(attr, this.element.getAttribute(attr));
} else if (opt_removeMissingAttrs) {
element.removeAttribute(attr);
}
element.setAttribute(attr, this.element.getAttribute(attr));
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/service/resources-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -1353,8 +1353,8 @@ export class Resources {
}

if (request) {
request.newHeight = newHeight || request.newHeight;
request.newWidth = newWidth || request.newWidth;
request.newHeight = newHeight;
request.newWidth = newWidth;
request.force = force || request.force;
request.callback = opt_callback;
} else {
Expand Down