-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
Support for bind expressions in mustache templates #7602
Changes from all commits
8d9f0af
1f0079f
f7844a7
bdafca6
86f9f9f
ed83a6d
dd7027d
7b8bf0b
fdc2f70
884c091
da8de76
f775df8
764e789
c454f57
1c8ef1d
fbff37b
737f943
6b988d7
8b38137
486a7cc
e24d628
43c1e7c
332bcb7
7524b47
ea642c7
ee698b7
3d2bdf6
ac0aeea
3223794
c80f2a2
e357373
433a5ce
85e759e
c698197
6d74820
35d70f5
840ed5b
eebfb5c
cf114f6
9f8c8c5
e35a7eb
95f42f2
3bdbbc4
42df400
2da454a
84ca405
540371f
e40cfe2
8476b7b
3cee65b
6b4a0d2
55e343d
155c915
8ae3107
3747e80
7beeb8e
7f4f93a
fbdafbc
dd524bc
55d6ff3
137eb3b
25b284b
a10d4f9
2d186d4
8217a84
d525d76
a714c2f
386c29d
7f6cb54
e617d6e
5eaa991
cd4d228
6c1b645
1bfef5d
757bd53
0f6de2e
1495793
eca9501
2053634
eab7dc2
08f7956
60b86f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
<!doctype html> | ||
<html ⚡> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>Forms Examples in AMP</title> | ||
<link rel="canonical" href="amps.html" > | ||
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1"> | ||
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript> | ||
<script async src="https://cdn.ampproject.org/v0.js"></script> | ||
<script async custom-element="amp-form" src="https://cdn.ampproject.org/v0/amp-form-0.1.js"></script> | ||
<script async custom-template="amp-mustache" src="https://cdn.ampproject.org/v0/amp-mustache-0.1.js"></script> | ||
<script async custom-element="amp-bind" src="https://cdn.ampproject.org/v0/amp-bind-0.1.js"></script> | ||
</head> | ||
<body> | ||
|
||
<p>Submit the form and then press a button to change the text in the below template</p> | ||
<button on="tap:AMP.setState(selected=1)">1</button> | ||
<button on="tap:AMP.setState(selected=2)">2</button> | ||
<button on="tap:AMP.setState(selected=3)">3</button> | ||
<button on="tap:AMP.setState(selected=4)">4</button> | ||
|
||
<h4>Enter your name and email.</h4> | ||
<form method="post" | ||
action-xhr="/form/echo-json/post" | ||
target="_blank"> | ||
<fieldset> | ||
<label> | ||
<span>Your name</span> | ||
<input type="text" name="name" id="name1" required> | ||
</label> | ||
<label> | ||
<span>Your email</span> | ||
<input type="email" name="email" id="email1" required> | ||
</label> | ||
<input type="submit" value="Subscribe"> | ||
</fieldset> | ||
|
||
<div submit-success> | ||
<template type="amp-mustache"> | ||
Success! Thanks {{name}} for entering your email: {{email}} | ||
|
||
<p>You have selected: <span [text]="selected ? selected : 'No selection'">0</span></p> | ||
</template> | ||
</div> | ||
</form> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ import {reportError} from '../../../src/error'; | |
import {resourcesForDoc} from '../../../src/resources'; | ||
import {filterSplice} from '../../../src/utils/array'; | ||
import {rewriteAttributeValue} from '../../../src/sanitizer'; | ||
import {timerFor} from '../../../src/timer'; | ||
|
||
const TAG = 'amp-bind'; | ||
|
||
|
@@ -100,10 +101,15 @@ export class Bind { | |
this.resources_ = resourcesForDoc(ampdoc); | ||
|
||
/** | ||
* True if a digest is triggered before scan for bindings completes. | ||
* @private {boolean} | ||
* @const @private {!Array<Promise>} | ||
*/ | ||
this.digestQueuedAfterScan_ = false; | ||
this.mutationPromises_ = []; | ||
|
||
/** | ||
* @const @private {MutationObserver} | ||
*/ | ||
this.mutationObserver_ = | ||
new MutationObserver(this.onMutationsObserved_.bind(this)); | ||
|
||
/** @const @private {boolean} */ | ||
this.workerExperimentEnabled_ = isExperimentOn(this.win_, 'web-worker'); | ||
|
@@ -280,7 +286,7 @@ export class Bind { | |
/** | ||
* Scans `node` for attributes that conform to bind syntax and returns | ||
* a tuple containing bound elements and binding data for the evaluator. | ||
* @param {!Element} node | ||
* @param {!Node} node | ||
* @return { | ||
* !Promise<{ | ||
* boundElements: !Array<BoundElementDef>, | ||
|
@@ -305,11 +311,18 @@ export class Bind { | |
// Helper function for scanning the tree walker's next node. | ||
// Returns true if the walker has no more nodes. | ||
const scanNextNode_ = () => { | ||
const element = walker.nextNode(); | ||
if (!element) { | ||
const node = walker.currentNode; | ||
if (!node) { | ||
return true; | ||
} | ||
// Walker is filtered to only return elements | ||
const element = dev().assertElement(node); | ||
const tagName = element.tagName; | ||
if (tagName === 'TEMPLATE') { | ||
// Listen for changes in amp-mustache templates | ||
this.observeElementForMutations_(element); | ||
} | ||
|
||
const boundProperties = this.scanElement_(element); | ||
if (boundProperties.length > 0) { | ||
boundElements.push({element, boundProperties}); | ||
|
@@ -323,7 +336,7 @@ export class Bind { | |
} | ||
expressionToElements[expressionString].push(element); | ||
}); | ||
return false; | ||
return !walker.nextNode(); | ||
}; | ||
|
||
return new Promise(resolve => { | ||
|
@@ -649,6 +662,61 @@ export class Bind { | |
} | ||
} | ||
|
||
/** | ||
* Begin observing mutations to element. Presently, all supported elements | ||
* that can add/remove bindings add new elements to their parent, so parent | ||
* node should be observed for mutations. | ||
* @private | ||
*/ | ||
observeElementForMutations_(element) { | ||
// TODO(kmh287): What if parent is the body tag? | ||
// TODO(kmh287): Generify logic for node observation strategy | ||
// when bind supprots more dynamic nodes. | ||
const elementToObserve = element.parentElement; | ||
this.mutationObserver_.observe(elementToObserve, {childList: true}); | ||
} | ||
|
||
/** | ||
* Respond to observed mutations. Adds all bindings for newly added elements | ||
* removes bindings for removed elements, then immediately applies the current | ||
* scope to the new bindings. | ||
* | ||
* @param mutations {Array<MutationRecord>} | ||
* @private | ||
*/ | ||
onMutationsObserved_(mutations) { | ||
mutations.forEach(mutation => { | ||
// Add bindings for new nodes first to ensure that a binding isn't removed | ||
// and then subsequently re-added. | ||
const addPromises = []; | ||
const addedNodes = mutation.addedNodes; | ||
for (let i = 0; i < addedNodes.length; i++) { | ||
const addedNode = addedNodes[i]; | ||
if (addedNode.nodeType == Node.ELEMENT_NODE) { | ||
const addedElement = dev().assertElement(addedNode); | ||
addPromises.push(this.addBindingsForNode_(addedElement)); | ||
} | ||
} | ||
const mutationPromise = Promise.all(addPromises).then(() => { | ||
const removePromises = []; | ||
const removedNodes = mutation.removedNodes; | ||
for (let i = 0; i < removedNodes.length; i++) { | ||
const removedNode = removedNodes[i]; | ||
if (removedNode.nodeType == Node.ELEMENT_NODE) { | ||
const removedElement = dev().assertElement(removedNode); | ||
removePromises.push(this.removeBindingsForNode_(removedElement)); | ||
} | ||
} | ||
return Promise.all(removePromises); | ||
}).then(() => { | ||
return this.digest_(); | ||
}); | ||
if (getMode().test) { | ||
this.mutationPromises_.push(mutationPromise); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Returns true if both arrays contain the same strings. | ||
* @param {!(IArrayLike<string>|Array<string>)} a | ||
|
@@ -719,4 +787,23 @@ export class Bind { | |
|
||
return false; | ||
} | ||
|
||
/** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really happy about this. I'm open to suggestions on how to improve this method. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, maybe dispatch an test-only event (or allow setting of a callback function). You can check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OOC, did you try the suggestion above before using polling? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, this was so many comments back that I missed it. Do you want me to try that approach instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional. |
||
* Wait for DOM mutation observer callbacks to fire. Returns a promise | ||
* that resolves when mutation callbacks have fired. | ||
* | ||
* @return {Promise} | ||
* | ||
* @visibleForTesting | ||
*/ | ||
waitForAllMutationsForTesting() { | ||
return timerFor(this.win_).poll(5, () => { | ||
return this.mutationPromises_.length > 0; | ||
}).then(() => { | ||
return Promise.all(this.mutationPromises_); | ||
}).then(() => { | ||
this.mutationPromises_.length = 0; | ||
}); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this array ever cleaned up in production? If it's only used for testing, you can document it as such and add
if (getMode().test)
guards as I suggested elsewhere.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.