From 7c2af00a0fb52a9c479163c20172578275b5e639 Mon Sep 17 00:00:00 2001 From: Micajuine Ho Date: Tue, 1 Jun 2021 13:48:58 -0700 Subject: [PATCH] =?UTF-8?q?Revert=20"=E2=9C=A8[amp-form]=20allow=20`form`?= =?UTF-8?q?=20attributes=20for=20form=20elements=20outside=20of=20`amp-for?= =?UTF-8?q?m`=20(#33095)"=20(#34640)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit be956c93b50f9481ccb130bb34ba805bc0415fe1. --- build-system/test-configs/forbidden-terms.js | 2 - extensions/amp-form/0.1/amp-form.js | 160 +---- extensions/amp-form/0.1/form-dirtiness.js | 29 +- extensions/amp-form/0.1/form-validators.js | 3 +- extensions/amp-form/0.1/form-verifiers.js | 3 +- .../test/integration/test-integration-form.js | 15 +- extensions/amp-form/0.1/test/test-amp-form.js | 465 ++----------- .../amp-form/0.1/test/test-form-dirtiness.js | 612 +++++++++--------- extensions/amp-form/amp-form.md | 8 +- validator/testdata/feature_tests/forms.html | 7 - validator/testdata/feature_tests/forms.out | 33 +- validator/validator-main.protoascii | 1 - 12 files changed, 417 insertions(+), 921 deletions(-) diff --git a/build-system/test-configs/forbidden-terms.js b/build-system/test-configs/forbidden-terms.js index 13260146e55f..61f7259977ba 100644 --- a/build-system/test-configs/forbidden-terms.js +++ b/build-system/test-configs/forbidden-terms.js @@ -304,8 +304,6 @@ const forbiddenTermsGlobal = { 'extensions/amp-a4a/0.1/amp-ad-template-helper.js', 'extensions/amp-analytics/0.1/instrumentation.js', 'extensions/amp-analytics/0.1/variables.js', - 'extensions/amp-form/0.1/amp-form.js', // References service defined in amp-form. - 'extensions/amp-form/0.1/form-dirtiness.js', // References service defined in amp-form. 'extensions/amp-fx-collection/0.1/providers/fx-provider.js', 'extensions/amp-gwd-animation/0.1/amp-gwd-animation.js', 'src/chunk.js', diff --git a/extensions/amp-form/0.1/amp-form.js b/extensions/amp-form/0.1/amp-form.js index ec8ed3918f6c..b8679dcf8c81 100644 --- a/extensions/amp-form/0.1/amp-form.js +++ b/extensions/amp-form/0.1/amp-form.js @@ -43,16 +43,14 @@ import {SsrTemplateHelper} from '../../../src/ssr-template-helper'; import { ancestorElementsByTag, childElementByAttr, - closestAncestorElementBySelector, createElementWithAttributes, iterateCursor, - matches, removeElement, tryFocus, } from '../../../src/dom'; -import {createCustomEvent, listen} from '../../../src/event-helper'; +import {createCustomEvent} from '../../../src/event-helper'; import {createFormDataWrapper} from '../../../src/form-data-wrapper'; -import {deepMerge, dict, hasOwn} from '../../../src/core/types/object'; +import {deepMerge, dict} from '../../../src/core/types/object'; import {dev, devAssert, user, userAssert} from '../../../src/log'; import {escapeCssSelectorIdent} from '../../../src/core/dom/css'; import { @@ -62,7 +60,6 @@ import { } from '../../../src/form'; import {getFormValidator, isCheckValiditySupported} from './form-validators'; import {getMode} from '../../../src/mode'; -import {getServiceForDocOrNull} from '../../../src/service'; import {installFormProxy} from './form-proxy'; import {installStylesForDoc} from '../../../src/style-installer'; import {isAmp4Email} from '../../../src/format'; @@ -147,9 +144,6 @@ export class AmpForm { /** @const @private {!../../../src/service/ampdoc-impl.AmpDoc} */ this.ampdoc_ = Services.ampdoc(this.form_); - /** @const @private {?AmpFormService} */ - this.ampFormService_ = getServiceForDocOrNull(this.ampdoc_, TAG); - /** @private {?Promise} */ this.dependenciesPromise_ = null; @@ -396,9 +390,6 @@ export class AmpForm { * Returns a promise that will be resolved when all dependencies used inside * the form tag are loaded and built (e.g. amp-selector) or 2 seconds timeout * - whichever is first. - * - * NOTE: amp-form allows elements that are not descendants of itself, but - * not s. See https://go.amp.dev/issue/33891 * @return {!Promise} * @private */ @@ -425,16 +416,14 @@ export class AmpForm { tryFocus(autofocus); } }); - devAssert(this.ampFormService_); - this.ampFormService_.addFormEventListener( - this.form_, + + this.form_.addEventListener( 'submit', this.handleSubmitEvent_.bind(this), true ); - this.ampFormService_.addFormEventListener( - this.form_, + this.form_.addEventListener( 'blur', (e) => { checkUserValidityAfterInteraction_(dev().assertElement(e.target)); @@ -443,8 +432,7 @@ export class AmpForm { true ); - this.ampFormService_.addFormEventListener( - this.form_, + this.form_.addEventListener( AmpEvents.FORM_VALUE_CHANGE, (e) => { checkUserValidityAfterInteraction_(dev().assertElement(e.target)); @@ -455,7 +443,7 @@ export class AmpForm { // Form verification is not supported when SSRing templates is enabled. if (!this.ssrTemplateHelper_.isEnabled()) { - this.ampFormService_.addFormEventListener(this.form_, 'change', (e) => { + this.form_.addEventListener('change', (e) => { this.verifier_.onCommit().then((updatedErrors) => { const {errors, updatedElements} = updatedErrors; updatedElements.forEach(checkUserValidityAfterInteraction_); @@ -482,7 +470,7 @@ export class AmpForm { }); } - this.ampFormService_.addFormEventListener(this.form_, 'change', (e) => { + this.form_.addEventListener('input', (e) => { checkUserValidityAfterInteraction_(dev().assertElement(e.target)); this.validator_.onInput(e); }); @@ -550,11 +538,10 @@ export class AmpForm { this.form_.classList.remove('user-valid'); this.form_.classList.remove('user-invalid'); - const validityElements = formElementsQuerySelectorAll( - this.form_, + const validityElements = this.form_.querySelectorAll( '.user-valid, .user-invalid' ); - validityElements.forEach((element) => { + iterateCursor(validityElements, (element) => { element.classList.remove('user-valid'); element.classList.remove('user-invalid'); }); @@ -709,15 +696,12 @@ export class AmpForm { /** * Get form fields that require variable substitutions - * @return {!Array} + * @return {!IArrayLike} * @private */ getVarSubsFields_() { // Fields that support var substitutions. - return formElementsQuerySelectorAll( - this.form_, - '[type="hidden"][data-amp-replace]' - ); + return this.form_.querySelectorAll('[type="hidden"][data-amp-replace]'); } /** @@ -894,7 +878,7 @@ export class AmpForm { /** * Perform asynchronous variable substitution on the fields that require it. - * @param {!Array} varSubsFields + * @param {!IArrayLike} varSubsFields * @return {!Promise} * @private */ @@ -954,9 +938,10 @@ export class AmpForm { * @private */ doVerifyXhr_() { - const noVerifyFields = formElementsQuerySelectorAll( - this.form_, - `[${escapeCssSelectorIdent(FORM_VERIFY_OPTOUT)}]` + const noVerifyFields = toArray( + this.form_.querySelectorAll( + `[${escapeCssSelectorIdent(FORM_VERIFY_OPTOUT)}]` + ) ); const denylist = noVerifyFields.map((field) => field.name || field.id); @@ -1142,14 +1127,13 @@ export class AmpForm { * @private */ assertNoSensitiveFields_() { - const fields = formElementsQuerySelectorAll( - this.form_, + const fields = this.form_.querySelectorAll( 'input[type=password],input[type=file]' ); userAssert( fields.length == 0, 'input[type=password] or input[type=file] ' + - 'may only appear with form[method=post]' + 'may only appear in form[method=post]' ); } @@ -1461,45 +1445,14 @@ export class AmpForm { } } -/** - * Returns all elements of form.elements - * that match the selectors. - * @param {!HTMLFormElement} form - * @param {string} query - * @return {!Array} - */ -export function formElementsQuerySelectorAll(form, query) { - return Array.from(form.elements).filter((element) => matches(element, query)); -} - -/** - * Returns the first element for the form.elements - * that match the selectors. - * @param {!HTMLFormElement} form - * @param {string} query - * @return {?HTMLElement} - */ -export function formElementsQuerySelector(form, query) { - for (let i = 0; i < form.elements.length; i++) { - const element = form.elements[i]; - if (matches(element, query)) { - return element; - } - } - return null; -} - /** * Checks user validity for all inputs, fieldsets and the form. * @param {!HTMLFormElement} form * @return {boolean} Whether the form is currently valid or not. */ function checkUserValidityOnSubmission(form) { - const elements = formElementsQuerySelectorAll( - form, - 'input,select,textarea,fieldset' - ); - elements.forEach((element) => checkUserValidity(element)); + const elements = form.querySelectorAll('input,select,textarea,fieldset'); + iterateCursor(elements, (element) => checkUserValidity(element)); return checkUserValidity(form); } @@ -1539,11 +1492,10 @@ function updateInvalidTypesClasses(element) { function removeValidityStateClasses(form) { const dummyInput = document.createElement('input'); for (const validityState in dummyInput.validity) { - const elements = formElementsQuerySelectorAll( - form, + const elements = form.querySelectorAll( `.${escapeCssSelectorIdent(validityState)}` ); - elements.forEach((element) => { + iterateCursor(elements, (element) => { dev().assertElement(element).classList.remove(validityState); }); } @@ -1623,7 +1575,6 @@ export function checkUserValidityAfterInteraction_(input) { /** * Bootstraps the amp-form elements - * @implements {../../src/service.Disposable} */ export class AmpFormService { /** @@ -1635,15 +1586,6 @@ export class AmpFormService { this.installHandlers_(ampdoc) ); - /** @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc */ - this.ampdoc_ = ampdoc; - - /** @const @private {!Array} */ - this.unlisteners_ = []; - - /** @const @private {!Object>>} */ - this.eventHandlers_ = {}; - // Dispatch a test-only event for integration tests. if (getMode().test) { this.whenInitialized_.then(() => { @@ -1724,62 +1666,6 @@ export class AmpFormService { }); } - /** @override */ - dispose() { - while (this.unlisteners_.length > 0) { - const unlisten = this.unlisteners_.pop(); - unlisten(); - } - } - - /** - * Adds handler for the form for a given type, when the - * rootNode gets the signal. - * @param {!HTMLFormElement} form - * @param {string} type - * @param {function(!Event)} handler - * @param {boolean=} opt_options - */ - addFormEventListener(form, type, handler, opt_options) { - if (!hasOwn(this.eventHandlers_, type)) { - this.eventHandlers_[type] = new WeakMap(); - this.unlisteners_.push( - listen( - this.ampdoc_.getRootNode(), - type, - (e) => { - let {form} = e.target; - - // If it's an AMP element that does not have a native form attribute, - // then find the form by either querySelector for based upon 'form' - // attribute on the element or traversing up. - if (!form) { - dev().assertElement(e.target); - const formId = e.target.getAttribute('form'); - form = formId - ? this.ampdoc_.getRootNode().querySelector(formId) - : closestAncestorElementBySelector(e.target, 'form'); - } - - // Only call handlers if the element has a registered form. - if (form && this.eventHandlers_[type].has(form)) { - this.eventHandlers_[type].get(form).forEach((handlerForForm) => { - handlerForForm(e); - }); - } - }, - opt_options - ) - ); - } - const handlersArr = this.eventHandlers_[type].get(form) || []; - handlersArr.push(handler); - this.eventHandlers_[type].set(form, handlersArr); - this.unlisteners_.push(() => { - this.eventHandlers_[type].delete(form); - }); - } - /** * Listen for Ctrl/Cmd + Enter in textarea elements * to trigger form submission when relevant. diff --git a/extensions/amp-form/0.1/form-dirtiness.js b/extensions/amp-form/0.1/form-dirtiness.js index 2c89fef6ff66..5e7b7ec79f77 100644 --- a/extensions/amp-form/0.1/form-dirtiness.js +++ b/extensions/amp-form/0.1/form-dirtiness.js @@ -15,12 +15,10 @@ */ import {AmpEvents} from '../../../src/core/constants/amp-events'; -import {Services} from '../../../src/services'; import {createCustomEvent} from '../../../src/event-helper'; import {createFormDataWrapper} from '../../../src/form-data-wrapper'; -import {dev, devAssert} from '../../../src/log'; +import {dev} from '../../../src/log'; import {dict, map} from '../../../src/core/types/object'; -import {getServiceForDocOrNull} from '../../../src/service'; import {isDisabled, isFieldDefault, isFieldEmpty} from '../../../src/form'; export const DIRTINESS_INDICATOR_CLASS = 'amp-form-dirty'; @@ -32,9 +30,6 @@ const SUPPORTED_TAG_NAMES = { 'TEXTAREA': true, }; -/** @const {string} */ -const TAG = 'amp-form'; - export class FormDirtiness { /** * @param {!HTMLFormElement} form @@ -47,12 +42,6 @@ export class FormDirtiness { /** @private @const {!Window} */ this.win_ = win; - /** @const {!../../../src/service/ampdoc-impl.AmpDoc} */ - const ampdoc = Services.ampdoc(this.form_); - - /** @const @private {?AmpFormService} */ - this.ampFormService_ = getServiceForDocOrNull(ampdoc, TAG); - /** @private {number} */ this.dirtyFieldCount_ = 0; @@ -141,22 +130,12 @@ export class FormDirtiness { * @private */ installEventHandlers_() { - devAssert(this.ampFormService_); - this.ampFormService_.addFormEventListener( - this.form_, - 'input', - this.onInput_.bind(this) - ); - this.ampFormService_.addFormEventListener( - this.form_, - 'reset', - this.onReset_.bind(this) - ); + this.form_.addEventListener('input', this.onInput_.bind(this)); + this.form_.addEventListener('reset', this.onReset_.bind(this)); // `amp-bind` dispatches the custom event `FORM_VALUE_CHANGE` when it // mutates the value of a form field (e.g. textarea, input, etc) - this.ampFormService_.addFormEventListener( - this.form_, + this.form_.addEventListener( AmpEvents.FORM_VALUE_CHANGE, this.onInput_.bind(this) ); diff --git a/extensions/amp-form/0.1/form-validators.js b/extensions/amp-form/0.1/form-validators.js index 1db4653d39d6..6f675befca75 100644 --- a/extensions/amp-form/0.1/form-validators.js +++ b/extensions/amp-form/0.1/form-validators.js @@ -18,7 +18,6 @@ import {Services} from '../../../src/services'; import {ValidationBubble} from './validation-bubble'; import {createCustomEvent} from '../../../src/event-helper'; import {dev} from '../../../src/log'; -import {formElementsQuerySelectorAll} from './amp-form'; import {iterateCursor} from '../../../src/dom'; import {toWin} from '../../../src/types'; @@ -116,7 +115,7 @@ export class FormValidator { /** @return {!NodeList} */ inputs() { - return formElementsQuerySelectorAll(this.form, 'input,select,textarea'); + return this.form.querySelectorAll('input,select,textarea'); } /** diff --git a/extensions/amp-form/0.1/form-verifiers.js b/extensions/amp-form/0.1/form-verifiers.js index 87c428fab979..f05878179a53 100644 --- a/extensions/amp-form/0.1/form-verifiers.js +++ b/extensions/amp-form/0.1/form-verifiers.js @@ -15,7 +15,6 @@ */ import {LastAddedResolver} from '../../../src/core/data-structures/promise'; -import {formElementsQuerySelector} from './amp-form.js'; import {isFieldDefault} from '../../../src/form'; import {iterateCursor} from '../../../src/dom'; import {user} from '../../../src/log'; @@ -248,7 +247,7 @@ export class AsyncVerifier extends FormVerifier { errors.every((error) => previousError.name !== error.name); const fixedElements = previousErrors .filter(isFixed) - .map((e) => formElementsQuerySelector(this.form_, `[name="${e.name}"]`)); + .map((e) => this.form_./*OK*/ querySelector(`[name="${e.name}"]`)); return /** @type {!UpdatedErrorsDef} */ ({ updatedElements: errorElements.concat(fixedElements), diff --git a/extensions/amp-form/0.1/test/integration/test-integration-form.js b/extensions/amp-form/0.1/test/integration/test-integration-form.js index 2c91ce0b5a3a..a267b79b55eb 100644 --- a/extensions/amp-form/0.1/test/integration/test-integration-form.js +++ b/extensions/amp-form/0.1/test/integration/test-integration-form.js @@ -14,7 +14,6 @@ * limitations under the License. */ -import * as Service from '../../../../../src/service'; import {AmpEvents} from '../../../../../src/core/constants/amp-events'; import {AmpForm, AmpFormService} from '../../amp-form'; import {AmpMustache} from '../../../../amp-mustache/0.1/amp-mustache'; @@ -35,14 +34,12 @@ describes.realWin( runtimeOn: true, ampdoc: 'single', }, - extensions: ['amp-form'], // amp-form is installed as service. mockFetch: false, }, (env) => { const {testServerPort} = window.ampTestRuntimeConfig; const baseUrl = `http://localhost:${testServerPort || '9876'}`; let doc; - let ampFormService; const realSetTimeout = window.setTimeout; const stubSetTimeout = (callback, delay) => { @@ -77,17 +74,7 @@ describes.realWin( stubElementsForDoc(env.ampdoc); - ampFormService = new AmpFormService(env.ampdoc); - const originalGetServiceForDocOrNull = Service.getServiceForDocOrNull; - - env.sandbox - .stub(Service, 'getServiceForDocOrNull') - .callsFake((ampdoc, id) => { - if (id === 'amp-form') { - return ampFormService; - } - return originalGetServiceForDocOrNull(ampdoc, id); - }); + new AmpFormService(env.ampdoc); // Wait for submit listener to be installed before starting tests. return installGlobalSubmitListenerForDoc(env.ampdoc); diff --git a/extensions/amp-form/0.1/test/test-amp-form.js b/extensions/amp-form/0.1/test/test-amp-form.js index b34d2555d461..e994f874e0a3 100644 --- a/extensions/amp-form/0.1/test/test-amp-form.js +++ b/extensions/amp-form/0.1/test/test-amp-form.js @@ -39,7 +39,6 @@ import { isFormDataWrapper, } from '../../../../src/form-data-wrapper'; import {fromIterator} from '../../../../src/core/types/array'; -import {macroTask} from '../../../../testing/yield.js'; import {parseQueryString} from '../../../../src/core/types/string/url'; import { setCheckValiditySupportedForTesting, @@ -227,54 +226,6 @@ describes.repeated( setReportValiditySupportedForTesting(undefined); }); - describe('AmpFormService event handler', () => { - let ampFormService; - - beforeEach(() => { - ampFormService = new AmpFormService(env.ampdoc); - }); - - it('should add handler for different types', () => { - const form = getForm(); - - ampFormService.addFormEventListener(form, 'blur', () => {}); - ampFormService.addFormEventListener(form, 'submit', () => {}); - ampFormService.addFormEventListener(form, 'change', () => {}); - expect(ampFormService.unlisteners_.length).to.equal(6); - expect(Object.keys(ampFormService.eventHandlers_).length).to.equal( - 3 - ); - }); - - it('should add handlers for different forms', () => { - const form = getForm(); - const form2 = getForm(); - - ampFormService.addFormEventListener(form, 'blur', () => {}); - ampFormService.addFormEventListener(form2, 'blur', () => {}); - ampFormService.addFormEventListener(form, 'submit', () => {}); - ampFormService.addFormEventListener(form2, 'submit', () => {}); - expect(ampFormService.unlisteners_.length).to.equal(6); - expect(Object.keys(ampFormService.eventHandlers_).length).to.equal( - 2 - ); - }); - - it('should dispose of all listeners', () => { - const form = getForm(); - - ampFormService.addFormEventListener(form, 'blur', () => {}); - ampFormService.addFormEventListener(form, 'submit', () => {}); - ampFormService.addFormEventListener(form, 'change', () => {}); - - ampFormService.dispose(); - expect(ampFormService.unlisteners_.length).to.equal(0); - Object.keys(ampFormService.eventHandlers_).forEach((key) => { - expect(ampFormService.eventHandlers_[key].has(form)).to.be.false; - }); - }); - }); - describe('Server side template rendering', () => { let getSsrAmpFormPromise; let event; @@ -656,33 +607,19 @@ describes.repeated( /Illegal input name, __amp_source_origin found/ ); }); - form.removeChild(illegalInput); - - form.setAttribute('id', 'registration'); - illegalInput.setAttribute('form', 'registration'); - document.body.appendChild(illegalInput); - allowConsoleError(() => { - expect(() => new AmpForm(form)).to.throw( - /Illegal input name, __amp_source_origin found/ - ); - }); document.body.removeChild(form); }); - it('should listen to submit, blur and input events', async () => { + it('should listen to submit, blur and input events', () => { const form = getForm(); document.body.appendChild(form); - const spy = env.sandbox.spy( - AmpFormService.prototype, - 'addFormEventListener' - ); + form.addEventListener = env.sandbox.spy(); form.setAttribute('action-xhr', 'https://example.com'); new AmpForm(form); - await macroTask(); - expect(spy).to.be.called; - expect(spy).to.be.calledWith(form, 'submit'); - expect(spy).to.be.calledWith(form, 'blur'); - expect(spy).to.be.calledWith(form, 'input'); + expect(form.addEventListener).to.be.called; + expect(form.addEventListener).to.be.calledWith('submit'); + expect(form.addEventListener).to.be.calledWith('blur'); + expect(form.addEventListener).to.be.calledWith('input'); expect(form.className).to.contain('i-amphtml-form'); document.body.removeChild(form); }); @@ -778,17 +715,6 @@ describes.repeated( emailInput.setAttribute('type', 'email'); emailInput.setAttribute('required', ''); form.appendChild(emailInput); - - const emailInput2 = createElement('input'); - emailInput2.setAttribute('name', 'email'); - emailInput2.setAttribute('type', 'email'); - emailInput2.setAttribute('required', ''); - form.setAttribute('id', 'registration'); - emailInput2.setAttribute('id', 'registration'); - - form.appendChild(emailInput); - document.body.appendChild(emailInput2); - const ampForm = new AmpForm(form); env.sandbox.stub(ampForm.xhr_, 'fetch').resolves(); const event = { @@ -799,7 +725,6 @@ describes.repeated( env.sandbox.spy(form, 'checkValidity'); env.sandbox.spy(emailInput, 'reportValidity'); - env.sandbox.spy(emailInput2, 'reportValidity'); env.sandbox.stub(ampForm, 'handleXhrSubmitSuccess_').resolves(); return ampForm.handleSubmitEvent_(event).then(() => { @@ -811,7 +736,6 @@ describes.repeated( // However reporting validity shouldn't happen when novalidate. expect(emailInput.reportValidity).to.not.be.called; - expect(emailInput2.reportValidity).to.not.be.called; }); }); @@ -959,32 +883,6 @@ describes.repeated( }); }); - it('should check validity on FORM_VALUE_CHANGE for inputs outside', () => { - setCheckValiditySupportedForTesting(true); - return getAmpForm(getForm()).then((ampForm) => { - const form = ampForm.form_; - const emailInput = createElement('input'); - emailInput.setAttribute('name', 'email'); - emailInput.setAttribute('required', ''); - form.setAttribute('id', 'registration'); - emailInput.setAttribute('form', 'registration'); - document.body.appendChild(emailInput); - document.body.appendChild(form); - env.sandbox.spy(form, 'checkValidity'); - env.sandbox.spy(ampForm.validator_, 'onInput'); - - const event = createCustomEvent( - env.win, - AmpEvents.FORM_VALUE_CHANGE, - /* detail */ null, - {bubbles: true} - ); - emailInput.dispatchEvent(event); - expect(form.checkValidity).to.be.called; - expect(ampForm.validator_.onInput).to.be.calledWith(event); - }); - }); - it('should allow verifying elements with a presubmit request', () => { const formPromise = getAmpForm(getVerificationForm()); const fetchReject = { @@ -1581,21 +1479,18 @@ describes.repeated( it('should trigger amp-form-submit after variables substitution', () => { return getAmpForm(getForm()).then((ampForm) => { const form = ampForm.form_; - form.setAttribute('id', 'registration'); const 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.setAttribute('form', 'registration'); canonicalUrlField.setAttribute('name', 'canonicalUrl'); canonicalUrlField.setAttribute('type', 'hidden'); canonicalUrlField.value = 'CANONICAL_URL'; canonicalUrlField.setAttribute('data-amp-replace', 'CANONICAL_URL'); - document.body.appendChild(canonicalUrlField); + form.appendChild(canonicalUrlField); env.sandbox.stub(form, 'checkValidity').returns(true); env.sandbox.stub(ampForm.xhr_, 'fetch').resolves({ @@ -1618,7 +1513,7 @@ describes.repeated( return Promise.all([submitPromise, fetchWhenCalledPromise]).then( () => { const expectedFormData = { - 'formId': 'registration', + 'formId': '', 'formFields[name]': 'John Miller', 'formFields[clientId]': env.sandbox.match(/amp-.+/), 'formFields[canonicalUrl]': @@ -1884,7 +1779,6 @@ describes.repeated( const form = ampForm.form_; ampForm.method_ = 'GET'; form.setAttribute('method', 'GET'); - form.setAttribute('id', 'registration'); env.sandbox.stub(ampForm.xhr_, 'fetch').resolves(); const fieldset = createElement('fieldset'); @@ -1896,8 +1790,7 @@ describes.repeated( const usernameInput = createElement('input'); usernameInput.setAttribute('name', 'nickname'); usernameInput.setAttribute('required', ''); - usernameInput.setAttribute('form', 'registration'); - document.body.appendChild(usernameInput); + fieldset.appendChild(usernameInput); form.appendChild(fieldset); const event = { stopImmediatePropagation: env.sandbox.spy(), @@ -1948,7 +1841,6 @@ describes.repeated( ampForm.xhr_.fetch.reset(); ampForm.xhr_.fetch.resolves(); fieldset.disabled = true; - usernameInput.disabled = true; submitEventPromise = ampForm.handleSubmitEvent_(event); @@ -1967,7 +1859,6 @@ describes.repeated( ampForm.xhr_.fetch.resolves(); fieldset.removeAttribute('disabled'); - usernameInput.removeAttribute('disabled'); usernameInput.removeAttribute('name'); emailInput.removeAttribute('required'); emailInput.value = ''; @@ -1989,7 +1880,6 @@ describes.repeated( const form = ampForm.form_; ampForm.method_ = 'GET'; form.setAttribute('method', 'GET'); - form.setAttribute('id', 'registration'); env.sandbox.stub(ampForm.xhr_, 'fetch').resolves(); @@ -1999,8 +1889,7 @@ describes.repeated( otherNamesFS.appendChild(otherName1Input); const otherName2Input = createElement('input'); otherName2Input.setAttribute('name', 'name'); - otherName2Input.setAttribute('form', 'registration'); - document.body.appendChild(otherName2Input); + otherNamesFS.appendChild(otherName2Input); form.appendChild(otherNamesFS); // Group of Radio buttons. @@ -2092,7 +1981,6 @@ describes.repeated( ampForm.setState_('submit-success'); femaleRadio.checked = true; otherName1Input.value = 'John Maller'; - otherName2Input.value = 'Mohn Jaller'; ampForm.xhr_.fetch.reset(); ampForm.xhr_.fetch.resolves(); @@ -2103,8 +1991,8 @@ describes.repeated( expect(ampForm.xhr_.fetch).to.be.calledOnce; expect(ampForm.xhr_.fetch).to.be.calledWith( 'https://example.com?name=John%20Miller&name=John%20Maller' + - '&name=Mohn%20Jaller&gender=Female&interests=Football' + - '&interests=Food&city=San%20Francisco' + '&name=&gender=Female&interests=Football&interests=Food&' + + 'city=San%20Francisco' ); return submitEventPromise; }); @@ -2118,23 +2006,16 @@ describes.repeated( setReportValiditySupportedForTesting(false); return getAmpForm(getForm(/*button1*/ true)).then((ampForm) => { 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('required', ''); fieldset.appendChild(emailInput); - const nameInput = createElement('input'); - nameInput.setAttribute('name', 'name'); - nameInput.setAttribute('form', 'registration'); - nameInput.setAttribute('required', ''); - document.body.appendChild(nameInput); form.appendChild(fieldset); env.sandbox.spy(form, 'checkValidity'); env.sandbox.spy(emailInput, 'checkValidity'); env.sandbox.spy(fieldset, 'checkValidity'); - env.sandbox.spy(nameInput, 'checkValidity'); const event = { target: ampForm.form_, @@ -2150,15 +2031,12 @@ describes.repeated( expect(form.checkValidity).to.be.called; expect(emailInput.checkValidity).to.be.called; expect(fieldset.checkValidity).to.be.called; - expect(nameInput.checkValidity).to.be.called; expect(form.className).to.contain('user-invalid'); expect(emailInput.className).to.contain('user-invalid'); - expect(nameInput.className).to.contain('user-invalid'); expect(event.preventDefault).to.be.called; expect(event.stopImmediatePropagation).to.be.called; emailInput.value = 'cool@bea.ns'; - nameInput.value = 'Bohn Jaller'; const submitPromise = ampForm.handleSubmitEvent_(event); expect(submitPromise).to.be.ok; @@ -2167,7 +2045,6 @@ describes.repeated( .then(() => { expect(form.className).to.contain('user-valid'); expect(emailInput.className).to.contain('user-valid'); - expect(nameInput.className).to.contain('user-valid'); }); }); }); @@ -2176,7 +2053,6 @@ describes.repeated( setReportValiditySupportedForTesting(false); return getAmpForm(getForm(/*button1*/ true)).then((ampForm) => { const form = ampForm.form_; - form.setAttribute('id', 'registration'); const fieldset = createElement('fieldset'); const emailInput = createElement('input'); emailInput.setAttribute('name', 'email'); @@ -2186,8 +2062,7 @@ describes.repeated( const usernameInput = createElement('input'); usernameInput.setAttribute('name', 'nickname'); usernameInput.setAttribute('required', ''); - usernameInput.setAttribute('form', 'registration'); - document.body.appendChild(usernameInput); + fieldset.appendChild(usernameInput); form.appendChild(fieldset); env.sandbox.spy(form, 'checkValidity'); env.sandbox.spy(emailInput, 'checkValidity'); @@ -2245,7 +2120,6 @@ describes.repeated( setReportValiditySupportedForTesting(false); return getAmpForm(getForm(/*button1*/ true)).then((ampForm) => { const form = ampForm.form_; - form.setAttribute('id', 'registration'); const fieldset = createElement('fieldset'); const emailInput = createElement('input'); emailInput.setAttribute('name', 'email'); @@ -2266,25 +2140,6 @@ describes.repeated( expect(fieldset.checkValidity).to.not.be.called; expect(emailInput.className).to.contain('user-valid'); expect(form.className).to.not.contain('user-valid'); - - emailInput.checkValidity.resetHistory(); - - const nameInput = createElement('input'); - nameInput.setAttribute('name', 'name'); - nameInput.setAttribute('value', 'Bohn Baller'); - nameInput.setAttribute('form', 'registration'); - nameInput.setAttribute('required', ''); - document.body.appendChild(nameInput); - env.sandbox.spy(nameInput, 'checkValidity'); - - checkUserValidityAfterInteraction_(nameInput); - - expect(nameInput.checkValidity).to.be.called; - expect(form.checkValidity).to.not.be.called; - expect(fieldset.checkValidity).to.not.be.called; - expect(emailInput.checkValidity).to.not.be.called; - expect(nameInput.className).to.contain('user-valid'); - expect(form.className).to.not.contain('user-valid'); }); }); }); @@ -2336,18 +2191,13 @@ describes.repeated( it('should handle clear action and restore initial values', () => { const form = getForm(); document.body.appendChild(form); - form.setAttribute('id', 'registration'); const emailInput = createElement('input'); emailInput.setAttribute('name', 'email'); emailInput.setAttribute('id', 'email'); emailInput.setAttribute('type', 'email'); emailInput.setAttribute('value', 'jack@poc.com'); - const nameInput = createElement('input'); - nameInput.setAttribute('name', 'name'); - nameInput.setAttribute('value', 'John Snow'); form.appendChild(emailInput); - document.body.appendChild(nameInput); return getAmpForm(form).then((ampForm) => { const initalFormValues = ampForm.getFormAsObject_(); @@ -2436,66 +2286,6 @@ describes.repeated( }); }); - it('should remove all form state classes when form is cleared w/ outside inputs', () => { - const form = getForm(); - form.setAttribute('method', 'GET'); - document.body.appendChild(form); - form.setAttribute('id', 'registration'); - - form.setAttribute( - 'custom-validation-reporting', - 'show-all-on-submit' - ); - - const usernameInput = createElement('input'); - usernameInput.setAttribute('name', 'username'); - usernameInput.setAttribute('id', 'username'); - usernameInput.setAttribute('type', 'text'); - usernameInput.setAttribute('required', ''); - usernameInput.setAttribute('value', 'Jack Sparrow'); - usernameInput.setAttribute('form', 'registration'); - document.body.appendChild(usernameInput); - - const emailInput = createElement('input'); - emailInput.setAttribute('name', 'email'); - emailInput.setAttribute('id', 'email1'); - emailInput.setAttribute('type', 'email'); - emailInput.setAttribute('required', ''); - emailInput.setAttribute('value', ''); - emailInput.setAttribute('form', 'registration'); - document.body.appendChild(emailInput); - - const validationMessage = createElement('span'); - validationMessage.setAttribute( - 'visible-when-invalid', - 'valueMissing' - ); - validationMessage.setAttribute('validation-for', 'email1'); - form.appendChild(validationMessage); - - return getAmpForm(form).then((ampForm) => { - // trigger form validations - ampForm.checkValidity_(); - const formValidator = ampForm.validator_; - // show validity message - formValidator.report(); - - expect(usernameInput.className).to.contain('user-valid'); - expect(emailInput.className).to.contain('user-invalid'); - expect(emailInput.className).to.contain('valueMissing'); - expect(ampForm.form_.className).to.contain('user-invalid'); - expect(validationMessage.className).to.contain('visible'); - - ampForm.handleClearAction_(); - - expect(usernameInput.className).to.not.contain('user-valid'); - expect(emailInput.className).to.not.contain('user-invalid'); - expect(emailInput.className).to.not.contain('valueMissing'); - expect(ampForm.form_.className).to.contain('amp-form-initial'); - expect(validationMessage.className).to.not.contain('visible'); - }); - }); - it('should submit after timeout of waiting for amp-selector', async function () { expectAsyncConsoleError(/Form submission failed/); this.timeout(3000); @@ -2563,25 +2353,16 @@ describes.repeated( }); describe('Var Substitution', () => { - 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'); + it('should substitute hidden fields variables in XHR async', () => { + return getAmpForm(getForm()).then((ampForm) => { + const form = ampForm.form_; + const clientIdField = createElement('input'); clientIdField.setAttribute('name', 'clientId'); clientIdField.setAttribute('type', 'hidden'); clientIdField.value = 'CLIENT_ID(form)'; clientIdField.setAttribute('data-amp-replace', 'CLIENT_ID'); - canonicalUrlField = createElement('input'); + form.appendChild(clientIdField); + const canonicalUrlField = createElement('input'); canonicalUrlField.setAttribute('name', 'clientId'); canonicalUrlField.setAttribute('type', 'hidden'); canonicalUrlField.value = 'CANONICAL_URL'; @@ -2589,78 +2370,36 @@ describes.repeated( 'data-amp-replace', 'CANONICAL_URL' ); - }); - - it('should substitute hidden fields variables in XHR async', async () => { - form.appendChild(clientIdField); form.appendChild(canonicalUrlField); - expect(ampForm.xhr_.xssiJson).to.have.not.been.called; - expect(ampForm.urlReplacement_.expandInputValueSync).to.not.have - .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); + 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; + const submitPromise = ampForm.submit_(ActionTrust.HIGH); + expect(ampForm.xhr_.fetch).to.have.not.been.called; expect(ampForm.urlReplacement_.expandInputValueSync).to.not.have .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); - 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'); - - env.ampdoc.getBody().appendChild(clientIdField); - env.ampdoc.getBody().appendChild(canonicalUrlField); - - 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(canonicalUrlField.value).to.equal('CANONICAL_URL'); + 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' + ); + }); + }); }); }); @@ -3027,30 +2766,6 @@ describes.repeated( }); }); - it('should not execute form submit with password field present', () => { - const form = getForm(); - const input = createElement('input'); - input.type = 'password'; - form.setAttribute('id', 'registration'); - input.setAttribute('form', 'registration'); - document.body.appendChild(input); - - return getAmpForm(form).then((ampForm) => { - const form = ampForm.form_; - ampForm.method_ = 'GET'; - ampForm.xhrAction_ = null; - env.sandbox.stub(form, 'submit'); - env.sandbox.stub(form, 'checkValidity').returns(true); - env.sandbox.stub(ampForm.xhr_, 'fetch').resolves(); - allowConsoleError(() => { - expect(() => - ampForm.handleSubmitAction_(/* invocation */ {}) - ).to.throw('input[type=password]'); - }); - expect(form.submit).to.have.not.been.called; - }); - }); - it('should not execute form submit with file field present', () => { const form = getForm(); const input = createElement('input'); @@ -3085,12 +2800,6 @@ describes.repeated( emailInput.setAttribute('value', 'j@hnmiller.com'); form.appendChild(emailInput); - const ageInput = createElement('input'); - ageInput.setAttribute('name', 'age'); - ageInput.setAttribute('value', '100'); - ageInput.setAttribute('form', 'registration'); - document.body.appendChild(ageInput); - const unnamedInput = createElement('input'); unnamedInput.setAttribute('type', 'text'); unnamedInput.setAttribute('value', 'unnamed'); @@ -3112,7 +2821,6 @@ describes.repeated( 'formId': 'registration', 'formFields[name]': 'John Miller', 'formFields[email]': 'j@hnmiller.com', - 'formFields[age]': '100', }; expect(form.submit).to.have.been.called; expect(ampForm.analyticsEvent_).to.be.calledWith( @@ -3135,12 +2843,6 @@ describes.repeated( emailInput.setAttribute('value', 'j@hnmiller.com'); form.appendChild(emailInput); - const ageInput = createElement('input'); - ageInput.setAttribute('name', 'age'); - ageInput.setAttribute('value', '100'); - ageInput.setAttribute('form', 'registration'); - document.body.appendChild(ageInput); - const unnamedInput = createElement('input'); unnamedInput.setAttribute('type', 'text'); unnamedInput.setAttribute('value', 'unnamed'); @@ -3165,7 +2867,6 @@ describes.repeated( 'formId': 'registration', 'formFields[name]': 'John Miller', 'formFields[email]': 'j@hnmiller.com', - 'formFields[age]': '100', }; return ampForm.xhrSubmitPromiseForTesting().then( @@ -3195,12 +2896,6 @@ describes.repeated( emailInput.setAttribute('value', 'j@hnmiller.com'); form.appendChild(emailInput); - const ageInput = createElement('input'); - ageInput.setAttribute('name', 'age'); - ageInput.setAttribute('value', '100'); - ageInput.setAttribute('form', 'registration'); - document.body.appendChild(ageInput); - const unnamedInput = createElement('input'); unnamedInput.setAttribute('type', 'text'); unnamedInput.setAttribute('value', 'unnamed'); @@ -3226,7 +2921,6 @@ describes.repeated( 'formId': 'registration', 'formFields[name]': 'John Miller', 'formFields[email]': 'j@hnmiller.com', - 'formFields[age]': '100', }; return submitEventPromise.then( @@ -3261,10 +2955,9 @@ describes.repeated( const canonicalUrlField = createElement('input'); canonicalUrlField.setAttribute('name', 'canonicalUrl'); canonicalUrlField.setAttribute('type', 'hidden'); - canonicalUrlField.setAttribute('form', 'registration'); canonicalUrlField.setAttribute('data-amp-replace', 'CANONICAL_URL'); canonicalUrlField.value = 'CANONICAL_URL'; - document.body.appendChild(canonicalUrlField); + form.appendChild(canonicalUrlField); env.sandbox.stub(form, 'submit'); env.sandbox.stub(form, 'checkValidity').returns(true); @@ -3597,18 +3290,12 @@ describes.repeated( }); describe('Form Dirtiness State', () => { - let form, ampForm, insideInput, outsideInput, inputs; + let form, ampForm, input; beforeEach(async () => { form = getForm(); - form.setAttribute('id', 'registration'); ampForm = await getAmpForm(form); - insideInput = form.querySelector('input[name=name]'); - outsideInput = createElement('input'); - outsideInput.setAttribute('name', 'name'); - outsideInput.setAttribute('form', 'registration'); - document.body.appendChild(outsideInput); - inputs = [insideInput, outsideInput]; + input = form.querySelector('input[name=name]'); }); function changeInput(element, value) { @@ -3617,50 +3304,40 @@ describes.repeated( element.dispatchEvent(event); } - for (let i = 0; i < 2; i++) { - describe(`${i === 0 ? 'Inside Input' : 'Outside Input'}`, () => { - let input; - - beforeEach(() => { - input = inputs[i]; - }); - - it('adds dirtiness class when a field changes', () => { - changeInput(input, 'Another Name'); - expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); - }); + it('adds dirtiness class when a field changes', () => { + changeInput(input, 'Another Name'); + expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('clears dirtiness class when submitted successfully without XHR', async () => { - ampForm.method_ = 'GET'; - ampForm.xhrAction_ = null; + it('clears dirtiness class when submitted successfully without XHR', async () => { + ampForm.method_ = 'GET'; + ampForm.xhrAction_ = null; - changeInput(input, 'Another Name'); - await ampForm.submit_(ActionTrust.HIGH); + changeInput(input, 'Another Name'); + await ampForm.submit_(ActionTrust.HIGH); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('clears dirtiness class when submitted successfully with XHR', async () => { - env.sandbox - .stub(ampForm.xhr_, 'fetch') - .resolves({json: async () => {}}); + it('clears dirtiness class when submitted successfully with XHR', async () => { + env.sandbox + .stub(ampForm.xhr_, 'fetch') + .resolves({json: async () => {}}); - changeInput(input, 'Another Name'); - await ampForm.submit_(ActionTrust.HIGH); + changeInput(input, 'Another Name'); + await ampForm.submit_(ActionTrust.HIGH); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('does not clear dirtiness class when submission XHR fails', async () => { - env.sandbox.stub(ampForm.xhr_, 'fetch').rejects({}); + it('does not clear dirtiness class when submission XHR fails', async () => { + env.sandbox.stub(ampForm.xhr_, 'fetch').rejects({}); - changeInput(input, 'Another Name'); - await ampForm.submit_(ActionTrust.HIGH); + changeInput(input, 'Another Name'); + await ampForm.submit_(ActionTrust.HIGH); - expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); - }); - }); - } + expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); + }); }); } ); diff --git a/extensions/amp-form/0.1/test/test-form-dirtiness.js b/extensions/amp-form/0.1/test/test-form-dirtiness.js index 8bf4cae97f4c..87206c70a881 100644 --- a/extensions/amp-form/0.1/test/test-form-dirtiness.js +++ b/extensions/amp-form/0.1/test/test-form-dirtiness.js @@ -19,7 +19,6 @@ import {DIRTINESS_INDICATOR_CLASS, FormDirtiness} from '../form-dirtiness'; import {Services} from '../../../../src/services'; import {closestAncestorElementBySelector} from '../../../../src/dom'; import {createCustomEvent, getDetail} from '../../../../src/event-helper'; -import {macroTask} from '../../../../testing/yield'; function getForm(doc) { const form = doc.createElement('form'); @@ -84,410 +83,397 @@ function captureEventDispatched(eventName, element, dispatchEventFunction) { return eventCaptured; } -describes.realWin( - 'form-dirtiness', - { - amp: { - runtimeOn: true, - extensions: ['amp-form'], // amp-form is installed as service. - }, - }, - (env) => { - let doc, form, dirtinessHandler; - - beforeEach(async () => { - doc = env.win.document; - form = getForm(doc); - env.sandbox.stub(Services, 'platformFor').returns({ - isIos() { - return false; - }, - }); - dirtinessHandler = new FormDirtiness(form, env.win); - await macroTask(); - }); +describes.realWin('form-dirtiness', {}, (env) => { + let doc, form, dirtinessHandler; - describe('ignored elements', () => { - it('does not add dirtiness class if a nameless element changes', () => { - const nameless = doc.createElement('input'); - form.appendChild(nameless); + beforeEach(() => { + doc = env.win.document; + form = getForm(doc); + env.sandbox.stub(Services, 'platformFor').returns({ + isIos() { + return false; + }, + }); + dirtinessHandler = new FormDirtiness(form, env.win); + }); - changeInput(nameless, 'changed'); + describe('ignored elements', () => { + it('does not add dirtiness class if a nameless element changes', () => { + const nameless = doc.createElement('input'); + form.appendChild(nameless); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + changeInput(nameless, 'changed'); - it('does not add dirtiness class if a hidden element changes', () => { - const hidden = doc.createElement('input'); - hidden.name = 'name'; - hidden.hidden = true; - form.appendChild(hidden); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); - changeInput(hidden, 'changed'); + it('does not add dirtiness class if a hidden element changes', () => { + const hidden = doc.createElement('input'); + hidden.name = 'name'; + hidden.hidden = true; + form.appendChild(hidden); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + changeInput(hidden, 'changed'); - it('does not add dirtiness class if a disabled element changes', () => { - const disabled = doc.createElement('input'); - disabled.name = 'name'; - disabled.disabled = true; - form.appendChild(disabled); - - changeInput(disabled, 'changed'); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); }); - describe('amp-bind changes', () => { - let input; + it('does not add dirtiness class if a disabled element changes', () => { + const disabled = doc.createElement('input'); + disabled.name = 'name'; + disabled.disabled = true; + form.appendChild(disabled); - beforeEach(() => { - input = doc.createElement('input'); - input.name = 'name'; - form.appendChild(input); - }); + changeInput(disabled, 'changed'); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); + }); - it('adds dirtiness class if an element is changed with amp-bind', () => { - input.value = 'changed'; - dispatchFormValueChangeEvent(input, env.win); + describe('amp-bind changes', () => { + let input; - expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); - }); + beforeEach(() => { + input = doc.createElement('input'); + input.name = 'name'; + form.appendChild(input); + }); - it('removes dirtiness class if a dirty element is cleared with amp-bind', () => { - changeInput(input, 'changed'); - input.value = ''; - dispatchFormValueChangeEvent(input, env.win); + it('adds dirtiness class if an element is changed with amp-bind', () => { + input.value = 'changed'; + dispatchFormValueChangeEvent(input, env.win); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); }); - describe('text field changes', () => { - let textField; + it('removes dirtiness class if a dirty element is cleared with amp-bind', () => { + changeInput(input, 'changed'); + input.value = ''; + dispatchFormValueChangeEvent(input, env.win); - beforeEach(() => { - // Element is inserted as HTML so that the `defaultValue` property is - // generated correctly, since it returns "the default value as - // **originally specified in the HTML** that created this object." - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement#Properties - const html = ''; - form.insertAdjacentHTML('afterbegin', html); - textField = form.querySelector('input'); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); + }); + + describe('text field changes', () => { + let textField; + + beforeEach(() => { + // Element is inserted as HTML so that the `defaultValue` property is + // generated correctly, since it returns "the default value as + // **originally specified in the HTML** that created this object." + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement#Properties + const html = ''; + form.insertAdjacentHTML('afterbegin', html); + textField = form.querySelector('input'); + }); - it('removes dirtiness class when text field is in default state', () => { - changeInput(textField, textField.defaultValue); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + it('removes dirtiness class when text field is in default state', () => { + changeInput(textField, textField.defaultValue); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('removes dirtiness class when text field is empty', () => { - changeInput(textField, ''); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + it('removes dirtiness class when text field is empty', () => { + changeInput(textField, ''); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('adds dirtiness class when text field is changed', () => { - changeInput(textField, 'changed'); - expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); - }); + it('adds dirtiness class when text field is changed', () => { + changeInput(textField, 'changed'); + expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('removes dirtiness class when its value matches the submitted value', () => { - changeInput(textField, 'submitted'); - dirtinessHandler.onSubmitting(); - dirtinessHandler.onSubmitSuccess(); - changeInput(textField, 'submitted'); + it('removes dirtiness class when its value matches the submitted value', () => { + changeInput(textField, 'submitted'); + dirtinessHandler.onSubmitting(); + dirtinessHandler.onSubmitSuccess(); + changeInput(textField, 'submitted'); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); }); + }); - describe('textarea changes', () => { - let textarea; + describe('textarea changes', () => { + let textarea; - beforeEach(() => { - const html = ''; - form.insertAdjacentHTML('afterbegin', html); - textarea = form.querySelector('textarea'); - }); + beforeEach(() => { + const html = ''; + form.insertAdjacentHTML('afterbegin', html); + textarea = form.querySelector('textarea'); + }); - it('removes dirtiness class when textarea is in default state', () => { - changeInput(textarea, textarea.defaultValue); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + it('removes dirtiness class when textarea is in default state', () => { + changeInput(textarea, textarea.defaultValue); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('removes dirtiness class when textarea is empty', () => { - changeInput(textarea, ''); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + it('removes dirtiness class when textarea is empty', () => { + changeInput(textarea, ''); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('adds dirtiness class when textarea is changed', () => { - changeInput(textarea, 'changed'); - expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); - }); + it('adds dirtiness class when textarea is changed', () => { + changeInput(textarea, 'changed'); + expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('removes dirtiness class when its value matches the submitted value', () => { - changeInput(textarea, 'submitted'); - dirtinessHandler.onSubmitting(); - dirtinessHandler.onSubmitSuccess(); - changeInput(textarea, 'submitted'); + it('removes dirtiness class when its value matches the submitted value', () => { + changeInput(textarea, 'submitted'); + dirtinessHandler.onSubmitting(); + dirtinessHandler.onSubmitSuccess(); + changeInput(textarea, 'submitted'); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); }); + }); - describe('checkbox changes', () => { - let checkbox; + describe('checkbox changes', () => { + let checkbox; - beforeEach(() => { - checkbox = createElement(doc, 'input', { - type: 'checkbox', - name: 'checkbox', - }); - form.appendChild(checkbox); + beforeEach(() => { + checkbox = createElement(doc, 'input', { + type: 'checkbox', + name: 'checkbox', }); + form.appendChild(checkbox); + }); - it('clears dirtiness class when checkbox is in default state', () => { - checkbox.setAttribute('checked', 'checked'); - checkInput(checkbox, true); + it('clears dirtiness class when checkbox is in default state', () => { + checkbox.setAttribute('checked', 'checked'); + checkInput(checkbox, true); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('clears dirtiness class when checkbox is not checked', () => { - checkInput(checkbox, false); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + it('clears dirtiness class when checkbox is not checked', () => { + checkInput(checkbox, false); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('adds dirtiness class when checkbox state has changed', () => { - checkInput(checkbox, true); - expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); - }); + it('adds dirtiness class when checkbox state has changed', () => { + checkInput(checkbox, true); + expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('clears dirtiness class when checkbox matches its submitted state', () => { - checkInput(checkbox, true); - dirtinessHandler.onSubmitting(); - dirtinessHandler.onSubmitSuccess(); - checkInput(checkbox, true); + it('clears dirtiness class when checkbox matches its submitted state', () => { + checkInput(checkbox, true); + dirtinessHandler.onSubmitting(); + dirtinessHandler.onSubmitSuccess(); + checkInput(checkbox, true); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); }); + }); - describe('radio button changes', () => { - let optionA, optionB; + describe('radio button changes', () => { + let optionA, optionB; - beforeEach(() => { - optionA = createElement(doc, 'input', {type: 'radio', name: 'radio'}); - optionB = createElement(doc, 'input', {type: 'radio', name: 'radio'}); - form.appendChild(optionA); - form.appendChild(optionB); - }); + beforeEach(() => { + optionA = createElement(doc, 'input', {type: 'radio', name: 'radio'}); + optionB = createElement(doc, 'input', {type: 'radio', name: 'radio'}); + form.appendChild(optionA); + form.appendChild(optionB); + }); - it('clears dirtiness class when radio button is in default state', () => { - optionA.setAttribute('checked', 'checked'); - checkInput(optionA, true); + it('clears dirtiness class when radio button is in default state', () => { + optionA.setAttribute('checked', 'checked'); + checkInput(optionA, true); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('clears dirtiness class when no radio button is checked', () => { - checkInput(optionA, false); - checkInput(optionB, false); + it('clears dirtiness class when no radio button is checked', () => { + checkInput(optionA, false); + checkInput(optionB, false); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('adds dirtiness class when radio button state has changed', () => { - checkInput(optionB, true); - expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); - }); + it('adds dirtiness class when radio button state has changed', () => { + checkInput(optionB, true); + expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('clears dirtiness class when radio button state matches its submitted state', () => { - checkInput(optionB, true); - dirtinessHandler.onSubmitting(); - dirtinessHandler.onSubmitSuccess(); - checkInput(optionB, true); + it('clears dirtiness class when radio button state matches its submitted state', () => { + checkInput(optionB, true); + dirtinessHandler.onSubmitting(); + dirtinessHandler.onSubmitSuccess(); + checkInput(optionB, true); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); }); + }); - describe('dropdown selection changes', () => { - let dropdown, optionA, optionB; + describe('dropdown selection changes', () => { + let dropdown, optionA, optionB; - beforeEach(() => { - dropdown = createElement(doc, 'select', {name: 'select'}); - optionA = createElement(doc, 'option', {value: 'A'}); - optionB = createElement(doc, 'option', {value: 'B'}); + beforeEach(() => { + dropdown = createElement(doc, 'select', {name: 'select'}); + optionA = createElement(doc, 'option', {value: 'A'}); + optionB = createElement(doc, 'option', {value: 'B'}); - dropdown.appendChild(optionA); - dropdown.appendChild(optionB); - form.appendChild(dropdown); - }); + dropdown.appendChild(optionA); + dropdown.appendChild(optionB); + form.appendChild(dropdown); + }); - it('clears dirtiness class when dropdown is in its default state', () => { - optionA.setAttribute('selected', 'selected'); - selectOption(optionA, true); + it('clears dirtiness class when dropdown is in its default state', () => { + optionA.setAttribute('selected', 'selected'); + selectOption(optionA, true); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('adds dirtiness class when dropdown is not in its default state', () => { - selectOption(optionB, true); - expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); - }); + it('adds dirtiness class when dropdown is not in its default state', () => { + selectOption(optionB, true); + expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('clears dirtiness class when dropdown selection matches its submitted state', () => { - selectOption(optionA, true); - dirtinessHandler.onSubmitting(); - dirtinessHandler.onSubmitSuccess(); - selectOption(optionA, true); + it('clears dirtiness class when dropdown selection matches its submitted state', () => { + selectOption(optionA, true); + dirtinessHandler.onSubmitting(); + dirtinessHandler.onSubmitSuccess(); + selectOption(optionA, true); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); }); + }); - describe('#onSubmitting', () => { - it('clears the dirtiness class', () => { - const input = doc.createElement('input'); - input.type = 'text'; - input.name = 'text'; - form.appendChild(input); + describe('#onSubmitting', () => { + it('clears the dirtiness class', () => { + const input = doc.createElement('input'); + input.type = 'text'; + input.name = 'text'; + form.appendChild(input); - changeInput(input, 'changed'); - dirtinessHandler.onSubmitting(); + changeInput(input, 'changed'); + dirtinessHandler.onSubmitting(); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); }); + }); - describe('#onSubmitError', () => { - let input; + describe('#onSubmitError', () => { + let input; - beforeEach(() => { - input = doc.createElement('input'); - input.type = 'text'; - input.name = 'text'; - form.appendChild(input); - }); + beforeEach(() => { + input = doc.createElement('input'); + input.type = 'text'; + input.name = 'text'; + form.appendChild(input); + }); - it('adds the dirtiness class if the form was dirty before submitting', () => { - changeInput(input, 'changed'); - dirtinessHandler.onSubmitting(); - dirtinessHandler.onSubmitError(); + it('adds the dirtiness class if the form was dirty before submitting', () => { + changeInput(input, 'changed'); + dirtinessHandler.onSubmitting(); + dirtinessHandler.onSubmitError(); - expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('does not add the dirtiness class if the form was clean before submitting', () => { - changeInput(input, ''); - dirtinessHandler.onSubmitting(); - dirtinessHandler.onSubmitError(); + it('does not add the dirtiness class if the form was clean before submitting', () => { + changeInput(input, ''); + dirtinessHandler.onSubmitting(); + dirtinessHandler.onSubmitError(); - expect(form).to.have.not.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.have.not.class(DIRTINESS_INDICATOR_CLASS); }); + }); - describe('#onSubmitSuccess', () => { - let input; + describe('#onSubmitSuccess', () => { + let input; - beforeEach(() => { - input = doc.createElement('input'); - input.type = 'text'; - input.name = 'text'; - form.appendChild(input); - }); + beforeEach(() => { + input = doc.createElement('input'); + input.type = 'text'; + input.name = 'text'; + form.appendChild(input); + }); - it('clears the dirtiness class', () => { - changeInput(input, 'changed'); - dirtinessHandler.onSubmitting(); - dirtinessHandler.onSubmitSuccess(); + it('clears the dirtiness class', () => { + changeInput(input, 'changed'); + dirtinessHandler.onSubmitting(); + dirtinessHandler.onSubmitSuccess(); - expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.not.have.class(DIRTINESS_INDICATOR_CLASS); + }); - it('tracks new changes after the form has been submitted', () => { - changeInput(input, 'changed'); - dirtinessHandler.onSubmitting(); - dirtinessHandler.onSubmitSuccess(); - changeInput(input, 'changed again'); + it('tracks new changes after the form has been submitted', () => { + changeInput(input, 'changed'); + dirtinessHandler.onSubmitting(); + dirtinessHandler.onSubmitSuccess(); + changeInput(input, 'changed again'); - expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); - }); + expect(form).to.have.class(DIRTINESS_INDICATOR_CLASS); }); + }); - describe('AmpEvents.FORM_DIRTINESS_CHANGE', () => { - let input; + describe('AmpEvents.FORM_DIRTINESS_CHANGE', () => { + let input; - beforeEach(() => { - input = createElement(doc, 'input', {type: 'text', name: 'text'}); - form.appendChild(input); - }); + beforeEach(() => { + input = createElement(doc, 'input', {type: 'text', name: 'text'}); + form.appendChild(input); + }); - it('dispatches an event when the form transitions from clean to dirty', () => { - const changeToDirty = () => changeInput(input, 'changed'); - const eventDispatched = captureEventDispatched( - AmpEvents.FORM_DIRTINESS_CHANGE, - form, - changeToDirty - ); + it('dispatches an event when the form transitions from clean to dirty', () => { + const changeToDirty = () => changeInput(input, 'changed'); + const eventDispatched = captureEventDispatched( + AmpEvents.FORM_DIRTINESS_CHANGE, + form, + changeToDirty + ); - expect(eventDispatched).to.exist; - expect(getDetail(eventDispatched).isDirty).to.be.true; - }); + expect(eventDispatched).to.exist; + expect(getDetail(eventDispatched).isDirty).to.be.true; + }); - it('dispatches an event when the form transitions from dirty to clean', () => { - changeInput(input, 'changed'); + it('dispatches an event when the form transitions from dirty to clean', () => { + changeInput(input, 'changed'); - const changeToClean = () => changeInput(input, ''); - const eventDispatched = captureEventDispatched( - AmpEvents.FORM_DIRTINESS_CHANGE, - form, - changeToClean - ); + const changeToClean = () => changeInput(input, ''); + const eventDispatched = captureEventDispatched( + AmpEvents.FORM_DIRTINESS_CHANGE, + form, + changeToClean + ); - expect(eventDispatched).to.exist; - expect(getDetail(eventDispatched).isDirty).to.be.false; - }); + expect(eventDispatched).to.exist; + expect(getDetail(eventDispatched).isDirty).to.be.false; + }); - it('does not dispatch an event when the dirtiness state does not change', () => { - changeInput(input, 'changed'); + it('does not dispatch an event when the dirtiness state does not change', () => { + changeInput(input, 'changed'); - const remainDirty = () => changeInput(input, 'still dirty'); - const eventDispatched = captureEventDispatched( - AmpEvents.FORM_DIRTINESS_CHANGE, - form, - remainDirty - ); + const remainDirty = () => changeInput(input, 'still dirty'); + const eventDispatched = captureEventDispatched( + AmpEvents.FORM_DIRTINESS_CHANGE, + form, + remainDirty + ); - expect(eventDispatched).to.not.exist; - }); + expect(eventDispatched).to.not.exist; }); + }); - describe('initial dirtiness', () => { - let newForm, input; - - beforeEach(() => { - newForm = getForm(doc); - input = createElement(doc, 'input', {type: 'text', name: 'text'}); - newForm.appendChild(input); - }); + describe('initial dirtiness', () => { + let newForm, input; - it('adds the dirtiness class if the form already has dirty fields', async () => { - changeInput(input, 'changed'); - dirtinessHandler = new FormDirtiness(newForm, env.win); - await macroTask(); + beforeEach(() => { + newForm = getForm(doc); + input = createElement(doc, 'input', {type: 'text', name: 'text'}); + newForm.appendChild(input); + }); - expect(newForm).to.have.class(DIRTINESS_INDICATOR_CLASS); - }); + it('adds the dirtiness class if the form already has dirty fields', () => { + changeInput(input, 'changed'); + dirtinessHandler = new FormDirtiness(newForm, env.win); - it('does not add the dirtiness class if the form does not have dirty fields', async () => { - dirtinessHandler = new FormDirtiness(newForm, env.win); - await macroTask(); + expect(newForm).to.have.class(DIRTINESS_INDICATOR_CLASS); + }); - expect(newForm).to.not.have.class(DIRTINESS_INDICATOR_CLASS); - }); + it('does not add the dirtiness class if the form does not have dirty fields', () => { + dirtinessHandler = new FormDirtiness(newForm, env.win); + expect(newForm).to.not.have.class(DIRTINESS_INDICATOR_CLASS); }); - } -); + }); +}); diff --git a/extensions/amp-form/amp-form.md b/extensions/amp-form/amp-form.md index 637bfa6d9fb0..02a2b75c8a24 100644 --- a/extensions/amp-form/amp-form.md +++ b/extensions/amp-form/amp-form.md @@ -36,7 +36,7 @@ If you're submitting data in your form, your server endpoint must implement the [/tip] -Before creating a `
`, you must include the required script for the `` 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. +Before creating a ``, you must include the required script for the `` 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 ``), you do not need to load the `amp-form` extension. [example preview="inline" playground="true" imports="amp-form" template="amp-mustache"] @@ -101,7 +101,7 @@ Before creating a ``, you must include the required script for the ``, `` -- Most of the form-related attributes on inputs including: `formaction`, `formtarget`, `formmethod` and others. +- Most of the form-related attributes on inputs including: `form`, `formaction`, `formtarget`, `formmethod` and others. [/filter] @@ -109,7 +109,7 @@ Before creating a ``, you must include the required script for the ``, `` - `` and `` -- Most of the form-related attributes on inputs including: `formaction`, `formtarget`, `formmethod` and others. +- Most of the form-related attributes on inputs including: `form`, `formaction`, `formtarget`, `formmethod` and others. [/filter] @@ -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](../../docs/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. +The `amp-form` extension allows [platform variable substitutions](../../docs/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. 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. diff --git a/validator/testdata/feature_tests/forms.html b/validator/testdata/feature_tests/forms.html index 719d460e1fab..adbc095e8d91 100644 --- a/validator/testdata/feature_tests/forms.html +++ b/validator/testdata/feature_tests/forms.html @@ -125,13 +125,6 @@ - - - -
- -
- | -| -| -| -|
-| -|
-| | | | >> ^~~~~~~~~ -feature_tests/forms.html:143:4 The attribute 'type' in tag 'input' is set to the invalid value 'image'. (see https://amp.dev/documentation/components/amp-form/) +feature_tests/forms.html:136:4 The attribute 'type' in tag 'input' is set to the invalid value 'image'. (see https://amp.dev/documentation/components/amp-form/) | | |
@@ -158,39 +151,39 @@ feature_tests/forms.html:143:4 The attribute 'type' in tag 'input' is set to the | | >> ^~~~~~~~~ -feature_tests/forms.html:152:4 The tag 'input' may only appear as a descendant of tag 'form [method=post]'. (see https://amp.dev/documentation/components/amp-form/) +feature_tests/forms.html:145:4 The tag 'input' may only appear as a descendant of tag 'form [method=post]'. (see https://amp.dev/documentation/components/amp-form/) |
| | |
|
>> ^~~~~~~~~ -feature_tests/forms.html:157:4 The attribute 'visible-when-invalid' in tag 'div' is missing or incorrect, but required by attribute 'validation-for'. +feature_tests/forms.html:150:4 The attribute 'visible-when-invalid' in tag 'div' is missing or incorrect, but required by attribute 'validation-for'. |
>> ^~~~~~~~~ -feature_tests/forms.html:158:4 The attribute 'visibile-when-invalid' may not appear in tag 'div'. +feature_tests/forms.html:151:4 The attribute 'visibile-when-invalid' may not appear in tag 'div'. |
| |
| >> ^~~~~~~~~ -feature_tests/forms.html:163:4 The attribute 'submit-success' may not appear in tag 'span'. +feature_tests/forms.html:156:4 The attribute 'submit-success' may not appear in tag 'span'. |
| |
>> ^~~~~~~~~ -feature_tests/forms.html:166:2 The attribute 'action' may not appear in tag 'form'. (see https://amp.dev/documentation/components/amp-form) +feature_tests/forms.html:159:2 The attribute 'action' may not appear in tag 'form'. (see https://amp.dev/documentation/components/amp-form) |
| |
>> ^~~~~~~~~ -feature_tests/forms.html:169:2 The mandatory attribute 'action' is missing in tag 'form'. (see https://amp.dev/documentation/components/amp-form) +feature_tests/forms.html:162:2 The mandatory attribute 'action' is missing in tag 'form'. (see https://amp.dev/documentation/components/amp-form) |
| |
>> ^~~~~~~~~ -feature_tests/forms.html:172:2 The mandatory attribute 'action' is missing in tag 'form'. (see https://amp.dev/documentation/components/amp-form) +feature_tests/forms.html:165:2 The mandatory attribute 'action' is missing in tag 'form'. (see https://amp.dev/documentation/components/amp-form) |
| |