xote 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +11 -18
- package/README.md +97 -41
- package/dist/xote.cjs +1 -0
- package/dist/xote.mjs +1041 -0
- package/dist/xote.umd.js +1 -0
- package/package.json +23 -3
- package/.gitattributes +0 -2
- package/.github/workflows/release.yml +0 -44
- package/dist/index.cjs +0 -2
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs +0 -9
- package/dist/index.mjs.map +0 -1
- package/dist/index.umd.js +0 -2
- package/dist/index.umd.js.map +0 -1
- package/docs/CHANGELOG.md +0 -27
- package/index.html +0 -28
- package/rescript.json +0 -18
- package/src/Xote.res +0 -5
- package/src/Xote.res.mjs +0 -21
- package/src/Xote__Component.res +0 -151
- package/src/Xote__Component.res.mjs +0 -202
- package/src/Xote__Computed.res +0 -43
- package/src/Xote__Computed.res.mjs +0 -61
- package/src/Xote__Core.res +0 -105
- package/src/Xote__Core.res.mjs +0 -148
- package/src/Xote__Effect.res +0 -36
- package/src/Xote__Effect.res.mjs +0 -57
- package/src/Xote__Example.res +0 -266
- package/src/Xote__Example.res.mjs +0 -303
- package/src/Xote__Id.res +0 -5
- package/src/Xote__Id.res.mjs +0 -17
- package/src/Xote__Observer.res +0 -12
- package/src/Xote__Observer.res.mjs +0 -12
- package/src/Xote__Signal.res +0 -31
- package/src/Xote__Signal.res.mjs +0 -64
- package/src/demo/TodoApp.res +0 -289
- package/src/demo/TodoApp.res.mjs +0 -326
- package/vite.config.js +0 -45
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
-
|
|
3
|
-
import * as Xote__Id from "./Xote__Id.res.mjs";
|
|
4
|
-
import * as Xote__Core from "./Xote__Core.res.mjs";
|
|
5
|
-
import * as Belt_MapInt from "rescript/lib/es6/belt_MapInt.js";
|
|
6
|
-
import * as Xote__Signal from "./Xote__Signal.res.mjs";
|
|
7
|
-
|
|
8
|
-
function make(calc) {
|
|
9
|
-
var s = Xote__Signal.make();
|
|
10
|
-
var initialized = {
|
|
11
|
-
contents: false
|
|
12
|
-
};
|
|
13
|
-
var id = Xote__Id.make();
|
|
14
|
-
var recompute = function () {
|
|
15
|
-
var next = calc();
|
|
16
|
-
if (initialized.contents === false) {
|
|
17
|
-
initialized.contents = true;
|
|
18
|
-
return Xote__Signal.set(s, next);
|
|
19
|
-
} else {
|
|
20
|
-
return Xote__Signal.set(s, next);
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
var o = {
|
|
24
|
-
id: id,
|
|
25
|
-
kind: {
|
|
26
|
-
NAME: "Computed",
|
|
27
|
-
VAL: s.id
|
|
28
|
-
},
|
|
29
|
-
run: recompute,
|
|
30
|
-
deps: undefined
|
|
31
|
-
};
|
|
32
|
-
Xote__Core.observers.contents = Belt_MapInt.set(Xote__Core.observers.contents, id, o);
|
|
33
|
-
Xote__Core.clearDeps(o);
|
|
34
|
-
Xote__Core.currentObserverId.contents = id;
|
|
35
|
-
o.run();
|
|
36
|
-
Xote__Core.currentObserverId.contents = undefined;
|
|
37
|
-
return s;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
var IntSet;
|
|
41
|
-
|
|
42
|
-
var IntMap;
|
|
43
|
-
|
|
44
|
-
var Signal;
|
|
45
|
-
|
|
46
|
-
var Core;
|
|
47
|
-
|
|
48
|
-
var Observer;
|
|
49
|
-
|
|
50
|
-
var Id;
|
|
51
|
-
|
|
52
|
-
export {
|
|
53
|
-
IntSet ,
|
|
54
|
-
IntMap ,
|
|
55
|
-
Signal ,
|
|
56
|
-
Core ,
|
|
57
|
-
Observer ,
|
|
58
|
-
Id ,
|
|
59
|
-
make ,
|
|
60
|
-
}
|
|
61
|
-
/* No side effect */
|
package/src/Xote__Core.res
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
module IntSet = Belt.Set.Int
|
|
2
|
-
module IntMap = Belt.Map.Int
|
|
3
|
-
module Observer = Xote__Observer
|
|
4
|
-
module Id = Xote__Id
|
|
5
|
-
|
|
6
|
-
type t<'a> = {id: int, value: ref<'a>, version: ref<int>}
|
|
7
|
-
|
|
8
|
-
/* Global tables */
|
|
9
|
-
let observers: ref<IntMap.t<Observer.t>> = ref(IntMap.empty)
|
|
10
|
-
let signalObservers: ref<IntMap.t<IntSet.t>> = ref(IntMap.empty) /* signal id -> observer ids */
|
|
11
|
-
let signalPeeks: ref<IntSet.t> = ref(IntSet.empty) /* optional; for debugging */
|
|
12
|
-
|
|
13
|
-
/* Currently running observer for tracking */
|
|
14
|
-
let currentObserverId: ref<option<int>> = ref(None)
|
|
15
|
-
|
|
16
|
-
/* Simple scheduler */
|
|
17
|
-
let pending: ref<IntSet.t> = ref(IntSet.empty)
|
|
18
|
-
let batching = ref(false)
|
|
19
|
-
|
|
20
|
-
let ensureSignalBucket = (sid: int) => {
|
|
21
|
-
switch IntMap.get(signalObservers.contents, sid) {
|
|
22
|
-
| Some(_) => ()
|
|
23
|
-
| None => signalObservers := IntMap.set(signalObservers.contents, sid, IntSet.empty)
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let addDep = (obsId: int, sid: int) => {
|
|
28
|
-
ensureSignalBucket(sid)
|
|
29
|
-
/* add obs -> dep */
|
|
30
|
-
let obs = Belt.Option.getExn(IntMap.get(observers.contents, obsId))
|
|
31
|
-
if currentObserverId.contents == Some(obsId) {
|
|
32
|
-
if obs.deps->IntSet.has(sid) == false {
|
|
33
|
-
obs.deps = obs.deps->IntSet.add(sid)
|
|
34
|
-
/* add dep -> obs */
|
|
35
|
-
let sset = Belt.Option.getExn(IntMap.get(signalObservers.contents, sid))
|
|
36
|
-
signalObservers := IntMap.set(signalObservers.contents, sid, sset->IntSet.add(obsId))
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let clearDeps = (obs: Observer.t) => {
|
|
42
|
-
/* remove obs from all signal buckets it was in */
|
|
43
|
-
obs.deps->IntSet.forEach(sid => {
|
|
44
|
-
switch IntMap.get(signalObservers.contents, sid) {
|
|
45
|
-
| None => ()
|
|
46
|
-
| Some(sset) =>
|
|
47
|
-
signalObservers := IntMap.set(signalObservers.contents, sid, sset->IntSet.remove(obs.id))
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
obs.deps = IntSet.empty
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
let schedule = (obsId: int) => {
|
|
54
|
-
pending := pending.contents->IntSet.add(obsId)
|
|
55
|
-
if batching.contents == false {
|
|
56
|
-
/* flush immediately (sync microtask-ish) */
|
|
57
|
-
let toRun = pending.contents
|
|
58
|
-
pending := IntSet.empty
|
|
59
|
-
toRun->IntSet.forEach(id => {
|
|
60
|
-
switch IntMap.get(observers.contents, id) {
|
|
61
|
-
| None => ()
|
|
62
|
-
| Some(o) => {
|
|
63
|
-
/* re-track */
|
|
64
|
-
clearDeps(o)
|
|
65
|
-
currentObserverId := Some(id)
|
|
66
|
-
o.run()
|
|
67
|
-
currentObserverId := None
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
let notify = (sid: int) => {
|
|
75
|
-
ensureSignalBucket(sid)
|
|
76
|
-
switch IntMap.get(signalObservers.contents, sid) {
|
|
77
|
-
| None => ()
|
|
78
|
-
| Some(sset) => sset->IntSet.forEach(schedule)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/* Run a function without tracking dependencies */
|
|
83
|
-
let untrack = (f: unit => 'a): 'a => {
|
|
84
|
-
let prev = currentObserverId.contents
|
|
85
|
-
currentObserverId := None
|
|
86
|
-
let r = f()
|
|
87
|
-
currentObserverId := prev
|
|
88
|
-
r
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/* Batch: defer scheduling until after block ends */
|
|
92
|
-
let batch = (f: unit => 'a): 'a => {
|
|
93
|
-
let prev = batching.contents
|
|
94
|
-
batching := true
|
|
95
|
-
let r = f()
|
|
96
|
-
batching := prev
|
|
97
|
-
|
|
98
|
-
/* flush anything queued */
|
|
99
|
-
if pending.contents != IntSet.empty {
|
|
100
|
-
let toRun = pending.contents
|
|
101
|
-
pending := IntSet.empty
|
|
102
|
-
toRun->IntSet.forEach(id => schedule(id))
|
|
103
|
-
}
|
|
104
|
-
r
|
|
105
|
-
}
|
package/src/Xote__Core.res.mjs
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
-
|
|
3
|
-
import * as Caml_obj from "rescript/lib/es6/caml_obj.js";
|
|
4
|
-
import * as Belt_MapInt from "rescript/lib/es6/belt_MapInt.js";
|
|
5
|
-
import * as Belt_Option from "rescript/lib/es6/belt_Option.js";
|
|
6
|
-
import * as Belt_SetInt from "rescript/lib/es6/belt_SetInt.js";
|
|
7
|
-
import * as Caml_option from "rescript/lib/es6/caml_option.js";
|
|
8
|
-
|
|
9
|
-
var observers = {
|
|
10
|
-
contents: undefined
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
var signalObservers = {
|
|
14
|
-
contents: undefined
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
var signalPeeks = {
|
|
18
|
-
contents: undefined
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
var currentObserverId = {
|
|
22
|
-
contents: undefined
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
var pending = {
|
|
26
|
-
contents: undefined
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
var batching = {
|
|
30
|
-
contents: false
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
function ensureSignalBucket(sid) {
|
|
34
|
-
var match = Belt_MapInt.get(signalObservers.contents, sid);
|
|
35
|
-
if (match !== undefined) {
|
|
36
|
-
return ;
|
|
37
|
-
} else {
|
|
38
|
-
signalObservers.contents = Belt_MapInt.set(signalObservers.contents, sid, undefined);
|
|
39
|
-
return ;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function addDep(obsId, sid) {
|
|
44
|
-
ensureSignalBucket(sid);
|
|
45
|
-
var obs = Belt_Option.getExn(Belt_MapInt.get(observers.contents, obsId));
|
|
46
|
-
if (!Caml_obj.equal(currentObserverId.contents, obsId)) {
|
|
47
|
-
return ;
|
|
48
|
-
}
|
|
49
|
-
if (Belt_SetInt.has(obs.deps, sid) !== false) {
|
|
50
|
-
return ;
|
|
51
|
-
}
|
|
52
|
-
obs.deps = Belt_SetInt.add(obs.deps, sid);
|
|
53
|
-
var sset = Belt_Option.getExn(Belt_MapInt.get(signalObservers.contents, sid));
|
|
54
|
-
signalObservers.contents = Belt_MapInt.set(signalObservers.contents, sid, Belt_SetInt.add(sset, obsId));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function clearDeps(obs) {
|
|
58
|
-
Belt_SetInt.forEach(obs.deps, (function (sid) {
|
|
59
|
-
var sset = Belt_MapInt.get(signalObservers.contents, sid);
|
|
60
|
-
if (sset !== undefined) {
|
|
61
|
-
signalObservers.contents = Belt_MapInt.set(signalObservers.contents, sid, Belt_SetInt.remove(Caml_option.valFromOption(sset), obs.id));
|
|
62
|
-
return ;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
}));
|
|
66
|
-
obs.deps = undefined;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function schedule(obsId) {
|
|
70
|
-
pending.contents = Belt_SetInt.add(pending.contents, obsId);
|
|
71
|
-
if (batching.contents !== false) {
|
|
72
|
-
return ;
|
|
73
|
-
}
|
|
74
|
-
var toRun = pending.contents;
|
|
75
|
-
pending.contents = undefined;
|
|
76
|
-
Belt_SetInt.forEach(toRun, (function (id) {
|
|
77
|
-
var o = Belt_MapInt.get(observers.contents, id);
|
|
78
|
-
if (o !== undefined) {
|
|
79
|
-
clearDeps(o);
|
|
80
|
-
currentObserverId.contents = id;
|
|
81
|
-
o.run();
|
|
82
|
-
currentObserverId.contents = undefined;
|
|
83
|
-
return ;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
}));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function notify(sid) {
|
|
90
|
-
ensureSignalBucket(sid);
|
|
91
|
-
var sset = Belt_MapInt.get(signalObservers.contents, sid);
|
|
92
|
-
if (sset !== undefined) {
|
|
93
|
-
return Belt_SetInt.forEach(Caml_option.valFromOption(sset), schedule);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function untrack(f) {
|
|
99
|
-
var prev = currentObserverId.contents;
|
|
100
|
-
currentObserverId.contents = undefined;
|
|
101
|
-
var r = f();
|
|
102
|
-
currentObserverId.contents = prev;
|
|
103
|
-
return r;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function batch(f) {
|
|
107
|
-
var prev = batching.contents;
|
|
108
|
-
batching.contents = true;
|
|
109
|
-
var r = f();
|
|
110
|
-
batching.contents = prev;
|
|
111
|
-
if (pending.contents !== undefined) {
|
|
112
|
-
var toRun = pending.contents;
|
|
113
|
-
pending.contents = undefined;
|
|
114
|
-
Belt_SetInt.forEach(toRun, (function (id) {
|
|
115
|
-
schedule(id);
|
|
116
|
-
}));
|
|
117
|
-
}
|
|
118
|
-
return r;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
var IntSet;
|
|
122
|
-
|
|
123
|
-
var IntMap;
|
|
124
|
-
|
|
125
|
-
var Observer;
|
|
126
|
-
|
|
127
|
-
var Id;
|
|
128
|
-
|
|
129
|
-
export {
|
|
130
|
-
IntSet ,
|
|
131
|
-
IntMap ,
|
|
132
|
-
Observer ,
|
|
133
|
-
Id ,
|
|
134
|
-
observers ,
|
|
135
|
-
signalObservers ,
|
|
136
|
-
signalPeeks ,
|
|
137
|
-
currentObserverId ,
|
|
138
|
-
pending ,
|
|
139
|
-
batching ,
|
|
140
|
-
ensureSignalBucket ,
|
|
141
|
-
addDep ,
|
|
142
|
-
clearDeps ,
|
|
143
|
-
schedule ,
|
|
144
|
-
notify ,
|
|
145
|
-
untrack ,
|
|
146
|
-
batch ,
|
|
147
|
-
}
|
|
148
|
-
/* No side effect */
|
package/src/Xote__Effect.res
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
module IntSet = Belt.Set.Int
|
|
2
|
-
module IntMap = Belt.Map.Int
|
|
3
|
-
|
|
4
|
-
module Id = Xote__Id
|
|
5
|
-
module Observer = Xote__Observer
|
|
6
|
-
module Signal = Xote__Signal
|
|
7
|
-
module Core = Xote__Core
|
|
8
|
-
|
|
9
|
-
type disposer = {dispose: unit => unit}
|
|
10
|
-
|
|
11
|
-
let run = (fn: unit => unit): disposer => {
|
|
12
|
-
let id = Id.make()
|
|
13
|
-
let rec o: Observer.t = {
|
|
14
|
-
id,
|
|
15
|
-
kind: #Effect,
|
|
16
|
-
run: () => fn(),
|
|
17
|
-
deps: IntSet.empty,
|
|
18
|
-
}
|
|
19
|
-
Core.observers := IntMap.set(Core.observers.contents, id, o)
|
|
20
|
-
/* initial run */
|
|
21
|
-
Core.clearDeps(o)
|
|
22
|
-
Core.currentObserverId := Some(id)
|
|
23
|
-
o.run()
|
|
24
|
-
Core.currentObserverId := None
|
|
25
|
-
|
|
26
|
-
let dispose = () => {
|
|
27
|
-
switch IntMap.get(Core.observers.contents, id) {
|
|
28
|
-
| None => ()
|
|
29
|
-
| Some(o) => {
|
|
30
|
-
Core.clearDeps(o)
|
|
31
|
-
Core.observers := IntMap.remove(Core.observers.contents, id)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
{dispose: dispose}
|
|
36
|
-
}
|
package/src/Xote__Effect.res.mjs
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
-
|
|
3
|
-
import * as Xote__Id from "./Xote__Id.res.mjs";
|
|
4
|
-
import * as Xote__Core from "./Xote__Core.res.mjs";
|
|
5
|
-
import * as Belt_MapInt from "rescript/lib/es6/belt_MapInt.js";
|
|
6
|
-
|
|
7
|
-
function run(fn) {
|
|
8
|
-
var id = Xote__Id.make();
|
|
9
|
-
var o = {
|
|
10
|
-
id: id,
|
|
11
|
-
kind: "Effect",
|
|
12
|
-
run: (function () {
|
|
13
|
-
fn();
|
|
14
|
-
}),
|
|
15
|
-
deps: undefined
|
|
16
|
-
};
|
|
17
|
-
Xote__Core.observers.contents = Belt_MapInt.set(Xote__Core.observers.contents, id, o);
|
|
18
|
-
Xote__Core.clearDeps(o);
|
|
19
|
-
Xote__Core.currentObserverId.contents = id;
|
|
20
|
-
o.run();
|
|
21
|
-
Xote__Core.currentObserverId.contents = undefined;
|
|
22
|
-
var dispose = function () {
|
|
23
|
-
var o = Belt_MapInt.get(Xote__Core.observers.contents, id);
|
|
24
|
-
if (o !== undefined) {
|
|
25
|
-
Xote__Core.clearDeps(o);
|
|
26
|
-
Xote__Core.observers.contents = Belt_MapInt.remove(Xote__Core.observers.contents, id);
|
|
27
|
-
return ;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
};
|
|
31
|
-
return {
|
|
32
|
-
dispose: dispose
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
var IntSet;
|
|
37
|
-
|
|
38
|
-
var IntMap;
|
|
39
|
-
|
|
40
|
-
var Id;
|
|
41
|
-
|
|
42
|
-
var Observer;
|
|
43
|
-
|
|
44
|
-
var Signal;
|
|
45
|
-
|
|
46
|
-
var Core;
|
|
47
|
-
|
|
48
|
-
export {
|
|
49
|
-
IntSet ,
|
|
50
|
-
IntMap ,
|
|
51
|
-
Id ,
|
|
52
|
-
Observer ,
|
|
53
|
-
Signal ,
|
|
54
|
-
Core ,
|
|
55
|
-
run ,
|
|
56
|
-
}
|
|
57
|
-
/* No side effect */
|
package/src/Xote__Example.res
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
open Xote
|
|
2
|
-
|
|
3
|
-
type todo = {
|
|
4
|
-
id: int,
|
|
5
|
-
text: string,
|
|
6
|
-
completed: bool,
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
let todos = Signal.make([])
|
|
10
|
-
let nextId = ref(0)
|
|
11
|
-
let inputValue = Signal.make("")
|
|
12
|
-
let darkMode = Signal.make(false)
|
|
13
|
-
|
|
14
|
-
// Computed values derived from todos
|
|
15
|
-
let completedCount = Computed.make(() => {
|
|
16
|
-
Signal.get(todos)->Array.filter(todo => todo.completed)->Array.length
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
let activeCount = Computed.make(() => {
|
|
20
|
-
Signal.get(todos)->Array.filter(todo => !todo.completed)->Array.length
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
let totalCount = Computed.make(() => {
|
|
24
|
-
Signal.get(todos)->Array.length
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
let addTodo = (text: string) => {
|
|
28
|
-
if String.trim(text) != "" {
|
|
29
|
-
Signal.update(todos, list => {
|
|
30
|
-
Array.concat(list, [{id: nextId.contents, text, completed: false}])
|
|
31
|
-
})
|
|
32
|
-
nextId := nextId.contents + 1
|
|
33
|
-
Signal.set(inputValue, "")
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let toggleTodo = (id: int) => {
|
|
38
|
-
Signal.update(todos, list => {
|
|
39
|
-
Array.map(list, todo => todo.id == id ? {...todo, completed: !todo.completed} : todo)
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
@val @scope("document") external querySelector: string => Nullable.t<Dom.element> = "querySelector"
|
|
44
|
-
@get external target: Dom.event => Dom.element = "target"
|
|
45
|
-
@get external value: Dom.element => string = "value"
|
|
46
|
-
@set external setValue: (Dom.element, string) => unit = "value"
|
|
47
|
-
@get external key: Dom.event => string = "key"
|
|
48
|
-
|
|
49
|
-
let clearInput = () => {
|
|
50
|
-
switch querySelector(".todo-input")->Nullable.toOption {
|
|
51
|
-
| Some(input) => setValue(input, "")
|
|
52
|
-
| None => ()
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
let handleInput = (evt: Dom.event) => {
|
|
57
|
-
let newValue = evt->target->value
|
|
58
|
-
Signal.set(inputValue, newValue)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let getInputValue = () => {
|
|
62
|
-
switch querySelector(".todo-input")->Nullable.toOption {
|
|
63
|
-
| Some(input) => input->value
|
|
64
|
-
| None => ""
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
let handleKeyDown = (evt: Dom.event) => {
|
|
69
|
-
if evt->key == "Enter" {
|
|
70
|
-
addTodo(getInputValue())
|
|
71
|
-
clearInput()
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let handleAddClick = (_evt: Dom.event) => {
|
|
76
|
-
addTodo(getInputValue())
|
|
77
|
-
clearInput()
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
let toggleTheme = (_evt: Dom.event) => {
|
|
81
|
-
Signal.update(darkMode, mode => !mode)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Effect to sync dark mode with HTML class
|
|
85
|
-
let _ = Effect.run(() => {
|
|
86
|
-
let isDark = Signal.get(darkMode)
|
|
87
|
-
if isDark {
|
|
88
|
-
%raw(`document.documentElement.classList.add('dark')`)
|
|
89
|
-
} else {
|
|
90
|
-
%raw(`document.documentElement.classList.remove('dark')`)
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
let todoItem = (todo: todo) => {
|
|
95
|
-
let checkboxAttrs = todo.completed
|
|
96
|
-
? [("type", "checkbox"), ("checked", "checked"), ("class", "w-5 h-5 cursor-pointer")]
|
|
97
|
-
: [("type", "checkbox"), ("class", "w-5 h-5 cursor-pointer")]
|
|
98
|
-
|
|
99
|
-
Component.li(
|
|
100
|
-
~attrs=[
|
|
101
|
-
(
|
|
102
|
-
"class",
|
|
103
|
-
"flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 mb-2 " ++
|
|
104
|
-
(todo.completed ? "completed" : ""),
|
|
105
|
-
),
|
|
106
|
-
],
|
|
107
|
-
~children=[
|
|
108
|
-
Component.input(~attrs=checkboxAttrs, ~events=[("change", _ => toggleTodo(todo.id))], ()),
|
|
109
|
-
Component.span(
|
|
110
|
-
~attrs=[("class", "flex-1 text-gray-900 dark:text-gray-100")],
|
|
111
|
-
~children=[Component.text(todo.text)],
|
|
112
|
-
(),
|
|
113
|
-
),
|
|
114
|
-
],
|
|
115
|
-
(),
|
|
116
|
-
)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let inputElement = Component.input(
|
|
120
|
-
~attrs=[
|
|
121
|
-
("type", "text"),
|
|
122
|
-
("placeholder", "What needs to be done?"),
|
|
123
|
-
("class", "todo-input flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"),
|
|
124
|
-
],
|
|
125
|
-
~events=[("input", handleInput), ("keydown", handleKeyDown)],
|
|
126
|
-
(),
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
let app = Component.div(
|
|
130
|
-
~attrs=[("class", "max-w-2xl mx-auto p-6 space-y-6")],
|
|
131
|
-
~children=[
|
|
132
|
-
Component.div(
|
|
133
|
-
~attrs=[("class", "bg-white dark:bg-gray-800 rounded-xl shadow-lg p-8")],
|
|
134
|
-
~children=[
|
|
135
|
-
Component.div(
|
|
136
|
-
~attrs=[("class", "flex items-center justify-between mb-6")],
|
|
137
|
-
~children=[
|
|
138
|
-
Component.h1(
|
|
139
|
-
~attrs=[("class", "text-3xl font-bold text-gray-900 dark:text-white")],
|
|
140
|
-
~children=[Component.text("Todo List")],
|
|
141
|
-
(),
|
|
142
|
-
),
|
|
143
|
-
Component.button(
|
|
144
|
-
~attrs=[
|
|
145
|
-
(
|
|
146
|
-
"class",
|
|
147
|
-
"px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors",
|
|
148
|
-
),
|
|
149
|
-
],
|
|
150
|
-
~events=[("click", toggleTheme)],
|
|
151
|
-
~children=[
|
|
152
|
-
Component.textSignal(
|
|
153
|
-
Computed.make(() => Signal.get(darkMode) ? "☀️ Light" : "🌙 Dark")
|
|
154
|
-
),
|
|
155
|
-
],
|
|
156
|
-
(),
|
|
157
|
-
),
|
|
158
|
-
],
|
|
159
|
-
(),
|
|
160
|
-
),
|
|
161
|
-
Component.div(
|
|
162
|
-
~attrs=[
|
|
163
|
-
(
|
|
164
|
-
"class",
|
|
165
|
-
"grid grid-cols-3 gap-4 mb-6 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700",
|
|
166
|
-
),
|
|
167
|
-
],
|
|
168
|
-
~children=[
|
|
169
|
-
Component.div(
|
|
170
|
-
~attrs=[("class", "flex flex-col items-center")],
|
|
171
|
-
~children=[
|
|
172
|
-
Component.span(
|
|
173
|
-
~attrs=[
|
|
174
|
-
("class", "text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1"),
|
|
175
|
-
],
|
|
176
|
-
~children=[Component.text("Total")],
|
|
177
|
-
(),
|
|
178
|
-
),
|
|
179
|
-
Component.span(
|
|
180
|
-
~attrs=[("class", "text-2xl font-bold text-gray-900 dark:text-white")],
|
|
181
|
-
~children=[
|
|
182
|
-
Component.textSignal(Computed.make(() => Int.toString(Signal.get(totalCount)))),
|
|
183
|
-
],
|
|
184
|
-
(),
|
|
185
|
-
),
|
|
186
|
-
],
|
|
187
|
-
(),
|
|
188
|
-
),
|
|
189
|
-
Component.div(
|
|
190
|
-
~attrs=[("class", "flex flex-col items-center")],
|
|
191
|
-
~children=[
|
|
192
|
-
Component.span(
|
|
193
|
-
~attrs=[
|
|
194
|
-
("class", "text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1"),
|
|
195
|
-
],
|
|
196
|
-
~children=[Component.text("Active")],
|
|
197
|
-
(),
|
|
198
|
-
),
|
|
199
|
-
Component.span(
|
|
200
|
-
~attrs=[("class", "text-2xl font-bold text-blue-600 dark:text-blue-400")],
|
|
201
|
-
~children=[
|
|
202
|
-
Component.textSignal(
|
|
203
|
-
Computed.make(() => Int.toString(Signal.get(activeCount)))
|
|
204
|
-
),
|
|
205
|
-
],
|
|
206
|
-
(),
|
|
207
|
-
),
|
|
208
|
-
],
|
|
209
|
-
(),
|
|
210
|
-
),
|
|
211
|
-
Component.div(
|
|
212
|
-
~attrs=[("class", "flex flex-col items-center")],
|
|
213
|
-
~children=[
|
|
214
|
-
Component.span(
|
|
215
|
-
~attrs=[
|
|
216
|
-
("class", "text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1"),
|
|
217
|
-
],
|
|
218
|
-
~children=[Component.text("Completed")],
|
|
219
|
-
(),
|
|
220
|
-
),
|
|
221
|
-
Component.span(
|
|
222
|
-
~attrs=[("class", "text-2xl font-bold text-green-600 dark:text-green-400")],
|
|
223
|
-
~children=[
|
|
224
|
-
Component.textSignal(
|
|
225
|
-
Computed.make(() => Int.toString(Signal.get(completedCount)))
|
|
226
|
-
),
|
|
227
|
-
],
|
|
228
|
-
(),
|
|
229
|
-
),
|
|
230
|
-
],
|
|
231
|
-
(),
|
|
232
|
-
),
|
|
233
|
-
],
|
|
234
|
-
(),
|
|
235
|
-
),
|
|
236
|
-
Component.div(
|
|
237
|
-
~attrs=[("class", "flex gap-2 mb-6")],
|
|
238
|
-
~children=[
|
|
239
|
-
inputElement,
|
|
240
|
-
Component.button(
|
|
241
|
-
~attrs=[
|
|
242
|
-
(
|
|
243
|
-
"class",
|
|
244
|
-
"px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
245
|
-
),
|
|
246
|
-
],
|
|
247
|
-
~events=[("click", handleAddClick)],
|
|
248
|
-
~children=[Component.text("Add")],
|
|
249
|
-
(),
|
|
250
|
-
),
|
|
251
|
-
],
|
|
252
|
-
(),
|
|
253
|
-
),
|
|
254
|
-
Component.ul(
|
|
255
|
-
~attrs=[("class", "todo-list space-y-2")],
|
|
256
|
-
~children=[Component.list(todos, todoItem)],
|
|
257
|
-
(),
|
|
258
|
-
),
|
|
259
|
-
],
|
|
260
|
-
(),
|
|
261
|
-
),
|
|
262
|
-
],
|
|
263
|
-
(),
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
Component.mountById(app, "app")
|