From 264bbc22c77f64ecb2a8148fc295fabc255ce5b3 Mon Sep 17 00:00:00 2001 From: rob Date: Mon, 27 Dec 2021 00:11:04 -0500 Subject: [PATCH 1/4] feat: HtmlFormControlsCollection and related elements --- CHANGELOG.md | 1 + .../Dom/Webapi__Dom__HtmlFormElement__test.js | 293 +++++++++++++++++- src/Webapi/Dom/Webapi__Dom__Document.res | 1 + .../Dom/Webapi__Dom__HtmlButtonElement.res | 55 ++++ .../Dom/Webapi__Dom__HtmlCollection.res | 3 + .../Dom/Webapi__Dom__HtmlFieldSetElement.res | 36 +++ ...ebapi__Dom__HtmlFormControlsCollection.res | 47 +++ .../Dom/Webapi__Dom__HtmlFormElement.res | 11 +- .../Dom/Webapi__Dom__HtmlObjectElement.res | 47 +++ .../Dom/Webapi__Dom__HtmlOptGroupElement.res | 23 ++ .../Dom/Webapi__Dom__HtmlOptionElement.res | 47 +++ .../Webapi__Dom__HtmlOptionsCollection.res | 33 ++ .../Dom/Webapi__Dom__HtmlOutputElement.res | 41 +++ .../Dom/Webapi__Dom__HtmlSelectElement.res | 97 ++++++ .../Dom/Webapi__Dom__HtmlTextAreaElement.res | 109 +++++++ src/Webapi/Dom/Webapi__Dom__NodeList.res | 3 + src/Webapi/Dom/Webapi__Dom__RadioNodeList.res | 18 ++ src/Webapi/Webapi__Dom.res | 24 +- .../Webapi__Dom__HtmlFormElement__test.res | 183 +++++++++++ 19 files changed, 1059 insertions(+), 13 deletions(-) create mode 100644 src/Webapi/Dom/Webapi__Dom__HtmlButtonElement.res create mode 100644 src/Webapi/Dom/Webapi__Dom__HtmlFieldSetElement.res create mode 100644 src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.res create mode 100644 src/Webapi/Dom/Webapi__Dom__HtmlObjectElement.res create mode 100644 src/Webapi/Dom/Webapi__Dom__HtmlOptGroupElement.res create mode 100644 src/Webapi/Dom/Webapi__Dom__HtmlOptionElement.res create mode 100644 src/Webapi/Dom/Webapi__Dom__HtmlOptionsCollection.res create mode 100644 src/Webapi/Dom/Webapi__Dom__HtmlOutputElement.res create mode 100644 src/Webapi/Dom/Webapi__Dom__HtmlSelectElement.res create mode 100644 src/Webapi/Dom/Webapi__Dom__HtmlTextAreaElement.res create mode 100644 src/Webapi/Dom/Webapi__Dom__RadioNodeList.res diff --git a/CHANGELOG.md b/CHANGELOG.md index 0085574e..38f35dae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * `DataTransfer` bindings (#51) * `WorkerGlobalScope`, `WindowOrWorkerGlobalScope`, `WorkerNavigator`, and `WorkerLocation` bindings (#57) * `Response` constructors to `Fetch` bindings (#64) +* `HTMLFormControlsCollection`, `HTMLOptionsCollection`, `HTMLFieldSetElement`, `Document.forms`, `HTMLFormElement.elements`, `HTMLObjectElement`, `HTMLOptGroupElement`, `HTMLOptionElement`, `HTMLOutputElement`, `HTMLSelectElement`, `HTMLTextAreaElement`, and `RadioNodeList` bindings (#73) ### Fixed * `ofElement` was incorrectly returning `Dom.htmlElement` type instead of the enclosing element type (#60) diff --git a/lib/js/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.js b/lib/js/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.js index 28900af1..4fba3b79 100644 --- a/lib/js/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.js +++ b/lib/js/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.js @@ -1,9 +1,300 @@ 'use strict'; +var Js_exn = require("rescript/lib/js/js_exn.js"); +var Belt_Option = require("rescript/lib/js/belt_Option.js"); +var Caml_option = require("rescript/lib/js/caml_option.js"); +var TestHelpers = require("../../testHelpers.js"); +var Webapi__Dom__Document = require("../../../src/Webapi/Dom/Webapi__Dom__Document.js"); +var Webapi__Dom__HtmlElement = require("../../../src/Webapi/Dom/Webapi__Dom__HtmlElement.js"); +var Webapi__Dom__HtmlFormElement = require("../../../src/Webapi/Dom/Webapi__Dom__HtmlFormElement.js"); +var Webapi__Dom__HtmlOptionElement = require("../../../src/Webapi/Dom/Webapi__Dom__HtmlOptionElement.js"); +var Webapi__Dom__HtmlSelectElement = require("../../../src/Webapi/Dom/Webapi__Dom__HtmlSelectElement.js"); +var Webapi__Dom__HtmlFormControlsCollection = require("../../../src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.js"); + +var partial_arg = document; + +function createElement(param) { + return partial_arg.createElement(param); +} + +var partial_arg$1 = document; + +function createTextNode(param) { + return partial_arg$1.createTextNode(param); +} + +function createInput(param) { + return createElement("input"); +} + +function createLabelWithText(text) { + var el = createElement("label"); + var textNode = createTextNode(text); + el.appendChild(textNode); + return el; +} + +var form = TestHelpers.unsafelyUnwrapOption(Webapi__Dom__HtmlFormElement.ofElement(createElement("form"))); + +var usernameInput = createElement("input"); + +usernameInput.setAttribute("type", "text"); + +usernameInput.setAttribute("name", "username"); + +var usernameLabel = createLabelWithText("Username:"); + +usernameLabel.appendChild(usernameInput); + +var passwordInput = createElement("input"); + +passwordInput.setAttribute("type", "password"); + +passwordInput.setAttribute("name", "password"); + +var passwordLabel = createLabelWithText("Password:"); + +passwordLabel.appendChild(passwordInput); + +var radioInput1 = createElement("input"); + +radioInput1.setAttribute("type", "radio"); + +radioInput1.setAttribute("name", "radiogroup"); + +radioInput1.setAttribute("value", "one"); + +radioInput1.setAttribute("checked", "true"); + +var radioLabel1 = createLabelWithText("Choice 1:"); + +radioLabel1.appendChild(radioInput1); + +var radioInput2 = createElement("input"); + +radioInput2.setAttribute("type", "radio"); + +radioInput2.setAttribute("name", "radiogroup"); + +radioInput2.setAttribute("value", "two"); + +var radioLabel2 = createLabelWithText("Choice 2:"); + +radioLabel2.appendChild(radioInput2); + +var select = createElement("select"); + +select.setAttribute("name", "select"); + +var selectLabel = createLabelWithText("Select:"); + +selectLabel.appendChild(select); + +var usernameContainer = createElement("div"); + +var passwordContainer = createElement("div"); + +var radioContainer = createElement("div"); + +var selectContainer = createElement("div"); + +usernameContainer.appendChild(usernameLabel); + +passwordContainer.appendChild(passwordLabel); + +radioContainer.appendChild(radioLabel1); + +radioContainer.appendChild(radioLabel2); + +selectContainer.appendChild(selectLabel); + +form.appendChild(usernameContainer); + +form.appendChild(passwordContainer); + +form.appendChild(radioContainer); + +form.appendChild(selectContainer); + +var body = TestHelpers.unsafelyUnwrapOption(Belt_Option.flatMap(Webapi__Dom__Document.asHtmlDocument(document), (function (prim) { + return Caml_option.nullable_to_opt(prim.body); + }))); + +body.appendChild(form); + +var collection = form.elements; + +console.log("HtmlFormElement.elements:", collection); + +var len = collection.length; + +console.log("HtmlFormControlsCollection.length:", len); + +var el0 = collection.item(0); + +console.log("HtmlFormControlsCollection.item:", (el0 == null) ? undefined : Caml_option.some(el0)); + +var el0$1 = Webapi__Dom__HtmlFormControlsCollection.namedItem(collection, "username"); + +console.log("HtmlFormControlsCollection.namedItem:", el0$1); + +var el1 = collection.item(1); + +console.log("HtmlFormControlsCollection.length:", (el1 == null) ? undefined : Caml_option.some(el1)); + +var el1$1 = Webapi__Dom__HtmlFormControlsCollection.namedItem(collection, "password"); + +console.log("HtmlFormControlsCollection.namedItem:", el1$1); + +var radioNodeList = collection.item(2); + +console.log("HtmlFormControlsCollection.namedItem:", (radioNodeList == null) ? undefined : Caml_option.some(radioNodeList)); + +var radioNodeList$1 = Webapi__Dom__HtmlFormControlsCollection.namedItem(collection, "radiogroup"); + +console.log("HtmlFormControlsCollection.namedItem:", radioNodeList$1); + +var radioNodeList$2 = TestHelpers.unsafelyUnwrapOption(radioNodeList$1); + +if (radioNodeList$2.TAG === /* RadioNodeList */0) { + console.log("RadioNodeList.value", radioNodeList$2._0.value); +} else { + Js_exn.raiseError("incorrect namedItem return value"); +} + +var select$1 = TestHelpers.unsafelyUnwrapOption(Webapi__Dom__HtmlSelectElement.ofElement(select)); + +var opts = select$1.options; + +console.log("HtmlSelectElement.options:", opts); + +opts.length = 3; + +console.log("collection length:", opts.length); + +opts[0] = null; + +console.log("collection length:", opts.length); + +opts[2] = Webapi__Dom__HtmlOptionElement.ofElement(createElement("option")); + +console.log("collection length:", opts.length); + +opts.length = 0; + +var opt1 = createElement("option"); + +opt1.setAttribute("value", "1"); + +opt1.appendChild(createTextNode("opt1")); + +({ + NAME: "Option", + VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt1) + }); + +opts.selectedIndex = 0; + +console.log("collection length:", opts.length); + +console.log("HtmlOptionsCollection.setSelectedIndex", undefined); + +var opt2 = createElement("option"); + +opt2.setAttribute("value", "2"); + +opt2.appendChild(createTextNode("opt2")); + +var item = opts.item(0); + +console.log("HtmlOptionsCollection.item:", (item == null) ? undefined : Caml_option.some(item)); + +console.log("collection length:", opts.length); + +opts.add(({ + NAME: "Option", + VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt2) + }).VAL, 0); + +var item$1 = opts.item(0); + +console.log("HtmlOptionsCollection.add:", (item$1 == null) ? undefined : Caml_option.some(item$1)); + +console.log("collection length:", opts.length); + +console.log("selected index", opts.selectedIndex); + +var opt3 = createElement("option"); + +opt3.setAttribute("value", "3"); + +opt3.appendChild(createTextNode("opt3")); + +opts.add(({ + NAME: "Option", + VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt3) + }).VAL, ({ + NAME: "Element", + VAL: Webapi__Dom__HtmlElement.ofElement(opt2) + }).VAL); + +var item$2 = opts.item(0); + +console.log("HtmlOptionsCollection.addBeforeElement:", (item$2 == null) ? undefined : Caml_option.some(item$2)); + +console.log("collection length:", opts.length); + +var item$3 = opts.selectedIndex; + +console.log("HtmlOptionsCollection.selectedIndex:", item$3); + +var item$4 = opts.selectedIndex; + +console.log("HtmlOptionsCollection.selectedIndex:", item$4); + +opts.remove(0); + +console.log("collection length:", opts.length); function test_data(formElement) { return new FormData(formElement).get("foo"); } +var formEl = form; + +var selectedIndex; + +exports.createElement = createElement; +exports.createTextNode = createTextNode; +exports.createInput = createInput; +exports.createLabelWithText = createLabelWithText; +exports.form = form; +exports.usernameInput = usernameInput; +exports.usernameLabel = usernameLabel; +exports.passwordInput = passwordInput; +exports.passwordLabel = passwordLabel; +exports.radioInput1 = radioInput1; +exports.radioLabel1 = radioLabel1; +exports.radioInput2 = radioInput2; +exports.radioLabel2 = radioLabel2; +exports.selectLabel = selectLabel; +exports.usernameContainer = usernameContainer; +exports.passwordContainer = passwordContainer; +exports.radioContainer = radioContainer; +exports.selectContainer = selectContainer; +exports.formEl = formEl; +exports.body = body; +exports.collection = collection; +exports.len = len; +exports.el0 = el0$1; +exports.el1 = el1$1; +exports.radioNodeList = radioNodeList$1; +exports.select = select$1; +exports.opts = opts; +exports.opt1 = opt1; +exports.selectedIndex = selectedIndex; +exports.opt2 = opt2; +exports.opt3 = opt3; +exports.item = item$4; exports.test_data = test_data; -/* No side effect */ +/* partial_arg Not a pure module */ diff --git a/src/Webapi/Dom/Webapi__Dom__Document.res b/src/Webapi/Dom/Webapi__Dom__Document.res index 7d389216..a7c3c0a0 100644 --- a/src/Webapi/Dom/Webapi__Dom__Document.res +++ b/src/Webapi/Dom/Webapi__Dom__Document.res @@ -33,6 +33,7 @@ module Impl = ( @get external doctype: T.t => Dom.documentType = "doctype" @get external documentElement: T.t => Dom.element = "documentElement" @get external documentURI: T.t => string = "documentURI" + @get external forms: T.t => Dom.htmlCollection = "forms" @get external hidden: T.t => bool = "hidden" @get external implementation: T.t => Dom.domImplementation = "implementation" @get external lastStyleSheetSet: T.t => string = "lastStyleSheetSet" diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlButtonElement.res b/src/Webapi/Dom/Webapi__Dom__HtmlButtonElement.res new file mode 100644 index 00000000..ae6001b2 --- /dev/null +++ b/src/Webapi/Dom/Webapi__Dom__HtmlButtonElement.res @@ -0,0 +1,55 @@ +/** + * Spec: https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element + */ + +type t = Dom.htmlButtonElement + +include Webapi__Dom__EventTarget.Impl({ + type t = t +}) +include Webapi__Dom__Node.Impl({ + type t = t +}) +include Webapi__Dom__Element.Impl({ + type t = t +}) +include Webapi__Dom__HtmlElement.Impl({ + type t = t +}) + +// common form element attributes +@return(nullable) @get +external form: t => option = "form" +@get external labels: t => Dom.nodeList = "labels" + +@get external name: t => string = "name" +@set external setName: (t, string) => unit = "name" + +@get external disabled: t => bool = "disabled" +@set external setDisabled: (t, bool) => unit = "disabled" + +@get external value: t => string = "value" +@set external setValue: (t, string) => unit = "value" + +/* Validation */ +@get external willValidate: t => bool = "willValidate" +@get external validity: t => Webapi__Dom__ValidityState.t = "validity" +@get external validationMessage: t => string = "validationMessage" +@send external setCustomValidity: (t, string) => unit = "setCustomValidity" +@send external checkValidity: t => bool = "checkValidity" +@send external reportValidity: t => bool = "reportValidity" + +@get external formAction: t => string = "formAction" +@set external setFormAction: (t, string) => unit = "formAction" + +@get external formEnctype: t => string = "formEnctype" +@set external setFormEnctype: (t, string) => unit = "formEnctype" + +@get external formMethod: t => string = "formMethod" +@set external setFormMethod: (t, string) => unit = "formMethod" + +@get external formNoValidate: t => bool = "formNoValidate" +@set external setFormNoValidate: (t, bool) => unit = "formNoValidate" + +@get external formTarget: t => string = "formTarget" +@set external setFormTarget: (t, string) => unit = "formTarget" diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlCollection.res b/src/Webapi/Dom/Webapi__Dom__HtmlCollection.res index 81dc8d27..aa99c960 100644 --- a/src/Webapi/Dom/Webapi__Dom__HtmlCollection.res +++ b/src/Webapi/Dom/Webapi__Dom__HtmlCollection.res @@ -1,3 +1,6 @@ +/** + * Spec: https://dom.spec.whatwg.org/#interface-htmlcollection + */ type t = Dom.htmlCollection @val @scope(("Array", "prototype", "slice")) diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlFieldSetElement.res b/src/Webapi/Dom/Webapi__Dom__HtmlFieldSetElement.res new file mode 100644 index 00000000..3176e1ed --- /dev/null +++ b/src/Webapi/Dom/Webapi__Dom__HtmlFieldSetElement.res @@ -0,0 +1,36 @@ +/** + * Spec: https://html.spec.whatwg.org/multipage/form-elements.html#the-fieldset-element + */ +type t = Dom.htmlFieldSetElement + +include Webapi__Dom__EventTarget.Impl({ + type t = t +}) +include Webapi__Dom__Node.Impl({ + type t = t +}) +include Webapi__Dom__Element.Impl({ + type t = t +}) +include Webapi__Dom__HtmlElement.Impl({ + type t = t +}) + +@return(nullable) @get +external form: t => option = "form" + +@get external name: t => string = "name" +@set external setName: (t, string) => unit = "name" + +@get external disabled: t => bool = "disabled" +@set external setDisabled: (t, bool) => unit = "disabled" + +@get external elements: t => Dom.htmlCollection = "elements" + +/* Validation */ +@get external willValidate: t => bool = "willValidate" +@get external validity: t => Webapi__Dom__ValidityState.t = "validity" +@get external validationMessage: t => string = "validationMessage" +@send external setCustomValidity: (t, string) => unit = "setCustomValidity" +@send external checkValidity: t => bool = "checkValidity" +@send external reportValidity: t => bool = "reportValidity" diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.res b/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.res new file mode 100644 index 00000000..dc4bbe6d --- /dev/null +++ b/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.res @@ -0,0 +1,47 @@ +/** + * Spec: https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#htmlformcontrolscollection + */ +type t = Dom.htmlFormControlsCollection + +@val @scope(("Array", "prototype", "slice")) +external toArray: t => array = "call" + +@get external length: t => int = "length" +@send @return(nullable) external item: (t, int) => option = "item" + +type t_namedItem = + | RadioNodeList(Dom.radioNodeList) + | Button(Dom.htmlButtonElement) + | FieldSet(Dom.htmlFieldSetElement) + | Input(Dom.htmlInputElement) + | Object(Dom.htmlObjectElement) + | Output(Dom.htmlOutputElement) + | Select(Dom.htmlSelectElement) + | TextArea(Dom.htmlTextAreaElement) + +let isRadioNodeList: 'a => bool = %raw(` + function(x) { return x instanceof RadioNodeList; } +`) + +@send @return(nullable) external _namedItem: (t, string) => option<'a> = "namedItem" + +let namedItem = (t, name): option => + switch _namedItem(t, name) { + | Some(el) => + if Webapi__Dom__RadioNodeList.unsafeAsRadioNodeList(el)->isRadioNodeList { + el->Obj.magic->RadioNodeList->Some + } else { + switch Webapi__Dom__Element.tagName(el) { + // fixme: this should be a classify function in Webapi__Dom__Element? + | "BUTTON" => el->Obj.magic->Button->Some + | "FIELDSET" => el->Obj.magic->FieldSet->Some + | "INPUT" => el->Obj.magic->Input->Some + | "OBJECT" => el->Obj.magic->Object->Some + | "OUTPUT" => el->Obj.magic->Output->Some + | "SELECT" => el->Obj.magic->Select->Some + | "TEXTAREA" => el->Obj.magic->TextArea->Some + | _ => None + } + } + | None => None + } diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlFormElement.res b/src/Webapi/Dom/Webapi__Dom__HtmlFormElement.res index 5c1deb9a..f9a36848 100644 --- a/src/Webapi/Dom/Webapi__Dom__HtmlFormElement.res +++ b/src/Webapi/Dom/Webapi__Dom__HtmlFormElement.res @@ -10,7 +10,16 @@ module Impl = ( ) => { type t_htmlFormElement = T.t - /* TODO: elements: HTMLFormControlsCollection */ + external unsafeAsFormElement: Dom.element => t_htmlFormElement = "%identity" + external asElement: t_htmlFormElement => Dom.element = "%identity" + + let ofElement = (el): option => + switch Webapi__Dom__Element.tagName(el)->Js.String2.toUpperCase { + | "FORM" => el->unsafeAsFormElement->Some + | _ => None + } + + @get external elements: t_htmlFormElement => Dom.htmlFormControlsCollection = "elements" @get external length: t_htmlFormElement => int = "length" @get external name: t_htmlFormElement => string = "name" @set external setName: (t_htmlFormElement, string) => unit = "name" diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlObjectElement.res b/src/Webapi/Dom/Webapi__Dom__HtmlObjectElement.res new file mode 100644 index 00000000..0c1f1ab1 --- /dev/null +++ b/src/Webapi/Dom/Webapi__Dom__HtmlObjectElement.res @@ -0,0 +1,47 @@ +/** + * Spec: https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element + */ + +type t = Dom.htmlObjectElement + +include Webapi__Dom__EventTarget.Impl({ + type t = t +}) +include Webapi__Dom__Node.Impl({ + type t = t +}) +include Webapi__Dom__Element.Impl({ + type t = t +}) +include Webapi__Dom__HtmlElement.Impl({ + type t = t +}) + +// common form element attributes +@return(nullable) @get +external form: t => option = "form" + +@get external data: t => string = "data" +@set external setData: (t, string) => unit = "data" + +@get external name: t => string = "name" +@set external setName: (t, string) => unit = "name" + +@get external width: t => string = "width" +@set external setWidth: (t, string) => unit = "width" + +@get external height: t => string = "height" +@set external setHeight: (t, string) => unit = "height" + +@return(nullable) @get external contentDocument: t => option = "contentDocument" +@return(nullable) @get external contentWindow: t => option = "contentWindow" + +@return(nullable) @send external getSVGDocument: t => option = "getSVGDocument" + +/* Validation */ +@get external willValidate: t => bool = "willValidate" +@get external validity: t => Webapi__Dom__ValidityState.t = "validity" +@get external validationMessage: t => string = "validationMessage" +@send external setCustomValidity: (t, string) => unit = "setCustomValidity" +@send external checkValidity: t => bool = "checkValidity" +@send external reportValidity: t => bool = "reportValidity" diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlOptGroupElement.res b/src/Webapi/Dom/Webapi__Dom__HtmlOptGroupElement.res new file mode 100644 index 00000000..5c0bf6c6 --- /dev/null +++ b/src/Webapi/Dom/Webapi__Dom__HtmlOptGroupElement.res @@ -0,0 +1,23 @@ +/** + * Spec: https://html.spec.whatwg.org/multipage/form-elements.html#the-optgroup-element + */ +type t = Dom.htmlOptGroupElement + +include Webapi__Dom__EventTarget.Impl({ + type t = t +}) +include Webapi__Dom__Node.Impl({ + type t = t +}) +include Webapi__Dom__Element.Impl({ + type t = t +}) +include Webapi__Dom__HtmlElement.Impl({ + type t = t +}) + +@get external disabled: t => bool = "disabled" +@set external setDisabled: (t, bool) => unit = "disabled" + +@get external label: t => string = "label" +@set external setLabel: (t, string) => unit = "label" diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlOptionElement.res b/src/Webapi/Dom/Webapi__Dom__HtmlOptionElement.res new file mode 100644 index 00000000..6bf9ef63 --- /dev/null +++ b/src/Webapi/Dom/Webapi__Dom__HtmlOptionElement.res @@ -0,0 +1,47 @@ +/** + * Spec: https://html.spec.whatwg.org/multipage/form-elements.html#the-option-element + */ +type t = Dom.htmlOptionElement + +include Webapi__Dom__EventTarget.Impl({ + type t = t +}) +include Webapi__Dom__Node.Impl({ + type t = t +}) +include Webapi__Dom__Element.Impl({ + type t = t +}) +include Webapi__Dom__HtmlElement.Impl({ + type t = t +}) + +let ofElement = (el): option => + switch Webapi__Dom__Element.tagName(el)->Js.String2.toUpperCase { + | "OPTION" => el->Obj.magic->Some + | _ => None + } + +// common form element attributes +@return(nullable) @get +external form: t => option = "form" + +@get external disabled: t => bool = "disabled" +@set external setDisabled: (t, bool) => unit = "disabled" + +@get external label: t => string = "label" +@set external setLabel: (t, string) => unit = "label" + +@get external selected: t => bool = "selected" +@set external setSelected: (t, bool) => unit = "selected" + +@get external defaultSelected: t => bool = "defaultSelected" +@set external setDefaultSelected: (t, bool) => unit = "defaultSelected" + +@get external value: t => string = "value" +@set external setValue: (t, string) => unit = "value" + +@get external text: t => string = "text" +@set external setText: (t, string) => unit = "text" + +@get external index: t => int = "index" diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlOptionsCollection.res b/src/Webapi/Dom/Webapi__Dom__HtmlOptionsCollection.res new file mode 100644 index 00000000..cfc910ba --- /dev/null +++ b/src/Webapi/Dom/Webapi__Dom__HtmlOptionsCollection.res @@ -0,0 +1,33 @@ +/** + * Spec: https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#htmloptionscollection + * Extends HTMLCollection + */ +type t = Dom.htmlOptionsCollection + +@val @scope(("Array", "prototype", "slice")) +external toArray: t => array = "call" + +@send @return(nullable) external item: (t, int) => option = "item" +@send @return(nullable) external namedItem: (t, string) => option = "namedItem" + +@get external length: t => int = "length" +@set external setLength: (t, int) => unit = "length" + +@set_index external setItem: (t, int, Dom.htmlOptionElement) => unit = "" +@set_index external clearItem: (t, int, @as(json`null`) _) => unit = "" + +@get external selectedIndex: t => int = "selectedIndex" +@set external setSelectedIndex: (t, int) => unit = "selectedIndex" + +@send external clearSelectedIndex: (t, @as(json`-1`) _) => unit = "selectedIndex" + +/** + * This method will throw a "HierarchyRequestError" DOMException if element is an ancestor of the element into which it is to be inserted. + */ +@send +external add: ( + t, + ~option: @unwrap [#Option(Dom.htmlOptionElement) | #OptGroup(Dom.htmlOptGroupElement)], + ~before: @unwrap [#Index(int) | #Element(Dom.htmlElement)]=?, +) => unit = "add" +@send external remove: (t, int) => unit = "remove" diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlOutputElement.res b/src/Webapi/Dom/Webapi__Dom__HtmlOutputElement.res new file mode 100644 index 00000000..cbf085fb --- /dev/null +++ b/src/Webapi/Dom/Webapi__Dom__HtmlOutputElement.res @@ -0,0 +1,41 @@ +/** + * Spec: https://html.spec.whatwg.org/multipage/form-elements.html#the-output-element + */ +type t = Dom.htmlOutputElement + +include Webapi__Dom__EventTarget.Impl({ + type t = t +}) +include Webapi__Dom__Node.Impl({ + type t = t +}) +include Webapi__Dom__Element.Impl({ + type t = t +}) +include Webapi__Dom__HtmlElement.Impl({ + type t = t +}) + +// common form element attributes +@return(nullable) @get +external form: t => option = "form" +@get external labels: t => Dom.nodeList = "labels" + +@get external name: t => string = "name" +@set external setName: (t, string) => unit = "name" + +@get external defaultValue: t => string = "defaultValue" +@set external setDefaultValue: (t, string) => unit = "defaultValue" + +@get external value: t => string = "value" +@set external setValue: (t, string) => unit = "value" + +@get external htmlFor: t => Dom.domTokenList = "htmlFor" + +/* Validation */ +@get external willValidate: t => bool = "willValidate" +@get external validity: t => Webapi__Dom__ValidityState.t = "validity" +@get external validationMessage: t => string = "validationMessage" +@send external setCustomValidity: (t, string) => unit = "setCustomValidity" +@send external checkValidity: t => bool = "checkValidity" +@send external reportValidity: t => bool = "reportValidity" diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlSelectElement.res b/src/Webapi/Dom/Webapi__Dom__HtmlSelectElement.res new file mode 100644 index 00000000..ed0c3d05 --- /dev/null +++ b/src/Webapi/Dom/Webapi__Dom__HtmlSelectElement.res @@ -0,0 +1,97 @@ +/** + * Spec: https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element + */ +type t = Dom.htmlSelectElement + +include Webapi__Dom__EventTarget.Impl({ + type t = t +}) +include Webapi__Dom__Node.Impl({ + type t = t +}) +include Webapi__Dom__Element.Impl({ + type t = t +}) +include Webapi__Dom__HtmlElement.Impl({ + type t = t +}) + +// common form element attributes +@return(nullable) @get +external form: t => option = "form" +@get external labels: t => Dom.nodeList = "labels" + +@get external name: t => string = "name" +@set external setName: (t, string) => unit = "name" + +@get external disabled: t => bool = "disabled" +@set external setDisabled: (t, bool) => unit = "disabled" + +@get external required: t => bool = "required" +@set external setRequired: (t, bool) => unit = "required" + +@get external value: t => string = "value" +@set external setValue: (t, string) => unit = "value" + +// specific to htmlSelectElement +@get external autocomplete: t => string = "autocomplete" +@set external setAutocomplete: (t, string) => unit = "autocomplete" + +@get external multiple: t => bool = "multiple" +@set external setMultiple: (t, bool) => unit = "multiple" + +@get external size: t => float = "size" +@set external setSize: (t, float) => unit = "size" + +/* Validation */ +@get external willValidate: t => bool = "willValidate" +@get external validity: t => Webapi__Dom__ValidityState.t = "validity" +@get external validationMessage: t => string = "validationMessage" +@send external setCustomValidity: (t, string) => unit = "setCustomValidity" +@send external checkValidity: t => bool = "checkValidity" +@send external reportValidity: t => bool = "reportValidity" + +external unsafeOfElement: Dom.element => t = "%identity" +external asElement: t => Dom.element = "%identity" +external asHtmlElement: t => Dom.htmlElement = "%identity" + +let ofElement = (el): option => + switch Webapi__Dom__Element.tagName(el) { + | "SELECT" => el->unsafeOfElement->Some + | _ => None + } + +@get external options: t => Dom.htmlOptionsCollection = "options" + +@get external length: t => int = "length" +@set external setLength: (t, int) => unit = "length" + +@get +external selectedOptions: t => Dom.htmlCollection = "selectedOptions" + +@send @return(nullable) +external item: (t, int) => option = "item" + +@send @return(nullable) +external namedItem: (t, string) => option = "namedItem" + +@send external remove: t => unit = "remove" // ChildNode overload +@send external removeIndex: (t, int) => unit = "remove" + +/** + * This method will throw a "HierarchyRequestError" DOMException if element is an ancestor of the element into which it is to be inserted. + */ +@send +external add: ( + t, + ~option: @unwrap [#Option(Dom.htmlOptionElement) | #OptGroup(Dom.htmlOptGroupElement)], + ~before: @unwrap [#Index(int) | #Element(Dom.htmlElement)]=?, +) => unit = "add" + +@set_index external setItem: (t, int, Dom.htmlOptionElement) => unit = "" +@set_index external clearItem: (t, int, @as(json`null`) _) => unit = "" + +@get external selectedIndex: t => int = "selectedIndex" +@set external setSelectedIndex: (t, int) => unit = "selectedIndex" +@send +external clearSelectedIndex: (t, @as(json`-1`) _) => unit = "selectedIndex" diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlTextAreaElement.res b/src/Webapi/Dom/Webapi__Dom__HtmlTextAreaElement.res new file mode 100644 index 00000000..cdfd954e --- /dev/null +++ b/src/Webapi/Dom/Webapi__Dom__HtmlTextAreaElement.res @@ -0,0 +1,109 @@ +/** + * Spec: https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element + */ +type t = Dom.htmlTextAreaElement + +include Webapi__Dom__EventTarget.Impl({ + type t = t +}) +include Webapi__Dom__Node.Impl({ + type t = t +}) +include Webapi__Dom__Element.Impl({ + type t = t +}) +include Webapi__Dom__HtmlElement.Impl({ + type t = t +}) + +// common form element attributes +@return(nullable) @get +external form: t => option = "form" +@get external labels: t => Dom.nodeList = "labels" + +@get external name: t => string = "name" +@set external setName: (t, string) => unit = "name" + +@get external defaultValue: t => string = "defaultValue" +@set external setDefaultValue: (t, string) => unit = "defaultValue" + +@get external value: t => string = "value" +@set external setValue: (t, string) => unit = "value" + +// textarea specific + +@get external autocomplete: t => string = "autocomplete" +@set external setAutocomplete: (t, string) => unit = "autocomplete" + +@get external cols: t => float = "cols" +@set external setCols: (t, float) => unit = "cols" + +@get external dirName: t => string = "dirName" +@set external setDirName: (t, string) => unit = "dirName" + +@get external disabled: t => bool = "disabled" +@set external setDisabled: (t, bool) => unit = "disabled" + +@get external maxLength: t => float = "maxLength" +@set external setMaxLength: (t, float) => unit = "maxLength" + +@get external minLength: t => float = "minLength" +@set external setMinLength: (t, float) => unit = "minLength" + +@get external placeholder: t => string = "placeholder" +@set external setPlaceholder: (t, string) => unit = "placeholder" + +@get external readOnly: t => bool = "readOnly" +@set external setReadOnly: (t, bool) => unit = "readOnly" + +@get external required: t => bool = "required" +@set external setRequired: (t, bool) => unit = "required" + +@get external rows: t => float = "rows" +@set external setRows: (t, float) => unit = "rows" + +@get external wrap: t => string = "wrap" +@set external setWrap: (t, string) => unit = "wrap" + +@get external textLength: t => float = "textLength" + +/* Validation */ +@get external willValidate: t => bool = "willValidate" +@get external validity: t => Webapi__Dom__ValidityState.t = "validity" +@get external validationMessage: t => string = "validationMessage" +@send external setCustomValidity: (t, string) => unit = "setCustomValidity" +@send external checkValidity: t => bool = "checkValidity" +@send external reportValidity: t => bool = "reportValidity" + +/* Selection */ +type selectionDirection = [#forward | #backward | #none] + +@send external select: t => unit = "select" +@get external selectionStart: t => int = "selectionStart" +@set external setSelectionStart: (t, int) => unit = "selectionStart" +@get external selectionEnd: t => int = "selectionEnd" +@set external setSelectionEnd: (t, int) => unit = "selectionEnd" +@get +external selectionDirection: t => selectionDirection = "selectionDirection" +@set +external setSelectionDirection: (t, selectionDirection) => unit = "selectionDirection" + +@send external setSelectionRange: (t, int, int) => unit = "setSelectionRange" +@send +external setSelectionRangeWithDirection: (t, int, int, selectionDirection) => unit = + "setSelectionRange" + +type selectionMode = [ + | #select + | #start + | #end + | #preserve +] + +@send +external setRangeTextWithinSelection: (t, string) => unit = "setRangeText" +@send +external setRangeTextWithinInterval: (t, string, int, int) => unit = "setRangeText" +@send +external setRangeTextWithinIntervalWithSelectionMode: (t, string, int, int, selectionMode) => unit = + "setRangeText" diff --git a/src/Webapi/Dom/Webapi__Dom__NodeList.res b/src/Webapi/Dom/Webapi__Dom__NodeList.res index 3a62e7cb..d16332ea 100644 --- a/src/Webapi/Dom/Webapi__Dom__NodeList.res +++ b/src/Webapi/Dom/Webapi__Dom__NodeList.res @@ -1,3 +1,6 @@ +/** + * Spec: https://dom.spec.whatwg.org/#interface-nodelist + */ type t = Dom.nodeList @val external toArray: t => array = "Array.prototype.slice.call" diff --git a/src/Webapi/Dom/Webapi__Dom__RadioNodeList.res b/src/Webapi/Dom/Webapi__Dom__RadioNodeList.res new file mode 100644 index 00000000..b3edecde --- /dev/null +++ b/src/Webapi/Dom/Webapi__Dom__RadioNodeList.res @@ -0,0 +1,18 @@ +/** + * Spec: https://html.spec.whatwg.org/#radionodelist + * Extends: NodeList + */ + +type t = Dom.radioNodeList + +@val external toArray: t => array = "Array.prototype.slice.call" + +@send external forEach: (t, (Dom.node, int) => unit) => unit = "forEach" + +@get external length: t => int = "length" + +@send @return(nullable) external item: (t, int) => option = "item" + +@get external value: t => string = "value" + +external unsafeAsRadioNodeList: 'a => t = "%identity" diff --git a/src/Webapi/Webapi__Dom.res b/src/Webapi/Webapi__Dom.res index dc060b22..3f95a650 100644 --- a/src/Webapi/Webapi__Dom.res +++ b/src/Webapi/Webapi__Dom.res @@ -3,11 +3,11 @@ module Attr = Webapi__Dom__Attr module BeforeUnloadEvent = Webapi__Dom__BeforeUnloadEvent module CdataSection = Webapi__Dom__CdataSection module CharacterData = Webapi__Dom__CharacterData -module Comment = Webapi__Dom__Comment -module CssStyleDeclaration = Webapi__Dom__CssStyleDeclaration module ClipboardEvent = Webapi__Dom__ClipboardEvent module CloseEvent = Webapi__Dom__CloseEvent +module Comment = Webapi__Dom__Comment module CompositionEvent = Webapi__Dom__CompositionEvent +module CssStyleDeclaration = Webapi__Dom__CssStyleDeclaration module CustomEvent = Webapi__Dom__CustomEvent module DataTransfer = Webapi__Dom__DataTransfer module DataTransferItem = Webapi__Dom__DataTransferItem @@ -26,12 +26,22 @@ module Event = Webapi__Dom__Event module EventTarget = Webapi__Dom__EventTarget module FocusEvent = Webapi__Dom__FocusEvent module History = Webapi__Dom__History +module HtmlButtonElement = Webapi__Dom__HtmlButtonElement module HtmlCollection = Webapi__Dom__HtmlCollection module HtmlDocument = Webapi__Dom__HtmlDocument module HtmlElement = Webapi__Dom__HtmlElement +module HtmlFieldSetElement = Webapi__Dom__HtmlFieldSetElement +module HtmlFormControlsCollection = Webapi__Dom__HtmlFormControlsCollection module HtmlFormElement = Webapi__Dom__HtmlFormElement module HtmlImageElement = Webapi__Dom__HtmlImageElement module HtmlInputElement = Webapi__Dom__HtmlInputElement +module HtmlObjectElement = Webapi__Dom__HtmlObjectElement +module HtmlOptGroupElement = Webapi__Dom__HtmlOptGroupElement +module HtmlOptionElement = Webapi__Dom__HtmlOptionElement +module HtmlOptionsCollection = Webapi__Dom__HtmlOptionsCollection +module HtmlOutputElement = Webapi__Dom__HtmlOutputElement +module HtmlSelectElement = Webapi__Dom__HtmlSelectElement +module HtmlTextAreaElement = Webapi__Dom__HtmlTextAreaElement module IdbVersionChangeEvent = Webapi__Dom__IdbVersionChangeEvent module Image = Webapi__Dom__Image module InputEvent = Webapi__Dom__InputEvent @@ -50,6 +60,7 @@ module PointerEvent = Webapi__Dom__PointerEvent module PopStateEvent = Webapi__Dom__PopStateEvent module ProcessingInstruction = Webapi__Dom__ProcessingInstruction module ProgressEvent = Webapi__Dom__ProgressEvent +module RadioNodeList = Webapi__Dom__RadioNodeList module Range = Webapi__Dom__Range module RectList = Webapi__Dom__RectList module RelatedEvent = Webapi__Dom__RelatedEvent @@ -138,18 +149,13 @@ include Webapi__Dom__Types HTMLMetaElement HTMLMeterElement HTMLModElement - HTMLObjectElement HTMLOListElement - HTMLOptGroupElement - HTMLOptionElement - HTMLOutputElement HTMLParagraphElement HTMLParamElement HTMLPreElement HTMLProgressElement HTMLQuoteElement HTMLScriptElement - HTMLSelectElement HTMLSourceElement HTMLSpanElement HTMLStyleElement @@ -161,7 +167,6 @@ include Webapi__Dom__Types HTMLTableColElement HTMLTableRowElement HTMLTableSectionElement - HTMLTextAreaElement HTMLTimeElement HTMLTitleElement HTMLTrackElement @@ -178,11 +183,8 @@ include Webapi__Dom__Types CanvasPixelArray NotifyAudioAvailableEvent HTMLAllCollection - HTMLFormControlsCollection - HTMLOptionsCollection HTMLPropertiesCollection DOMStringMap - RadioNodeList MediaError /* SVG Element interfaces */ diff --git a/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.res b/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.res index 25ade7a0..6e00fb45 100644 --- a/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.res +++ b/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.res @@ -1,4 +1,187 @@ +open Webapi.Dom open Webapi.FormData open Webapi.Dom.HtmlFormElement +let createElement = Document.createElement(document) +let createTextNode = Document.createTextNode(document) +let createInput = () => createElement("input") +let createLabelWithText = text => { + let el = createElement("label") + let textNode = createTextNode(text) + Element.appendChild(el, textNode) + el +} + +let form = createElement("form")->ofElement->TestHelpers.unsafelyUnwrapOption + +let usernameInput = createInput() +Element.setAttribute(usernameInput, "type", "text") +Element.setAttribute(usernameInput, "name", "username") + +let usernameLabel = createLabelWithText("Username:") +Element.appendChild(usernameLabel, usernameInput) + +let passwordInput = createInput() +Element.setAttribute(passwordInput, "type", "password") +Element.setAttribute(passwordInput, "name", "password") + +let passwordLabel = createLabelWithText("Password:") +Element.appendChild(passwordLabel, passwordInput) + +let radioInput1 = createInput() +Element.setAttribute(radioInput1, "type", "radio") +Element.setAttribute(radioInput1, "name", "radiogroup") +Element.setAttribute(radioInput1, "value", "one") +Element.setAttribute(radioInput1, "checked", "true") + +let radioLabel1 = createLabelWithText("Choice 1:") +Element.appendChild(radioLabel1, radioInput1) + +let radioInput2 = createInput() +Element.setAttribute(radioInput2, "type", "radio") +Element.setAttribute(radioInput2, "name", "radiogroup") +Element.setAttribute(radioInput2, "value", "two") +// Element.setAttribute(radioInput2, "checked", "true"); + +let radioLabel2 = createLabelWithText("Choice 2:") +Element.appendChild(radioLabel2, radioInput2) + +let select = createElement("select") +Element.setAttribute(select, "name", "select") +let selectLabel = createLabelWithText("Select:") +Element.appendChild(selectLabel, select) + +let usernameContainer = createElement("div") +let passwordContainer = createElement("div") +let radioContainer = createElement("div") +let selectContainer = createElement("div") + +let formEl = form->asElement + +Element.appendChild(usernameContainer, usernameLabel) +Element.appendChild(passwordContainer, passwordLabel) +Element.appendChild(radioContainer, radioLabel1) +Element.appendChild(radioContainer, radioLabel2) +Element.appendChild(selectContainer, selectLabel) +Element.appendChild(formEl, usernameContainer) +Element.appendChild(formEl, passwordContainer) +Element.appendChild(formEl, radioContainer) +Element.appendChild(formEl, selectContainer) + +let body = + Document.asHtmlDocument(document) + ->Belt.Option.flatMap(HtmlDocument.body) + ->TestHelpers.unsafelyUnwrapOption + +Element.appendChild(body, formEl) + +let collection = elements(form) + +Js.log2("HtmlFormElement.elements:", collection) + +let len = HtmlFormControlsCollection.length(collection) +Js.log2("HtmlFormControlsCollection.length:", len) + +let el0 = HtmlFormControlsCollection.item(collection, 0) +Js.log2("HtmlFormControlsCollection.item:", el0) + +let el0 = HtmlFormControlsCollection.namedItem(collection, "username") +Js.log2("HtmlFormControlsCollection.namedItem:", el0) + +let el1 = HtmlFormControlsCollection.item(collection, 1) +Js.log2("HtmlFormControlsCollection.length:", el1) + +let el1 = HtmlFormControlsCollection.namedItem(collection, "password") +Js.log2("HtmlFormControlsCollection.namedItem:", el1) + +let radioNodeList = HtmlFormControlsCollection.item(collection, 2) +Js.log2("HtmlFormControlsCollection.namedItem:", radioNodeList) + +let radioNodeList = HtmlFormControlsCollection.namedItem(collection, "radiogroup") +Js.log2("HtmlFormControlsCollection.namedItem:", radioNodeList) + +switch TestHelpers.unsafelyUnwrapOption(radioNodeList) { +| Button(_) +| FieldSet(_) +| Input(_) +| Object(_) +| Output(_) +| Select(_) +| TextArea(_) => + Js.Exn.raiseError("incorrect namedItem return value") +| RadioNodeList(radioNodeList) => + Js.log2("RadioNodeList.value", RadioNodeList.value(radioNodeList)) +} + +let select = HtmlSelectElement.ofElement(select)->TestHelpers.unsafelyUnwrapOption + +let opts = HtmlSelectElement.options(select) +Js.log2("HtmlSelectElement.options:", opts) + +HtmlOptionsCollection.setLength(opts, 3) +Js.log2("collection length:", HtmlOptionsCollection.length(opts)) +HtmlOptionsCollection.clearItem(opts, 0) +Js.log2("collection length:", HtmlOptionsCollection.length(opts)) +HtmlOptionsCollection.setItem( + opts, + 2, + createElement("option")->HtmlOptionElement.ofElement->Belt.Option.getUnsafe, +) +Js.log2("collection length:", HtmlOptionsCollection.length(opts)) + +HtmlOptionsCollection.setLength(opts, 0) + +let opt1 = createElement("option") +Element.setAttribute(opt1, "value", "1") +Element.appendChild(opt1, createTextNode("opt1")) + +let _ = HtmlOptionsCollection.add( + opts, + ~option=#Option(opt1->HtmlOptionElement.ofElement->Belt.Option.getUnsafe), +) +let selectedIndex = HtmlOptionsCollection.setSelectedIndex(opts, 0) +Js.log2("collection length:", HtmlOptionsCollection.length(opts)) +Js.log2("HtmlOptionsCollection.setSelectedIndex", selectedIndex) + +let opt2 = createElement("option") +Element.setAttribute(opt2, "value", "2") +Element.appendChild(opt2, createTextNode("opt2")) + +let item = HtmlOptionsCollection.item(opts, 0) +Js.log2("HtmlOptionsCollection.item:", item) +Js.log2("collection length:", HtmlOptionsCollection.length(opts)) + +HtmlOptionsCollection.add( + opts, + ~option=#Option(opt2->HtmlOptionElement.ofElement->Belt.Option.getUnsafe), + ~before=#Index(0), +) + +let item = HtmlOptionsCollection.item(opts, 0) +Js.log2("HtmlOptionsCollection.add:", item) +Js.log2("collection length:", HtmlOptionsCollection.length(opts)) +Js.log2("selected index", HtmlOptionsCollection.selectedIndex(opts)) + +let opt3 = createElement("option") +Element.setAttribute(opt3, "value", "3") +Element.appendChild(opt3, createTextNode("opt3")) + +HtmlOptionsCollection.add( + opts, + ~option=#Option(opt3->HtmlOptionElement.ofElement->Belt.Option.getUnsafe), + ~before=#Element(opt2->HtmlElement.ofElement->Belt.Option.getUnsafe), +) +let item = HtmlOptionsCollection.item(opts, 0) +Js.log2("HtmlOptionsCollection.addBeforeElement:", item) +Js.log2("collection length:", HtmlOptionsCollection.length(opts)) + +let item = HtmlOptionsCollection.selectedIndex(opts) +Js.log2("HtmlOptionsCollection.selectedIndex:", item) + +let item = HtmlOptionsCollection.selectedIndex(opts) +Js.log2("HtmlOptionsCollection.selectedIndex:", item) + +HtmlOptionsCollection.remove(opts, 0) +Js.log2("collection length:", HtmlOptionsCollection.length(opts)) + let test_data = formElement => formElement->data->get("foo") From 5d853b58e01a8e3ed13612b2468ddf52a99d8daa Mon Sep 17 00:00:00 2001 From: rob Date: Mon, 27 Dec 2021 23:58:25 -0500 Subject: [PATCH 2/4] chore: cleanup, add test case for clearSelectedIndex and HtmlSelectElement --- .../Dom/Webapi__Dom__HtmlFormElement__test.js | 235 +++++++++++++++--- ...ebapi__Dom__HtmlFormControlsCollection.res | 32 +-- ...bapi__Dom__HtmlFormControlsCollection.resi | 17 ++ .../Webapi__Dom__HtmlOptionsCollection.res | 5 +- .../Dom/Webapi__Dom__HtmlSelectElement.res | 2 +- src/Webapi/Dom/Webapi__Dom__RadioNodeList.res | 4 + .../Webapi__Dom__HtmlFormElement__test.res | 122 ++++++++- 7 files changed, 349 insertions(+), 68 deletions(-) create mode 100644 src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.resi diff --git a/lib/js/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.js b/lib/js/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.js index 4fba3b79..63e0d4ff 100644 --- a/lib/js/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.js +++ b/lib/js/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.js @@ -42,6 +42,8 @@ usernameInput.setAttribute("type", "text"); usernameInput.setAttribute("name", "username"); +usernameInput.setAttribute("value", "username"); + var usernameLabel = createLabelWithText("Username:"); usernameLabel.appendChild(usernameInput); @@ -52,6 +54,8 @@ passwordInput.setAttribute("type", "password"); passwordInput.setAttribute("name", "password"); +passwordInput.setAttribute("value", "password"); + var passwordLabel = createLabelWithText("Password:"); passwordLabel.appendChild(passwordInput); @@ -126,38 +130,62 @@ var collection = form.elements; console.log("HtmlFormElement.elements:", collection); +console.assert(!(collection == null), "collection exists"); + var len = collection.length; console.log("HtmlFormControlsCollection.length:", len); +console.assert(len === 5, "initial length is 5"); + var el0 = collection.item(0); -console.log("HtmlFormControlsCollection.item:", (el0 == null) ? undefined : Caml_option.some(el0)); +var el0$1 = (el0 == null) ? undefined : Caml_option.some(el0); + +console.log("HtmlFormControlsCollection.item:", el0$1); + +console.assert(Belt_Option.isSome(el0$1), "item at index 0 exists"); -var el0$1 = Webapi__Dom__HtmlFormControlsCollection.namedItem(collection, "username"); +var el0$2 = Webapi__Dom__HtmlFormControlsCollection.namedItem(collection, "username"); -console.log("HtmlFormControlsCollection.namedItem:", el0$1); +console.log("HtmlFormControlsCollection.namedItem:", el0$2); + +console.assert(Belt_Option.isSome(el0$2), "namedItem 'username' exists"); var el1 = collection.item(1); -console.log("HtmlFormControlsCollection.length:", (el1 == null) ? undefined : Caml_option.some(el1)); +var el1$1 = (el1 == null) ? undefined : Caml_option.some(el1); -var el1$1 = Webapi__Dom__HtmlFormControlsCollection.namedItem(collection, "password"); +console.log("HtmlFormControlsCollection.item:", el1$1); -console.log("HtmlFormControlsCollection.namedItem:", el1$1); +console.assert(Belt_Option.isSome(el1$1), "item at index 1 exists"); -var radioNodeList = collection.item(2); +var el1$2 = Webapi__Dom__HtmlFormControlsCollection.namedItem(collection, "password"); + +console.log("HtmlFormControlsCollection.namedItem:", el1$2); -console.log("HtmlFormControlsCollection.namedItem:", (radioNodeList == null) ? undefined : Caml_option.some(radioNodeList)); +console.assert(Belt_Option.isSome(el1$2), "namedItem 'password' exists"); -var radioNodeList$1 = Webapi__Dom__HtmlFormControlsCollection.namedItem(collection, "radiogroup"); +var radioNodeList = collection.item(2); + +var radioNodeList$1 = (radioNodeList == null) ? undefined : Caml_option.some(radioNodeList); console.log("HtmlFormControlsCollection.namedItem:", radioNodeList$1); -var radioNodeList$2 = TestHelpers.unsafelyUnwrapOption(radioNodeList$1); +console.assert(Belt_Option.isSome(radioNodeList$1), "item at index 2 exists"); + +var radioNodeList$2 = Webapi__Dom__HtmlFormControlsCollection.namedItem(collection, "radiogroup"); -if (radioNodeList$2.TAG === /* RadioNodeList */0) { - console.log("RadioNodeList.value", radioNodeList$2._0.value); +console.log("HtmlFormControlsCollection.namedItem:", radioNodeList$2); + +console.assert(Belt_Option.isSome(radioNodeList$2), "namedItem 'radiogroup' exists"); + +var radioNodeList$3 = TestHelpers.unsafelyUnwrapOption(radioNodeList$2); + +if (radioNodeList$3.TAG === /* RadioNodeList */0) { + var radioNodeList$4 = radioNodeList$3._0; + console.log("RadioNodeList.value", radioNodeList$4.value); + console.assert(radioNodeList$4.value === "one", "RadioNodeList.value is one"); } else { Js_exn.raiseError("incorrect namedItem return value"); } @@ -168,16 +196,24 @@ var opts = select$1.options; console.log("HtmlSelectElement.options:", opts); +console.assert(!(opts == null), "HtmlSelectElement.options returns something"); + opts.length = 3; console.log("collection length:", opts.length); +console.assert(opts.length === 3, "setLength works"); + opts[0] = null; +console.assert(opts.length === 2, "clearItem works"); + console.log("collection length:", opts.length); opts[2] = Webapi__Dom__HtmlOptionElement.ofElement(createElement("option")); +console.assert(opts.length === 3, "setItem works"); + console.log("collection length:", opts.length); opts.length = 0; @@ -188,16 +224,16 @@ opt1.setAttribute("value", "1"); opt1.appendChild(createTextNode("opt1")); -({ - NAME: "Option", - VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt1) - }); +opts.add(({ + NAME: "Option", + VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt1) + }).VAL, undefined); opts.selectedIndex = 0; -console.log("collection length:", opts.length); +console.assert(opts.selectedIndex === 0, "setSelectedIndex works"); -console.log("HtmlOptionsCollection.setSelectedIndex", undefined); +console.log("collection length:", opts.length); var opt2 = createElement("option"); @@ -207,23 +243,35 @@ opt2.appendChild(createTextNode("opt2")); var item = opts.item(0); -console.log("HtmlOptionsCollection.item:", (item == null) ? undefined : Caml_option.some(item)); +var item$1 = (item == null) ? undefined : Caml_option.some(item); + +console.assert(Belt_Option.isSome(item$1), "HtmlOptionsCollection.item should return an item"); + +console.log("HtmlOptionsCollection.item:", item$1); console.log("collection length:", opts.length); +console.assert(opts.length === 1, "HtmlOptionsCollection.length should be 1"); + opts.add(({ NAME: "Option", VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt2) }).VAL, 0); -var item$1 = opts.item(0); +console.assert(opts.length === 2, "HtmlOptionsCollection.length should be 2"); + +var item$2 = opts.item(0); -console.log("HtmlOptionsCollection.add:", (item$1 == null) ? undefined : Caml_option.some(item$1)); +console.assert(Belt_Option.isSome((item$2 == null) ? undefined : Caml_option.some(item$2)), "HtmlOptionsCollection.add works"); console.log("collection length:", opts.length); console.log("selected index", opts.selectedIndex); +opts.selectedIndex = -1; + +console.assert(opts.selectedIndex === -1, "HtmlOptionsCollection.clearSelectedIndex sets index to -1"); + var opt3 = createElement("option"); opt3.setAttribute("value", "3"); @@ -238,31 +286,143 @@ opts.add(({ VAL: Webapi__Dom__HtmlElement.ofElement(opt2) }).VAL); -var item$2 = opts.item(0); +var item$3 = opts.item(0); -console.log("HtmlOptionsCollection.addBeforeElement:", (item$2 == null) ? undefined : Caml_option.some(item$2)); +console.log("HtmlOptionsCollection.add w/before:", (item$3 == null) ? undefined : Caml_option.some(item$3)); console.log("collection length:", opts.length); -var item$3 = opts.selectedIndex; - -console.log("HtmlOptionsCollection.selectedIndex:", item$3); - var item$4 = opts.selectedIndex; console.log("HtmlOptionsCollection.selectedIndex:", item$4); +var item$5 = opts.selectedIndex; + +console.log("HtmlOptionsCollection.selectedIndex:", item$5); + opts.remove(0); console.log("collection length:", opts.length); +console.assert(opts.length === 2, "HtmlOptionsCollection.remove works"); + +opts.length = 0; + +select$1.length = 3; + +console.log("collection length:", select$1.length); + +console.assert(select$1.length === 3, "setLength works"); + +select$1[0] = null; + +console.assert(select$1.length === 2, "clearItem works"); + +console.log("collection length:", select$1.length); + +select$1[2] = Webapi__Dom__HtmlOptionElement.ofElement(createElement("option")); + +console.assert(select$1.length === 3, "setItem works"); + +console.log("collection length:", select$1.length); + +select$1.length = 0; + +var opt1$1 = createElement("option"); + +opt1$1.setAttribute("value", "1"); + +opt1$1.appendChild(createTextNode("opt1")); + +select$1.add(({ + NAME: "Option", + VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt1$1) + }).VAL, undefined); + +select$1.selectedIndex = 0; + +console.assert(select$1.selectedIndex === 0, "setSelectedIndex works"); + +console.log("collection length:", select$1.length); + +var opt2$1 = createElement("option"); + +opt2$1.setAttribute("value", "2"); + +opt2$1.appendChild(createTextNode("opt2")); + +var item$6 = select$1.item(0); + +var item$7 = (item$6 == null) ? undefined : Caml_option.some(item$6); + +console.assert(Belt_Option.isSome(item$7), "HtmlSelectElement.item should return an item"); + +console.log("HtmlSelectElement.item:", item$7); + +console.log("collection length:", select$1.length); + +console.assert(select$1.length === 1, "HtmlSelectElement.length should be 1"); + +select$1.add(({ + NAME: "Option", + VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt2$1) + }).VAL, 0); + +console.assert(select$1.length === 2, "HtmlSelectElement.length should be 2"); + +var item$8 = select$1.item(0); + +console.assert(Belt_Option.isSome((item$8 == null) ? undefined : Caml_option.some(item$8)), "HtmlSelectElement.add works"); + +console.log("collection length:", select$1.length); + +console.log("selected index", select$1.selectedIndex); + +select$1.selectedIndex = -1; + +console.assert(select$1.selectedIndex === -1, "HtmlSelectElement.clearSelectedIndex sets index to -1"); + +var opt3$1 = createElement("option"); + +opt3$1.setAttribute("value", "3"); + +opt3$1.appendChild(createTextNode("opt3")); + +select$1.add(({ + NAME: "Option", + VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt3$1) + }).VAL, ({ + NAME: "Element", + VAL: Webapi__Dom__HtmlElement.ofElement(opt2$1) + }).VAL); + +var item$9 = select$1.item(0); + +console.log("HtmlSelectElement.add w/before:", (item$9 == null) ? undefined : Caml_option.some(item$9)); + +console.log("collection length:", select$1.length); + +var item$10 = select$1.selectedIndex; + +console.log("HtmlSelectElement.selectedIndex:", item$10); + +var item$11 = select$1.selectedIndex; + +console.log("HtmlSelectElement.selectedIndex:", item$11); + +select$1.remove(0); + +console.log("collection length:", select$1.length); + +console.assert(select$1.length === 2, "HtmlSelectElement.remove works"); + function test_data(formElement) { - return new FormData(formElement).get("foo"); + return new FormData(formElement).get("username"); } -var formEl = form; +console.log(new FormData(form).get("username")); -var selectedIndex; +var formEl = form; exports.createElement = createElement; exports.createTextNode = createTextNode; @@ -286,15 +446,14 @@ exports.formEl = formEl; exports.body = body; exports.collection = collection; exports.len = len; -exports.el0 = el0$1; -exports.el1 = el1$1; -exports.radioNodeList = radioNodeList$1; +exports.el0 = el0$2; +exports.el1 = el1$2; +exports.radioNodeList = radioNodeList$2; exports.select = select$1; exports.opts = opts; -exports.opt1 = opt1; -exports.selectedIndex = selectedIndex; -exports.opt2 = opt2; -exports.opt3 = opt3; -exports.item = item$4; +exports.opt1 = opt1$1; +exports.opt2 = opt2$1; +exports.opt3 = opt3$1; +exports.item = item$11; exports.test_data = test_data; /* partial_arg Not a pure module */ diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.res b/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.res index dc4bbe6d..8c366942 100644 --- a/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.res +++ b/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.res @@ -19,29 +19,21 @@ type t_namedItem = | Select(Dom.htmlSelectElement) | TextArea(Dom.htmlTextAreaElement) -let isRadioNodeList: 'a => bool = %raw(` - function(x) { return x instanceof RadioNodeList; } -`) - -@send @return(nullable) external _namedItem: (t, string) => option<'a> = "namedItem" +@send @return(nullable) external _namedItem: (t, string) => option<'element> = "namedItem" let namedItem = (t, name): option => switch _namedItem(t, name) { - | Some(el) => - if Webapi__Dom__RadioNodeList.unsafeAsRadioNodeList(el)->isRadioNodeList { - el->Obj.magic->RadioNodeList->Some - } else { - switch Webapi__Dom__Element.tagName(el) { - // fixme: this should be a classify function in Webapi__Dom__Element? - | "BUTTON" => el->Obj.magic->Button->Some - | "FIELDSET" => el->Obj.magic->FieldSet->Some - | "INPUT" => el->Obj.magic->Input->Some - | "OBJECT" => el->Obj.magic->Object->Some - | "OUTPUT" => el->Obj.magic->Output->Some - | "SELECT" => el->Obj.magic->Select->Some - | "TEXTAREA" => el->Obj.magic->TextArea->Some - | _ => None - } + | Some(el) if Webapi__Dom__RadioNodeList.isRadioNodeList(el) => el->Obj.magic->RadioNodeList->Some + | Some(el) => switch Webapi__Dom__Element.tagName(el) { + // fixme: this should be a classify function in Webapi__Dom__Element? + | "BUTTON" => el->Obj.magic->Button->Some + | "FIELDSET" => el->Obj.magic->FieldSet->Some + | "INPUT" => el->Obj.magic->Input->Some + | "OBJECT" => el->Obj.magic->Object->Some + | "OUTPUT" => el->Obj.magic->Output->Some + | "SELECT" => el->Obj.magic->Select->Some + | "TEXTAREA" => el->Obj.magic->TextArea->Some + | _ => None } | None => None } diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.resi b/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.resi new file mode 100644 index 00000000..aa41f546 --- /dev/null +++ b/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.resi @@ -0,0 +1,17 @@ +type t = Dom.htmlFormControlsCollection + +@val @scope(("Array", "prototype", "slice")) external toArray: t => array = "call" +@get external length: t => int = "length" +@send @return(nullable) external item: (t, int) => option = "item" + +type t_namedItem = + | RadioNodeList(Dom.radioNodeList) + | Button(Dom.htmlButtonElement) + | FieldSet(Dom.htmlFieldSetElement) + | Input(Dom.htmlInputElement) + | Object(Dom.htmlObjectElement) + | Output(Dom.htmlOutputElement) + | Select(Dom.htmlSelectElement) + | TextArea(Dom.htmlTextAreaElement) + +let namedItem: (t, string) => option diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlOptionsCollection.res b/src/Webapi/Dom/Webapi__Dom__HtmlOptionsCollection.res index cfc910ba..448df361 100644 --- a/src/Webapi/Dom/Webapi__Dom__HtmlOptionsCollection.res +++ b/src/Webapi/Dom/Webapi__Dom__HtmlOptionsCollection.res @@ -8,7 +8,8 @@ type t = Dom.htmlOptionsCollection external toArray: t => array = "call" @send @return(nullable) external item: (t, int) => option = "item" -@send @return(nullable) external namedItem: (t, string) => option = "namedItem" +@send @return(nullable) +external namedItem: (t, string) => option = "namedItem" @get external length: t => int = "length" @set external setLength: (t, int) => unit = "length" @@ -19,7 +20,7 @@ external toArray: t => array = "call" @get external selectedIndex: t => int = "selectedIndex" @set external setSelectedIndex: (t, int) => unit = "selectedIndex" -@send external clearSelectedIndex: (t, @as(json`-1`) _) => unit = "selectedIndex" +@set external clearSelectedIndex: (t, @as(json`-1`) _) => unit = "selectedIndex" /** * This method will throw a "HierarchyRequestError" DOMException if element is an ancestor of the element into which it is to be inserted. diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlSelectElement.res b/src/Webapi/Dom/Webapi__Dom__HtmlSelectElement.res index ed0c3d05..2503d6a3 100644 --- a/src/Webapi/Dom/Webapi__Dom__HtmlSelectElement.res +++ b/src/Webapi/Dom/Webapi__Dom__HtmlSelectElement.res @@ -93,5 +93,5 @@ external add: ( @get external selectedIndex: t => int = "selectedIndex" @set external setSelectedIndex: (t, int) => unit = "selectedIndex" -@send +@set external clearSelectedIndex: (t, @as(json`-1`) _) => unit = "selectedIndex" diff --git a/src/Webapi/Dom/Webapi__Dom__RadioNodeList.res b/src/Webapi/Dom/Webapi__Dom__RadioNodeList.res index b3edecde..c5c18b22 100644 --- a/src/Webapi/Dom/Webapi__Dom__RadioNodeList.res +++ b/src/Webapi/Dom/Webapi__Dom__RadioNodeList.res @@ -16,3 +16,7 @@ type t = Dom.radioNodeList @get external value: t => string = "value" external unsafeAsRadioNodeList: 'a => t = "%identity" + +let isRadioNodeList: Dom.htmlElement => bool = %raw(` + function(x) { return x instanceof RadioNodeList; } +`) diff --git a/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.res b/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.res index 6e00fb45..1ea02ab2 100644 --- a/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.res +++ b/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.res @@ -2,6 +2,10 @@ open Webapi.Dom open Webapi.FormData open Webapi.Dom.HtmlFormElement +@val +@scope("console") +external jsAssert: (bool, string) => unit = "assert" + let createElement = Document.createElement(document) let createTextNode = Document.createTextNode(document) let createInput = () => createElement("input") @@ -17,6 +21,7 @@ let form = createElement("form")->ofElement->TestHelpers.unsafelyUnwrapOption let usernameInput = createInput() Element.setAttribute(usernameInput, "type", "text") Element.setAttribute(usernameInput, "name", "username") +Element.setAttribute(usernameInput, "value", "username") let usernameLabel = createLabelWithText("Username:") Element.appendChild(usernameLabel, usernameInput) @@ -24,6 +29,7 @@ Element.appendChild(usernameLabel, usernameInput) let passwordInput = createInput() Element.setAttribute(passwordInput, "type", "password") Element.setAttribute(passwordInput, "name", "password") +Element.setAttribute(passwordInput, "value", "password") let passwordLabel = createLabelWithText("Password:") Element.appendChild(passwordLabel, passwordInput) @@ -78,27 +84,35 @@ Element.appendChild(body, formEl) let collection = elements(form) Js.log2("HtmlFormElement.elements:", collection) +jsAssert(!Js.testAny(collection), "collection exists") let len = HtmlFormControlsCollection.length(collection) Js.log2("HtmlFormControlsCollection.length:", len) +jsAssert(len == 5, "initial length is 5") let el0 = HtmlFormControlsCollection.item(collection, 0) Js.log2("HtmlFormControlsCollection.item:", el0) +jsAssert(Belt.Option.isSome(el0), "item at index 0 exists") let el0 = HtmlFormControlsCollection.namedItem(collection, "username") Js.log2("HtmlFormControlsCollection.namedItem:", el0) +jsAssert(Belt.Option.isSome(el0), "namedItem 'username' exists") let el1 = HtmlFormControlsCollection.item(collection, 1) -Js.log2("HtmlFormControlsCollection.length:", el1) +Js.log2("HtmlFormControlsCollection.item:", el1) +jsAssert(Belt.Option.isSome(el1), "item at index 1 exists") let el1 = HtmlFormControlsCollection.namedItem(collection, "password") Js.log2("HtmlFormControlsCollection.namedItem:", el1) +jsAssert(Belt.Option.isSome(el1), "namedItem 'password' exists") let radioNodeList = HtmlFormControlsCollection.item(collection, 2) Js.log2("HtmlFormControlsCollection.namedItem:", radioNodeList) +jsAssert(Belt.Option.isSome(radioNodeList), "item at index 2 exists") let radioNodeList = HtmlFormControlsCollection.namedItem(collection, "radiogroup") Js.log2("HtmlFormControlsCollection.namedItem:", radioNodeList) +jsAssert(Belt.Option.isSome(radioNodeList), "namedItem 'radiogroup' exists") switch TestHelpers.unsafelyUnwrapOption(radioNodeList) { | Button(_) @@ -111,22 +125,27 @@ switch TestHelpers.unsafelyUnwrapOption(radioNodeList) { Js.Exn.raiseError("incorrect namedItem return value") | RadioNodeList(radioNodeList) => Js.log2("RadioNodeList.value", RadioNodeList.value(radioNodeList)) + jsAssert(RadioNodeList.value(radioNodeList) == "one", "RadioNodeList.value is one") } let select = HtmlSelectElement.ofElement(select)->TestHelpers.unsafelyUnwrapOption let opts = HtmlSelectElement.options(select) Js.log2("HtmlSelectElement.options:", opts) +jsAssert(!Js.testAny(opts), "HtmlSelectElement.options returns something") HtmlOptionsCollection.setLength(opts, 3) Js.log2("collection length:", HtmlOptionsCollection.length(opts)) +jsAssert(HtmlOptionsCollection.length(opts) == 3, "setLength works") HtmlOptionsCollection.clearItem(opts, 0) +jsAssert(HtmlOptionsCollection.length(opts) == 2, "clearItem works") Js.log2("collection length:", HtmlOptionsCollection.length(opts)) HtmlOptionsCollection.setItem( opts, 2, createElement("option")->HtmlOptionElement.ofElement->Belt.Option.getUnsafe, ) +jsAssert(HtmlOptionsCollection.length(opts) == 3, "setItem works") Js.log2("collection length:", HtmlOptionsCollection.length(opts)) HtmlOptionsCollection.setLength(opts, 0) @@ -135,33 +154,40 @@ let opt1 = createElement("option") Element.setAttribute(opt1, "value", "1") Element.appendChild(opt1, createTextNode("opt1")) -let _ = HtmlOptionsCollection.add( +HtmlOptionsCollection.add( opts, ~option=#Option(opt1->HtmlOptionElement.ofElement->Belt.Option.getUnsafe), + ~before=?None, ) -let selectedIndex = HtmlOptionsCollection.setSelectedIndex(opts, 0) +HtmlOptionsCollection.setSelectedIndex(opts, 0) +jsAssert(HtmlOptionsCollection.selectedIndex(opts) == 0, "setSelectedIndex works") Js.log2("collection length:", HtmlOptionsCollection.length(opts)) -Js.log2("HtmlOptionsCollection.setSelectedIndex", selectedIndex) let opt2 = createElement("option") Element.setAttribute(opt2, "value", "2") Element.appendChild(opt2, createTextNode("opt2")) let item = HtmlOptionsCollection.item(opts, 0) +jsAssert(Belt.Option.isSome(item), "HtmlOptionsCollection.item should return an item") Js.log2("HtmlOptionsCollection.item:", item) Js.log2("collection length:", HtmlOptionsCollection.length(opts)) +jsAssert(HtmlOptionsCollection.length(opts) == 1, "HtmlOptionsCollection.length should be 1") HtmlOptionsCollection.add( opts, ~option=#Option(opt2->HtmlOptionElement.ofElement->Belt.Option.getUnsafe), ~before=#Index(0), ) +jsAssert(HtmlOptionsCollection.length(opts) == 2, "HtmlOptionsCollection.length should be 2") let item = HtmlOptionsCollection.item(opts, 0) -Js.log2("HtmlOptionsCollection.add:", item) +jsAssert(Belt.Option.isSome(item), "HtmlOptionsCollection.add works") Js.log2("collection length:", HtmlOptionsCollection.length(opts)) Js.log2("selected index", HtmlOptionsCollection.selectedIndex(opts)) +HtmlOptionsCollection.clearSelectedIndex(opts) +jsAssert(HtmlOptionsCollection.selectedIndex(opts) == -1, "HtmlOptionsCollection.clearSelectedIndex sets index to -1") + let opt3 = createElement("option") Element.setAttribute(opt3, "value", "3") Element.appendChild(opt3, createTextNode("opt3")) @@ -172,7 +198,7 @@ HtmlOptionsCollection.add( ~before=#Element(opt2->HtmlElement.ofElement->Belt.Option.getUnsafe), ) let item = HtmlOptionsCollection.item(opts, 0) -Js.log2("HtmlOptionsCollection.addBeforeElement:", item) +Js.log2("HtmlOptionsCollection.add w/before:", item) Js.log2("collection length:", HtmlOptionsCollection.length(opts)) let item = HtmlOptionsCollection.selectedIndex(opts) @@ -183,5 +209,87 @@ Js.log2("HtmlOptionsCollection.selectedIndex:", item) HtmlOptionsCollection.remove(opts, 0) Js.log2("collection length:", HtmlOptionsCollection.length(opts)) +jsAssert(HtmlOptionsCollection.length(opts) == 2, "HtmlOptionsCollection.remove works") + +HtmlOptionsCollection.setLength(opts, 0) + +HtmlSelectElement.setLength(select, 3) +Js.log2("collection length:", HtmlSelectElement.length(select)) +jsAssert(HtmlSelectElement.length(select) == 3, "setLength works") +HtmlSelectElement.clearItem(select, 0) +jsAssert(HtmlSelectElement.length(select) == 2, "clearItem works") +Js.log2("collection length:", HtmlSelectElement.length(select)) +HtmlSelectElement.setItem( + select, + 2, + createElement("option")->HtmlOptionElement.ofElement->Belt.Option.getUnsafe, +) +jsAssert(HtmlSelectElement.length(select) == 3, "setItem works") +Js.log2("collection length:", HtmlSelectElement.length(select)) + +HtmlSelectElement.setLength(select, 0) + +let opt1 = createElement("option") +Element.setAttribute(opt1, "value", "1") +Element.appendChild(opt1, createTextNode("opt1")) + +HtmlSelectElement.add( + select, + ~option=#Option(opt1->HtmlOptionElement.ofElement->Belt.Option.getUnsafe), + ~before=?None, +) +HtmlSelectElement.setSelectedIndex(select, 0) +jsAssert(HtmlSelectElement.selectedIndex(select) == 0, "setSelectedIndex works") +Js.log2("collection length:", HtmlSelectElement.length(select)) + +let opt2 = createElement("option") +Element.setAttribute(opt2, "value", "2") +Element.appendChild(opt2, createTextNode("opt2")) + +let item = HtmlSelectElement.item(select, 0) +jsAssert(Belt.Option.isSome(item), "HtmlSelectElement.item should return an item") +Js.log2("HtmlSelectElement.item:", item) +Js.log2("collection length:", HtmlSelectElement.length(select)) +jsAssert(HtmlSelectElement.length(select) == 1, "HtmlSelectElement.length should be 1") + +HtmlSelectElement.add( + select, + ~option=#Option(opt2->HtmlOptionElement.ofElement->Belt.Option.getUnsafe), + ~before=#Index(0), +) +jsAssert(HtmlSelectElement.length(select) == 2, "HtmlSelectElement.length should be 2") + +let item = HtmlSelectElement.item(select, 0) +jsAssert(Belt.Option.isSome(item), "HtmlSelectElement.add works") +Js.log2("collection length:", HtmlSelectElement.length(select)) +Js.log2("selected index", HtmlSelectElement.selectedIndex(select)) + +HtmlSelectElement.clearSelectedIndex(select) +jsAssert(HtmlSelectElement.selectedIndex(select) == -1, "HtmlSelectElement.clearSelectedIndex sets index to -1") + +let opt3 = createElement("option") +Element.setAttribute(opt3, "value", "3") +Element.appendChild(opt3, createTextNode("opt3")) + +HtmlSelectElement.add( + select, + ~option=#Option(opt3->HtmlOptionElement.ofElement->Belt.Option.getUnsafe), + ~before=#Element(opt2->HtmlElement.ofElement->Belt.Option.getUnsafe), +) +let item = HtmlSelectElement.item(select, 0) +Js.log2("HtmlSelectElement.add w/before:", item) +Js.log2("collection length:", HtmlSelectElement.length(select)) + +let item = HtmlSelectElement.selectedIndex(select) +Js.log2("HtmlSelectElement.selectedIndex:", item) + +let item = HtmlSelectElement.selectedIndex(select) +Js.log2("HtmlSelectElement.selectedIndex:", item) + +HtmlSelectElement.removeIndex(select, 0) +Js.log2("collection length:", HtmlSelectElement.length(select)) +jsAssert(HtmlSelectElement.length(select) == 2, "HtmlSelectElement.remove works") + +let test_data = formElement => formElement->data->get("username") -let test_data = formElement => formElement->data->get("foo") +Js.log(test_data(form)) From 9a137fd0bec1dc8aad83fb4e47264a8ebdca4ee2 Mon Sep 17 00:00:00 2001 From: rob Date: Tue, 28 Dec 2021 17:33:19 -0500 Subject: [PATCH 3/4] refactor: refine namedItem implementation --- ...ebapi__Dom__HtmlFormControlsCollection.res | 45 +++++++++++++------ src/Webapi/Dom/Webapi__Dom__RadioNodeList.res | 4 -- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.res b/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.res index 8c366942..ff3471d1 100644 --- a/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.res +++ b/src/Webapi/Dom/Webapi__Dom__HtmlFormControlsCollection.res @@ -19,21 +19,40 @@ type t_namedItem = | Select(Dom.htmlSelectElement) | TextArea(Dom.htmlTextAreaElement) -@send @return(nullable) external _namedItem: (t, string) => option<'element> = "namedItem" +type elementOrRadioNodeList -let namedItem = (t, name): option => +@send @return(nullable) +external _namedItem: (t, string) => option = "namedItem" + +let testRadioNodeList: elementOrRadioNodeList => option = %raw(` + function(el) { + if ((el.constructor.name != null && el.constructor.name === "RadioNodeList") + || /^\[object RadioNodeList\]$/.test(el.constructor.toString())) { + return el; + } + } +`) + +external castElement: elementOrRadioNodeList => Dom.element = "%identity" + +let namedItem = (t, name): option => { switch _namedItem(t, name) { - | Some(el) if Webapi__Dom__RadioNodeList.isRadioNodeList(el) => el->Obj.magic->RadioNodeList->Some - | Some(el) => switch Webapi__Dom__Element.tagName(el) { - // fixme: this should be a classify function in Webapi__Dom__Element? - | "BUTTON" => el->Obj.magic->Button->Some - | "FIELDSET" => el->Obj.magic->FieldSet->Some - | "INPUT" => el->Obj.magic->Input->Some - | "OBJECT" => el->Obj.magic->Object->Some - | "OUTPUT" => el->Obj.magic->Output->Some - | "SELECT" => el->Obj.magic->Select->Some - | "TEXTAREA" => el->Obj.magic->TextArea->Some - | _ => None + | Some(value) => + switch testRadioNodeList(value) { + | Some(radioNodeList) => radioNodeList->RadioNodeList->Some + | _ => + switch value->castElement->Webapi__Dom__Element.tagName->Js.String2.toUpperCase { + // non-html documents may return in different casing + | "BUTTON" => value->Obj.magic->Button->Some + | "FIELDSET" => value->Obj.magic->FieldSet->Some + | "INPUT" => value->Obj.magic->Input->Some + | "OBJECT" => value->Obj.magic->Object->Some + | "OUTPUT" => value->Obj.magic->Output->Some + | "SELECT" => value->Obj.magic->Select->Some + | "TEXTAREA" => value->Obj.magic->TextArea->Some + | _ => None + } } | None => None } +} diff --git a/src/Webapi/Dom/Webapi__Dom__RadioNodeList.res b/src/Webapi/Dom/Webapi__Dom__RadioNodeList.res index c5c18b22..b3edecde 100644 --- a/src/Webapi/Dom/Webapi__Dom__RadioNodeList.res +++ b/src/Webapi/Dom/Webapi__Dom__RadioNodeList.res @@ -16,7 +16,3 @@ type t = Dom.radioNodeList @get external value: t => string = "value" external unsafeAsRadioNodeList: 'a => t = "%identity" - -let isRadioNodeList: Dom.htmlElement => bool = %raw(` - function(x) { return x instanceof RadioNodeList; } -`) From 25147fa3b6a7f3b79b23cf76652ff2ea4a28b357 Mon Sep 17 00:00:00 2001 From: rob Date: Tue, 28 Dec 2021 17:33:51 -0500 Subject: [PATCH 4/4] refactor: remove unnecessary ofElement overrides --- .../Dom/Webapi__Dom__HtmlFormElement__test.js | 35 ++++++++++--------- .../Dom/Webapi__Dom__HtmlFormElement.res | 9 ----- .../Dom/Webapi__Dom__HtmlOptionElement.res | 6 ---- .../Webapi__Dom__HtmlFormElement__test.res | 5 ++- 4 files changed, 20 insertions(+), 35 deletions(-) diff --git a/lib/js/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.js b/lib/js/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.js index 63e0d4ff..7ce30006 100644 --- a/lib/js/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.js +++ b/lib/js/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.js @@ -1,5 +1,6 @@ 'use strict'; +var Curry = require("rescript/lib/js/curry.js"); var Js_exn = require("rescript/lib/js/js_exn.js"); var Belt_Option = require("rescript/lib/js/belt_Option.js"); var Caml_option = require("rescript/lib/js/caml_option.js"); @@ -34,7 +35,9 @@ function createLabelWithText(text) { return el; } -var form = TestHelpers.unsafelyUnwrapOption(Webapi__Dom__HtmlFormElement.ofElement(createElement("form"))); +var formEl = createElement("form"); + +var form = TestHelpers.unsafelyUnwrapOption(Curry._1(Webapi__Dom__HtmlFormElement.ofElement, formEl)); var usernameInput = createElement("input"); @@ -112,19 +115,19 @@ radioContainer.appendChild(radioLabel2); selectContainer.appendChild(selectLabel); -form.appendChild(usernameContainer); +formEl.appendChild(usernameContainer); -form.appendChild(passwordContainer); +formEl.appendChild(passwordContainer); -form.appendChild(radioContainer); +formEl.appendChild(radioContainer); -form.appendChild(selectContainer); +formEl.appendChild(selectContainer); var body = TestHelpers.unsafelyUnwrapOption(Belt_Option.flatMap(Webapi__Dom__Document.asHtmlDocument(document), (function (prim) { return Caml_option.nullable_to_opt(prim.body); }))); -body.appendChild(form); +body.appendChild(formEl); var collection = form.elements; @@ -210,7 +213,7 @@ console.assert(opts.length === 2, "clearItem works"); console.log("collection length:", opts.length); -opts[2] = Webapi__Dom__HtmlOptionElement.ofElement(createElement("option")); +opts[2] = Curry._1(Webapi__Dom__HtmlOptionElement.ofElement, createElement("option")); console.assert(opts.length === 3, "setItem works"); @@ -226,7 +229,7 @@ opt1.appendChild(createTextNode("opt1")); opts.add(({ NAME: "Option", - VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt1) + VAL: Curry._1(Webapi__Dom__HtmlOptionElement.ofElement, opt1) }).VAL, undefined); opts.selectedIndex = 0; @@ -255,7 +258,7 @@ console.assert(opts.length === 1, "HtmlOptionsCollection.length should be 1"); opts.add(({ NAME: "Option", - VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt2) + VAL: Curry._1(Webapi__Dom__HtmlOptionElement.ofElement, opt2) }).VAL, 0); console.assert(opts.length === 2, "HtmlOptionsCollection.length should be 2"); @@ -280,7 +283,7 @@ opt3.appendChild(createTextNode("opt3")); opts.add(({ NAME: "Option", - VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt3) + VAL: Curry._1(Webapi__Dom__HtmlOptionElement.ofElement, opt3) }).VAL, ({ NAME: "Element", VAL: Webapi__Dom__HtmlElement.ofElement(opt2) @@ -320,7 +323,7 @@ console.assert(select$1.length === 2, "clearItem works"); console.log("collection length:", select$1.length); -select$1[2] = Webapi__Dom__HtmlOptionElement.ofElement(createElement("option")); +select$1[2] = Curry._1(Webapi__Dom__HtmlOptionElement.ofElement, createElement("option")); console.assert(select$1.length === 3, "setItem works"); @@ -336,7 +339,7 @@ opt1$1.appendChild(createTextNode("opt1")); select$1.add(({ NAME: "Option", - VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt1$1) + VAL: Curry._1(Webapi__Dom__HtmlOptionElement.ofElement, opt1$1) }).VAL, undefined); select$1.selectedIndex = 0; @@ -365,7 +368,7 @@ console.assert(select$1.length === 1, "HtmlSelectElement.length should be 1"); select$1.add(({ NAME: "Option", - VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt2$1) + VAL: Curry._1(Webapi__Dom__HtmlOptionElement.ofElement, opt2$1) }).VAL, 0); console.assert(select$1.length === 2, "HtmlSelectElement.length should be 2"); @@ -390,7 +393,7 @@ opt3$1.appendChild(createTextNode("opt3")); select$1.add(({ NAME: "Option", - VAL: Webapi__Dom__HtmlOptionElement.ofElement(opt3$1) + VAL: Curry._1(Webapi__Dom__HtmlOptionElement.ofElement, opt3$1) }).VAL, ({ NAME: "Element", VAL: Webapi__Dom__HtmlElement.ofElement(opt2$1) @@ -422,12 +425,11 @@ function test_data(formElement) { console.log(new FormData(form).get("username")); -var formEl = form; - exports.createElement = createElement; exports.createTextNode = createTextNode; exports.createInput = createInput; exports.createLabelWithText = createLabelWithText; +exports.formEl = formEl; exports.form = form; exports.usernameInput = usernameInput; exports.usernameLabel = usernameLabel; @@ -442,7 +444,6 @@ exports.usernameContainer = usernameContainer; exports.passwordContainer = passwordContainer; exports.radioContainer = radioContainer; exports.selectContainer = selectContainer; -exports.formEl = formEl; exports.body = body; exports.collection = collection; exports.len = len; diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlFormElement.res b/src/Webapi/Dom/Webapi__Dom__HtmlFormElement.res index f9a36848..48be0ed4 100644 --- a/src/Webapi/Dom/Webapi__Dom__HtmlFormElement.res +++ b/src/Webapi/Dom/Webapi__Dom__HtmlFormElement.res @@ -10,15 +10,6 @@ module Impl = ( ) => { type t_htmlFormElement = T.t - external unsafeAsFormElement: Dom.element => t_htmlFormElement = "%identity" - external asElement: t_htmlFormElement => Dom.element = "%identity" - - let ofElement = (el): option => - switch Webapi__Dom__Element.tagName(el)->Js.String2.toUpperCase { - | "FORM" => el->unsafeAsFormElement->Some - | _ => None - } - @get external elements: t_htmlFormElement => Dom.htmlFormControlsCollection = "elements" @get external length: t_htmlFormElement => int = "length" @get external name: t_htmlFormElement => string = "name" diff --git a/src/Webapi/Dom/Webapi__Dom__HtmlOptionElement.res b/src/Webapi/Dom/Webapi__Dom__HtmlOptionElement.res index 6bf9ef63..eb1c6baa 100644 --- a/src/Webapi/Dom/Webapi__Dom__HtmlOptionElement.res +++ b/src/Webapi/Dom/Webapi__Dom__HtmlOptionElement.res @@ -16,12 +16,6 @@ include Webapi__Dom__HtmlElement.Impl({ type t = t }) -let ofElement = (el): option => - switch Webapi__Dom__Element.tagName(el)->Js.String2.toUpperCase { - | "OPTION" => el->Obj.magic->Some - | _ => None - } - // common form element attributes @return(nullable) @get external form: t => option = "form" diff --git a/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.res b/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.res index 1ea02ab2..6d544b88 100644 --- a/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.res +++ b/tests/Webapi/Dom/Webapi__Dom__HtmlFormElement__test.res @@ -16,7 +16,8 @@ let createLabelWithText = text => { el } -let form = createElement("form")->ofElement->TestHelpers.unsafelyUnwrapOption +let formEl = createElement("form") +let form = formEl->ofElement->TestHelpers.unsafelyUnwrapOption let usernameInput = createInput() Element.setAttribute(usernameInput, "type", "text") @@ -62,8 +63,6 @@ let passwordContainer = createElement("div") let radioContainer = createElement("div") let selectContainer = createElement("div") -let formEl = form->asElement - Element.appendChild(usernameContainer, usernameLabel) Element.appendChild(passwordContainer, passwordLabel) Element.appendChild(radioContainer, radioLabel1)