-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement form verification (async form validation) #9054
Conversation
1b22b04
to
9b012b2
Compare
6195d98
to
26dff6f
Compare
examples/form/zipcodes.csv
Outdated
@@ -0,0 +1,4431 @@ | |||
RecordNumber,Zipcode,City,State |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is alllllllllllllllll of this needed for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original title of the issue was forms: zip code input validation, but it was changed. I thought reading through real California zip codes and cities would be an interesting demo. If you think it's too heavy weight we can just change it to a smaller dict.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's massive.
* @return {?Element} | ||
*/ | ||
function getConfig_(form) { | ||
return form.getElementsByTagName('script')[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is finds deep elements. Should this be looking for a child script instead?
}); | ||
const config = json && json[CONFIG_KEY]; | ||
if (!config.length) { | ||
throw new Error(`The amp-form verification config should contain an array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any future uses for this configuration script? Maybe we shouldn't be throwing an error if they don't specify a verification group?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #getFormVerifier
. #parseConfig_
is not called if a verification group is not specified.
throw new Error('Failed to parse amp-form config. Is it valid JSON?'); | ||
}); | ||
const config = json && json[CONFIG_KEY]; | ||
if (!config.length) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will throw for an empty JSON object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't that correct behavior? If the publisher specifies {"verificationGroups": []}
or {}
in a form config, it seems like it is a mistake they should be made aware of through an error. Let me know if you'd prefer to allow an empty config.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's fine for this to be an error, but it this line causes an error. As in, the test, not the error we throw on the line below.
* Clear the validity state of this group's elements. | ||
*/ | ||
clearErrors() { | ||
this.elements_.forEach(element => element.setCustomValidity('')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are all elements guaranteed to have #setCustomValidity
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The HTML Constraints spec specifies that submittable elements are guaranteed to have #setCustomValidity
. A publisher would have to put a non-submittable element in a verification group for this to throw, and it seems unlikely. I could check that every element in the group has a #setCustomValidity
method in #parseConfig
, let me know if you think that's a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yah, I would just limit them to the form input elements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. See #isSubmittable_
export class FormVerifier { | ||
/** | ||
* @param {!HTMLFormElement} form | ||
* @param {!Array<!VerificationGroup>} config |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: rename to groups
// Set the error message on each element that caused an error. | ||
for (let i = 0; i < errors.length; i++) { | ||
const error = errors[i]; | ||
const element = scopedQuerySelector(this.form_, `[name="${error.name}"]`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scoped query selector is unnecessary in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got an error in the presubmit on Travis telling me not to this.form_.querySelector
because of strange behavior. Why is it unnecessary in this case?
If you mean use document.querySelector
, multiple elements can have identical names so it could select the wrong element.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scopedQuerySelector
is only necessary when there is a space in the selector (really, when you're matching a descendent of a descendent, see #7260). In this case, you're only matching a single descendent, so no issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. We may want to update the message querySelectorAll produces during the travis presubmit or a comment in the jsdoc for scopedQuerySelector
to make that clearer. Thanks!
// Remove the dirty flag from the successful groups that have values. | ||
for (let i = 0; i < this.groups_.length; i++) { | ||
const group = this.groups_[i]; | ||
if (group.isFilledOut() && group.containsNoElementOf(errorElements)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use #shouldVerify
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement and the function of #shouldVerify
are not equivalent. Could you please explain why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is #verify
, which means we're only verifying the elements that #shouldVerify
returned true for.
* @param {!Array<!Element>} elements | ||
* @return {boolean} | ||
*/ | ||
containsNoElementOf(elements) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: rename to #containsNone
*/ | ||
maybeVerify_(afterVerify) { | ||
if (this.shouldVerify_()) { | ||
this.doXhr_().then(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're gonna get into a LOT of race conditions here. Need to verify that this is a response to the last issued XHR, not any before it.
82e22af
to
61354f1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
first round of comments for the first half.
@@ -57,6 +57,34 @@ | |||
color: #dc4e41; | |||
} | |||
|
|||
.verification-form label { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/cc @spacedino let's work with Abby on UI design of this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just for the example page we use for development, right? It isn't baked in to amp-form
. The publisher should be responsible for the spinner UX
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
less so for this example page and more so to kick of the design for AmpByExample and/or AMP Start.
examples/forms.amp.html
Outdated
}, | ||
{ | ||
"name": "fullAddress", | ||
"elements": ["[name='addressLine2']", "[name='city']", "[name='zip']"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's restrict this to just name
(considering error response has name
which is used to find the target elements later)
maybe: "fields" = ['addressLine2', 'city', 'zip']
.
this.validator_.onBlur(e); | ||
}, true); | ||
this.form_.addEventListener('change', e => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be guarded by custom-validation-reporting='as-you-go
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, "as-you-go" is sort of poorly named. It should be called something that implies "use your own error message elements instead of using Chrome's validation tooltip, AND do so as the user fills out fields". The change
listener is still necessary when using verification with Chrome's default validation UI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wow ok. so what is the default browser UI strategy? is it always "on-submit" and not changeable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's right, unless you call form.reportValidation
, which by default happens on submit.
extensions/amp-form/0.1/amp-form.js
Outdated
} | ||
} | ||
|
||
getVarSubsFields_() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jsdoc for this a few other private methods added
extensions/amp-form/0.1/amp-form.js
Outdated
|
||
this.setState_(FormState_.VERIFYING); | ||
const verifyXhr = this.doVarSubs_(this.getVarSubsFields_()).then( | ||
() => this.doXhr_(map({verify: true}))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'verify' can collide with existing inputs named verify
. Do __amp_form_verify
and then check if any input has that name same way SOURCE_ORIGIN_PARAM
is checked in the constructor of AMP form.
function parseConfig_(script) { | ||
if (isJsonScriptTag(script)) { | ||
const json = tryParseJson(script.textContent, () => { | ||
throw new Error('Failed to parse amp-form config. Is it valid JSON?'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
throw user().createError()
here and couple of places below.
* data in ways not possible with standard form validation, or check | ||
* values against sets of data too large to fit in browser memory | ||
* e.g. ensuring zip codes match with cities. | ||
* @visibleForTesting |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
extensions/amp-form/0.1/amp-form.js
Outdated
|
||
this.setState_(FormState_.VERIFYING); | ||
const verifyXhr = this.doVarSubs_(this.getVarSubsFields_()).then( | ||
() => this.doXhr_(map({verify: true}))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the map
necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably not. Could you help me understand what use-cases require our map
utility function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it was to make for-in loops more robust. I iterate over this object using a for-in loop in #doXhr
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It has an old JS meaning and a valid CS meaning.
Old JS, people used to put enumerable properties on the Object.prototype
object, which would then magically appear in all for-in loops. We don't do that. Actually we did, but Ali fixed that.
The CS meaning is when the object is treated as a true Map instance with arbitrary accesses. In that case, anything in the prototype chain could be accessed, which is incorrect in 99% of cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, map
is more for storing and accessing values, and we don't need it here because it's just a descriptive value and not used for storage.
extensions/amp-form/0.1/amp-form.js
Outdated
const isHeadOrGet = this.method_ == 'GET' || this.method_ == 'HEAD'; | ||
|
||
const p = this.doVarSubs_(varSubsFields) | ||
.then(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whitespace.
extensions/amp-form/0.1/amp-form.js
Outdated
return; | ||
} | ||
|
||
this.setState_(FormState_.VERIFYING); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we going to do any blocking in this state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like blocking of the submit button? No, that is not planned. This state is intended to allow a spinner to show during the verify request. If the user wants to submit while the verify request is pending, they should be able to.
a278ebf
to
d2f3150
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Second round.
Excellent PR overall Carlos!
this.validator_.onBlur(e); | ||
}, true); | ||
this.form_.addEventListener('change', e => { | ||
const input = dev().assertElement(e.target); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's create an experiment (maybe amp-form-verifier
?) and guard entry points to this. Ideally being sure when experiment is off, code paths to existing forms functionality does not change (much).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
extensions/amp-form/0.1/amp-form.js
Outdated
|
||
const verifyXhr = this.doVarSubs_(this.getVarSubsFields_()) | ||
.then(() => this.doXhr_({[FORM_VERIFY_PARAM]: true})); | ||
const reset = () => this.setState_(FormState_.INITIAL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if multiple are still running and one fails while others are still running? we don't want to lose VERIFYING state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, I'll move this to the afterVerify
callback
// Set the error message on each element that caused an error. | ||
for (let i = 0; i < errors.length; i++) { | ||
const error = errors[i]; | ||
const element = this.form_./*OK*/querySelector(`[name="${error.name}"]`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scopedQuerySelector(this.form_,
[name="${error.name}"])
from dom.js
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jridgewell and I discussed this above, and he shared that scopedQuerySelector
is not necessary unless there is a space in the selector. Is that your understanding as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding is that scopedQuerySelector(parent)
can match the parent element itself but parent.querySelector
can never match parent. so in that sense yeah no need to use scopedQuerySelector
.
With my comment I was hoping to get rid of /*OK*/
as they should be used for exceptions not common cases, looks like querySelector
is allowed without `/OK/ anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neither can match the parent. The issue is #querySelector
is scoped to the document, then filtered to elements inside the parent (div span
can match a <div>
outside parent and then a <span>
inside parent). scopedQuerySelector
however, is scoped to the parent only (div span
can only match a <div>
inside parent and a <span>
inside that <div>
).
#querySelector
is allowed when there is a single string without any spaces (meaning, you can not have '* *'). Because this is a template string, it could do all kinds of weird things. But we know better than the linter in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jridgewell wow good to know (I probably should have known this but never too late to learn :) ). Good job preventing this with presubmit rules.
const error = errors[i]; | ||
const element = this.form_./*OK*/querySelector(`[name="${error.name}"]`); | ||
if (element && element.checkValidity()) { | ||
element.setCustomValidity(error.message); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess if multiple elements have the same name (common with radio buttons) the first one gets it which is fine, let's just document here as a comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/service/xhr-impl.js
Outdated
const status = response.status; | ||
if (status < 200 || status >= 300) { | ||
const retriable = isRetriable(status); | ||
const err = user().createCustomError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should not log the full response (since we store logs and response body can contain sensitive info)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only thing sent in the error report is the message first argument. The other information is just tacked on to the error object and not sent, so the full response won't be logged.
Also, createError
doesn't report errors so this error isn't logged anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, createError
don't report but whoever is catching .fetch()
error may report it. (or we might be reporting all unhandledrejection
error, @jridgewell do we report unhandledrejection
?)
You are right that response
does got get reported right now because it is just a field on FetchError, but do we need it? I like dump error objects that one can't do anything but log them instead of error objects that can be inspected and used to switch code path. (e.g. not a fan of .catch(err => if( err.response.code == 500) {...} )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing the property from the error would involve slightly more refactoring effort. The code was already augmenting the error with the response and responseJson properties, just not doing so with a custom error class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do log all unhandledrejection
s.
src/utils/promise.js
Outdated
/** | ||
* Resolves with the result of the last promise added. | ||
*/ | ||
export class LastAddedResolver { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
awesome!
src/utils/promise.js
Outdated
this.count_ = 0; | ||
|
||
if (opt_promises) { | ||
for (let i = 0; i > opt_promises.length; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please add a test that would catch the bug in this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah that's a pretty goofy mistake I made. Adding the test.
extensions/amp-form/0.1/amp-form.js
Outdated
inputs[i].name != SOURCE_ORIGIN_PARAM, | ||
'Illegal input name, %s found: %s', SOURCE_ORIGIN_PARAM, inputs[i]); | ||
const name = inputs[i].name; | ||
user().assert(!name || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First check is unnecessary.
updatedElements.forEach(updatedElement => { | ||
checkUserValidityAfterInteraction_(updatedElement); | ||
}); | ||
this.validator_.onBlur(e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we blurring here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function's name is not very clear. It's part of the amp-form
FormValidator
API and it hides any displayed messages. Originally it was always called on blur in the form, but that behavior is also needed when the verifier calls afterVerify
.
extensions/amp-form/0.1/amp-form.js
Outdated
|
||
const verifyXhr = this.doVarSubs_(this.getVarSubsFields_()) | ||
.then(() => this.doXhr_({[FORM_VERIFY_PARAM]: true})); | ||
const reset = () => this.setState_(FormState_.INITIAL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably needs to make sure form state is still VERIFYING
extensions/amp-form/0.1/amp-form.js
Outdated
this.renderTemplate_(json || {}); | ||
this.maybeHandleRedirect_(response); | ||
}, error => { | ||
rethrowAsync('Failed to parse response JSON:', error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a user error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Can you help me understand when something should be a user error? I haven't been able to find anything in the docs/comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Developer errors are only for state that we absolutely know to be true, and isn't. So, if we expect our BaseElement
instance to have an element attached, but it doesn't, that goes to dev logs.
But anything that has to do with user input, like parsing JSON that pub provides, or a response that pub provides, is on them. We don't need to know about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wasn't a user error before I made any changes, this is just split out from a larger function. A user error needs a tag, correct? What value should I put for tag
given this is not in an amp element.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a mistake then. This should be "FORM"
extensions/amp-form/0.1/amp-form.js
Outdated
this.setState_(FormState_.SUBMIT_ERROR); | ||
this.renderTemplate_(error.responseJson || {}); | ||
this.maybeHandleRedirect_(error.response); | ||
rethrowAsync('Form submission failed:', error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
User error.
// Set the error message on each element that caused an error. | ||
for (let i = 0; i < errors.length; i++) { | ||
const error = errors[i]; | ||
const element = this.form_./*OK*/querySelector(`[name="${error.name}"]`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neither can match the parent. The issue is #querySelector
is scoped to the document, then filtered to elements inside the parent (div span
can match a <div>
outside parent and then a <span>
inside parent). scopedQuerySelector
however, is scoped to the parent only (div span
can only match a <div>
inside parent and a <span>
inside that <div>
).
#querySelector
is allowed when there is a single string without any spaces (meaning, you can not have '* *'). Because this is a template string, it could do all kinds of weird things. But we know better than the linter in this case.
src/utils/promise.js
Outdated
}, error => { | ||
// Match the behavior of standard functions by rejecting when an error | ||
// occurs even if it's out of order. e.g. Promise.race and Promise.all | ||
this.reject_(error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is incorrect, we're looking for more of a cancel behavior and not a race behavior. This should just reject if the most recently added one rejects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I was on the fence about which way to go. I think it can be made to be equivalent through add
ing promises with a catch
at the end, but I agree it's convenient to not have to do that for our common use-case.
src/utils/promise.js
Outdated
/** | ||
* Resolves with the result of the last promise added. | ||
*/ | ||
export class LastAddedResolver { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ampproject/a4a will be interested in this.
src/utils/promise.js
Outdated
* Get the result promise. | ||
* @return {!Promise} | ||
*/ | ||
get() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be nice if this followed the thenable spec instead (we can get rid of this method then):
then(onFulfilled, onRejected) {
return this.promise_.then(onFulfilled, onRejected);
}
src/utils/promise.js
Outdated
get() { | ||
return this.promise_; | ||
then(opt_resolve, opt_reject) { | ||
if (!opt_resolve && !opt_reject) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let the promise implementation handle this.
17ed8cc
to
fba0ab1
Compare
src/service/xhr-impl.js
Outdated
if (status < 200 || status >= 300) { | ||
const retriable = isRetriable(status); | ||
const err = user().createCustomError( | ||
new FetchError(`HTTP error ${status}`, response, retriable)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we creating custom Error classes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying to adapt the existing code to work better with the type system, since gulp check-types
would complain that I was accessing properties not available on an Error
instance. The previous implementation instantiated a new Error
and augmented it with the response
and the responseJson
, in effect rejecting an error and passing data together. It was added in #3669. This felt hacky and unclear since the type was sort of lying (it says it's an Error, but it has these extra fields) so I wanted to make the augmentation more formal.
src/log.js
Outdated
* @param {string} message | ||
*/ | ||
constructor(message) { | ||
super(message); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Danger! This may or may not behave as you expect based on how compliant the browser is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran the unit test for this on our SauceLabs suite and it passed on all platforms. Does that mitigate the danger? Do those tests cover every browser/device we support?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm more worried about older versions. Extending native classes has been fraught with bugs (see https://github.com/ampproject/amphtml/blob/master/src/custom-element.js#L409-L414).
extensions/amp-form/amp-form.md
Outdated
@@ -361,6 +361,81 @@ The `show-all-on-submit` reporting option shows all validation errors on all inv | |||
#### As You Go | |||
The `as-you-go` reporting option allows your user to see validation messages as they're interacting with the input. For example, if the user types an invalid email address, the user will see the error right away. Once they correct the value, the error goes away. | |||
|
|||
## Verification |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
## Verification (Experimental)
also add a first sentence that points out the experimental nature and instructions on how to enable the experiment (e.g. link to https://www.ampproject.org/docs/reference/experimental )
- Prevent verify request race condition with LastAddedResolver - Add amp-form tests with verifier - Add custom error class to simplify logic with XHR errors
- Fix error on connection fail. - Fix for-loop bug. - Fix state bug.
I've tested this against some pages in the wild and our manual tests and I feel comfortable merging this now. |
merged |
Closes #8369
Needs documentation, opening this PR for feedback.