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
Add script tag support for enclosing a template #19346
Conversation
cc\ @zhangsu |
this.template_ = container./*OK*/innerHTML; | ||
} else if (this.element.tagName == 'SCRIPT') { | ||
//TODO(alabiaga): handle nested templates. | ||
this.template_ = this.element.text.trim(); |
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.
textContent
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.
yes that is the data type but it is the template. I'd like to keep a name that can be used for both cases when either template is defined via tag or <script> tag. In both cases, it is still just a string representing the template.
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.
I'd like to keep the name as I reuse it for the 'TEMPLATE' type use case as well. The data type is text but for both cases this.template_ is a string representing the template contents. Would this.templateString_ be a better name?
this.template_ = container./*OK*/innerHTML; | ||
} else if (this.element.tagName == 'SCRIPT') { | ||
//TODO(alabiaga): handle nested templates. | ||
this.template_ = this.element.text.trim(); |
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.
We don't trim in the <template>
case.
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.
right. mustache parsing won't care about the spaces anyway. removed in the script case.
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.
right. removed. mustache parse will ignore anyway
* | ||
* @param {!Element} content | ||
*/ | ||
processNestedScriptTemplates_(content) { |
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.
Why are we duplicating?
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.
Initially, what was written v0.2 was already the same logic to what was in amp-mustache v0.1 and inlined and not just extracted into its own method. I just pulled it out to its own method. I will pull it out and the template versions can share the logic once I add support for nested templates in the next PR.
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.
Hm, it's the diff that's screwed up. It's not showing me the old code.
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.
This function isn't called anywhere.
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.
removed. Thanks, initially was going to try and add support for nested script templates but deferring it as it's not a priority and I filed a ticket for it as mentioned in another review comment.
src/service/template-impl.js
Outdated
'Template element must be a "template" tag %s', templateElement); | ||
const templateTagName = templateElement.tagName; | ||
user().assert( | ||
templateTagName == 'TEMPLATE' || templateTagName == 'SCRIPT', |
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.
Assert script has type="template"
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.
the template tag also has a type that is enforced by the validator but isn't in the code base.
e.g. <template type="amp-mustache>..</template>
Maybe it is enough to do that there and the script way of defining templates is going to follow the same model. I will do the validation changes in a separate PR.
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.
<script type="template">
will need to be validated the same way a <template>
. But, without this runtime check, I can pass a selector that will be a <script type="text/json">
, which won't be validated. That would be a security issue.
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.
You can pass in json data with the following markup <script type="text/json"> but the sanitizer which runs during mustache rendering will strip out unsafe markup, script tags included. But to be on the safe side, you're right we should validate the type. Though I was pointing out that during runtime..the type for template is not validated, which it is in the validator. I will not add this change, as this will probably break clients who currently do not set this attribute.
Nested templates via <script type="template/amp-mustache"> will be whitelisted and allowed in the sanitizer in my next PR when I add support for nested script tags.
Adding validation for checking <script type="template/amp-mustache"> rather than just <script type="template"> to be more in line with the template syntax we support, . Thanks
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.
We should use a valid MIME type string to avoid future collisions, e.g. text/plain
. We can use a non-standard attribute template="amp-mustache"
to denote template syntax.
https://html.spec.whatwg.org/multipage/scripting.html#attr-script-type
src/service/template-impl.js
Outdated
@@ -285,7 +288,9 @@ export class Templates { | |||
} else if (opt_querySelector) { | |||
return scopedQuerySelector(parent, opt_querySelector); | |||
} else { | |||
return childElementByTag(parent, 'template'); | |||
const template = childElementByTag(parent, 'template'); | |||
const templateByScript = childElementByTag(parent, 'script'); |
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.
Use a query selector to do this in one pass.
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.
testing/describes.js
Outdated
* @param {!Array} values | ||
* @param {func} func | ||
*/ | ||
export function testCases(values, func) { |
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.
Huh?
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.
I use this in the tests (test-amp-mustache.js) so that I can reuse all the tests and just run them using a template creation with either in one test run and then <script> in the next.
e.g.
`testCases(['script', 'template'], templateType => {
beforeEach(
document.createElement(templateType);
)
})`
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.
Did you try using describes.repeated
?
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.
I did not. I was looking and briefly mentioned this in a standup and was under the impression that it didn't exist. Changed! Thanks.
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.
thanks Justin.
this.template_ = container./*OK*/innerHTML; | ||
} else if (this.element.tagName == 'SCRIPT') { | ||
//TODO(alabiaga): handle nested templates. | ||
this.template_ = this.element.text.trim(); |
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.
I'd like to keep the name as I reuse it for the 'TEMPLATE' type use case as well. The data type is text but for both cases this.template_ is a string representing the template contents. Would this.templateString_ be a better name?
this.template_ = container./*OK*/innerHTML; | ||
} else if (this.element.tagName == 'SCRIPT') { | ||
//TODO(alabiaga): handle nested templates. | ||
this.template_ = this.element.text.trim(); |
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.
right. removed. mustache parse will ignore anyway
this.template_ = container./*OK*/innerHTML; | ||
} else if (this.element.tagName == 'SCRIPT') { | ||
//TODO(alabiaga): handle nested templates. | ||
this.template_ = this.element.text; |
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.
No, use element.textContent
instead of .text
. 😛
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.
ah..Wish there was support for highlighting the exact code you are referring rather than just the line. Done. Thanks.
}); | ||
// This test suite runs twice. First by creating templates of type template | ||
// and then by creating templates encapsulated within script. | ||
testCases(['script', 'template'], templateType => { |
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.
You're looking for describes.repeated
:
describes.repeated('toggle', {
'name': data
}, (name, data) => {
});
* | ||
* @param {!Element} content | ||
*/ | ||
processNestedScriptTemplates_(content) { |
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.
Hm, it's the diff that's screwed up. It's not showing me the old code.
src/service/template-impl.js
Outdated
'Template element must be a "template" tag %s', templateElement); | ||
const templateTagName = templateElement.tagName; | ||
user().assert( | ||
templateTagName == 'TEMPLATE' || templateTagName == 'SCRIPT', |
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.
<script type="template">
will need to be validated the same way a <template>
. But, without this runtime check, I can pass a selector that will be a <script type="text/json">
, which won't be validated. That would be a security issue.
container.appendChild(content); | ||
this.template_ = container./*OK*/innerHTML; | ||
} else if (this.element.tagName == 'SCRIPT') { | ||
//TODO(alabiaga): handle nested templates. |
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.
This is lower priority, remove and file a tracking issue. No need to address yet. :)
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 filed #19450
@choumx friendly ping since justin is away. |
* | ||
* @param {!Element} content | ||
*/ | ||
processNestedScriptTemplates_(content) { |
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.
This function isn't called anywhere.
src/service/template-impl.js
Outdated
const templateTagName = templateElement.tagName; | ||
user().assert( | ||
templateTagName == 'TEMPLATE' || templateTagName == 'SCRIPT', | ||
'Template must be a "template" tag or defined in a "script" tag %s', |
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.
"Template must defined in a <template> or <script type=text/plain> tag."
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.
src/service/template-impl.js
Outdated
'Script template attribute must be "text/plain"'); | ||
user().assert( | ||
templateElement.getAttribute('template') == 'amp-mustache', | ||
'Script template attribute must be "amp-mustache"'); |
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.
We shouldn't couple template-impl.js to amp-mustache. Mustache is just a flavor of template.
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.
right. removed. In the future we can just add support for it when other template types are supported and just default to amp-mustache if omitted perhaps.
src/service/template-impl.js
Outdated
if (templateTagName == 'SCRIPT') { | ||
user().assert( | ||
templateElement.getAttribute('type') == 'text/plain', | ||
'Script template attribute must be "text/plain"'); |
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.
Just include this in the expression above. Also, what about case sensitivity?
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. tagName in DOM trees always return in uppercase. https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName
testing/describes.js
Outdated
* @param {!Array} values | ||
* @param {func} func | ||
*/ | ||
export function testCases(values, func) { |
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.
Did you try using describes.repeated
?
examples/amp-list.amp.html
Outdated
@@ -57,6 +57,24 @@ | |||
</template> | |||
</amp-list> | |||
|
|||
<!-- ## Using a script to enclose the template. --> | |||
<amp-list width="auto" height="100" layout="fixed-height" src="comments.example.json"> | |||
<script type="amp-mustache"> |
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.
type="text/plain" template="amp-mustache"
, right? Did we test that this example works?
Let's also add an integration test to extensions/amp-list/0.1/test/integration/test-amp-list.js
to make sure it doesn't stop working. :)
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.
yes, I did test this but this was before the whole discussion about the changes to these attributes so yes this stopped working with the recent changes. Thanks.
made test changes.
@@ -285,7 +296,7 @@ export class Templates { | |||
} else if (opt_querySelector) { | |||
return scopedQuerySelector(parent, opt_querySelector); | |||
} else { | |||
return childElementByTag(parent, 'template'); | |||
return parent.querySelector('template, script'); |
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.
Why this change?
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.
This change is required to find the script or template tag defined in an amp-list.
Example
<amp-list>
<script id="amp-template-id" type="text/plain" template="amp-mustache">
...
</script>
</amp-list>
Note that it will return the first match found. We can make the validator rule changes enforce that only 1 template type is defined so that we can't do the following.
<amp-list>
<script type="text/plain" template="amp-mustache">
...
</script>
<template type="amp-mustache">
...
</template>
</amp-list>
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.
Gotcha, missed the , script
part.
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.
thanks Will!
* | ||
* @param {!Element} content | ||
*/ | ||
processNestedScriptTemplates_(content) { |
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.
removed. Thanks, initially was going to try and add support for nested script templates but deferring it as it's not a priority and I filed a ticket for it as mentioned in another review comment.
src/service/template-impl.js
Outdated
const templateTagName = templateElement.tagName; | ||
user().assert( | ||
templateTagName == 'TEMPLATE' || templateTagName == 'SCRIPT', | ||
'Template must be a "template" tag or defined in a "script" tag %s', |
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.
src/service/template-impl.js
Outdated
'Script template attribute must be "text/plain"'); | ||
user().assert( | ||
templateElement.getAttribute('template') == 'amp-mustache', | ||
'Script template attribute must be "amp-mustache"'); |
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.
right. removed. In the future we can just add support for it when other template types are supported and just default to amp-mustache if omitted perhaps.
@@ -285,7 +296,7 @@ export class Templates { | |||
} else if (opt_querySelector) { | |||
return scopedQuerySelector(parent, opt_querySelector); | |||
} else { | |||
return childElementByTag(parent, 'template'); | |||
return parent.querySelector('template, script'); |
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.
This change is required to find the script or template tag defined in an amp-list.
Example
<amp-list>
<script id="amp-template-id" type="text/plain" template="amp-mustache">
...
</script>
</amp-list>
Note that it will return the first match found. We can make the validator rule changes enforce that only 1 template type is defined so that we can't do the following.
<amp-list>
<script type="text/plain" template="amp-mustache">
...
</script>
<template type="amp-mustache">
...
</template>
</amp-list>
testing/describes.js
Outdated
* @param {!Array} values | ||
* @param {func} func | ||
*/ | ||
export function testCases(values, func) { |
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.
I did not. I was looking and briefly mentioned this in a standup and was under the impression that it didn't exist. Changed! Thanks.
src/service/template-impl.js
Outdated
if (templateTagName == 'SCRIPT') { | ||
user().assert( | ||
templateElement.getAttribute('type') == 'text/plain', | ||
'Script template attribute must be "text/plain"'); |
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. tagName in DOM trees always return in uppercase. https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName
examples/amp-list.amp.html
Outdated
@@ -57,6 +57,24 @@ | |||
</template> | |||
</amp-list> | |||
|
|||
<!-- ## Using a script to enclose the template. --> | |||
<amp-list width="auto" height="100" layout="fixed-height" src="comments.example.json"> | |||
<script type="amp-mustache"> |
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.
yes, I did test this but this was before the whole discussion about the changes to these attributes so yes this stopped working with the recent changes. Thanks.
made test changes.
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.
Overall looks good to me. Please wait for @choumx to review.
(templateTagName == 'TEMPLATE' || (templateTagName == 'SCRIPT' | ||
&& templateElement.getAttribute('type') === 'text/plain')), | ||
'Template must be defined in a <template> or ' | ||
+ '<script type="text/plain"> tag'); |
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.
Maybe text/html
?
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.
Considered this but technically mustache templates aren't HTML, which is why table foster parenting semantics won't affect this etc.
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.
Please file an issue to add an integration test for amp-list for the new script case.
describe('amp-mustache 0.1', () => { | ||
describes.repeated('amp-mustache 0.1', { | ||
'template script': {templateType: 'script'}, | ||
'template template': {templateType: 'template'}, |
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.
'template template'
is confusing.
'with script[type=text/plain][template=amp-mustache]'
'with template[type=amp-mustache]'
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
'text before a template text after a template'); | ||
done(); | ||
} else { | ||
done(); |
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.
Passing and calling done
shouldn't be necessary here. Also, extract the isTemplateType
conditional:
if (isTemplateType) {
describe('with template[type=amp-mustache]') {
it(...);
it(...);
}
}
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.
the test would hang otherwise on the the condition script[type=text/plain] [template=amp-mustache]. But extracting the condition outside and not running it at all does remove the need for done(), if that's you are saying. Thanks
done(); | ||
} | ||
}); | ||
}); |
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.
Ditto, except do the same for the script case.
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.
'</tr>' + | ||
'{{/replies}}' + | ||
'</tbody>' + | ||
'</table>'); |
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.
Can we use a more minimal example than this? It's also worth running the same example on the template case to highlight the difference.
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. I removed some table data to simplify the example.
@@ -285,7 +296,7 @@ export class Templates { | |||
} else if (opt_querySelector) { | |||
return scopedQuerySelector(parent, opt_querySelector); | |||
} else { | |||
return childElementByTag(parent, 'template'); | |||
return parent.querySelector('template, script'); |
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.
Gotcha, missed the , script
part.
integration test issue here #19583 |
}); | ||
if (isTemplateTypeScript) { | ||
it('should not foster text nodes in script template', () => { | ||
allowConsoleError(() => { |
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.
You need to return this otherwise Mocha won't execute the embedded code.
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
if (isTemplateTypeScript) { | ||
it('should foster text nodes in template[type="amp-mustache"]' | ||
+ 'destroying the templating', () => { | ||
allowConsoleError(() => { |
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.
Ditto.
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
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.
LGTM except let's double check that one test.
}); | ||
}); | ||
} | ||
if (isTemplateTypeScript) { |
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.
This should be isTemplateType
right?
}); | ||
} | ||
if (isTemplateTypeScript) { | ||
it('should foster text nodes in template[type="amp-mustache"]' |
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.
Nit: Missing trailing space.
@@ -19,40 +19,76 @@ import * as service from '../../../../src/service'; | |||
import {AmpMustache} from '../amp-mustache'; | |||
import mustache from '../../../../third_party/mustache/mustache'; | |||
|
|||
describe('amp-mustache 0.1', () => { | |||
describes.repeated('amp-mustache 0.1', { | |||
'with script[type=text/plain][template=amp-mustache': |
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.
Nit: Missing trailing ]
.
sandbox.stub(Services, 'templatesFor').returns(templates); | ||
describes.repeated('amp-list', { | ||
'template type script': {templateType: 'script'}, | ||
'template type': {templateType: 'template'}, |
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.
Nit: Use the same repeated test labels as test-amp-mustache.
* merge * add todo for nested script templates * fix test * address jridgewell comments * lint * address comments * fix merge * fix example * lint * address choumx comments * address choumx comments * address choumx comments
fixes #19321, #11205