作るもの
ReScriptでDOMを扱います。
サンプルとして、以下を作成します。
See the Pen 漱石先生クイズ by sgigagaeru (@sgigagaeru) on CodePen.
EventListenerの登録
まずはEventListerを登録して、以下のような動きをするところまで作ります。
See the Pen 漱石先生クイズ#2 by sgigagaeru (@sgigagaeru) on CodePen.
Global JS Valueであるdocumentとwindowをバインドします。1
Global Valueなので@valが付きます。
まずは型にこだわらず多層型の`aでバインドします。
@val external doc: 'a = "document"
@val external win: 'a = "window"
doc["getElementById"]("btn")["addEventListener"]("click", () => {
win["alert"]("吾輩は猫である。名前はまだ無い。")
}, false);
これでは型付き言語の良さが全く活きないではないかということで、
@val external document: Dom.document = "document"
@val external windows: Dom.window = "window"
としたいところです。
しかしすでにdocumentはDom.document型に、windowはDom.window型にバインドされているので23、そのまま使います。
docment["getElementById"]("btn")["addEventListener"]("click", () => {
window["alert"]("吾輩は猫である。名前はまだ無い。")
}, false);
Object Methodの登録
getElementById、addEventLister、alertをバインドします。4
Object Methodなので@sendが付きます。
@send external getElementById: (Dom.document, string) => Dom.element = "getElementById"
@send external addEventListener: (Dom.element, string, () => unit, bool) => unit = "addEventListener"
@send external alert: (Dom.window, string) => unit = "alert"
登録したObject Methodを用いて書くと、こうなります。
@send external getElementById: (Dom.document, string) => Dom.element = "getElementById"
@send external addEventListener: (Dom.element, string, () => unit, bool) => unit = "addEventListener"
@send external alert: (Dom.window, string) => unit = "alert"
addEventListener(getElementById(document, "btn"), "click", () => {
alert(window, "吾輩は猫である。名前はまだ無い。")
}, false);
@send external getElementById: (Dom.document, string) => Dom.element = "getElementById"
@send external addEventListener: (Dom.element, string, () => unit, bool) => unit = "addEventListener"
@send external alert: (Dom.window, string) => unit = "alert"
document->getElementById("btn")->addEventListener("click", () => {
window->alert("吾輩は猫である。名前はまだ無い。")
}, false);
としておきます。これも動きます。
Form情報の取得
getElementsByNameを用いてinputの情報をとっていきます。
@send external getElementsByName: (Dom.document, string) => Dom.nodeList = "getElementsByName"
@send external item: (Dom.nodeList, int) => Dom.element = "item"
@get external getValue: Dom.element => string = "value"
getValueはオブジェクトのプロパティvalueの値を取得しているので@getを付けます。6
document->getElementById("btn")->addEventListener("click", () => {
window->alert(document->getElementsByName("Q1")->item(0)->getValue)
}, false)
alertで表示させる表示を変えました。これも動きます。
getElementsByNameはDom.nodeListを返します。Dom.nodeListは配列でもリストでもないので、これをリスト化するgetlistedNodeListを定義しました。
その際にgetLengthをバインドしています。
@get external getLength: Dom.nodeList => int = "length"
正解の選択肢にはdata-correct-choice="true"を設定しています。
この属性が設定されたinputタグのvalueを取得するgetCorrectAnswerを定義します。
data-*属性を取得するのはdatasetでdomStringMapを返します。そのdomStringMapのcorrectChoice属性にdata-correct-choiceの値が入っています。7
バインドします。
@get external getDataset: Dom.element => Dom.domStringMap = "dataset"
@get external getCorrectChoice: Dom.domStringMap => string = "correctChoice"
選択肢が選ばれているかは、checkedを調べれば分かります。
@get external getChecked: Dom.element => bool = "checked"
これを用いてgetSelectedAnswerを定義しました。
だいたい動くバージョンができました。
DOM情報の書き換え
あとはinnerHTMLを書き換えて、ページを書き換えます。
innerHTMLへのアクセスにはsetter4を用います。
@get external getBody: Dom.document => Dom.element = "body"
@set external setInnerHTML: (Dom.element, string) => unit = "innerHTML"
moduleを使った方が良いのかも
いまバインドのところは
@send external getElementById: (Dom.document, string) => Dom.element = "getElementById"
@send external addEventListener: (Dom.element, string, () => unit, bool) => unit = "addEventListener"
@send external alert: (Dom.window, string) => unit = "alert"
@send external getElementsByName: (Dom.document, string) => Dom.nodeList = "getElementsByName"
@send external item: (Dom.nodeList, int) => Dom.element = "item"
@get external getValue: Dom.element => string = "value"
@get external getLength: Dom.nodeList => int = "length"
@get external getDataset: Dom.element => Dom.domStringMap = "dataset"
@get external getCorrectChoice: Dom.domStringMap => string = "correctChoice"
@get external getChecked: Dom.element => bool = "checked"
@get external getBody: Dom.document => Dom.element = "body"
@set external setInnerHTML: (Dom.element, string) => unit = "innerHTML"
となっています。さすがにこれは、見通しが良くないので、
module Document = {
@send external getElementById: (Dom.document, string) => Dom.element = "getElementById"
@send external getElementsByName: (Dom.document, string) => Dom.nodeList = "getElementsByName"
@get external getBody: Dom.document => Dom.element = "body"
}
module Element = {
@send external addEventListener: (Dom.element, string, () => unit, bool) => unit = "addEventListener"
@get external getDataset: Dom.element => Dom.domStringMap = "dataset"
@get external getValue: Dom.element => string = "value"
@get external getChecked: Dom.element => bool = "checked"
@set external setInnerHTML: (Dom.element, string) => unit = "innerHTML"
}
module NodeList = {
@get external getLength: Dom.nodeList => int = "length"
@send external item: (Dom.nodeList, int) => Dom.element = "item"
}
module DomStringMap = {
@get external getCorrectChoice: Dom.domStringMap => string = "correctChoice"
}
と、moduleで分けるのかなと思いました。
その他、関数をまとめたりする変更を加えて、完成版としました。
ReScriptファイルはモジュールです
ReScriptでは一つのファイルは一つのモジュールとして扱われます。
そのため生成されるJSファイルにはexport(esmoduleの場合)かexports(commonjsの場合)が書かれます。
ブラウザに読み込ませる場合はesmoduleにする必要があり、モジュールとして扱われるのでscriptタグでtype="module"の設定が必要です。
そしてモジュールをローカルで読み込むとCORSエラーになるので、試験時はローカルのWebサーバ経由でアクセスする必要があります。
-
https://rescript-lang.org/docs/manual/latest/bind-to-global-js-values ↩
-
https://rescript-lang.org/docs/manual/latest/api/core#value-document ↩
-
https://rescript-lang.org/docs/manual/latest/api/core#value-window ↩
-
https://rescript-lang.org/docs/manual/latest/bind-to-js-function#object-method ↩ ↩2
-
https://rescript-lang.org/docs/manual/latest/bind-to-js-object#bind-using-special-getter-and-setter-attributes ↩
-
https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/dataset ↩