Skip to content

Commit

Permalink
Refactor form verifiers API. Move checkUserValidity.
Browse files Browse the repository at this point in the history
  • Loading branch information
Carlos Vializ committed Jun 8, 2017
1 parent 246092f commit 0c259f0
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 490 deletions.
19 changes: 1 addition & 18 deletions examples/forms.amp.html
Expand Up @@ -655,27 +655,10 @@ <h4>Asynchronous verification example</h4>
<form
method="post"
action-xhr="/form/verify-search-json/post"
verify-xhr="/form/verify-search-json/post"
target="_blank"
class="verification-form"
>
<script type="application/json">
{
"verificationGroups": [
{
"name": "coolName",
"elements": ["name"]
},
{
"name": "fullAddress",
"elements": ["addressLine2", "city", "zip"]
},
{
"name": "validCarrier",
"elements": ["phone"]
}
]
}
</script>
<fieldset>
<label>
<span>Name</span>
Expand Down
125 changes: 77 additions & 48 deletions extensions/amp-form/0.1/amp-form.js
Expand Up @@ -88,10 +88,6 @@ const UserValidityState = {
};


/** @typedef {!HTMLInputElement|!HTMLSelectElement|!HTMLTextAreaElement} */
let FormFieldDef;


/** @private @const {string} */
const REDIRECT_TO_HEADER = 'AMP-Redirect-To';

Expand Down Expand Up @@ -161,6 +157,15 @@ export class AmpForm {
this.form_);
}

/** @const @private {?string} */
this.xhrVerify_ = this.form_.getAttribute('verify-xhr');
if (this.xhrVerify_) {
assertHttpsUrl(this.xhrVerify_, this.form_, 'verify-xhr');
user().assert(!isProxyOrigin(this.xhrVerify_),
'form verify-xhr should not be on AMP CDN: %s',
this.form_);
}

/**
* Indicates that the action will submit to canonical or not.
* @private {boolean|undefined}
Expand All @@ -182,8 +187,8 @@ export class AmpForm {
/** @const @private {!Array<!Element>} */
this.submitButtons_ = toArray(submitButtons);

/** @private {?string} */
this.state_ = null;
/** @private {!FormState_} */
this.state_ = FormState_.INITIAL;

const inputs = this.form_.elements;
for (let i = 0; i < inputs.length; i++) {
Expand Down Expand Up @@ -242,27 +247,32 @@ export class AmpForm {
installEventHandlers_() {
this.form_.addEventListener(
'submit', this.handleSubmitEvent_.bind(this), true);

this.form_.addEventListener('blur', e => {
checkUserValidityAfterInteraction_(dev().assertElement(e.target));
this.validator_.onBlur(e);
}, true);

const cleanup = () => {
// Move from the VERIFYING state back to INITIAL
if (this.state_ === FormState_.VERIFYING) {
this.setState_(FormState_.INITIAL);
}
};
this.form_.addEventListener('change', e => {
const input = dev().assertElement(e.target);
this.verifier_.onCommit(input, updatedElements => {
updatedElements.forEach(updatedElement => {
checkUserValidityAfterInteraction_(updatedElement);
});
this.validator_.onBlur(e);
// Move from the VERIFYING state back to INITIAL
if (this.state_ === FormState_.VERIFYING) {
this.setState_(FormState_.INITIAL);
}
});
this.verifier_.onCommit()
.then(updatedElements => {
updatedElements.forEach(checkUserValidityAfterInteraction_);
this.validator_.onBlur(e);
}, () => {
checkUserValidityAfterInteraction_(dev().assertElement(e.target));
})
.then(cleanup, cleanup);
});

this.form_.addEventListener('input', e => {
checkUserValidityAfterInteraction_(dev().assertElement(e.target));
this.validator_.onInput(e);
this.verifier_.onMutate(dev().assertElement(e.target));
});
}

Expand Down Expand Up @@ -392,25 +402,27 @@ export class AmpForm {
}
this.setState_(FormState_.VERIFYING);

const verifyXhr = this.doVarSubs_(this.getVarSubsFields_())
.then(() => this.doXhr_({[FORM_VERIFY_PARAM]: true}));
return verifyXhr;
return this.doVarSubs_(this.getVarSubsFields_())
.then(() => this.doVerifyXhr_());
}

/**
* @param {!IArrayLike<!HTMLInputElement>} varSubsFields
* @private
*/
handleXhrSubmit_(varSubsFields) {
this.cleanupRenderedTemplate_();
this.setState_(FormState_.SUBMITTING);

const p = this.doVarSubs_(varSubsFields)
.then(() => {
this.triggerFormSubmitInAnalytics_();
this.actions_.trigger(this.form_, 'submit', /*event*/ null);

// After variable substitution
const values = this.getFormAsObject_();
this.renderTemplate_(values);
})
.then(() => this.doXhr_())
.then(() => this.doActionXhr_())
.then(response => this.handleXhrSubmitSuccess_(response),
error => {
return this.handleXhrSubmitFailure_(/** @type {!Error} */(error));
Expand Down Expand Up @@ -438,28 +450,49 @@ export class AmpForm {

/**
* Send a request to the form's action endpoint.
* @param {!Object<string, string>=} opt_extraValues
* @private
*/
doXhr_(opt_extraValues) {
const isHeadOrGet = this.method_ == 'GET' || this.method_ == 'HEAD';
const values = this.getFormAsObject_(opt_extraValues);
this.renderTemplate_(values);
doActionXhr_() {
return this.doXhr_(dev().assertString(this.xhrAction_), this.method_);
}

/**
* Send a request to the form's verify endpoint.
* @private
*/
doVerifyXhr_() {
return this.doXhr_(dev().assertString(this.xhrVerify_), this.method_,
{[FORM_VERIFY_PARAM]: true});
}

/**
* Send a request to a form endpoint.
* @param {string} url
* @param {string} method
* @param {!Object<string, string>=} opt_extraFields
* @private
*/
doXhr_(url, method, opt_extraFields) {
let xhrUrl, body;
const isHeadOrGet = method == 'GET' || method == 'HEAD';

if (isHeadOrGet) {
xhrUrl = addParamsToUrl(dev().assertString(this.xhrAction_), values);
const values = this.getFormAsObject_();
if (opt_extraFields) {
deepMerge(values, opt_extraFields);
}
xhrUrl = addParamsToUrl(url, values);
} else {
xhrUrl = this.xhrAction_;
xhrUrl = url;
body = new FormData(this.form_);
for (const key in opt_extraValues) {
body.append(key, opt_extraValues[key]);
for (const key in opt_extraFields) {
body.append(key, opt_extraFields[key]);
}
}

return this.xhr_.fetch(dev().assertString(xhrUrl), {
return this.xhr_.fetch(xhrUrl, {
body,
method: this.method_,
method,
credentials: 'include',
headers: {
Accept: 'application/json',
Expand All @@ -477,7 +510,6 @@ export class AmpForm {
return response.json().then(json => {
this.triggerAction_(/* success */ true, json);
this.analyticsEvent_('amp-form-submit-success');
this.cleanupRenderedTemplate_();
this.setState_(FormState_.SUBMIT_SUCCESS);
this.renderTemplate_(json || {});
this.maybeHandleRedirect_(response);
Expand All @@ -501,7 +533,6 @@ export class AmpForm {
return promise.then(responseJson => {
this.triggerAction_(/* success */ false, responseJson);
this.analyticsEvent_('amp-form-submit-error');
this.cleanupRenderedTemplate_();
this.setState_(FormState_.SUBMIT_ERROR);
this.renderTemplate_(responseJson || {});
this.maybeHandleRedirect_(error.response);
Expand Down Expand Up @@ -614,11 +645,10 @@ export class AmpForm {

/**
* Returns form data as an object.
* @param {!Object<string, string>=} opt_extraFields
* @return {!JSONType}
* @private
*/
getFormAsObject_(opt_extraFields) {
getFormAsObject_() {
const data = /** @type {!JSONType} */ ({});
const inputs = this.form_.elements;
const submittableTagsRegex = /^(?:input|select|textarea)$/i;
Expand All @@ -638,21 +668,20 @@ export class AmpForm {
}
data[input.name].push(input.value);
}
if (opt_extraFields) {
deepMerge(data, opt_extraFields);
}

return data;
}

/**
* Adds proper classes for the state passed.
* @param {string} newState
* @param {!FormState_} newState
* @private
*/
setState_(newState) {
const previousState = this.state_;
this.form_.classList.remove(`amp-form-${previousState}`);
this.form_.classList.add(`amp-form-${newState}`);
this.cleanupRenderedTemplate_(previousState);
this.state_ = newState;
this.submitButtons_.forEach(button => {
if (newState == FormState_.SUBMITTING) {
Expand Down Expand Up @@ -707,10 +736,12 @@ export class AmpForm {
}

/**
* Removes the template for the passed state.
* @param {!FormState_} state
* @private
*/
cleanupRenderedTemplate_() {
const container = this.form_./*OK*/querySelector(`[${this.state_}]`);
cleanupRenderedTemplate_(state) {
const container = this.form_./*OK*/querySelector(`[${state}]`);
if (!container) {
return;
}
Expand Down Expand Up @@ -755,10 +786,8 @@ function reportValidity(state) {
* @return {boolean} Whether the form is currently valid or not.
*/
function checkUserValidityOnSubmission(form) {
const inputs = form.querySelectorAll('input,select,textarea,fieldset');
for (let i = 0; i < inputs.length; i++) {
checkUserValidity(inputs[i]);
}
const elements = form.querySelectorAll('input,select,textarea,fieldset');
iterateCursor(elements, element => checkUserValidity(element));
return checkUserValidity(form);
}

Expand Down

0 comments on commit 0c259f0

Please sign in to comment.