Auto merge of #8747 - KiChjang:form-interactive-validation, r=Manishearth

Implement basic framework for static and interactive validation on forms

This came along when I was trying to do #3648. I was about to do form validation as well, but then I saw it was actually intended to be a student project. Hopefully I didn't step on too many toes on this one; I did leave most of the important bits out (the fun bits I believe, e.g. interactive validation doesn't actually interactively validate anything right now).

<!-- Reviewable:start -->
[<img src="https://reviewable.io/review_button.png" height=40 alt="Review on Reviewable"/>](https://reviewable.io/reviews/servo/servo/8747)
<!-- Reviewable:end -->
This commit is contained in:
bors-servo 2015-12-17 05:41:45 +05:30
commit d9bf1ba616
7 changed files with 165 additions and 249 deletions

View file

@ -115,6 +115,12 @@ impl HTMLButtonElementMethods for HTMLButtonElement {
// https://html.spec.whatwg.org/multipage/#dom-fs-formtarget
make_setter!(SetFormTarget, "formtarget");
// https://html.spec.whatwg.org/multipage/#attr-fs-formnovalidate
make_bool_getter!(FormNoValidate, "formnovalidate");
// https://html.spec.whatwg.org/multipage/#attr-fs-formnovalidate
make_bool_setter!(SetFormNoValidate, "formnovalidate");
// https://html.spec.whatwg.org/multipage/#dom-fe-name
make_getter!(Name, "name");

View file

@ -9,6 +9,7 @@ use dom::bindings::codegen::Bindings::HTMLButtonElementBinding::HTMLButtonElemen
use dom::bindings::codegen::Bindings::HTMLFormElementBinding;
use dom::bindings::codegen::Bindings::HTMLFormElementBinding::HTMLFormElementMethods;
use dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods;
use dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding::HTMLTextAreaElementMethods;
use dom::bindings::conversions::DerivedFrom;
use dom::bindings::global::GlobalRef;
use dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId};
@ -17,10 +18,13 @@ use dom::bindings::reflector::Reflectable;
use dom::document::Document;
use dom::element::Element;
use dom::event::{Event, EventBubbles, EventCancelable};
use dom::htmlbuttonelement::{HTMLButtonElement};
use dom::eventtarget::EventTarget;
use dom::htmlbuttonelement::HTMLButtonElement;
use dom::htmldatalistelement::HTMLDataListElement;
use dom::htmlelement::HTMLElement;
use dom::htmlinputelement::HTMLInputElement;
use dom::htmlobjectelement::HTMLObjectElement;
use dom::htmlselectelement::HTMLSelectElement;
use dom::htmltextareaelement::HTMLTextAreaElement;
use dom::node::{Node, document_from_node, window_from_node};
use dom::virtualmethods::VirtualMethods;
@ -140,7 +144,7 @@ impl HTMLFormElementMethods for HTMLFormElement {
}
}
#[derive(Copy, Clone, HeapSizeOf)]
#[derive(Copy, Clone, HeapSizeOf, PartialEq)]
pub enum SubmittedFrom {
FromFormSubmitMethod,
NotFromFormSubmitMethod
@ -154,32 +158,54 @@ pub enum ResetFrom {
impl HTMLFormElement {
pub fn submit(&self, _submit_method_flag: SubmittedFrom, submitter: FormSubmitter) {
/// [Form submission](https://html.spec.whatwg.org/multipage/#concept-form-submit)
pub fn submit(&self, submit_method_flag: SubmittedFrom, submitter: FormSubmitter) {
// Step 1
let doc = document_from_node(self);
let win = window_from_node(self);
let base = doc.url();
// TODO: Handle browsing contexts
// TODO: Handle validation
let event = Event::new(GlobalRef::Window(win.r()),
atom!("submit"),
EventBubbles::Bubbles,
EventCancelable::Cancelable);
event.fire(self.upcast());
if event.DefaultPrevented() {
return;
// Step 4
if submit_method_flag == SubmittedFrom::NotFromFormSubmitMethod
&& !submitter.no_validate(self)
{
if self.interactive_validation().is_err() {
// TODO: Implement event handlers on all form control elements
// XXXKiChjang: We're also calling the following two statements quite often,
// we should refactor it into a function
let event = Event::new(GlobalRef::Window(win.r()),
atom!("invalid"),
EventBubbles::DoesNotBubble,
EventCancelable::NotCancelable);
event.fire(self.upcast());
return;
}
}
// Step 5
if submit_method_flag == SubmittedFrom::NotFromFormSubmitMethod {
let event = Event::new(GlobalRef::Window(win.r()),
atom!("submit"),
EventBubbles::Bubbles,
EventCancelable::Cancelable);
event.fire(self.upcast());
if event.DefaultPrevented() {
return;
}
}
// Step 6
let form_data = self.get_form_dataset(Some(submitter));
// Step 7-8
// Step 7
let mut action = submitter.action();
// Step 8
if action.is_empty() {
action = DOMString::from(base.serialize());
}
// TODO: Resolve the url relative to the submitter element
// Step 10-15
let action_components =
UrlParser::new().base_url(base).parse(&action).unwrap_or((*base).clone());
// Step 9-11
let action_components = match UrlParser::new().base_url(base).parse(&action) {
Ok(url) => url,
Err(_) => return
};
// Step 12-15
let _action = action_components.serialize();
let scheme = action_components.scheme.clone();
let enctype = submitter.enctype();
@ -219,13 +245,61 @@ impl HTMLFormElement {
win.pipeline(), load_data)).unwrap();
}
/// Interactively validate the constraints of form elements
/// https://html.spec.whatwg.org/multipage/#interactively-validate-the-constraints
fn interactive_validation(&self) -> Result<(), ()> {
// Step 1-3
let unhandled_invalid_controls = match self.static_validation() {
Ok(()) => return Ok(()),
Err(err) => err
};
// TODO: Report the problems with the constraints of at least one of
// the elements given in unhandled invalid controls to the user
// Step 4
Err(())
}
/// Statitically validate the constraints of form elements
/// https://html.spec.whatwg.org/multipage/#statically-validate-the-constraints
fn static_validation(&self) -> Result<(), Vec<FormSubmittableElement>> {
let node = self.upcast::<Node>();
// FIXME(#3553): This is an incorrect way of getting controls owned by the
// form, refactor this when html5ever's form owner PR lands
// Step 1-3
let invalid_controls = node.traverse_preorder().filter_map(|field| {
if let Some(el) = field.downcast::<Element>() {
None // Remove this line if you decide to refactor
// XXXKiChjang: Form control elements should each have a candidate_for_validation
// and satisfies_constraints methods
} else {
None
}
}).collect::<Vec<FormSubmittableElement>>();
// Step 4
if invalid_controls.is_empty() { return Ok(()); }
// Step 5-6
let win = window_from_node(self);
let unhandled_invalid_controls = invalid_controls.into_iter().filter_map(|field| {
let event = Event::new(GlobalRef::Window(win.r()),
atom!("invalid"),
EventBubbles::DoesNotBubble,
EventCancelable::Cancelable);
event.fire(field.as_event_target());
if !event.DefaultPrevented() { return Some(field); }
None
}).collect::<Vec<FormSubmittableElement>>();
// Step 7
Err(unhandled_invalid_controls)
}
/// https://html.spec.whatwg.org/multipage/#constructing-the-form-data-set
/// Steps range from 1 to 3
fn get_unclean_dataset(&self, submitter: Option<FormSubmitter>) -> Vec<FormDatum> {
let node = self.upcast::<Node>();
// TODO: This is an incorrect way of getting controls owned
// by the form, but good enough until html5ever lands
// Step 1-2
// FIXME(#3553): This is an incorrect way of getting controls owned
// by the form, but good enough until html5ever lands
node.traverse_preorder().filter_map(|child| {
// Step 3.1: The field element is disabled.
match child.downcast::<Element>() {
@ -382,6 +456,29 @@ pub enum FormMethod {
FormDialog
}
#[derive(HeapSizeOf)]
pub enum FormSubmittableElement {
ButtonElement(Root<HTMLButtonElement>),
InputElement(Root<HTMLInputElement>),
// TODO: HTMLKeygenElement unimplemented
// KeygenElement(&'a HTMLKeygenElement),
ObjectElement(Root<HTMLObjectElement>),
SelectElement(Root<HTMLSelectElement>),
TextAreaElement(Root<HTMLTextAreaElement>)
}
impl FormSubmittableElement {
fn as_event_target(&self) -> &EventTarget {
match *self {
FormSubmittableElement::ButtonElement(ref button) => button.r().upcast(),
FormSubmittableElement::InputElement(ref input) => input.r().upcast(),
FormSubmittableElement::ObjectElement(ref object) => object.r().upcast(),
FormSubmittableElement::SelectElement(ref select) => select.r().upcast(),
FormSubmittableElement::TextAreaElement(ref textarea) => textarea.r().upcast()
}
}
}
#[derive(Copy, Clone, HeapSizeOf)]
pub enum FormSubmitter<'a> {
FormElement(&'a HTMLFormElement),
@ -466,6 +563,22 @@ impl<'a> FormSubmitter<'a> {
}
}
}
fn no_validate(&self, form_owner: &HTMLFormElement) -> bool {
match *self {
FormSubmitter::FormElement(form) => form.NoValidate(),
FormSubmitter::InputElement(input_element) => {
input_element.get_form_boolean_attribute(&atom!("formnovalidate"),
|i| i.FormNoValidate(),
|f| f.NoValidate())
}
FormSubmitter::ButtonElement(button_element) => {
button_element.get_form_boolean_attribute(&atom!("formnovalidate"),
|i| i.FormNoValidate(),
|f| f.NoValidate())
}
}
}
}
pub trait FormControl: DerivedFrom<Element> + Reflectable {
@ -506,9 +619,28 @@ pub trait FormControl: DerivedFrom<Element> + Reflectable {
}
}
fn get_form_boolean_attribute<InputFn, OwnerFn>(&self,
attr: &Atom,
input: InputFn,
owner: OwnerFn)
-> bool
where InputFn: Fn(&Self) -> bool,
OwnerFn: Fn(&HTMLFormElement) -> bool
{
if self.to_element().has_attribute(attr) {
input(self)
} else {
self.form_owner().map_or(false, |t| owner(t.r()))
}
}
fn to_element(&self) -> &Element {
self.upcast()
}
// XXXKiChjang: Implement these on inheritors
// fn candidate_for_validation(&self) -> bool;
// fn satisfies_constraints(&self) -> bool;
}
impl VirtualMethods for HTMLFormElement {

View file

@ -340,6 +340,12 @@ impl HTMLInputElementMethods for HTMLInputElement {
// https://html.spec.whatwg.org/multipage/#dom-input-formtarget
make_setter!(SetFormTarget, "formtarget");
// https://html.spec.whatwg.org/multipage/#attr-fs-formnovalidate
make_bool_getter!(FormNoValidate, "formnovalidate");
// https://html.spec.whatwg.org/multipage/#attr-fs-formnovalidate
make_bool_setter!(SetFormNoValidate, "formnovalidate");
// https://html.spec.whatwg.org/multipage/#dom-input-maxlength
make_int_getter!(MaxLength, "maxlength", DEFAULT_MAX_LENGTH);

View file

@ -11,7 +11,7 @@ interface HTMLButtonElement : HTMLElement {
attribute DOMString formAction;
attribute DOMString formEnctype;
attribute DOMString formMethod;
// attribute boolean formNoValidate;
attribute boolean formNoValidate;
attribute DOMString formTarget;
attribute DOMString name;
attribute DOMString type;

View file

@ -18,7 +18,7 @@ interface HTMLInputElement : HTMLElement {
attribute DOMString formAction;
attribute DOMString formEnctype;
attribute DOMString formMethod;
// attribute boolean formNoValidate;
attribute boolean formNoValidate;
attribute DOMString formTarget;
// attribute unsigned long height;
attribute boolean indeterminate;