diff --git a/.gitignore b/.gitignore
index e78b0b2..74dca84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ target/
node_modules
dist
+
diff --git a/README.md b/README.md
index a4b1609..b862e0f 100644
--- a/README.md
+++ b/README.md
@@ -20,13 +20,13 @@ This is an experimental hobby project exploring MoonBit bindings for React. The
- `use_state[T](initial: T) -> (T, (T) -> Unit)` - State management hook
- `use_reducer[S: Default, A](initial?: S, reducer: (S, A) -> S) -> (S, (A) -> Unit)` - Reducer hook
- `use_effect_once(effect: () -> Unit) -> Unit` - Effect hook that runs only once
-- `use_effect_deps(effect: () -> Unit, deps: Array[JsValue]) -> Unit` - Effect hook with dependencies
-- `use_layout_effect_deps(effect: () -> Unit, deps: Array[JsValue]) -> Unit` - Layout effect hook
-- `use_memo_deps[A](factory: () -> A, deps: Array[JsValue]) -> A` - Memoization hook
-- `use_callback_deps[F](callback: F, deps: Array[JsValue]) -> F` - Callback memoization hook
-- `use_callback0_deps(f: () -> Unit, deps: Array[JsValue]) -> () -> Unit` - Zero-argument callback hook
+- `use_effect_deps(effect: () -> Unit, deps: Array[JsObscure]) -> Unit` - Effect hook with dependencies
+- `use_layout_effect_deps(effect: () -> Unit, deps: Array[JsObscure]) -> Unit` - Layout effect hook
+- `use_memo_deps[A](factory: () -> A, deps: Array[JsObscure]) -> A` - Memoization hook
+- `use_callback_deps[F](callback: F, deps: Array[JsObscure]) -> F` - Callback memoization hook
+- `use_callback0_deps(f: () -> Unit, deps: Array[JsObscure]) -> () -> Unit` - Zero-argument callback hook
- `use_ref[T: JsValueTrait](initial: T) -> ReactRef[T]` - Reference hook
-- `obscure[T](v: T) -> JsValue` - Dependency conversion helper function
+- `obscure[T](v: T) -> JsObscure` - Dependency conversion helper function
### HTML Element Bindings
@@ -61,6 +61,16 @@ This is an experimental hobby project exploring MoonBit bindings for React. The
## Quick Start
+Before writing any MoonBit code, make sure to include the React bindings in your project.
+
+```js
+import * as React from "react";
+import * as ReactDOMClient from "react-dom/client";
+
+window.React = React;
+window.ReactDOMClient = ReactDOMClient;
+```
+
Here's a simple example of how to use this library:
```moonbit
@@ -68,12 +78,12 @@ Here's a simple example of how to use this library:
struct ContainerProps {} derive(Default)
// Implement JsValueTrait for props
-impl @react.JsValueTrait for ContainerProps with to_value(_self) -> @react.JsValue {
+impl @react.JsValueTrait for ContainerProps with to_value(_self) -> @dom.JsObscure {
@react.JsObject::new().to_value()
}
impl @react.JsValueTrait for ContainerProps with from_value(
- _value : @react.JsValue,
+ _value : @dom.JsObscure,
) -> ContainerProps {
ContainerProps::default()
}
diff --git a/index.html b/index.html
index 0006726..d84e92a 100644
--- a/index.html
+++ b/index.html
@@ -4,7 +4,6 @@
TodoMVC - MoonBit + React
-
diff --git a/moon.mod.json b/moon.mod.json
index 7226f2d..f2db2ba 100644
--- a/moon.mod.json
+++ b/moon.mod.json
@@ -1,8 +1,8 @@
{
"name": "tiye/react",
- "version": "0.0.2-a3",
+ "version": "0.0.3",
"deps": {
- "tiye/dom-ffi": "0.1.1",
+ "tiye/dom-ffi": "0.1.2",
"tiye/respo_css": "0.1.2"
},
"readme": "README.md",
diff --git a/package.json b/package.json
index c59cce5..62b680e 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
},
"dependencies": {
"react": "^19.1.1",
- "react-dom": "^19.1.1"
+ "react-dom": "^19.1.1",
+ "todomvc-app-css": "^2.4.3"
}
}
diff --git a/src/hooks.mbt b/src/hooks.mbt
index 4320859..be9bb70 100644
--- a/src/hooks.mbt
+++ b/src/hooks.mbt
@@ -1,137 +1,129 @@
///|
// React Hooks wrappers, aligned with existing use_state style
+typealias @dom.JsObscure
+
///|
-extern "js" fn react_use_effect(effect : JsValue, deps : JsValue) -> Unit =
+extern "js" fn react_use_effect(
+ effect : @dom.JsObscure,
+ deps : Array[@dom.JsObscure],
+) -> Unit =
#| (effect, deps) => window.React.useEffect(effect, deps)
///|
extern "js" fn react_use_layout_effect(
- effect : JsValue,
- deps : JsValue,
+ effect : @dom.JsObscure,
+ deps : Array[@dom.JsObscure],
) -> Unit =
#| (effect, deps) => window.React.useLayoutEffect(effect, deps)
///|
-extern "js" fn react_use_memo(factory : JsValue, deps : JsValue) -> JsValue =
+extern "js" fn react_use_memo(
+ factory : @dom.JsObscure,
+ deps : Array[@dom.JsObscure],
+) -> @dom.JsObscure =
#| (factory, deps) => window.React.useMemo(factory, deps)
///|
extern "js" fn react_use_callback(
- callback : JsValue,
- deps : JsValue,
-) -> JsValue =
+ callback : @dom.JsObscure,
+ deps : Array[@dom.JsObscure],
+) -> JsObscure =
#| (callback, deps) => window.React.useCallback(callback, deps)
///|
-extern "js" fn react_use_ref(initial : JsValue) -> JsValue =
+extern "js" fn react_use_ref(initial : @dom.JsObscure) -> @dom.JsObscure =
#| (initial) => window.React.useRef(initial)
///|
-extern "js" fn react_ref_get_value(r : JsValue) -> JsValue =
+extern "js" fn react_ref_get_value(r : @dom.JsObscure) -> @dom.JsObscure =
#| (ref) => ref.current
///|
-extern "js" fn react_ref_set_value(r : JsValue, value : JsValue) -> Unit =
+extern "js" fn react_ref_set_value(
+ r : @dom.JsObscure,
+ value : JsObscure,
+) -> Unit =
#| (ref, value) => ref.current = value
///|
extern "js" fn react_use_reducer(
- reducer : JsValue,
- initial : JsValue,
-) -> JsValue =
+ reducer : JsObscure,
+ initial : JsObscure,
+) -> JsObscure =
#| (reducer, initial) => window.React.useReducer(reducer, initial)
///|
-extern "js" fn fn0_from_value(v : JsValue) -> () -> Unit =
- #| (v) => v
+fn fn0_to_js_obscure(f : () -> Unit) -> @dom.JsObscure = "%identity"
///|
-extern "js" fn fn1_from_value(v : JsValue) -> (JsValue) -> Unit =
- #| (v) => v
-
-///|
-/// 最多支持 8 个依赖项
-pub(all) enum Deps[A, B, C, D, E, F, G, H] {
- Dep0
- Dep1(A)
- Dep2(A, B)
- Dep3(A, B, C)
- Dep4(A, B, C, D)
- Dep5(A, B, C, D, E)
- Dep6(A, B, C, D, E, F)
- Dep7(A, B, C, D, E, F, G)
- Dep8(A, B, C, D, E, F, G, H)
-}
-
-///|
-fn deps_to_js_array(deps : Array[JsValue]) -> JsValue {
- let arr = JsArray::new()
- for d in deps {
- arr.push(d)
- }
- arr.to_value()
-}
+fn fn1_from_js_obscure(v : JsObscure) -> (JsObscure) -> Unit = "%identity"
///|
// useLayoutEffect
pub fn use_layout_effect_deps(
effect : () -> Unit,
- deps : Array[JsValue],
+ deps : Array[@dom.JsObscure],
) -> Unit {
- react_use_layout_effect(any_to_js_value(effect), deps_to_js_array(deps))
+ react_use_layout_effect(fn0_to_js_obscure(effect), deps)
}
///|
// useMemo
-pub fn[A] use_memo_deps(factory : () -> A, deps : Array[JsValue]) -> A {
- let v = react_use_memo(any_to_js_value(factory), deps_to_js_array(deps))
- any_from_js_value(v)
+pub fn[A] use_memo_deps(factory : () -> A, deps : Array[@dom.JsObscure]) -> A {
+ let v = react_use_memo(@dom.v_to_js_obscure(factory), deps)
+ @dom.js_obscure_to_v(v)
}
///|
// useCallback
-pub fn[F] use_callback_deps(callback : F, deps : Array[JsValue]) -> F {
- let v = react_use_callback(any_to_js_value(callback), deps_to_js_array(deps))
- any_from_js_value(v)
+pub fn[F] use_callback_deps(callback : F, deps : Array[@dom.JsObscure]) -> F {
+ let v = react_use_callback(@dom.v_to_js_obscure(callback), deps)
+ @dom.js_obscure_to_v(v)
}
///|
// useEffect
pub fn use_effect_once(effect : () -> Unit) -> Unit {
- react_use_effect(any_to_js_value(effect), JsArray::new().to_value())
+ react_use_effect(fn0_to_js_obscure(effect), [])
}
///|
-/// a short hand to turn value into JsValue in hook deps
-pub fn[T] obscure(v : T) -> JsValue = "%identity"
+/// a short hand to turn value into JsObscure in hook deps
+pub fn[T] obscure(v : T) -> @dom.JsObscure = "%identity"
///|
-pub fn use_effect_deps(effect : () -> Unit, deps : Array[JsValue]) -> Unit {
- react_use_effect(any_to_js_value(effect), deps_to_js_array(deps))
+pub fn use_effect_deps(
+ effect : () -> Unit,
+ deps : Array[@dom.JsObscure],
+) -> Unit {
+ react_use_effect(fn0_to_js_obscure(effect), deps)
}
///|
// useCallback (no argument)
-pub fn use_callback0_deps(f : () -> Unit, deps : Array[JsValue]) -> () -> Unit {
- let raw = react_use_callback(any_to_js_value(f), deps_to_js_array(deps))
- fn0_from_value(raw)
+pub fn use_callback0_deps(
+ f : () -> Unit,
+ deps : Array[@dom.JsObscure],
+) -> () -> Unit {
+ let raw = react_use_callback(@dom.v_to_js_obscure(f), deps)
+ @dom.js_obscure_to_v(raw)
}
///|
struct ReactRef[T] {
- js_value : JsValue
+ js_value : @dom.JsObscure
mut _v0 : T
}
///|
pub fn[T] ReactRef::from(v : T) -> ReactRef[T] {
- ReactRef::{ js_value: react_use_ref(any_to_js_value(v)), _v0: v }
+ ReactRef::{ js_value: react_use_ref(@dom.v_to_js_obscure(v)), _v0: v }
}
///|
pub fn[T] ReactRef::get(self : ReactRef[T]) -> T {
- any_from_js_value(react_ref_get_value(self.js_value))
+ @dom.js_obscure_to_v(react_ref_get_value(self.js_value))
}
///|
@@ -142,8 +134,11 @@ pub fn[T] ReactRef::set(self : ReactRef[T], value : T) -> Unit {
///|
// useRef
-pub fn[T : JsValueTrait] use_ref(initial : T) -> ReactRef[T] {
- ReactRef::{ js_value: react_use_ref(initial.to_value()), _v0: initial }
+pub fn[T] use_ref(initial : T) -> ReactRef[T] {
+ ReactRef::{
+ js_value: react_use_ref(@dom.v_to_js_obscure(initial)),
+ _v0: initial,
+ }
}
///|
@@ -155,8 +150,8 @@ pub fn[S : Default, A] use_reducer(
let pair = react_use_reducer(
any_to_js_value(reducer),
any_to_js_value(initial.unwrap_or_default()),
- ).to_array()
- let s0 = any_from_js_value(pair[0])
- let dispatch_raw = fn1_from_value(pair[1])
+ )
+ let s0 = any_from_js_value(pair.get("0"))
+ let dispatch_raw = fn1_from_js_obscure(pair.get("1"))
(s0, fn(a : A) { dispatch_raw(any_to_js_value(a)) })
}
diff --git a/src/js-value-trait.mbt b/src/js-value-trait.mbt
deleted file mode 100644
index 8d733d7..0000000
--- a/src/js-value-trait.mbt
+++ /dev/null
@@ -1,73 +0,0 @@
-///|
-pub(open) trait JsValueTrait {
- to_value(Self) -> JsValue
- from_value(JsValue) -> Self
-}
-
-///|
-pub(open) trait CastJsValueTrait {
- to_js_value(Self) -> JsValue = _
-}
-
-///|
-impl CastJsValueTrait with to_js_value(self) -> JsValue = "%identity"
-
-///|
-pub impl JsValueTrait for String with to_value(self) -> JsValue {
- JsValue::from_string(self)
-}
-
-///|
-pub impl JsValueTrait for String with from_value(value : JsValue) -> String {
- value.to_string()
-}
-
-///|
-pub impl JsValueTrait for Float with from_value(value : JsValue) -> Float {
- value.to_number()
-}
-
-///|
-pub impl JsValueTrait for Float with to_value(self) -> JsValue {
- JsValue::from_number(self)
-}
-
-///|
-pub impl JsValueTrait for Int with from_value(value : JsValue) -> Int {
- value.to_number().reinterpret_as_int()
-}
-
-///|
-pub impl JsValueTrait for Int with to_value(self) -> JsValue {
- JsValue::from_number(self.reinterpret_as_float())
-}
-
-///|
-pub impl JsValueTrait for Bool with to_value(self) -> JsValue {
- JsValue::from_bool(self)
-}
-
-///|
-pub impl JsValueTrait for Bool with from_value(value : JsValue) -> Bool {
- value.to_bool()
-}
-
-///|
-pub impl JsValueTrait for JsObject with to_value(self) -> JsValue {
- JsValue::from_object(self)
-}
-
-///|
-pub impl JsValueTrait for JsObject with from_value(value : JsValue) -> JsObject {
- value.to_object()
-}
-
-///|
-pub impl JsValueTrait for JsArray with to_value(self) -> JsValue {
- JsValue::from_array(self)
-}
-
-///|
-pub impl JsValueTrait for JsArray with from_value(value : JsValue) -> JsArray {
- value.to_array()
-}
diff --git a/src/js-value.mbt b/src/js-value.mbt
deleted file mode 100644
index 5af6b23..0000000
--- a/src/js-value.mbt
+++ /dev/null
@@ -1,125 +0,0 @@
-///|
-type JsValue
-
-///|
-extern "js" fn JsValue::from_string(value : String) -> JsValue =
- #| (value) => value
-
-///|
-/// unsafe
-extern "js" fn JsValue::to_string(self : JsValue) -> String =
- #| (self) => self
-
-///|
-extern "js" fn JsValue::from_number(value : Float) -> JsValue =
- #| (value) => value
-
-///|
-/// unsafe
-extern "js" fn JsValue::to_number(self : JsValue) -> Float =
- #| (self) => self
-
-///|
-extern "js" fn JsValue::from_bool(value : Bool) -> JsValue =
- #| (value) => value
-
-///|
-/// unsafe
-extern "js" fn JsValue::to_bool(self : JsValue) -> Bool =
- #| (self) => self
-
-///|
-pub extern "js" fn JsValue::from_object(value : JsObject) -> JsValue =
- #| (value) => value
-
-///|
-/// unsafe
-extern "js" fn JsValue::to_object(self : JsValue) -> JsObject =
- #| (self) => self
-
-///|
-extern "js" fn JsValue::from_array(value : JsArray) -> JsValue =
- #| (value) => value
-
-///|
-/// unsafe
-extern "js" fn JsValue::to_array(self : JsValue) -> JsArray =
- #| (self) => self
-
-///|
-extern "js" fn JsValue::from_fn(value : () -> JsValue) -> JsValue =
- #| (value) => value
-
-///|
-/// unsafe
-extern "js" fn JsValue::to_fn(self : JsValue) -> () -> JsValue =
- #| (self) => self
-
-///|
-type JsObject
-
-///|
-pub extern "js" fn JsObject::new() -> JsObject =
- #| () => ({})
-
-///|
-extern "js" fn JsObject::set(
- self : JsObject,
- key : String,
- value : JsValue,
-) -> Unit =
- #| (self, key, value) => self[key] = value
-
-///|
-pub extern "js" fn JsObject::get(self : JsObject, key : String) -> JsValue =
- #| (self, key) => self[key]
-
-///|
-/// Converts a JavaScript object to a JavaScript value.
-///
-/// Parameters:
-///
-/// * `self` : The JavaScript object to convert.
-///
-/// Returns a `JsValue` representing the same object.
-///
-/// Example:
-///
-/// ```moonbit
-/// let obj = @tiye/react.JsObject::new()
-/// let _value = obj.as_value()
-/// ```
-///
-pub extern "js" fn JsObject::as_value(self : JsObject) -> JsValue =
- #| (self) => self
-
-///|
-pub type JsArray
-
-///|
-pub extern "js" fn JsArray::to_string(self : JsArray) -> String =
- #| (self) => `${self}`
-
-///|
-pub extern "js" fn JsArray::new() -> JsArray =
- #| () => []
-
-///|
-pub extern "js" fn JsArray::op_get(self : JsArray, index : Int) -> JsValue =
- #| (self, index) => self[index]
-
-///|
-pub extern "js" fn JsArray::op_set(
- self : JsArray,
- index : Int,
- value : JsValue,
-) -> Unit =
- #| (self, index, value) => self[index] = value
-
-///|
-extern "js" fn JsArray::push(self : JsArray, value : JsValue) -> Unit =
- #| (self, value) => self.push(value)
-
-///|
-pub extern "js" fn JsArray::as_value(self : JsArray) -> JsValue =
- #| (self) => self
diff --git a/src/main/hooks-demo.mbt b/src/main/hooks-demo.mbt
index 1114436..c3b57e5 100644
--- a/src/main/hooks-demo.mbt
+++ b/src/main/hooks-demo.mbt
@@ -1,23 +1,11 @@
///|
-fnalias @react.obscure
+fnalias @react.(obscure, li)
///|
/// Hooks Demo Component - 展示新增的 React Hooks API 功能
///
struct HooksDemoProps {} derive(Default)
-///|
-impl @react.JsValueTrait for HooksDemoProps with to_value(_self) -> @react.JsValue {
- @react.JsObject::new().to_value()
-}
-
-///|
-impl @react.JsValueTrait for HooksDemoProps with from_value(
- _value : @react.JsValue,
-) -> HooksDemoProps {
- HooksDemoProps::default()
-}
-
///|
/// 主要的 Hooks 演示组件
fn comp_hooks_demo(_props : HooksDemoProps) -> VirtualNode {
@@ -130,51 +118,47 @@ fn comp_hooks_demo(_props : HooksDemoProps) -> VirtualNode {
set_timer_count(new_val)
},
style=@css.respo_style(
- padding=8.0 |> @css.Px,
- background_color=@css.Hsl(120, 70, 40),
- color=@css.White,
+ padding=8.0 |> Px,
+ background_color=Hsl(120, 70, 40),
+ color=White,
border_radius=4.0,
cursor=Pointer,
),
- [@react.Text("启动定时器")],
+ [Text("启动定时器")],
),
]),
// 功能说明区域
@react.div(
style=@css.respo_style(
- background_color=@css.Hsl(60, 30, 95),
+ background_color=Hsl(60, 30, 95),
padding=15.0 |> Px,
border_radius=6.0,
- border=CssBorder(1.0, Solid, @css.Hsl(60, 30, 80)),
+ border=CssBorder(1.0, Solid, Hsl(60, 30, 80)),
),
[
- @react.h3([@react.Text("📚 功能说明")]),
+ @react.h3([Text("📚 功能说明")]),
@react.ul([
- @react.li([
- @react.Text(
+ li([
+ Text(
"🎯 use_effect_once: 组件挂载时执行一次,查看控制台日志",
),
]),
- @react.li([
- @react.Text(
+ li([
+ Text(
"📊 use_effect_deps: 依赖计数器变化,每次点击增加都会触发",
),
]),
- @react.li([
- @react.Text(
+ li([
+ Text(
"🧮 use_memo_deps: 缓存计算结果,只在计数变化时重新计算",
),
]),
- @react.li([
- @react.Text(
- "🎮 use_callback_deps: 缓存回调函数,优化性能",
- ),
+ li([
+ Text("🎮 use_callback_deps: 缓存回调函数,优化性能"),
]),
- @react.li([
- @react.Text(
- "📝 use_ref: 创建可变引用,用于定时器计数",
- ),
+ li([
+ Text("📝 use_ref: 创建可变引用,用于定时器计数"),
]),
]),
],
diff --git a/src/main/main.mbt b/src/main/main.mbt
index 36a113d..ea3f844 100644
--- a/src/main/main.mbt
+++ b/src/main/main.mbt
@@ -1,18 +1,6 @@
///|
struct ContainerProps {} derive(Default)
-///|
-impl @react.JsValueTrait for ContainerProps with to_value(_self) -> @react.JsValue {
- @react.JsObject::new().to_value()
-}
-
-///|
-impl @react.JsValueTrait for ContainerProps with from_value(
- _value : @react.JsValue,
-) -> ContainerProps {
- ContainerProps::default()
-}
-
///|
fn comp_container(_v : ContainerProps) -> VirtualNode {
let (counter, set_counter) = @react.use_state(0.0.to_float())
@@ -27,10 +15,7 @@ fn comp_container(_v : ContainerProps) -> VirtualNode {
set_counter(counter + 1.0)
},
class_list=[style_counter],
- [
- @react.Fragment([@react.Text("Demo: ")]),
- @react.Text("Counter \{counter}"),
- ],
+ [Fragment([Text("Demo: ")]), Text("Counter \{counter}")],
),
@react.component(comp_todolist, TodoListProps::default(), []),
@react.component(comp_hooks_demo, HooksDemoProps::default(), []),
diff --git a/src/main/todolist.mbt b/src/main/todolist.mbt
index 7ff7fde..e523267 100644
--- a/src/main/todolist.mbt
+++ b/src/main/todolist.mbt
@@ -12,6 +12,10 @@ struct Todo {
completed : Bool
} derive(Default)
+///|
+// TodoList 组件的 Props
+struct TodoListProps {} derive(Default)
+
///|
// 编辑状态结构
struct EditingState {
@@ -27,22 +31,6 @@ enum Filter {
Completed
} derive(Eq)
-///|
-// TodoList 组件的 Props
-struct TodoListProps {} derive(Default)
-
-///|
-impl @react.JsValueTrait for TodoListProps with to_value(_self) -> @react.JsValue {
- @react.JsObject::new().to_value()
-}
-
-///|
-impl @react.JsValueTrait for TodoListProps with from_value(
- _value : @react.JsValue,
-) -> TodoListProps {
- TodoListProps::default()
-}
-
///|
/// TodoAction 枚举类型,表示对 todos 数组的不同操作
enum TodoAction {
@@ -193,9 +181,7 @@ fn comp_todolist(_v : TodoListProps) -> VirtualNode {
}
// 取消编辑的函数
- let cancel_editing = fn() {
- set_editing_state(EditingState::default())
- }
+ let cancel_editing = fn() { set_editing_state(EditingState::default()) }
// 生成 todo 项的函数
let create_todo_item = fn(todo : Todo) {
@@ -203,11 +189,9 @@ fn comp_todolist(_v : TodoListProps) -> VirtualNode {
Some(editing_id) => editing_id == todo.id
None => false
}
-
let checkbox_attrs = @react.ElementAttrs::new()
// 修复受控组件问题:始终设置 checked 属性
checkbox_attrs.set("checked", if todo.completed { "true" } else { "false" })
-
let li_class = {
let classes = []
if todo.completed {
@@ -218,7 +202,6 @@ fn comp_todolist(_v : TodoListProps) -> VirtualNode {
}
classes.join(" ")
}
-
if is_editing {
// 编辑模式
@react.li(class_name=li_class, [
@@ -271,7 +254,7 @@ fn comp_todolist(_v : TodoListProps) -> VirtualNode {
})
events
},
- [@react.Text(todo.text)],
+ [Text(todo.text)],
),
@react.button(
class_name="destroy",
@@ -305,7 +288,7 @@ fn comp_todolist(_v : TodoListProps) -> VirtualNode {
@react.section(class_name="todoapp", [
// 头部标题
@react.header(class_name="header", [
- @react.h1([@react.Text("todos")]),
+ @react.h1([Text("todos")]),
@react.input(
class_name="new-todo",
placeholder="What needs to be done?",
@@ -334,45 +317,40 @@ fn comp_todolist(_v : TodoListProps) -> VirtualNode {
// 主要内容区域
@react.section(class_name="main", [
@react.input(id="toggle-all", class_name="toggle-all", type_=Checkbox),
- @react.label(for_="toggle-all", [@react.Text("Mark all as complete")]),
+ @react.label(for_="toggle-all", [Text("Mark all as complete")]),
@react.ul(class_name="todo-list", todo_items),
]),
// 底部过滤器和统计
@react.footer(class_name="footer", [
- @react.span(class_name="todo-count", [
- @react.Text(active_count.to_string() + " items left"),
- ]),
+ @react.span(class_name="todo-count", [Text("\{active_count} items left")]),
@react.ul(class_name="filters", [
@react.li([
@react.a(
class_name=if current_filter == All { "selected" } else { "" },
- href="#/",
on_click=fn(_) { set_current_filter(All) },
- [@react.Text("All")],
+ [Text("All")],
),
]),
@react.li([
@react.a(
class_name=if current_filter == Active { "selected" } else { "" },
- href="#/active",
on_click=fn(_) { set_current_filter(Active) },
- [@react.Text("Active")],
+ [Text("Active")],
),
]),
@react.li([
@react.a(
class_name=if current_filter == Completed { "selected" } else { "" },
- href="#/completed",
on_click=fn(_) { set_current_filter(Completed) },
- [@react.Text("Completed")],
+ [Text("Completed")],
),
]),
]),
@react.button(
class_name="clear-completed",
on_click=fn(_) { dispatch(ClearCompleted) },
- [@react.Text("Clear completed")],
+ [Text("Clear completed")],
),
]),
])
diff --git a/src/react.mbt b/src/react.mbt
index ea59f2b..b184150 100644
--- a/src/react.mbt
+++ b/src/react.mbt
@@ -5,7 +5,7 @@ fnalias @css.respo_style
typealias @css.RespoStyle
///|
-extern "js" fn react_render(vdom : JsValue, parent : @dom.Node) -> Unit =
+extern "js" fn react_render(vdom : @dom.JsObscure, parent : @dom.Node) -> Unit =
#| (vdom, parent) => {
#| const root = window.ReactDOMClient.createRoot(parent);
#| root.render(vdom);
@@ -35,40 +35,42 @@ extern "js" fn react_render(vdom : JsValue, parent : @dom.Node) -> Unit =
/// render(custom_elem.to_node(), document.body)
/// ```
pub fn render(vdom : VirtualNode, parent : @dom.Element) -> Unit {
- react_render(vdom.to_js_value(), parent.reinterpret_as_node())
+ react_render(vdom.to_js_obscure(), parent.reinterpret_as_node())
}
///|
extern "js" fn react_create_element(
tag : String,
- props : JsValue,
- children : JsValue,
-) -> JsValue =
+ props : @dom.JsObjectObscure,
+ children : Array[@dom.JsObscure],
+) -> @dom.JsObscure =
#| (tag, props, children) => window.React.createElement(tag, props, ...children)
///|
-extern "js" fn react_fragment(props : JsValue, children : JsValue) -> JsValue =
+extern "js" fn react_fragment(
+ props : @dom.JsObjectObscure,
+ children : Array[@dom.JsObscure],
+) -> @dom.JsObscure =
#| (props, children) => window.React.createElement(window.React.Fragment, props, ...children)
///|
-extern "js" fn react_use_state(initial : JsValue) -> JsValue =
+extern "js" fn react_use_state(initial : JsObscure) -> JsObscure =
#| (initial) => { return window.React.useState(initial)}
///|
-extern "js" fn state_updater_from_value(v : JsValue) -> (JsValue) -> Unit =
- #| (v) => v
+fn state_updater_from_value(v : JsObscure) -> (JsObscure) -> Unit = "%identity"
///|
-fn[T] any_to_js_value(v : T) -> JsValue = "%identity"
+fn[T] any_to_js_value(v : T) -> JsObscure = "%identity"
///|
-fn[T] any_from_js_value(v : JsValue) -> T = "%identity"
+fn[T] any_from_js_value(v : JsObscure) -> T = "%identity"
///|
pub fn[T] use_state(initial : T) -> (T, (T) -> Unit) {
- let pair = react_use_state(any_to_js_value(initial)).to_array()
- let s0 = pair[0]
- let s1 = state_updater_from_value(pair[1])
+ let pair = react_use_state(any_to_js_value(initial))
+ let s0 = pair.get("0")
+ let s1 = state_updater_from_value(pair.get("1"))
(any_from_js_value(s0), fn(value : T) { s1(any_to_js_value(value)) })
}
@@ -103,7 +105,7 @@ pub fn ElementAttrs::set(
pub type DOMEvent
///|
-pub fn DOMEvent::to_js_any_value(self : DOMEvent) -> JsValue = "%identity"
+pub fn DOMEvent::to_js_obscure(self : DOMEvent) -> JsObscure = "%identity"
///|
/// 获取事件目标元素的值(通常用于 input、textarea 等表单元素)
@@ -161,7 +163,7 @@ pub extern "js" fn DOMEvent::meta_key(self : DOMEvent) -> Bool =
#| (event) => event.metaKey || false
///|
-pub extern "js" fn console_log2(msg : String, v : JsValue) -> Unit =
+pub extern "js" fn console_log2(msg : String, v : JsObscure) -> Unit =
#| (msg, v) => { console.log(msg, v) }
///|
@@ -313,8 +315,7 @@ pub fn ElementEvents::new() -> ElementEvents {
}
///|
-extern "js" fn DOMEventHandler::to_js_func(self : DOMEventHandler) -> JsValue =
- #| (f) => { return f }
+fn DOMEventHandler::to_js_func(self : DOMEventHandler) -> JsObscure = "%identity"
///|
/// 使用 DOMEventType 添加事件处理器
@@ -340,12 +341,12 @@ pub fn ElementEvents::set(
}
///|
-pub fn ElementEvents::to_js_value(self : ElementEvents) -> JsValue {
- let obj = JsObject::new()
+pub fn ElementEvents::to_js_value(self : ElementEvents) -> JsObscure {
+ let obj = @dom.JsObjectObscure::new()
for event_type, value in self.inner() {
obj.set(event_type.to_string(), value.to_js_func())
}
- JsValue::from_object(obj)
+ obj.to_js_obscure()
}
///|
@@ -361,22 +362,22 @@ pub(all) enum VirtualNode {
/// A text node containing plain string content
Text(String)
/// A pre-converted JavaScript value for advanced usage (e.g., from `component` function)
- /// already converted to JsValue, hard to convert back
- JsNode(JsValue) // for advanced usage, e.g. connect
+ /// already converted to JsObscure, hard to convert back
+ JsNode(@dom.JsObscure) // for advanced usage, e.g. connect
}
///|
-pub fn VirtualNode::to_js_value(self : VirtualNode) -> JsValue {
+pub fn VirtualNode::to_js_obscure(self : VirtualNode) -> @dom.JsObscure {
let ret = match self {
Element(el) => el.to_js_value()
Fragment(children) => {
- let v = JsArray::new()
+ let v = []
for child in children {
- v.push(child.to_js_value())
+ v.push(child.to_js_obscure())
}
- react_fragment(JsObject::new().to_value(), v.to_value())
+ react_fragment(@dom.JsObjectObscure::new(), v)
}
- Text(t) => JsValue::from_string(t)
+ Text(t) => JsObscure::from_string(t)
JsNode(v) => v
}
ret
@@ -475,32 +476,54 @@ pub fn css_prop_to_camel_case(name : String) -> String {
}
///|
-fn VirtualElement::to_js_value(self : VirtualElement) -> JsValue {
- let props = JsObject::new()
+/// Converts a RespoStyle to a JavaScript style object.
+/// This function takes CSS properties from RespoStyle and converts them to
+/// React-compatible camelCase property names with string values.
+///
+/// # Parameters
+/// - `style`: RespoStyle containing CSS properties
+///
+/// # Returns
+/// `JsObject` - JavaScript object with camelCase CSS properties
+///
+/// # Example
+/// ```moonbit_nocheck
+/// let style = @css.respo_style(background_color=Red, font_size=16.0 |> Px)
+/// let js_style = convert_style_to_js_object(style)
+/// // Results in: { backgroundColor: "red", fontSize: "16px" }
+/// ```
+
+///|
+pub fn convert_style_to_js_object(
+ style : @css.RespoStyle,
+) -> @dom.JsObjectObscure {
+ let style_obj = @dom.JsObjectObscure::new()
+ for _idx, pair in style.0 {
+ let (key, value) = pair
+ style_obj.set(css_prop_to_camel_case(key), JsObscure::from_string(value))
+ }
+ style_obj
+}
+
+///|
+fn VirtualElement::to_js_value(self : VirtualElement) -> @dom.JsObscure {
+ let props = @dom.JsObjectObscure::new()
for key, value in self.attrs.inner() {
let react_prop_name = dom_attr_to_react_prop(key)
let prop_value = convert_prop_value(react_prop_name, value)
props.set(react_prop_name, prop_value)
}
- let style = JsObject::new()
- for _idx, pair in self.style.0 {
- let (key, value) = pair
- style.set(css_prop_to_camel_case(key), JsValue::from_string(value))
- }
- props.set("style", JsValue::from_object(style))
- let children = JsArray::new()
+ let style = convert_style_to_js_object(self.style)
+ props.set("style", style.to_js_obscure())
+ let children = []
for child in self.children {
- children.push(child.to_js_value())
+ children.push(child.to_js_obscure())
}
for event_type, value in self.event.inner() {
let event_name = dom_event_to_react_handler(event_type)
- props.set(event_name, value.to_js_func())
+ props.set(event_name, @dom.v_to_js_obscure(value))
}
- react_create_element(
- self.name,
- JsValue::from_object(props),
- JsValue::from_array(children),
- )
+ react_create_element(self.name, props, children)
}
///|
@@ -539,10 +562,10 @@ pub fn create_element(
///|
extern "js" fn create_factory(
- f : (JsValue) -> JsValue,
- props : JsValue,
- children : Array[JsValue],
-) -> JsValue =
+ f : (@dom.JsObscure) -> @dom.JsObscure,
+ props : @dom.JsObscure,
+ children : Array[@dom.JsObscure],
+) -> @dom.JsObscure =
#| (f, props, children) => {
#| let h0 = window.React.createElement(f, props, ...children);
#| return h0;
@@ -567,7 +590,7 @@ extern "js" fn create_factory(
///
/// let node = component(my_button, ButtonProps { text: "Click me", disabled: false }, [])
/// ```
-pub fn[T : JsValueTrait] component(
+pub fn[T] component(
/// Component function that transforms props into a virtual node
f : (T) -> VirtualNode,
/// Props to pass to the component function
@@ -575,12 +598,12 @@ pub fn[T : JsValueTrait] component(
/// Child nodes to render inside the component
children : Array[VirtualNode],
) -> VirtualNode {
- let children_js = children.map(fn(child) { child.to_js_value() })
+ let children_js = children.map(fn(child) { child.to_js_obscure() })
let r = create_factory(
- fn(p) { f(T::from_value(p)).to_js_value() },
+ fn(p) { f(@dom.js_obscure_to_v(p)).to_js_obscure() },
// TODO maybe better to use { value: PROPS} instead of props.to_value()
// need future refactor to explore it
- props.to_value(),
+ @dom.v_to_js_obscure(props),
children_js,
)
JsNode(r)
@@ -722,21 +745,19 @@ pub fn is_boolean_prop(prop_name : String) -> Bool {
/// - `value`: String value from the attribute.
///
/// # Returns
-/// `JsValue` - Properly typed JavaScript value for React.
+/// `JsObscure` - Properly typed JavaScript value for React.
///
/// # Inspect Examples
-/// inspect(convert_prop_value("checked", "true"), content="JsValue::from_bool(true)")
-/// inspect(convert_prop_value("className", "my-class"), content="JsValue::from_string(\"my-class\")")
-pub fn convert_prop_value(prop_name : String, value : String) -> JsValue {
+/// inspect(convert_prop_value("checked", "true"), content="JsObscure::from_bool(true)")
+/// inspect(convert_prop_value("className", "my-class"), content="JsObscure::from_string(\"my-class\")")
+pub fn convert_prop_value(prop_name : String, value : String) -> @dom.JsObscure {
if is_boolean_prop(prop_name) {
match value {
- "true" => JsValue::from_bool(true)
- "false" => JsValue::from_bool(false)
- _ => JsValue::from_string(value) // fallback for non-standard values
+ "true" => JsObscure::from_bool(true)
+ "false" => JsObscure::from_bool(false)
+ _ => @dom.v_to_js_obscure(value) // fallback for non-standard values
}
} else {
- JsValue::from_string(value)
+ @dom.v_to_js_obscure(value)
}
}
-
-/// ElementEvents::new()
diff --git a/src/react.mjs b/src/react.mjs
index a1dcbec..665c6c5 100644
--- a/src/react.mjs
+++ b/src/react.mjs
@@ -1,5 +1,7 @@
import * as React from "react";
import * as ReactDOMClient from "react-dom/client";
+// 导入官方 TodoMVC CSS
+import "todomvc-app-css/index.css";
console.log(ReactDOMClient);
diff --git a/src/preact_test.mbt b/src/react_test.mbt
similarity index 100%
rename from src/preact_test.mbt
rename to src/react_test.mbt
diff --git a/styles/todomvc.css b/styles/todomvc.css
deleted file mode 100644
index 8f64f57..0000000
--- a/styles/todomvc.css
+++ /dev/null
@@ -1,381 +0,0 @@
-html,
-body {
- margin: 0;
- padding: 0;
-}
-
-button {
- margin: 0;
- padding: 0;
- border: 0;
- background: none;
- font-size: 100%;
- vertical-align: baseline;
- font-family: inherit;
- font-weight: inherit;
- color: inherit;
- -webkit-appearance: none;
- appearance: none;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-body {
- font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
- line-height: 1.4em;
- background: #f5f5f5;
- color: #111111;
- min-width: 230px;
- max-width: 550px;
- margin: 0 auto;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- font-weight: 300;
-}
-
-:focus {
- outline: 0;
-}
-
-.hidden {
- display: none;
-}
-
-.todoapp {
- background: #fff;
- margin: 130px 0 40px 0;
- position: relative;
- box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
- 0 25px 50px 0 rgba(0, 0, 0, 0.1);
-}
-
-.todoapp input::-webkit-input-placeholder {
- font-style: italic;
- font-weight: 300;
- color: rgba(0, 0, 0, 0.4);
-}
-
-.todoapp input::-moz-placeholder {
- font-style: italic;
- font-weight: 300;
- color: rgba(0, 0, 0, 0.4);
-}
-
-.todoapp input::input-placeholder {
- font-style: italic;
- font-weight: 300;
- color: rgba(0, 0, 0, 0.4);
-}
-
-.todoapp h1 {
- position: absolute;
- top: -140px;
- width: 100%;
- font-size: 80px;
- font-weight: 200;
- text-align: center;
- color: #b83f45;
- -webkit-text-rendering: optimizeLegibility;
- -moz-text-rendering: optimizeLegibility;
- text-rendering: optimizeLegibility;
-}
-
-.new-todo,
-.edit {
- position: relative;
- margin: 0;
- width: 100%;
- font-size: 24px;
- font-family: inherit;
- font-weight: inherit;
- line-height: 1.4em;
- color: inherit;
- padding: 6px;
- border: 1px solid #999;
- box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
- box-sizing: border-box;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-.new-todo {
- padding: 16px 16px 16px 60px;
- border: none;
- background: rgba(0, 0, 0, 0.003);
- box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
-}
-
-.main {
- position: relative;
- z-index: 2;
- border-top: 1px solid #e6e6e6;
-}
-
-.toggle-all {
- width: 1px;
- height: 1px;
- border: none; /* Mobile Safari */
- opacity: 0;
- position: absolute;
- right: 100%;
- bottom: 100%;
-}
-
-.toggle-all + label {
- width: 60px;
- height: 34px;
- font-size: 0;
- position: absolute;
- top: -52px;
- left: -13px;
- -webkit-transform: rotate(90deg);
- transform: rotate(90deg);
-}
-
-.toggle-all + label:before {
- content: '❯';
- font-size: 22px;
- color: #e6e6e6;
- padding: 10px 27px 10px 27px;
-}
-
-.toggle-all:checked + label:before {
- color: #737373;
-}
-
-.todo-list {
- margin: 0;
- padding: 0;
- list-style: none;
-}
-
-.todo-list li {
- position: relative;
- font-size: 24px;
- border-bottom: 1px solid #ededed;
-}
-
-.todo-list li:last-child {
- border-bottom: none;
-}
-
-.todo-list li.editing {
- border-bottom: none;
- padding: 0;
-}
-
-.todo-list li.editing .edit {
- display: block;
- width: calc(100% - 43px);
- padding: 12px 16px;
- margin: 0 0 0 43px;
-}
-
-.todo-list li.editing .view {
- display: none;
-}
-
-.todo-list li .toggle {
- text-align: center;
- width: 40px;
- /* auto, since non-WebKit browsers doesn't support input styling */
- height: auto;
- position: absolute;
- top: 0;
- bottom: 0;
- margin: auto 0;
- border: none; /* Mobile Safari */
- -webkit-appearance: none;
- appearance: none;
-}
-
-.todo-list li .toggle {
- opacity: 0;
-}
-
-.todo-list li .toggle + label {
- /*
- Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
- IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
- */
- background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
- background-repeat: no-repeat;
- background-position: center left;
-}
-
-.todo-list li .toggle:checked + label {
- background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
-}
-
-.todo-list li label {
- word-break: break-all;
- padding: 15px 15px 15px 60px;
- display: block;
- line-height: 1.2;
- transition: color 0.4s;
- font-weight: 400;
- color: #4d4d4d;
-}
-
-.todo-list li.completed label {
- color: #cdcdcd;
- text-decoration: line-through;
-}
-
-.todo-list li .destroy {
- display: none;
- position: absolute;
- top: 0;
- right: 10px;
- bottom: 0;
- width: 40px;
- height: 40px;
- margin: auto 0;
- font-size: 30px;
- color: #cc9a9a;
- margin-bottom: 11px;
- transition: color 0.2s ease-out;
-}
-
-.todo-list li .destroy:hover {
- color: #af5b5e;
-}
-
-.todo-list li .destroy:after {
- content: '×';
-}
-
-.todo-list li:hover .destroy {
- display: block;
-}
-
-.todo-list li .edit {
- display: none;
-}
-
-.todo-list li.editing:last-child {
- margin-bottom: -1px;
-}
-
-.footer {
- padding: 10px 15px;
- height: 20px;
- text-align: center;
- font-size: 15px;
- border-top: 1px solid #e6e6e6;
- color: #777;
-}
-
-.footer:before {
- content: '';
- position: absolute;
- right: 0;
- bottom: 0;
- left: 0;
- height: 50px;
- overflow: hidden;
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
- 0 8px 0 -3px #f6f6f6,
- 0 9px 1px -3px rgba(0, 0, 0, 0.2),
- 0 16px 0 -6px #f6f6f6,
- 0 17px 2px -6px rgba(0, 0, 0, 0.2);
-}
-
-.todo-count {
- float: left;
- text-align: left;
-}
-
-.todo-count strong {
- font-weight: 300;
-}
-
-.filters {
- margin: 0;
- padding: 0;
- list-style: none;
- position: absolute;
- right: 0;
- left: 0;
-}
-
-.filters li {
- display: inline;
-}
-
-.filters li a {
- color: inherit;
- margin: 3px;
- padding: 3px 7px;
- text-decoration: none;
- border: 1px solid transparent;
- border-radius: 3px;
-}
-
-.filters li a:hover {
- border-color: rgba(175, 47, 47, 0.1);
-}
-
-.filters li a.selected {
- border-color: rgba(175, 47, 47, 0.2);
-}
-
-.clear-completed,
-html .clear-completed:active {
- float: right;
- position: relative;
- line-height: 19px;
- text-decoration: none;
- cursor: pointer;
-}
-
-.clear-completed:hover {
- text-decoration: underline;
-}
-
-.info {
- margin: 65px auto 0;
- color: #4d4d4d;
- font-size: 11px;
- text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
- text-align: center;
-}
-
-.info p {
- line-height: 1;
-}
-
-.info a {
- color: inherit;
- text-decoration: none;
- font-weight: 400;
-}
-
-.info a:hover {
- text-decoration: underline;
-}
-
-/*
- Hack to remove background from Mobile Safari.
- Can't use it globally since it destroys checkboxes in Firefox
-*/
-@media screen and (-webkit-min-device-pixel-ratio:0) {
- .toggle-all,
- .todo-list li .toggle {
- background: none;
- }
-
- .todo-list li .toggle {
- height: 40px;
- }
-}
-
-@media (max-width: 430px) {
- .footer {
- height: 50px;
- }
-
- .filters {
- bottom: 10px;
- }
-}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 246fadb..a241e6d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -320,6 +320,11 @@ source-map-js@^1.2.1:
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+todomvc-app-css@^2.4.3:
+ version "2.4.3"
+ resolved "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.4.3.tgz"
+ integrity sha512-mSnWZaKBWj9aQcFRsGguY/a8O8NR8GmecD48yU1rzwNemgZa/INLpIsxxMiToFGVth+uEKBrQ7IhWkaXZxwq5Q==
+
vite@^5.4.8:
version "5.4.14"
resolved "https://registry.npmmirror.com/vite/-/vite-5.4.14.tgz"