├── .gitignore
├── .ocamlformat
├── LICENSE
├── README.md
├── bsconfig.json
├── package.json
├── src
└── ppx_tea_jsx.ml
└── test
├── test.ml
└── test.re
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | /node_modules
4 | *.cmi
5 | *.cmj
6 | *.cmt
7 | /lib
8 | *.js
9 | .merlin
10 |
--------------------------------------------------------------------------------
/.ocamlformat:
--------------------------------------------------------------------------------
1 | profile = sparse
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Ozan Sener
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ppx-tea-jsx [![Version 0.6.0][npm-img]][npm]
2 |
3 | [npm-img]: https://img.shields.io/npm/v/ppx-tea-jsx.svg
4 | [npm]: https://www.npmjs.com/package/ppx-tea-jsx
5 |
6 | [Reason](https://reasonml.github.io) JSX syntax support for
7 | [BuckleScript-TEA](https://github.com/OvermindDL1/bucklescript-tea) library.
8 |
9 | ## Installation
10 |
11 | 1. Install via npm or Yarn:
12 |
13 | ```sh
14 | yarn add --dev ppx-tea-jsx
15 | ```
16 |
17 | 2. Add the PPX to your `bsconfig.json`:
18 |
19 | ```json
20 | {
21 | "ppx-flags": ["ppx-tea-jsx/ppx"]
22 | }
23 | ```
24 |
25 | ## Usage
26 |
27 | You can pass JSX expressions to any function that expects `Vdom.t`.
28 |
29 | ### HTML elements
30 |
31 | ```reason
32 | let view = _model =>
33 |
34 |
35 |
;
36 | ```
37 |
38 | ```reason
39 |
40 |
41 | "Sorry, your browser doesn't support embedded videos."
42 |
43 | ```
44 |
45 | #### Literals
46 |
47 | ```reason
48 | "Addition: " 5 '+' 25.5
49 | ```
50 |
51 | #### Punning
52 |
53 | ```reason
54 | let id = "fifty";
55 | let className = None;
56 |
57 | let _ =
;
58 | ```
59 |
60 | #### Children spread
61 |
62 | ```reason
63 | let buttons_list_fn = () => [ , , ];
64 |
65 | let buttons_fragment = <> >;
66 |
67 | let view = _model =>
68 |
69 | ...{buttons_list_fn()}
70 | ...buttons_fragment
71 |
;
72 | ```
73 |
74 | #### Events
75 |
76 | You can trigger messages:
77 |
78 | ```reason
79 |
80 | ```
81 |
82 | And return `option(msg)` from callbacks:
83 |
84 | ```reason
85 | {
89 | e##preventDefault();
90 | Some(Reset);
91 | }
92 | }
93 | />
94 | ```
95 |
96 | ```reason
97 | Set(int_of_string(value))} />
98 | ```
99 |
100 | ```reason
101 | let view = model => {
102 | let onChangeOpt = value =>
103 | try (value->int_of_string->Set->Some) {
104 | | _ => None
105 | };
106 | ;
107 | };
108 | ```
109 |
110 | ### Modules (Capitalized Tag)
111 |
112 | ` "Hi" ` is mapped to `Foo.view(~bar="baz", [text("Hi")])`.
113 |
114 | ```reason
115 | type person = {
116 | name: string,
117 | age: option(int),
118 | };
119 |
120 | module OnlyChild = {
121 | let view = children => children
;
122 | };
123 |
124 | module Person = {
125 | let view = (~className=?, ~name, ~age=?, children) => {
126 | open Tea.Html;
127 | let age =
128 | age
129 | ->Belt.Option.mapWithDefault(noNode, x =>
130 | " (" x->string_of_int->text ")"
131 | );
132 |
133 | ...children {text(name)} age
;
134 | };
135 | };
136 |
137 | let view = model => {
138 | let img =
139 | ;
143 |
144 |
145 |
...img
146 |
147 | "Hi "
148 |
149 |
;
150 | };
151 | ```
152 |
--------------------------------------------------------------------------------
/bsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ppx-tea-jsx",
3 | "refmt": 3,
4 | "sources": [
5 | { "dir": "src", "type": "ppx" },
6 | { "dir": "test", "type": "dev", "subdirs": true, "ppx": ["Ppx_tea_jsx"] }
7 | ],
8 | "ocaml-dependencies": ["compiler-libs"],
9 | "entries": [
10 | {
11 | "backend": "native",
12 | "type": "ppx",
13 | "main-module": "Ppx_tea_jsx"
14 | }
15 | ],
16 | "public": ["Ppx_tea_jsx"],
17 | "warnings": {
18 | "number": "+R",
19 | "error": "+5+101+8+R"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ppx-tea-jsx",
3 | "version": "0.2.1",
4 | "description": "Reason JSX syntax for bucklescript-tea",
5 | "license": "MIT",
6 | "repository": "osener/ppx-tea-jsx",
7 | "keywords": [
8 | "bucklescript",
9 | "bucklescript-tea",
10 | "elm",
11 | "reason",
12 | "reasonml",
13 | "jsx"
14 | ],
15 | "author": {
16 | "name": "Ozan Sener",
17 | "email": "ozan@ozansener.com",
18 | "url": "github.com/osener"
19 | },
20 | "dependencies": {
21 | "bsb-native": "^4.0.6"
22 | },
23 | "scripts": {
24 | "build": "bsb -make-world -backend native",
25 | "clean": "bsb -clean-world && rm ppx",
26 | "postinstall": "bsb -make-world -backend native && cp lib/bs/native/ppx_tea_jsx.native ppx"
27 | },
28 | "files": [
29 | "src/ppx_tea_jsx.ml",
30 | " test/test.re",
31 | "bsconfig.json"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/src/ppx_tea_jsx.ml:
--------------------------------------------------------------------------------
1 | open Ast_mapper
2 | open Ast_helper
3 | open Asttypes
4 | open Parsetree
5 | open Longident
6 |
7 | let str_const ~pexp_loc s =
8 | { pexp_desc = Pexp_constant (Const_string (s, None))
9 | ; pexp_loc
10 | ; pexp_attributes = [] }
11 |
12 |
13 | let filter_map f l =
14 | let rec recurse acc l =
15 | match l with
16 | | [] ->
17 | List.rev acc
18 | | x :: l' ->
19 | let acc' = match f x with None -> acc | Some y -> y :: acc in
20 | recurse acc' l'
21 | in
22 | recurse [] l
23 |
24 |
25 | let rec classify = function
26 | (**************
27 | * Attributes *
28 | **************)
29 | | ( "accept"
30 | | "challenge"
31 | | "charset"
32 | | "content"
33 | | "contextmenu"
34 | | "datetime"
35 | | "draggable"
36 | | "enctype"
37 | | "form"
38 | | "formaction"
39 | | "href"
40 | | "itemprop"
41 | | "list"
42 | | "manifest"
43 | | "max"
44 | | "media"
45 | | "min"
46 | | "pubdate"
47 | | "rel"
48 | | "src"
49 | | "step"
50 | | "title" ) as name ->
51 | (* attribute("href", value) *)
52 | Some (`Attribute name)
53 | (* aliases *)
54 | | "acceptCharset" ->
55 | Some (`Attribute "accept-charset")
56 | | ("disabled" | "selected") as name ->
57 | (* if (value) { attribute("readOnly", "true") } else { noProp } *)
58 | Some (`Bool_attribute name)
59 | | ( "cols"
60 | | "colspan"
61 | | "height"
62 | | "maxlength"
63 | | "minlength"
64 | | "rows"
65 | | "rowspan"
66 | | "size"
67 | | "tabindex"
68 | | "width" ) as name ->
69 | (* attribute("cols", string_of_int(value)) *)
70 | Some (`Int_attribute name)
71 | (**************
72 | * Properties *
73 | **************)
74 | | ( "action"
75 | | "align"
76 | | "alt"
77 | | "cite"
78 | | "className"
79 | | "coords"
80 | | "defaultValue"
81 | | "dir"
82 | | "dropzone"
83 | | "headers"
84 | | "hreflang"
85 | | "htmlFor"
86 | | "id"
87 | | "keytype"
88 | | "kind"
89 | | "lang"
90 | | "language"
91 | | "name"
92 | | "pattern"
93 | | "ping"
94 | | "placeholder"
95 | | "poster"
96 | | "preload"
97 | | "sandbox"
98 | | "scope"
99 | | "scoped"
100 | | "shape"
101 | | "srcdoc"
102 | | "srclang"
103 | | "target"
104 | | "usemap"
105 | | "value"
106 | | "wrap" ) as name ->
107 | (* prop("className", value) *)
108 | Some (`Property name)
109 | (* aliases *)
110 | | "class'" ->
111 | Some (`Property "className")
112 | | "downloadAs" ->
113 | Some (`Property "download")
114 | | "httpEquiv" ->
115 | Some (`Property "http-equiv")
116 | | "method'" | "method_" ->
117 | Some (`Property "method")
118 | | "type'" | "type_" ->
119 | Some (`Property "type")
120 | | "start" ->
121 | (* prop("start", string_of_int(value)) *)
122 | Some (`Int_property "start")
123 | | "autocomplete" ->
124 | (* prop("autocomplete", if (value) { "on" } else { "off" }) *)
125 | Some (`On_off_property "autocomplete")
126 | | "accesskey" ->
127 | (* prop("accesskey", String.mk(1, value)) *)
128 | Some (`Char_property "accesskey")
129 | | ( "async"
130 | | "autofocus"
131 | | "autoplay"
132 | | "checked"
133 | | "contenteditable"
134 | | "controls"
135 | | "default"
136 | | "defer"
137 | | "hidden"
138 | | "ismap"
139 | | "loop"
140 | | "multiple"
141 | | "muted"
142 | | "novalidate"
143 | | "readonly"
144 | | "required"
145 | | "reversed"
146 | | "seamless"
147 | | "spellcheck" ) as name ->
148 | (* if (value) { prop("readOnly", "readOnly") } else { noProp } *)
149 | Some (`Bool_property name)
150 | | "download" ->
151 | (* if (value) { prop("download", "") } else { noProp } *)
152 | Some (`Empty_bool_property "download")
153 | (**********
154 | * Events *
155 | **********)
156 | | ( "onAbort"
157 | | "onAnimationEnd"
158 | | "onAnimationIteration"
159 | | "onAnimationStart"
160 | | "onBlur"
161 | | "onCanPlay"
162 | | "onCanPlayThrough"
163 | | "onClick"
164 | | "onContextMenu"
165 | | "onDblClick"
166 | | "onDrag"
167 | | "onDragEnd"
168 | | "onDragEnter"
169 | | "onDragExit"
170 | | "onDragLeave"
171 | | "onDragOver"
172 | | "onDragStart"
173 | | "onDrop"
174 | | "onDurationChange"
175 | | "onEmptied"
176 | | "onEncrypted"
177 | | "onEnded"
178 | | "onError"
179 | | "onFocus"
180 | | "onGotPointerCapture"
181 | | "onInvalid"
182 | | "onKeyDown"
183 | | "onKeyPress"
184 | | "onKeyUp"
185 | | "onLoad"
186 | | "onLoadStart"
187 | | "onLoadedData"
188 | | "onLoadedMetadata"
189 | | "onLostPointerCapture"
190 | | "onMouseDown"
191 | | "onMouseEnter"
192 | | "onMouseLeave"
193 | | "onMouseMove"
194 | | "onMouseOut"
195 | | "onMouseOver"
196 | | "onMouseUp"
197 | | "onPause"
198 | | "onPlay"
199 | | "onPlaying"
200 | | "onPointerCancel"
201 | | "onPointerDown"
202 | | "onPointerEnter"
203 | | "onPointerLeave"
204 | | "onPointerMove"
205 | | "onPointerOut"
206 | | "onPointerOver"
207 | | "onPointerUp"
208 | | "onProgress"
209 | | "onRateChange"
210 | | "onScroll"
211 | | "onSeeked"
212 | | "onSeeking"
213 | | "onSelect"
214 | | "onStalled"
215 | | "onSuspend"
216 | | "onTimeUpdate"
217 | | "onToggle"
218 | | "onTouchCancel"
219 | | "onTouchEnd"
220 | | "onTouchMove"
221 | | "onTouchStart"
222 | | "onTransitionEnd"
223 | | "onVolumeChange"
224 | | "onWaiting"
225 | | "onWheel" ) as event ->
226 | let event =
227 | String.sub event 2 (String.length event - 2) |> String.lowercase
228 | in
229 | Some (`On event)
230 | | "onDoubleClick" ->
231 | classify "onDblClick"
232 | | ( "classList"
233 | | "onChange"
234 | | "onChangeOpt"
235 | | "onCheck"
236 | | "onCheckOpt"
237 | | "onInput"
238 | | "onInputOpt" ) as name ->
239 | (* These map to helper functions under Tea.Html *)
240 | Some (`Call name)
241 | | "style" ->
242 | Some `Style
243 | | "" | "children" ->
244 | None
245 | | property ->
246 | failwith ("Unrecognized property: " ^ property)
247 |
248 |
249 | (* -> div([onClick(Increment)], []) *)
250 | (* -> div([onCB("click", "", callback)], []) *)
251 | let map_event = function
252 | | {pexp_desc = Pexp_construct _; _} as value ->
253 | [%expr Vdom.EventHandlerMsg [%e value]]
254 | | value ->
255 | [%expr Vdom.EventHandlerCallback ("", [%e value])]
256 |
257 |
258 | let map_property (kind, value) =
259 | let {pexp_loc; _} = value in
260 | match kind with
261 | | `Attribute name ->
262 | [%expr Vdom.Attribute ("", [%e str_const ~pexp_loc name], [%e value])]
263 | | `Int_attribute name ->
264 | [%expr
265 | Vdom.Attribute
266 | ("", [%e str_const ~pexp_loc name], string_of_int [%e value])]
267 | | `Bool_attribute name ->
268 | [%expr
269 | if [%e value]
270 | then Vdom.Attribute ("", [%e str_const ~pexp_loc name], "true")
271 | else Vdom.NoProp]
272 | | `Property name ->
273 | [%expr Vdom.RawProp ([%e str_const ~pexp_loc name], [%e value])]
274 | | `Bool_property name ->
275 | [%expr
276 | if [%e value]
277 | then
278 | Vdom.RawProp
279 | ([%e str_const ~pexp_loc name], [%e str_const ~pexp_loc name])
280 | else Vdom.NoProp]
281 | | `Int_property name ->
282 | [%expr
283 | Vdom.RawProp ([%e str_const ~pexp_loc name], string_of_int [%e value])]
284 | | `Char_property name ->
285 | [%expr
286 | Vdom.RawProp ([%e str_const ~pexp_loc name], String.make 1 [%e value])]
287 | | `On_off_property name ->
288 | [%expr
289 | if [%e value]
290 | then Vdom.RawProp ([%e str_const ~pexp_loc name], "on")
291 | else Vdom.RawProp ([%e str_const ~pexp_loc name], "off")]
292 | | `Empty_bool_property name ->
293 | [%expr
294 | if [%e value]
295 | then Vdom.RawProp ([%e str_const ~pexp_loc name], "")
296 | else Vdom.NoProp]
297 | | `On name ->
298 | let name = str_const ~pexp_loc name in
299 | [%expr Vdom.Event ([%e name], [%e map_event value], ref None)]
300 | | `Call name ->
301 | Exp.mk
302 | (Pexp_apply
303 | ( Exp.mk
304 | @@ Pexp_ident
305 | { txt = Ldot (Ldot (Lident "Tea", "Html"), name)
306 | ; loc = pexp_loc }
307 | , [("", value)] ))
308 | | `Style ->
309 | [%expr Vdom.Style [%e value]]
310 |
311 |
312 | let map_args args =
313 | List.fold_right
314 | (fun (name, value) properties ->
315 | let name =
316 | if String.length name > 0 && name.[0] = '?'
317 | then
318 | let name = String.sub name 1 (String.length name - 1) in
319 | `Optional name
320 | else `Regular name
321 | in
322 | match name with
323 | | `Optional name ->
324 | ( match classify name with
325 | | None ->
326 | properties
327 | | Some kind ->
328 | let property =
329 | [%expr
330 | match [%e value] with
331 | | Some value ->
332 | [%e map_property (kind, [%expr value])]
333 | | None ->
334 | Vdom.NoProp]
335 | in
336 | [%expr [%e property] :: [%e properties]] )
337 | | `Regular name ->
338 | ( match classify name with
339 | | None ->
340 | properties
341 | | Some kind ->
342 | [%expr [%e map_property (kind, value)] :: [%e properties]] ) )
343 | args
344 | [%expr []]
345 |
346 |
347 | let rewrite_const = function
348 | | {pexp_desc = Pexp_constant (Const_string _); _} as const ->
349 | [%expr Vdom.Text [%e const]]
350 | | {pexp_desc = Pexp_constant (Const_char _); _} as const ->
351 | [%expr Vdom.Text (String.make 1 [%e const])]
352 | | {pexp_desc = Pexp_constant (Const_int _); _} as const ->
353 | [%expr Vdom.Text (string_of_int [%e const])]
354 | | {pexp_desc = Pexp_constant (Const_float _); _} as const ->
355 | [%expr Vdom.Text (string_of_float [%e const])]
356 | | {pexp_desc = Pexp_constant (Const_int32 _); _} as const ->
357 | [%expr Vdom.Text (Int32.to_string [%e const])]
358 | | {pexp_desc = Pexp_constant (Const_int64 _); _} as const ->
359 | [%expr Vdom.Text (Int64.to_string [%e const])]
360 | | {pexp_desc = Pexp_constant (Const_nativeint _); _} as const ->
361 | [%expr Vdom.Text (Nativeint.to_string [%e const])]
362 | | expr ->
363 | expr
364 |
365 |
366 | let rec map_children = function
367 | | { pexp_desc =
368 | Pexp_construct
369 | ( ({txt = Lident "::"; _} as cons)
370 | , Some ({pexp_desc = Pexp_tuple tuple; _} as a) ); _ } as e ->
371 | let tuple =
372 | match tuple with
373 | | [({pexp_desc = Pexp_constant _; _} as car); cdr] ->
374 | [rewrite_const car; map_children cdr]
375 | | [car; cdr] ->
376 | [car; map_children cdr]
377 | | x ->
378 | x
379 | in
380 | { e with
381 | pexp_desc =
382 | Pexp_construct (cons, Some {a with pexp_desc = Pexp_tuple tuple}) }
383 | | x ->
384 | x
385 |
386 |
387 | let extract_tea_properties props =
388 | List.fold_left
389 | (fun (ns, key, unique, props) prop ->
390 | match prop with
391 | | "ns", ns | "namespace", ns ->
392 | (ns, key, unique, props)
393 | | "key", key ->
394 | (ns, key, unique, props)
395 | | "unique", unique ->
396 | (ns, key, unique, props)
397 | | prop ->
398 | (ns, key, unique, prop :: props) )
399 | ([%expr ""], [%expr ""], [%expr ""], [])
400 | props
401 |
402 |
403 | let mapper =
404 | { default_mapper with
405 | expr =
406 | (fun mapper e ->
407 | match e with
408 | | { pexp_attributes = [({txt = "JSX"; _}, PStr [])]
409 | ; pexp_desc =
410 | Pexp_apply
411 | ({pexp_desc = Pexp_ident {txt = Lident html_tag; _}; _}, args)
412 | ; pexp_loc } ->
413 | let html_tag =
414 | { pexp_desc = Pexp_constant (Const_string (html_tag, None))
415 | ; pexp_loc
416 | ; pexp_attributes = [] }
417 | in
418 | let ns, key, unique, args = extract_tea_properties args in
419 | let properties = map_args args in
420 | let children =
421 | args
422 | |> List.find (fun (name, _) -> name = "children")
423 | |> snd
424 | |> map_children
425 | in
426 | [%expr
427 | Vdom.Node
428 | ( [%e ns]
429 | , [%e html_tag]
430 | , [%e key]
431 | , [%e unique]
432 | , [%e properties]
433 | , [%e default_mapper.expr mapper children] )]
434 | | { pexp_attributes = [({txt = "JSX"; _}, PStr [])]
435 | ; pexp_desc =
436 | Pexp_apply
437 | ( ( { pexp_desc =
438 | Pexp_ident
439 | ({txt = Ldot (module', "createElement"); _} as ident); _
440 | } as expr )
441 | , args )
442 | ; pexp_loc } ->
443 | let args =
444 | args
445 | |> filter_map (function
446 | | "children", children ->
447 | (* Map children to the last argument *)
448 | Some ("", default_mapper.expr mapper children)
449 | | ( ""
450 | , { pexp_desc =
451 | Pexp_construct ({txt = Lident "()"; _}, None); _
452 | } ) ->
453 | None
454 | | x ->
455 | Some x )
456 | in
457 | Pexp_apply
458 | ( { expr with
459 | pexp_desc =
460 | Pexp_ident {ident with txt = Ldot (module', "view")} }
461 | , args )
462 | |> Exp.mk ~loc:pexp_loc
463 | | e ->
464 | default_mapper.expr mapper e ) }
465 |
466 |
467 | let () = run_main (fun _argv -> mapper)
468 |
--------------------------------------------------------------------------------
/test/test.ml:
--------------------------------------------------------------------------------
1 | module Web = struct module Node = struct type event_cb
2 | type event end end
3 | module Vdom =
4 | struct
5 | type 'msg eventHandler =
6 | | EventHandlerCallback of string* (Web.Node.event -> 'msg option)
7 | | EventHandlerMsg of 'msg
8 | type 'msg eventCache =
9 | {
10 | handler: Web.Node.event_cb;
11 | cb: (Web.Node.event -> 'msg option) ref;}
12 | type 'msg property =
13 | | NoProp
14 | | RawProp of string* string
15 | | Attribute of string* string* string
16 | | Data of string* string
17 | | Event of string* 'msg eventHandler* 'msg eventCache option ref
18 | | Style of (string* string) list
19 | type 'msg properties = 'msg property list
20 | type 'msg t =
21 | | Node of string* string* string* string* 'msg properties* 'msg t list
22 | | Text of string
23 | end
24 | module Tea =
25 | struct
26 | module Html =
27 | struct let onChange x = failwith (("yo")[@reason.raw_literal "yo"]) end
28 | end
29 |
30 | module User =struct
31 |
32 | end
33 | type msg =
34 | | Increment
35 | | Decrement
36 | | Reset
37 | | Set of int
38 | let view model =
39 | ((User.createElement ~name:(("Ozan")[@reason.raw_literal "Ozan"])
40 | ?age:((Some (27))[@explicit_arity ]) ~children:[] ())[@JSX ])
41 |
--------------------------------------------------------------------------------
/test/test.re:
--------------------------------------------------------------------------------
1 | module Web = {
2 | module Node = {
3 | type event_cb;
4 | type event;
5 | };
6 | };
7 |
8 | module Vdom = {
9 | type eventHandler('msg) =
10 | | EventHandlerCallback(string, Web.Node.event => option('msg))
11 | | EventHandlerMsg('msg);
12 |
13 | type eventCache('msg) = {
14 | handler: Web.Node.event_cb,
15 | cb: ref(Web.Node.event => option('msg)),
16 | };
17 |
18 | type property('msg) =
19 | | NoProp
20 | | RawProp(string, string)
21 | | Attribute(string, string, string)
22 | | Event(string, eventHandler('msg), ref(option(eventCache('msg))))
23 | | Style(list((string, string)));
24 |
25 | type properties('msg) = list(property('msg));
26 |
27 | type t('msg) =
28 | | Node(string, string, string, string, properties('msg), list(t('msg)))
29 | | Text(string);
30 | };
31 |
32 | module Tea = {
33 | module Html = {
34 | let text: string => Vdom.t('msg) = _ => Obj.magic();
35 | let noNode = Obj.magic();
36 | let classList: list((string, bool)) => Vdom.property('msg) =
37 | _ => Obj.magic();
38 | let onChange = _ => Obj.magic();
39 | let onChangeOpt = _ => Obj.magic();
40 | };
41 | };
42 | type msg =
43 | | Increment
44 | | Set(int);
45 |
46 | module Picture = {
47 | let view = (~className=?, _children) =>
48 | ;
53 | };
54 |
55 | module User = {
56 | let view = (~classList=?, ~age=?, ~name, _children) => {
57 | let age =
58 | Tea.Html.(
59 | switch (age) {
60 | | Some(age) => "( " {text(string_of_int(age))} " )"
61 | | None => noNode
62 | }
63 | );
64 | let className = Some("large");
65 | let pictures = [
66 | ,
67 | ,
68 | ,
69 | ];
70 | let buttons =
71 | <> >;
72 |
73 |
74 | "Hi "
75 | {Tea.Html.text(" " ++ name ++ " ")}
76 | age
77 |
...pictures
78 |
...buttons
79 |
80 |
81 |
;
82 | };
83 | };
84 |
85 | let _view = _model =>
86 |
87 |
"Hello"
88 |
89 |
Set(int_of_string(value))}
94 | />
95 |
99 | try (value->int_of_string->Set->Some) {
100 | | _ => None
101 | }
102 | }
103 | />
104 |
105 |
106 |
107 | "Sorry, your browser doesn't support embedded videos."
108 |
109 |
110 | 5
111 | 5.0
112 | 5L
113 | 5l
114 | 5n
115 | '5'
116 |
117 |
;
118 |
119 | let _view = model => {
120 | let onChangeOpt = value =>
121 | try (value->int_of_string->Set->Some) {
122 | | _ => None
123 | };
124 | ;
125 | };
126 |
--------------------------------------------------------------------------------