Skip to content

Commit

Permalink
Init
Browse files Browse the repository at this point in the history
  • Loading branch information
Micajuine Ho committed Mar 5, 2021
1 parent 734d305 commit feb6466
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 52 deletions.
25 changes: 21 additions & 4 deletions extensions/amp-form/0.1/amp-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -694,12 +694,19 @@ export class AmpForm {

/**
* Get form fields that require variable substitutions
* @return {!IArrayLike<!HTMLInputElement>}
* @return {!Array<!HTMLInputElement>}
* @private
*/
getVarSubsFields_() {
// Fields that support var substitutions.
return this.form_.querySelectorAll('[type="hidden"][data-amp-replace]');
const {elements} = this.form_;
return Array.from(elements).filter((ele) => {
return (
ele.tagName.toUpperCase() === 'INPUT' &&
ele.hasAttribute('data-amp-replace') &&
ele.type.toLocaleLowerCase() === 'hidden'
);
});
}

/**
Expand Down Expand Up @@ -1448,8 +1455,18 @@ export class AmpForm {
* @return {boolean} Whether the form is currently valid or not.
*/
function checkUserValidityOnSubmission(form) {
const elements = form.querySelectorAll('input,select,textarea,fieldset');
iterateCursor(elements, (element) => checkUserValidity(element));
const {elements} = form;
const formElementTagNames = ['INPUT', 'SELECT', 'TEXTAREA', 'FIELDSET'];
const elementsToBeChecked = Array.from(elements).filter((ele) => {
const tagName = ele.tagName.toUpperCase();
// Only allow allow-listed elements and elements must be direct descendant
// of form or an <input>
return formElementTagNames.indexOf(tagName) > -1 && (
tagName === 'INPUT' ||
form.contains(ele)
);
});
iterateCursor(elementsToBeChecked, (element) => checkUserValidity(element));
return checkUserValidity(form);
}

Expand Down
150 changes: 120 additions & 30 deletions extensions/amp-form/0.1/test/test-amp-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -2050,6 +2050,35 @@ describes.repeated(
});
});

it('should manage valid/invalid on submit for elements outside form', async () => {
setReportValiditySupportedForTesting(false);
const ampForm = await getAmpForm(getForm(/*button1*/ true));
const form = ampForm.form_;
form.setAttribute('id', 'registration');
const fieldset = createElement('fieldset');
const emailInput = createElement('input');
emailInput.setAttribute('name', 'email');
emailInput.setAttribute('type', 'email');
emailInput.setAttribute('form', 'registration');
emailInput.setAttribute('required', '');
fieldset.setAttribute('form', 'registration');
fieldset.appendChild(emailInput);
env.ampdoc.getBody().appendChild(fieldset);

env.sandbox.spy(form, 'checkValidity');
env.sandbox.spy(emailInput, 'checkValidity');
env.sandbox.spy(fieldset, 'checkValidity');

ampForm.checkValidity_();
expect(form.checkValidity).to.be.called;
expect(emailInput.checkValidity).to.be.called;
// Fieldset is not a direct descendant of the form
// and is not an input
expect(fieldset.checkValidity).to.not.be.called;
expect(form.className).to.contain('user-invalid');
expect(emailInput.className).to.contain('user-invalid');
});

it('should manage valid/invalid on input user interaction', () => {
setReportValiditySupportedForTesting(false);
return getAmpForm(getForm(/*button1*/ true)).then((ampForm) => {
Expand Down Expand Up @@ -2354,53 +2383,114 @@ describes.repeated(
});

describe('Var Substitution', () => {
it('should substitute hidden fields variables in XHR async', () => {
return getAmpForm(getForm()).then((ampForm) => {
const form = ampForm.form_;
const clientIdField = createElement('input');
describe('basic XHR request', () => {
let form, ampForm, clientIdField, canonicalUrlField;

beforeEach(async () => {
ampForm = await getAmpForm(getForm());
form = ampForm.form_;
form.setAttribute('id', 'registration');
env.sandbox.stub(form, 'checkValidity').returns(true);
env.sandbox.stub(ampForm.xhr_, 'xssiJson').resolves();
env.sandbox.stub(ampForm.xhr_, 'fetch').resolves();
env.sandbox.stub(ampForm, 'handleSubmitSuccess_');
env.sandbox.spy(ampForm.urlReplacement_, 'expandInputValueAsync');
env.sandbox.stub(ampForm.urlReplacement_, 'expandInputValueSync');
clientIdField = createElement('input');
clientIdField.setAttribute('name', 'clientId');
clientIdField.setAttribute('type', 'hidden');
clientIdField.value = 'CLIENT_ID(form)';
clientIdField.setAttribute('data-amp-replace', 'CLIENT_ID');
form.appendChild(clientIdField);
const canonicalUrlField = createElement('input');
canonicalUrlField = createElement('input');
canonicalUrlField.setAttribute('name', 'clientId');
canonicalUrlField.setAttribute('type', 'hidden');
canonicalUrlField.value = 'CANONICAL_URL';
canonicalUrlField.setAttribute(
'data-amp-replace',
'CANONICAL_URL'
);
});

it('should substitute hidden fields variables in XHR async', async () => {
form.appendChild(clientIdField);
form.appendChild(canonicalUrlField);

env.sandbox.stub(form, 'checkValidity').returns(true);
env.sandbox.stub(ampForm.xhr_, 'fetch').resolves();
env.sandbox.stub(ampForm, 'handleSubmitSuccess_');
env.sandbox.spy(ampForm.urlReplacement_, 'expandInputValueAsync');
env.sandbox.stub(ampForm.urlReplacement_, 'expandInputValueSync');
expect(ampForm.xhr_.xssiJson).to.have.not.been.called;
expect(ampForm.urlReplacement_.expandInputValueSync).to.not.have
.been.called;

const submitPromise = ampForm.submit_(ActionTrust.HIGH);
expect(ampForm.xhr_.fetch).to.have.not.been.called;
await ampForm.submit_(ActionTrust.HIGH);
expect(ampForm.urlReplacement_.expandInputValueAsync).to.have.been
.calledTwice;
expect(
ampForm.urlReplacement_.expandInputValueAsync
).to.have.been.calledWith(clientIdField);
expect(
ampForm.urlReplacement_.expandInputValueAsync
).to.have.been.calledWith(canonicalUrlField);

expect(ampForm.xhr_.xssiJson).to.be.called;
expect(clientIdField.value).to.match(/amp-.+/);
expect(canonicalUrlField.value).to.equal(
'https%3A%2F%2Fexample.com%2Famps.html'
);
});

it('should substitute input fields outside of the form', async () => {
// Outside input fields must have `form` attribute
clientIdField.setAttribute('form', 'registration');
canonicalUrlField.setAttribute('form', 'registration');
env.ampdoc.getBody().appendChild(clientIdField);
env.ampdoc.getBody().appendChild(canonicalUrlField);

expect(ampForm.xhr_.xssiJson).to.have.not.been.called;
expect(ampForm.urlReplacement_.expandInputValueSync).to.not.have
.been.called;

submitPromise.then(() => {
expect(ampForm.urlReplacement_.expandInputValueAsync).to.have
.been.calledTwice;
expect(
ampForm.urlReplacement_.expandInputValueAsync
).to.have.been.calledWith(clientIdField);
expect(
ampForm.urlReplacement_.expandInputValueAsync
).to.have.been.calledWith(canonicalUrlField);
return whenCalled(ampForm.xhr_.fetch).then(() => {
expect(ampForm.xhr_.fetch).to.be.called;
expect(clientIdField.value).to.match(/amp-.+/);
expect(canonicalUrlField.value).to.equal(
'https%3A%2F%2Fexample.com%2Famps.html'
);
});
});
await ampForm.submit_(ActionTrust.HIGH);
expect(ampForm.urlReplacement_.expandInputValueAsync).to.have.been
.calledTwice;
expect(
ampForm.urlReplacement_.expandInputValueAsync
).to.have.been.calledWith(clientIdField);
expect(
ampForm.urlReplacement_.expandInputValueAsync
).to.have.been.calledWith(canonicalUrlField);
await whenCalled(ampForm.xhr_.fetch);
expect(ampForm.xhr_.fetch).to.be.called;
expect(clientIdField.value).to.match(/amp-.+/);
expect(canonicalUrlField.value).to.equal(
'https%3A%2F%2Fexample.com%2Famps.html'
);
});

it('should not do var substitution on invalid elements', async () => {
// remove hidden
clientIdField.removeAttribute('type');

// wrong form
clientIdField.removeAttribute('type');

// not an input
const randomField = createElement('textarea');
randomField.setAttribute('name', 'clientId');
randomField.setAttribute('type', 'hidden');
randomField.value = 'RANDOM';
randomField.setAttribute('data-amp-replace', 'RANDOM');
randomField.setAttribute('form', 'registration');

env.ampdoc.getBody().appendChild(clientIdField);
env.ampdoc.getBody().appendChild(canonicalUrlField);
env.ampdoc.getBody().appendChild(randomField);

await ampForm.submit_(ActionTrust.HIGH);
expect(ampForm.urlReplacement_.expandInputValueAsync).to.not.be
.called;
await whenCalled(ampForm.xhr_.fetch);
expect(ampForm.xhr_.fetch).to.be.called;
expect(clientIdField.value).to.equal('CLIENT_ID(form)');
expect(randomField.value).to.equal('RANDOM');
expect(canonicalUrlField.value).to.equal('CANONICAL_URL');
});
});

Expand Down
8 changes: 4 additions & 4 deletions extensions/amp-form/amp-form.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ If you're submitting data in your form, your server endpoint must implement the

[/tip]

Before creating a `<form>`, you must include the required script for the `<amp-form>` extension, otherwise your document will be invalid. If you're using `input` tags for purposes other than submitting their values (e.g., inputs not inside a `<form>`), you do not need to load the `amp-form` extension.
Before creating a `<form>`, you must include the required script for the `<amp-form>` extension, otherwise your document will be invalid. If you're using `input` tags for purposes other than submitting their values, you do not need to load the `amp-form` extension.

[example preview="inline" playground="true" imports="amp-form" template="amp-mustache"]

Expand Down Expand Up @@ -101,15 +101,15 @@ Before creating a `<form>`, you must include the required script for the `<amp-f
[filter formats="websites, ads"]

- `<input type=button>`, `<input type=image>`
- Most of the form-related attributes on inputs including: `form`, `formaction`, `formtarget`, `formmethod` and others.
- Most of the form-related attributes on inputs including: `formaction`, `formtarget`, `formmethod` and others.

[/filter]<!-- formats="websites, ads" -->

[filter formats="email"]

- `<input type=button>`, `<input type=image>`
- `<input type=password>` and `<input type=file>`
- Most of the form-related attributes on inputs including: `form`, `formaction`, `formtarget`, `formmethod` and others.
- Most of the form-related attributes on inputs including: `formaction`, `formtarget`, `formmethod` and others.

[/filter]<!-- formats="email" -->

Expand Down Expand Up @@ -434,7 +434,7 @@ For more examples, see [examples/forms.amp.html](../../examples/forms.amp.html).

### Variable substitutions

The `amp-form` extension allows [platform variable substitutions](../../spec/amp-var-substitutions.md) for inputs that are hidden and that have the `data-amp-replace` attribute. On each form submission, `amp-form` finds all `input[type=hidden][data-amp-replace]` inside the form and applies variable substitutions to its `value` attribute and replaces it with the result of the substitution.
The `amp-form` extension allows [platform variable substitutions](../../spec/amp-var-substitutions.md) for inputs that are hidden and that have the `data-amp-replace` attribute. On each form submission, `amp-form` finds all `input[type=hidden][data-amp-replace]` inside the form (or referenced via the `form` attribute) and applies variable substitutions to its `value` attribute and replaces it with the result of the substitution.

You must provide the variables you are using for each substitution on each input by specifying a space-separated string of the variables used in `data-amp-replace` (see example below). AMP will not replace variables that are not explicitly specified.

Expand Down
7 changes: 7 additions & 0 deletions validator/testdata/feature_tests/forms.html
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@
<form method="post" action-xhr="https://example/subscribe" target="_new">
<input type="submit" value="Subscribe">
</form>
<!-- Valid: input with form attribute can be outside the form. -->
<input form="my-form3" type="text" name="name">
<input form="my-form3" type="hidden" data-amp-replace="RANDOM">
<form id="my-form3" method="post" action-xhr="https://example-cdn.ampproject.org/subscribe" target="_blank">
<input type="submit" value="Subscribe">
</form>
<textarea name="comment"></textarea>
<!-- Invalid: input, select, option and textarea must be children of form. -->
<input type="text" name="name">
<select name="language">
Expand Down
Loading

0 comments on commit feb6466

Please sign in to comment.