git.iko.soy/rowtype-yoga/purescript-yoga-react-om
on main help sign in
yoga-react-om v0.1.0: Om-powered React hooks
e7583cbMark
.gitignorenew file+5
@@ -0,0 +1,5 @@
1+node_modules
2+output
3+.spago
4+.psc-ide-port
5+spago.lock
.psc-ide-portnew file+1
@@ -0,0 +1 @@
1+15582
.spec-resultsnew file+93
@@ -0,0 +1,93 @@
1+[
2+ [
3+ "SSR renderToStaticMarkup produces clean HTML",
4+ {
5+ "timestamp": "1771275969421.0",
6+ "success": true
7+ }
8+ ],
9+ [
10+ "SSR renderToString produces HTML with context values",
11+ {
12+ "timestamp": "1771275969421.0",
13+ "success": true
14+ }
15+ ],
16+ [
17+ "composable context-slicing hooks infers ctx as union of hook requirements",
18+ {
19+ "timestamp": "1771275969421.0",
20+ "success": true
21+ }
22+ ],
23+ [
24+ "omComponent + OmLayer wires layers and renders from provided context",
25+ {
26+ "timestamp": "1771275969421.0",
27+ "success": true
28+ }
29+ ],
30+ [
31+ "omComponent + Ref interaction mutates a Ref from ctx and re-renders",
32+ {
33+ "timestamp": "1771275969421.0",
34+ "success": true
35+ }
36+ ],
37+ [
38+ "omComponent captures context and renders it",
39+ {
40+ "timestamp": "1771275969421.0",
41+ "success": true
42+ }
43+ ],
44+ [
45+ "omComponent different context produces different output",
46+ {
47+ "timestamp": "1771275969421.0",
48+ "success": true
49+ }
50+ ],
51+ [
52+ "omComponent error channel allows error-free construction",
53+ {
54+ "timestamp": "1771275969421.0",
55+ "success": true
56+ }
57+ ],
58+ [
59+ "omComponent error channel propagates typed errors from before component creation",
60+ {
61+ "timestamp": "1771275969421.0",
62+ "success": true
63+ }
64+ ],
65+ [
66+ "shared context across components two components share a Ref via Om context",
67+ {
68+ "timestamp": "1771275969421.0",
69+ "success": true
70+ }
71+ ],
72+ [
73+ "useSignal multiple signal updates",
74+ {
75+ "timestamp": "1771275969421.0",
76+ "success": true
77+ }
78+ ],
79+ [
80+ "useSignal re-renders when signal is modified",
81+ {
82+ "timestamp": "1771275969421.0",
83+ "success": true
84+ }
85+ ],
86+ [
87+ "useSignal renders initial signal value",
88+ {
89+ "timestamp": "1771275969421.0",
90+ "success": true
91+ }
92+ ]
93+]
bun.locknew file+151
@@ -0,0 +1,151 @@
1+{
2+ "lockfileVersion": 1,
3+ "workspaces": {
4+ "": {
5+ "name": "purescript-yoga-react-om",
6+ "dependencies": {
7+ "react": "^19.0.0",
8+ "react-dom": "^19.0.0",
9+ },
10+ "devDependencies": {
11+ "@testing-library/react": "^16.0.0",
12+ "@testing-library/user-event": "^14.5.0",
13+ "global-jsdom": "^28.0.0",
14+ "jsdom": "^28.0.0",
15+ },
16+ },
17+ },
18+ "packages": {
19+ "@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="],
20+
21+ "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.2", "", { "dependencies": { "@csstools/css-calc": "^3.0.0", "@csstools/css-color-parser": "^4.0.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.5" } }, "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg=="],
22+
23+ "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.8.1", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.6" } }, "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ=="],
24+
25+ "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
26+
27+ "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
28+
29+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
30+
31+ "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
32+
33+ "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
34+
35+ "@csstools/color-helpers": ["@csstools/color-helpers@6.0.1", "", {}, "sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ=="],
36+
37+ "@csstools/css-calc": ["@csstools/css-calc@3.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ=="],
38+
39+ "@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.1", "", { "dependencies": { "@csstools/color-helpers": "^6.0.1", "@csstools/css-calc": "^3.0.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw=="],
40+
41+ "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
42+
43+ "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.27", "", {}, "sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow=="],
44+
45+ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
46+
47+ "@exodus/bytes": ["@exodus/bytes@1.14.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ=="],
48+
49+ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
50+
51+ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
52+
53+ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
54+
55+ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
56+
57+ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
58+
59+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
60+
61+ "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
62+
63+ "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
64+
65+ "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
66+
67+ "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
68+
69+ "cssstyle": ["cssstyle@6.0.1", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.2", "@csstools/css-syntax-patches-for-csstree": "^1.0.26", "css-tree": "^3.1.0", "lru-cache": "^11.2.5" } }, "sha512-IoJs7La+oFp/AB033wBStxNOJt4+9hHMxsXUPANcoXL2b3W4DZKghlJ2cI/eyeRZIQ9ysvYEorVhjrcYctWbog=="],
70+
71+ "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
72+
73+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
74+
75+ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
76+
77+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
78+
79+ "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
80+
81+ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
82+
83+ "global-jsdom": ["global-jsdom@28.0.0", "", { "peerDependencies": { "jsdom": ">=28 <29" } }, "sha512-G0uJuTt17fnWWoIOxgXTANKwMzlrQehWH5qyD+mJ+C4vT1PaAhtU2UBwDD+mRZaHoMObhwXdt3kbXLgZaPHLpQ=="],
84+
85+ "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
86+
87+ "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
88+
89+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
90+
91+ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
92+
93+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
94+
95+ "jsdom": ["jsdom@28.1.0", "", { "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", "@bramus/specificity": "^2.4.2", "@exodus/bytes": "^1.11.0", "cssstyle": "^6.0.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "undici": "^7.21.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug=="],
96+
97+ "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
98+
99+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
100+
101+ "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
102+
103+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
104+
105+ "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="],
106+
107+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
108+
109+ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
110+
111+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
112+
113+ "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
114+
115+ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
116+
117+ "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
118+
119+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
120+
121+ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
122+
123+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
124+
125+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
126+
127+ "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
128+
129+ "tldts": ["tldts@7.0.23", "", { "dependencies": { "tldts-core": "^7.0.23" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw=="],
130+
131+ "tldts-core": ["tldts-core@7.0.23", "", {}, "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ=="],
132+
133+ "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="],
134+
135+ "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
136+
137+ "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="],
138+
139+ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
140+
141+ "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
142+
143+ "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
144+
145+ "whatwg-url": ["whatwg-url@16.0.0", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ=="],
146+
147+ "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
148+
149+ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
150+ }
151+}
package.jsonnew file+19
@@ -0,0 +1,19 @@
1+{
2+ "name": "purescript-yoga-react-om",
3+ "type": "module",
4+ "private": true,
5+ "scripts": {
6+ "build": "spago build",
7+ "test": "spago test"
8+ },
9+ "devDependencies": {
10+ "@testing-library/react": "^16.0.0",
11+ "@testing-library/user-event": "^14.5.0",
12+ "global-jsdom": "^28.0.0",
13+ "jsdom": "^28.0.0"
14+ },
15+ "dependencies": {
16+ "react": "^19.0.0",
17+ "react-dom": "^19.0.0"
18+ }
19+}
spago.yamlnew file+37
@@ -0,0 +1,37 @@
1+workspace: {}
2+
3+package:
4+ name: yoga-react-om
5+ publish:
6+ version: 0.1.0
7+ license: MIT
8+ location:
9+ githubOwner: rowtype-yoga
10+ githubRepo: purescript-yoga-react-om
11+ dependencies:
12+ - effect: ">=4.0.0 <5.0.0"
13+ - hyrule: ">=2.4.0 <3.0.0"
14+ - indexed-monad: ">=3.0.0 <4.0.0"
15+ - maybe: ">=6.0.0 <7.0.0"
16+ - newtype: ">=5.0.0 <6.0.0"
17+ - prelude: ">=6.0.2 <7.0.0"
18+ - react-basic: ">=17.0.0 <18.0.0"
19+ - react-basic-hooks: ">=8.2.0 <9.0.0"
20+ - refs: ">=6.0.0 <7.0.0"
21+ - st: ">=6.2.0 <7.0.0"
22+ - tuples: ">=7.0.0 <8.0.0"
23+ - type-equality: ">=4.0.1 <5.0.0"
24+ - unsafe-coerce: ">=6.0.0 <7.0.0"
25+ - yoga-om: ">=1.4.0 <2.0.0"
26+ test:
27+ main: Test.Main
28+ dependencies:
29+ - aff
30+ - react-basic-hooks
31+ - react-testing-library
32+ - refs
33+ - spec
34+ - spec-discovery
35+ - yoga-om-layer
36+ - yoga-react-dom
37+ - spec-om
src/Yoga/React/Om.pursnew file+320
@@ -0,0 +1,320 @@
1+module Yoga.React.Om
2+ ( OmRender
3+ , omComponent
4+ , useCtx
5+ , useOm
6+ , useSignal
7+ , useEvent
8+ , UseCtx
9+ , UseOm
10+ , UseSignal
11+ , UseEvent
12+ , liftRender
13+ , bind
14+ , discard
15+ , pure
16+ -- Re-exported React hooks, lifted into OmRender
17+ , useState
18+ , useState'
19+ , useEffect
20+ , useEffectOnce
21+ , useEffectAlways
22+ , useLayoutEffect
23+ , useLayoutEffectOnce
24+ , useLayoutEffectAlways
25+ , useRef
26+ , useMemo
27+ , useReducer
28+ , useId
29+ , useTransition
30+ , useDeferredValue
31+ , module ReExports
32+ ) where
33+
34+import Prelude hiding (bind, discard, pure)
35+import Prelude (bind, discard, pure) as Prelude
36+
37+import Control.Applicative.Indexed (class IxApplicative)
38+import Control.Apply.Indexed (class IxApply)
39+import Control.Bind.Indexed (class IxBind, ibind)
40+import Control.Monad.Indexed (class IxMonad)
41+import Data.Functor.Indexed (class IxFunctor)
42+import Data.Maybe (Maybe(..))
43+import Data.Newtype (class Newtype, un)
44+import Data.Tuple.Nested ((/\), type (/\))
45+import Control.Monad.ST.Class (liftST)
46+import Effect (Effect)
47+import Effect.Class (liftEffect)
48+import Effect.Ref as Ref
49+import Effect.Unsafe (unsafePerformEffect)
50+import FRP.Event as Event
51+import React.Basic (JSX)
52+import React.Basic.Hooks (Reducer, UseState, UseEffect, UseLayoutEffect, UseRef, UseMemo, UseReducer, UseId, UseTransition, UseDeferredValue) as ReExports
53+import React.Basic.Hooks as Hooks
54+import React.Basic.Hooks.Internal (Render, unsafeHook, unsafeRenderEffect)
55+import React.Basic.Hooks.Internal as Render
56+import Yoga.React.Om.Signal (Signal)
57+import Type.Equality (class TypeEquals)
58+import Unsafe.Coerce (unsafeCoerce)
59+import Yoga.Om (Om)
60+import Yoga.Om as Om
61+
62+-- | A render context that tracks the Om context at the type level.
63+-- | At runtime this is identical to `Render` — the ctx is phantom.
64+-- | Use qualified-do with this module (e.g. `Om.do`) to compose hooks.
65+newtype OmRender :: Row Type -> Type -> Type -> Type -> Type
66+newtype OmRender ctx x y a = OmRender (Render x y a)
67+
68+derive instance Newtype (OmRender ctx x y a) _
69+
70+-- The stash: a mutable slot written by omComponent before each render,
71+-- read by useCtx/useOm during render. Safe because React renders are
72+-- synchronous and single-threaded.
73+ctxStash :: Ref.Ref (forall r. { | r })
74+ctxStash = unsafePerformEffect (Ref.new (unsafeCoerce {}))
75+
76+-- Helper: unwrap OmRender to the raw Effect
77+toEffect :: forall ctx x y a. OmRender ctx x y a -> Effect a
78+toEffect = unsafeCoerce
79+
80+-- Helper: wrap raw Effect into OmRender
81+fromEffect :: forall ctx x y a. Effect a -> OmRender ctx x y a
82+fromEffect = unsafeCoerce
83+
84+-- ============================================================================
85+-- Indexed monad instances (for qualified-do)
86+-- ============================================================================
87+
88+instance IxFunctor (OmRender ctx) where
89+ imap f m = fromEffect (map f (toEffect m))
90+
91+instance IxApply (OmRender ctx) where
92+ iapply f a = fromEffect (apply (toEffect f) (toEffect a))
93+
94+instance IxApplicative (OmRender ctx) where
95+ ipure a = fromEffect (Prelude.pure a)
96+
97+instance IxBind (OmRender ctx) where
98+ ibind m f = fromEffect (Prelude.bind (toEffect m) \a -> toEffect (f a))
99+
100+instance IxMonad (OmRender ctx)
101+
102+-- ============================================================================
103+-- Standard monad instances (when x ~ y, for `pure`)
104+-- ============================================================================
105+
106+instance Functor (OmRender ctx x y) where
107+ map f m = fromEffect (map f (toEffect m))
108+
109+instance TypeEquals x y => Apply (OmRender ctx x y) where
110+ apply f a = fromEffect (apply (toEffect f) (toEffect a))
111+
112+instance TypeEquals x y => Applicative (OmRender ctx x y) where
113+ pure a = fromEffect (Prelude.pure a)
114+
115+instance TypeEquals x y => Bind (OmRender ctx x y) where
116+ bind m f = fromEffect (Prelude.bind (toEffect m) \a -> toEffect (f a))
117+
118+instance TypeEquals x y => Monad (OmRender ctx x y)
119+
120+instance (TypeEquals x y, Semigroup a) => Semigroup (OmRender ctx x y a) where
121+ append a b = fromEffect (append (toEffect a) (toEffect b))
122+
123+instance (TypeEquals x y, Monoid a) => Monoid (OmRender ctx x y a) where
124+ mempty = fromEffect mempty
125+
126+-- ============================================================================
127+-- Qualified-do exports
128+-- ============================================================================
129+
130+bind :: forall a b x y z ctx. OmRender ctx x y a -> (a -> OmRender ctx y z b) -> OmRender ctx x z b
131+bind = ibind
132+
133+discard :: forall a b x y z ctx. OmRender ctx x y a -> (a -> OmRender ctx y z b) -> OmRender ctx x z b
134+discard = ibind
135+
136+pure :: forall a x ctx. a -> OmRender ctx x x a
137+pure a = fromEffect (Prelude.pure a)
138+
139+-- ============================================================================
140+-- Lifting
141+-- ============================================================================
142+
143+-- | Lift a standard React hook into OmRender.
144+liftRender :: forall ctx x y a. Render x y a -> OmRender ctx x y a
145+liftRender = OmRender
146+
147+-- ============================================================================
148+-- Hooks
149+-- ============================================================================
150+
151+data UseCtx :: Row Type -> Type -> Type
152+data UseCtx ctx hooks
153+
154+-- | Read the full Om context record inside a component.
155+useCtx :: forall @ctx hooks. OmRender ctx hooks (UseCtx ctx hooks) { | ctx }
156+useCtx = OmRender (unsafeHook readCtx)
157+ where
158+ readCtx :: Effect { | ctx }
159+ readCtx = unsafeCoerce (Ref.read ctxStash)
160+
161+data UseOm :: Row Type -> Type -> Type -> Type
162+data UseOm ctx a hooks
163+
164+-- | Run a synchronous Effect that has access to the Om context.
165+useOm
166+ :: forall @ctx hooks a
167+ . ({ | ctx } -> Effect a)
168+ -> OmRender ctx hooks (UseOm ctx a hooks) a
169+useOm f = OmRender (unsafeHook go)
170+ where
171+ go :: Effect a
172+ go = Prelude.do
173+ ctx :: { | ctx } <- unsafeCoerce (Ref.read ctxStash)
174+ f ctx
175+
176+data UseSignal :: Type -> Type -> Type
177+data UseSignal a hooks
178+
179+useSignal
180+ :: forall ctx hooks a
181+ . Signal a
182+ -> OmRender ctx hooks (UseSignal a hooks) a
183+useSignal signal = fromEffect Prelude.do
184+ initial <- signal.read
185+ val /\ setVal <- toEffect (liftRender (Hooks.useState initial))
186+ toEffect
187+ ( liftRender
188+ ( Hooks.useEffectOnce Prelude.do
189+ Event.subscribe signal.event (\new -> setVal (const new)) # liftST # map liftST
190+ )
191+ )
192+ Prelude.pure val
193+
194+data UseEvent :: Type -> Type -> Type
195+data UseEvent a hooks
196+
197+useEvent
198+ :: forall ctx hooks a
199+ . Event.Event a
200+ -> OmRender ctx hooks (UseEvent a hooks) (Maybe a)
201+useEvent event = fromEffect Prelude.do
202+ val /\ setVal <- toEffect (liftRender (Hooks.useState Nothing))
203+ toEffect
204+ ( liftRender
205+ ( Hooks.useEffectOnce Prelude.do
206+ Event.subscribe event (\new -> setVal (const (Just new))) # liftST # map liftST
207+ )
208+ )
209+ Prelude.pure val
210+-- ============================================================================
211+-- Re-exported React hooks, lifted into OmRender
212+-- ============================================================================
213+
214+useState
215+ :: forall ctx state hooks
216+ . state
217+ -> OmRender ctx hooks (Hooks.UseState state hooks) (state /\ ((state -> state) -> Effect Unit))
218+useState s = liftRender (Hooks.useState s)
219+
220+useState'
221+ :: forall ctx state hooks
222+ . state
223+ -> OmRender ctx hooks (Hooks.UseState state hooks) (state /\ (state -> Effect Unit))
224+useState' s = liftRender (Hooks.useState' s)
225+
226+useEffect
227+ :: forall ctx deps hooks
228+ . Eq deps
229+ => deps
230+ -> Effect (Effect Unit)
231+ -> OmRender ctx hooks (Hooks.UseEffect deps hooks) Unit
232+useEffect deps effect = liftRender (Hooks.useEffect deps effect)
233+
234+useEffectOnce
235+ :: forall ctx hooks
236+ . Effect (Effect Unit)
237+ -> OmRender ctx hooks (Hooks.UseEffect Unit hooks) Unit
238+useEffectOnce effect = liftRender (Hooks.useEffectOnce effect)
239+
240+useEffectAlways
241+ :: forall ctx hooks
242+ . Effect (Effect Unit)
243+ -> OmRender ctx hooks (Hooks.UseEffect Unit hooks) Unit
244+useEffectAlways effect = liftRender (Hooks.useEffectAlways effect)
245+
246+useLayoutEffect
247+ :: forall ctx deps hooks
248+ . Eq deps
249+ => deps
250+ -> Effect (Effect Unit)
251+ -> OmRender ctx hooks (Hooks.UseLayoutEffect deps hooks) Unit
252+useLayoutEffect deps effect = liftRender (Hooks.useLayoutEffect deps effect)
253+
254+useLayoutEffectOnce
255+ :: forall ctx hooks
256+ . Effect (Effect Unit)
257+ -> OmRender ctx hooks (Hooks.UseLayoutEffect Unit hooks) Unit
258+useLayoutEffectOnce effect = liftRender (Hooks.useLayoutEffectOnce effect)
259+
260+useLayoutEffectAlways
261+ :: forall ctx hooks
262+ . Effect (Effect Unit)
263+ -> OmRender ctx hooks (Hooks.UseLayoutEffect Unit hooks) Unit
264+useLayoutEffectAlways effect = liftRender (Hooks.useLayoutEffectAlways effect)
265+
266+useRef
267+ :: forall ctx a hooks
268+ . a
269+ -> OmRender ctx hooks (Hooks.UseRef a hooks) (Hooks.Ref a)
270+useRef a = liftRender (Hooks.useRef a)
271+
272+useMemo
273+ :: forall ctx deps a hooks
274+ . Eq deps
275+ => deps
276+ -> (Unit -> a)
277+ -> OmRender ctx hooks (Hooks.UseMemo deps a hooks) a
278+useMemo deps f = liftRender (Hooks.useMemo deps f)
279+
280+useReducer
281+ :: forall ctx state action hooks
282+ . state
283+ -> Hooks.Reducer state action
284+ -> OmRender ctx hooks (Hooks.UseReducer state action hooks) (state /\ (action -> Effect Unit))
285+useReducer s r = liftRender (Hooks.useReducer s r)
286+
287+useId :: forall ctx hooks. OmRender ctx hooks (Hooks.UseId hooks) String
288+useId = liftRender Hooks.useId
289+
290+useTransition
291+ :: forall ctx hooks
292+ . OmRender ctx hooks (Hooks.UseTransition hooks) (Boolean /\ ((Effect Unit) -> Effect Unit))
293+useTransition = liftRender Hooks.useTransition
294+
295+useDeferredValue
296+ :: forall ctx a hooks
297+ . a
298+ -> OmRender ctx hooks (Hooks.UseDeferredValue a hooks) a
299+useDeferredValue a = liftRender (Hooks.useDeferredValue a)
300+
301+-- ============================================================================
302+-- Component construction
303+-- ============================================================================
304+
305+-- | Create a React component with Om-based dependency injection.
306+-- | The context is captured once at construction time and made available
307+-- | to hooks via `useCtx` and `useOm` inside qualified-do blocks.
308+omComponent
309+ :: forall @ctx @err hooks props
310+ . String
311+ -> (props -> OmRender ctx Unit hooks JSX)
312+ -> Om { | ctx } err (props -> JSX)
313+omComponent name renderFn = Prelude.do
314+ ctx <- Om.ask
315+ let
316+ wrappedRender :: props -> Render Unit hooks JSX
317+ wrappedRender props =
318+ unsafeRenderEffect (Ref.write (unsafeCoerce ctx) ctxStash)
319+ `Render.discard` \_ -> un OmRender (renderFn props)
320+ Hooks.component name wrappedRender # liftEffect
src/Yoga/React/Om/Signal.pursnew file+30
@@ -0,0 +1,30 @@
1+module Yoga.React.Om.Signal
2+ ( Signal
3+ , mkSignal
4+ ) where
5+
6+import Prelude
7+
8+import Control.Monad.ST.Class (liftST)
9+import Effect (Effect)
10+import Effect.Ref as Ref
11+import FRP.Event (Event)
12+import FRP.Event as Event
13+
14+type Signal a =
15+ { read :: Effect a
16+ , modify :: (a -> a) -> Effect Unit
17+ , event :: Event a
18+ }
19+
20+mkSignal :: forall a. a -> Effect (Signal a)
21+mkSignal initial = do
22+ ref <- Ref.new initial
23+ { event, push } <- Event.create # liftST
24+ pure
25+ { read: Ref.read ref
26+ , modify: \f -> do
27+ new <- Ref.modify f ref
28+ push new
29+ , event
30+ }
test/Test/Main.jsnew file+3
@@ -0,0 +1,3 @@
1+import "global-jsdom/register";
2+
3+export function setupJsdom() {}
test/Test/Main.pursnew file+14
@@ -0,0 +1,14 @@
1+module Test.Main where
2+
3+import Prelude
4+
5+import Effect (Effect)
6+import Test.Spec.Discovery (discoverAndRunSpecs)
7+import Test.Spec.Reporter (consoleReporter)
8+
9+foreign import setupJsdom :: Effect Unit
10+
11+main :: Effect Unit
12+main = do
13+ setupJsdom
14+ discoverAndRunSpecs [ consoleReporter ] """Test\.Yoga\.React\.Om\..*Spec"""
test/Test/Yoga/React/Om/ComposableHooksSpec.pursnew file+46
@@ -0,0 +1,46 @@
1+module Test.Yoga.React.Om.ComposableHooksSpec where
2+
3+import Prelude
4+
5+import Effect (Effect)
6+import Effect.Ref (Ref)
7+import Yoga.Om (ask, toOm)
8+import Yoga.Om.Ref as Ref
9+import React.TestingLibrary (cleanup, render)
10+import Yoga.Om.Test (Spec, after_, describe, it, shouldEqual, textContentShouldEqual)
11+import Yoga.React.Om (OmRender, UseOm, omComponent, useOm)
12+import Yoga.React.Om as Om
13+import Yoga.React.DOM.Internal (text)
14+
15+useCounter
16+ :: forall ctx hooks
17+ . OmRender (counter :: Ref Int | ctx) hooks (UseOm (counter :: Ref Int | ctx) Int hooks) Int
18+useCounter = useOm \{ counter } -> Ref.read counter
19+
20+useLogger
21+ :: forall ctx hooks
22+ . String
23+ -> OmRender (logger :: String -> Effect Unit | ctx) hooks (UseOm (logger :: String -> Effect Unit | ctx) Unit hooks) Unit
24+useLogger msg = useOm \{ logger } -> logger msg
25+
26+mkDashboard = omComponent "Dashboard" \_ -> Om.do
27+ count <- useCounter
28+ useLogger ("Rendered with count: " <> show count)
29+ Om.pure $ text ("Dashboard: " <> show count)
30+
31+spec :: Spec Unit
32+spec = after_ cleanup do
33+ describe "composable context-slicing hooks" mkCtx do
34+ it "infers ctx as union of hook requirements" do
35+ { logRef } <- ask
36+ dashboard <- mkDashboard
37+ { findByText } <- render (dashboard unit)
38+ result <- findByText "Dashboard: 99" # toOm
39+ result `textContentShouldEqual` "Dashboard: 99"
40+ logged <- Ref.read logRef
41+ logged `shouldEqual` "Rendered with count: 99"
42+ where
43+ mkCtx = do
44+ logRef <- Ref.new ""
45+ counterRef <- Ref.new 99
46+ pure { counter: counterRef, logger: (\msg -> Ref.write msg logRef), logRef }
test/Test/Yoga/React/Om/ErrorChannelSpec.pursnew file+46
@@ -0,0 +1,46 @@
1+module Test.Yoga.React.Om.ErrorChannelSpec where
2+
3+import Prelude
4+
5+import Data.Either (Either(..))
6+import Data.Variant (match)
7+import React.Basic (JSX)
8+import Test.Spec (Spec, describe, it)
9+import Yoga.Om.Test (shouldEqual)
10+import Yoga.Om as Om
11+import Yoga.React.Om (omComponent)
12+import Yoga.React.Om as Om
13+import Yoga.React.DOM.Internal (text)
14+
15+type LoadErr r = (loadFailed :: String | r)
16+
17+spec :: Spec Unit
18+spec = describe "omComponent error channel" do
19+ it "propagates typed errors from before component creation" do
20+ result <- Om.runReader {} failingApp
21+ case result of
22+ Left err -> err # match
23+ { loadFailed: \msg -> msg `shouldEqual` "worker init failed"
24+ , exception: \_ -> pure unit
25+ }
26+ Right _ -> "should have failed" `shouldEqual` "but didn't"
27+
28+ it "allows error-free construction" do
29+ result <- Om.runReader {} successApp
30+ case result of
31+ Left _ -> "should have succeeded" `shouldEqual` "but didn't"
32+ Right _ -> pure unit
33+
34+loadWorker :: forall ctx err. Om.Om { | ctx } (LoadErr err) String
35+loadWorker = Om.throw { loadFailed: "worker init failed" }
36+
37+failingApp :: forall ctx err. Om.Om { | ctx } (LoadErr err) (Unit -> JSX)
38+failingApp = do
39+ workerUrl <- loadWorker
40+ omComponent "Never" \_ -> Om.do
41+ Om.pure $ text workerUrl
42+
43+successApp :: forall ctx err. Om.Om { | ctx } (LoadErr err) (Unit -> JSX)
44+successApp = do
45+ omComponent "OK" \_ -> Om.do
46+ Om.pure $ text "ok"
test/Test/Yoga/React/Om/LayerSpec.pursnew file+39
@@ -0,0 +1,39 @@
1+module Test.Yoga.React.Om.LayerSpec where
2+
3+import Prelude
4+
5+import Effect.Ref (Ref)
6+import Yoga.Om (toOm)
7+import Yoga.Om.Ref as Ref
8+import React.Basic (JSX)
9+import React.TestingLibrary (cleanup, render)
10+import Yoga.Om.Test (Spec, after_, describe, it, textContentShouldEqual)
11+import Yoga.Om.Layer (OmLayer, makeLayer, runLayer, provide)
12+import Yoga.React.Om (omComponent, useOm)
13+import Yoga.React.Om as Om
14+import Yoga.React.DOM.Internal (text)
15+
16+counterLayer :: OmLayer () () { counter :: Ref Int }
17+counterLayer = makeLayer do
18+ counter <- Ref.new 42
19+ pure { counter }
20+
21+mkApp :: OmLayer (counter :: Ref Int) () { app :: Unit -> JSX }
22+mkApp = makeLayer do
23+ omComponent "CounterDisplay"
24+ ( \_ -> Om.do
25+ n <- useOm \{ counter } -> Ref.read counter
26+ Om.pure $ text ("Count: " <> show n)
27+ ) <#> { app: _ }
28+
29+appLayer :: OmLayer () () { app :: Unit -> JSX }
30+appLayer = mkApp `provide` counterLayer
31+
32+spec :: Spec Unit
33+spec = after_ cleanup do
34+ describe "omComponent + OmLayer" (pure {}) do
35+ it "wires layers and renders from provided context" do
36+ { app } <- runLayer {} appLayer
37+ { findByText } <- render (app unit)
38+ result <- findByText "Count: 42" # toOm
39+ result `textContentShouldEqual` "Count: 42"
test/Test/Yoga/React/Om/OmComponentSpec.pursnew file+31
@@ -0,0 +1,31 @@
1+module Test.Yoga.React.Om.OmComponentSpec where
2+
3+import Prelude
4+
5+import React.TestingLibrary (cleanup, render)
6+import Yoga.Om (toOm)
7+import Yoga.Om.Test (Spec, after_, describe, it, itWith, textContentShouldEqual)
8+import Yoga.React.Om (omComponent, useCtx)
9+import Yoga.React.Om as Om
10+import Yoga.React.DOM.Internal (text)
11+
12+mkGreeter = omComponent "Greeter" \{ name } -> Om.do
13+ { greeting, magicNumber } <- useCtx
14+ Om.pure $ text (greeting <> ", " <> name <> "! You are " <> show magicNumber <> ".")
15+
16+spec :: Spec Unit
17+spec = after_ cleanup do
18+ describe "omComponent" (pure { greeting: "Hello", magicNumber: 42 }) do
19+ it "captures context and renders it" do
20+ comp <- mkGreeter
21+ { findByText } <- render (comp { name: "World" })
22+ result <- findByText "Hello, World! You are 42." # toOm
23+ result `textContentShouldEqual` "Hello, World! You are 42."
24+
25+ itWith "different context produces different output"
26+ (pure { greeting: "Howdy", magicNumber: 7 })
27+ do
28+ comp <- mkGreeter
29+ { findByText } <- render (comp { name: "Partner" })
30+ result <- findByText "Howdy, Partner! You are 7." # toOm
31+ result `textContentShouldEqual` "Howdy, Partner! You are 7."
test/Test/Yoga/React/Om/RefSpec.pursnew file+43
@@ -0,0 +1,43 @@
1+module Test.Yoga.React.Om.RefSpec where
2+
3+import Prelude
4+
5+import Data.Tuple.Nested ((/\))
6+import Effect.Class (liftEffect)
7+import Effect.Ref as Ref
8+import React.Basic (fragment)
9+import React.TestingLibrary (cleanup, fireEventClick, render)
10+import Yoga.Om (toOm)
11+import Yoga.Om.Test (Spec, after_, describe, it)
12+import Yoga.React.Om (omComponent, useCtx, useState)
13+import Yoga.React.Om as Om
14+import Yoga.React.DOM.HTML.Button (button)
15+import Yoga.React.DOM.Handler (onClick)
16+import Yoga.React.DOM.Internal (text)
17+
18+spec :: Spec Unit
19+spec = after_ cleanup do
20+ describe "omComponent + Ref interaction" mkCtx do
21+ it "mutates a Ref from ctx and re-renders" do
22+ counter <- omComponent "Counter" \_ -> Om.do
23+ { counter: ctr } <- useCtx
24+ count /\ setCount <- useState 0
25+ Om.pure $ fragment
26+ [ text ("Count: " <> show count)
27+ , button
28+ { onClick: onClick \_ -> do
29+ n <- Ref.modify (_ + 1) ctr
30+ setCount (const n)
31+ }
32+ "inc"
33+ ]
34+ { findByText } <- render (counter unit)
35+ _ <- findByText "Count: 0" # toOm
36+ btn <- findByText "inc" # toOm
37+ fireEventClick btn
38+ _ <- findByText "Count: 1" # toOm
39+ pure unit
40+ where
41+ mkCtx = do
42+ ref <- liftEffect $ Ref.new 0
43+ pure { counter: ref }
test/Test/Yoga/React/Om/SSRSpec.pursnew file+37
@@ -0,0 +1,37 @@
1+module Test.Yoga.React.Om.SSRSpec where
2+
3+import Prelude
4+
5+import Effect.Ref (Ref)
6+import Yoga.Om (Om)
7+import Yoga.Om.Ref as Ref
8+import React.Basic (JSX)
9+import Yoga.Om.Test (Spec, describe, it, itWith, shouldEqual, shouldNotEqual)
10+import Yoga.React.Om (omComponent, useOm)
11+import Yoga.React.Om as Om
12+import Yoga.React.DOM.Internal (text)
13+import Yoga.React.DOM.Server (renderToString, renderToStaticMarkup)
14+
15+spec :: Spec Unit
16+spec = describe "SSR" mkCtx do
17+ it "renderToString produces HTML with context values" do
18+ comp <- mkCounter
19+ let html = renderToString (comp unit)
20+ html `shouldNotEqual` ""
21+
22+ itWith "renderToStaticMarkup produces clean HTML"
23+ (mkCtxWith 7)
24+ do
25+ comp <- mkCounter
26+ let html = renderToStaticMarkup (comp unit)
27+ html `shouldEqual` "Count: 7"
28+ where
29+ mkCtxWith n = Ref.new n <#> { counter: _ }
30+ mkCtx = mkCtxWith 42
31+
32+mkCounter
33+ :: forall ctx err
34+ . Om { counter :: Ref Int | ctx } err (Unit -> JSX)
35+mkCounter = omComponent "SSRCounter" \_ -> Om.do
36+ n <- useOm (Ref.read <<< _.counter)
37+ Om.pure $ text $ "Count: " <> show n
test/Test/Yoga/React/Om/SharedCtxSpec.pursnew file+65
@@ -0,0 +1,65 @@
1+module Test.Yoga.React.Om.SharedCtxSpec where
2+
3+import Prelude
4+
5+import Data.Tuple.Nested ((/\))
6+import Effect (Effect)
7+import Effect.Class (liftEffect)
8+import Effect.Ref (Ref)
9+import Yoga.Om (Om, ask, toOm)
10+import Yoga.Om.Ref as Ref
11+import React.Basic (JSX, fragment)
12+import React.TestingLibrary (cleanup, fireEventClick, render)
13+import Yoga.Om.Test (Spec, after_, describe, it)
14+import Yoga.React.Om (omComponent, useCtx, useState, useEffectOnce)
15+import Yoga.React.Om as Om
16+import Yoga.React.DOM.HTML.Button (button)
17+import Yoga.React.DOM.Handler (onClick)
18+import Yoga.React.DOM.Internal (text)
19+
20+spec :: Spec Unit
21+spec = after_ cleanup do
22+ describe "shared context across components" mkCtx do
23+ it "two components share a Ref via Om context" do
24+ { notifyRef } <- ask
25+ inc /\ display <- do
26+ i <- mkIncrementer
27+ d <- mkDisplay
28+ pure (i /\ d)
29+ { findByText } <- render (fragment [ inc unit, display unit ])
30+ _ <- findByText "Value: 0" # toOm
31+ btn <- findByText "+1" # toOm
32+ fireEventClick btn
33+ liftEffect do
34+ cb <- Ref.read notifyRef
35+ cb
36+ _ <- findByText "Value: 1" # toOm
37+ pure unit
38+ where
39+ mkCtx = do
40+ ref <- Ref.new 0
41+ notifyRef <- Ref.new (pure unit :: Effect Unit)
42+ pure { counter: ref, notify: (\cb -> Ref.write cb notifyRef), notifyRef }
43+
44+mkIncrementer
45+ :: forall ctx err
46+ . Om { counter :: Ref Int | ctx } err (Unit -> JSX)
47+mkIncrementer =
48+ omComponent "Incrementer" \_props -> Om.do
49+ { counter } <- useCtx
50+ Om.pure $ button
51+ { onClick: onClick \_ -> Ref.modify_ (_ + 1) counter }
52+ "+1"
53+
54+mkDisplay
55+ :: forall ctx err
56+ . Om { counter :: Ref Int, notify :: (Effect Unit -> Effect Unit) | ctx } err (Unit -> JSX)
57+mkDisplay =
58+ omComponent "Display" \_ ->
59+ Om.do
60+ { counter, notify } <- useCtx
61+ n /\ setN <- useState 0
62+ useEffectOnce do
63+ notify (Ref.read counter >>= \v -> setN (const v))
64+ pure mempty
65+ Om.pure $ text ("Value: " <> show n)
test/Test/Yoga/React/Om/SignalSpec.pursnew file+52
@@ -0,0 +1,52 @@
1+module Test.Yoga.React.Om.SignalSpec where
2+
3+import Prelude
4+
5+import Effect.Class (liftEffect)
6+import React.Basic (JSX)
7+import React.TestingLibrary (cleanup, render)
8+import Yoga.Om (Om, ask, toOm)
9+import Yoga.Om.Test (Spec, after_, describe, it, itWith, textContentShouldEqual)
10+import Yoga.React.Om (omComponent, useCtx, useSignal)
11+import Yoga.React.Om as Om
12+import Yoga.React.Om.Signal (Signal, mkSignal)
13+import Yoga.React.DOM.Internal (text)
14+
15+spec :: Spec Unit
16+spec = after_ cleanup do
17+ describe "useSignal" (mkCtx 42) do
18+ it "renders initial signal value" do
19+ comp <- mkCounter
20+ { findByText } <- render (comp unit)
21+ result <- findByText "Count: 42" # toOm
22+ result `textContentShouldEqual` "Count: 42"
23+
24+ itWith "re-renders when signal is modified" (mkCtx 0) do
25+ { count: sig } <- ask
26+ comp <- mkCounter
27+ { findByText } <- render (comp unit)
28+ _ <- findByText "Count: 0" # toOm
29+ liftEffect $ sig.modify (_ + 1)
30+ result <- findByText "Count: 1" # toOm
31+ result `textContentShouldEqual` "Count: 1"
32+
33+ itWith "multiple signal updates" (mkCtx 0) do
34+ { count: sig } <- ask
35+ comp <- mkCounter
36+ { findByText } <- render (comp unit)
37+ _ <- findByText "Count: 0" # toOm
38+ sig.modify (_ + 10) # toOm
39+ _ <- findByText "Count: 10" # toOm
40+ sig.modify (_ + 5) # toOm
41+ result <- findByText "Count: 15" # toOm
42+ result `textContentShouldEqual` "Count: 15"
43+ where
44+ mkCtx n = liftEffect $ mkSignal n <#> { count: _ }
45+
46+mkCounter
47+ :: forall ctx err
48+ . Om { count :: Signal Int | ctx } err (Unit -> JSX)
49+mkCounter = omComponent "SignalCounter" \_ -> Om.do
50+ { count } <- useCtx
51+ n <- useSignal count
52+ Om.pure $ text ("Count: " <> show n)