diff --git a/moon.mod.json b/moon.mod.json index 770ee92..1df9497 100644 --- a/moon.mod.json +++ b/moon.mod.json @@ -1,6 +1,6 @@ { "name": "tiye/respo", - "version": "0.0.11", + "version": "0.0.12", "deps": { "tiye/dom-ffi": "0.0.4", "tiye/cirru-parser": "0.0.7" diff --git a/src/lib/app.mbt b/src/lib/app.mbt index 9ce2f45..bdf672d 100644 --- a/src/lib/app.mbt +++ b/src/lib/app.mbt @@ -1,28 +1,73 @@ -// pub trait RespoApp { -// // TODO better use RespoAction instead of Show -// dispatch(Self, (Show) -> UInt) -> Unit!@node.RespoCommonError -// pick_storage_key(Self) -> String -// get_mount_target(Self) -> @dom_ffi.Node -// get_store(Self) -> @json.FromJson -// // get_loop_delay() -> UInt? -// // view(Self) -> @node.RespoNode[T]!@node.RespoCommonError -// // render_loop(Self) -> Unit!@node.RespoCommonError -// backup_model_beforeunload(Self) -> Unit!@node.RespoCommonError -// try_load_storage(Self) -> Unit!@node.RespoCommonError -// } +/// get basic App structure +pub struct RespoApp[Model] { + store : Ref[Model] + storage_key : String + mount_target : @dom_ffi.Node +} -// impl RespoApp with try_load_storage(self) { -// let window = @dom_ffi.window() -// let storage = window.local_storage() -// let key = self.pick_storage_key() -// match storage.get_item(key) { -// Some(s) => { -// let store = self.get_store() -// store.val = try_from_string!(s) -// } -// None => @dom_ffi.log("no storage") -// } -// } +/// backup store to local storage before unload + +pub fn backup_model_beforeunload[Model : ToJson]( + self : RespoApp[Model] +) -> Unit { + let window = @dom_ffi.window() + let storage = window.local_storage() + let p = self.storage_key + let store = self.store.val + let beforeunload = fn(_e : @dom_ffi.BeforeUnloadEvent) { + let content = store.to_json().stringify() + // util::log!("before unload {} {}", p, content); + storage.set_item(p, content) + } + window.set_onbeforeunload(beforeunload) +} + +pub fn try_load_storage[Model : @json.FromJson + Default]( + key : String +) -> Model { + let window = @dom_ffi.window() + let storage = window.local_storage() + match storage.get_item(key) { + Some(s) => + match @json.parse?(s) { + Ok(j) => + match @json.from_json?(j) { + Ok(s) => s + Err(e) => { + @dom_ffi.log("failed to parse storage: \{e}") + Model::default() + } + } + Err(e) => { + @dom_ffi.log("failed to parse storage: \{e}") + Model::default() + } + } + None => { + @dom_ffi.log("no storage") + Model::default() + } + } +} + +pub fn RespoApp::render_loop[Model, ActionOp]( + self : RespoApp[Model], + renderer : () -> @node.RespoNode[ActionOp]!@node.RespoCommonError, + dispatch_action : (ActionOp) -> Unit!@node.RespoCommonError +) -> Unit { + let mount_target = self.mount_target + let ret = render_node?( + mount_target, + self.store, + renderer, + dispatch_action, + Some(100), + ) + match ret { + Ok(_) => () + Err(e) => @dom_ffi.error_log(e.to_string()) + } +} // pub trait RespoStore: ToJson + @json.FromJson { // // TODO better use RespoAction instead of Show diff --git a/src/lib/dialog/drawer.mbt b/src/lib/dialog/drawer.mbt index 8a0465f..f5b16e0 100644 --- a/src/lib/dialog/drawer.mbt +++ b/src/lib/dialog/drawer.mbt @@ -16,7 +16,7 @@ type DrawerRenderer[T] ((@node.DispatchFn[T]) -> Unit!@node.RespoCommonError) -> ]!@node.RespoCommonError fn to_string[T](self : DrawerRenderer[T]) -> String { - "(DrawerRenderer)" + "(DrawerRenderer \{self})" } pub fn DrawerRenderer::default[T]() -> DrawerRenderer[T] { diff --git a/src/lib/dialog/prompt.mbt b/src/lib/dialog/prompt.mbt index 8ef0d80..4f32a3c 100644 --- a/src/lib/dialog/prompt.mbt +++ b/src/lib/dialog/prompt.mbt @@ -27,7 +27,7 @@ pub struct PromptOptions { pub type PromptValidator (String) -> Result[Unit, String] pub fn to_string(self : PromptValidator) -> String { - "(PromptValidator)" + "(PromptValidator \{self})" } impl Show for PromptValidator with output(self, logger) { @@ -73,10 +73,10 @@ fn comp_prompt_modal[T : @node.RespoAction]( // @dom_ffi.log("Prompt state: " + state.to_json().stringify(indent=2)) let submit = on_submit let close = on_close - @dom_ffi.warn_log( - "recreate prompt modal with states: " + - states.data.to_json().stringify(indent=2), - ) + // @dom_ffi.warn_log( + // "recreate prompt modal with states: " + + // states.data.to_json().stringify(indent=2), + // ) let on_text_input = fn( e : @node.RespoEvent, dispatch : @node.DispatchFn[T] diff --git a/src/lib/node/dom-change.mbt b/src/lib/node/dom-change.mbt index 75c7f0c..a3025f2 100644 --- a/src/lib/node/dom-change.mbt +++ b/src/lib/node/dom-change.mbt @@ -131,7 +131,7 @@ pub fn to_cirru[T](self : DomChange[T]) -> @cirru.Cirru { effect_type.to_cirru(), coord_path_to_cirru(coord), dom_path_to_cirru(dom_path), - // skip_indexes.to_cirru(), // TODO + indexes_to_cirru(skip_indexes), ], ) ReplaceElement(~coord, ~dom_path, ~node) => @@ -279,7 +279,7 @@ pub fn to_cirru[T](self : ChildDomOp[T]) -> @cirru.Cirru { coord_path_to_cirru(nested_coord), dom_path_to_cirru(nested_dom_path), effect_type.to_cirru(), - // skip_indexes.to_cirru(), // TODO + indexes_to_cirru(skip_indexes), ], ) } @@ -296,7 +296,7 @@ pub fn get_dom_path[T](self : DomChange[T]) -> Array[UInt] { } } -pub fn to_cirru(self : RespoCoord) -> @cirru.Cirru { +pub fn RespoCoord::to_cirru(self : RespoCoord) -> @cirru.Cirru { match self { Key(key) => Leaf(key.to_string()) Comp(comp) => List([@cirru.Leaf("::Comp"), @cirru.Leaf(comp)]) @@ -312,3 +312,12 @@ pub fn dom_path_to_cirru(dom_path : Array[UInt]) -> @cirru.Cirru { let items = dom_path.map(fn(x) { @cirru.Leaf(x.to_string()) }) @cirru.List(items) } + +// turn @hashset.T[Int] to Cirru +pub fn indexes_to_cirru(xs : @hashset.T[Int]) -> @cirru.Cirru { + let items = [] + for x in xs { + items.push(@cirru.Leaf(x.to_string())) + } + @cirru.List(items) +} diff --git a/src/lib/renderer.mbt b/src/lib/renderer.mbt index aa2ffa2..d973182 100644 --- a/src/lib/renderer.mbt +++ b/src/lib/renderer.mbt @@ -23,12 +23,12 @@ pub fn mark_need_rerender() -> Unit { pub fn render_node[T, U]( mount_target : @dom_ffi.Node, // TODO it copies the whole store, need to optimize - get_store : () -> U, + store : Ref[U], renderer : () -> @node.RespoNode[T]!@node.RespoCommonError, dispatch_action : (T) -> Unit!@node.RespoCommonError, _interval : Float? ) -> Unit!@node.RespoCommonError { - let prev_store = @ref.new(get_store()) + let prev_store = @ref.new(store.val) let tree0 : @node.RespoNode[T] = renderer!() let prev_tree = @ref.new(tree0) let handle_event = fn( @@ -65,7 +65,7 @@ pub fn render_node[T, U]( @node.raf_loop( fn(_t : Float) -> Unit!@node.RespoCommonError { if drain_rerender_status() { - prev_store.val = get_store() + prev_store.val = store.val let new_tree = renderer!() let changes : Ref[Array[@node.DomChange[T]]] = Ref::new([]) @node.diff_tree!(new_tree, prev_tree.val, [], [], changes) diff --git a/src/main/main.mbt b/src/main/main.mbt index a0182a7..7281f99 100644 --- a/src/main/main.mbt +++ b/src/main/main.mbt @@ -1,28 +1,8 @@ let app_store_key : String = "demo_respo_store" -struct App { - store : Ref[Store] - mount_target : @dom_ffi.Node -} - -fn get_store(self : App) -> Ref[Store] { - self.store -} - -fn get_mount_target(self : App) -> @dom_ffi.Node { - self.mount_target -} - -fn dispatch(self : App, op : ActionOp) -> Unit { - // TODO intent - @dom_ffi.log("Action: " + op.to_string()) - self.store.val.update(op) -} - fn view( - self : App + store : Store ) -> @respo_node.RespoNode[ActionOp]!@respo_node.RespoCommonError { - let store = self.store.val // @dom_ffi.log("Store to render: " + store.to_json().stringify(indent=2)) let states = store.get_states() @respo_node.div( @@ -38,92 +18,28 @@ fn view( ) } -fn pick_storage_key() -> String { - app_store_key -} - -fn try_load_storage(self : App) -> Unit!@respo_node.RespoCommonError { - let window = @dom_ffi.window() - let storage = window.local_storage() - let key = pick_storage_key() - match storage.get_item(key) { - Some(s) => { - let store = self.get_store() - store.val = try_from_string!(s) - } - None => @dom_ffi.log("no storage") - } -} - -/// backup store to local storage before unload - -fn backup_model_beforeunload(self : App) -> Unit { - let window = @dom_ffi.window() - let storage = window.local_storage() - let p = pick_storage_key() - let store = self.get_store().val - let beforeunload = fn(_e : @dom_ffi.BeforeUnloadEvent) { - let content = store.to_string() - // util::log!("before unload {} {}", p, content); - storage.set_item(p, content) - } - window.set_onbeforeunload(beforeunload) -} - fn main { - let app : App = { - mount_target: @dom_ffi.window() + let window = @dom_ffi.window() + let mount_target = window .document() .query_selector(".app") - .reinterpret_as_node(), - store: Ref::new(Store::default()), - } - let ret = app.try_load_storage?() - match ret { - Ok(_) => () - Err(e) => @dom_ffi.error_log(e.to_string()) + .reinterpret_as_node() + let app : @respo.RespoApp[Store] = { + store: Ref::new(@respo.try_load_storage(app_store_key)), + mount_target, + storage_key: app_store_key, } app.backup_model_beforeunload() - @dom_ffi.log("store: " + app.store.val.to_json().stringify(indent=2)) - app.render_loop() - let window = @dom_ffi.window() + // @dom_ffi.log("store: " + app.store.val.to_json().stringify(indent=2)) + app.render_loop( + fn() { view!(app.store.val) }, + fn(op) { + @dom_ffi.log("Action: " + op.to_string()) + app.store.val.update(op) + }, + ) let dev_mode = @dom_ffi.get_url_search_params( window.get_location().get_search(), ).get("mode") @dom_ffi.log("dev mode: " + dev_mode.to_string()) } - -fn App::render_loop(self : App) -> Unit { - let mount_target = self.mount_target - // let store_to_action = global_store.to_owned() - - let dispatch_action = fn(op : ActionOp) -> Unit!@respo_node.RespoCommonError { - // @dom_ffi.log("Action: \{op.to_string()}") - // @dom_ffi.log("Global store: \{global_store.val.to_json()}") - - // Self::dispatch(store_to_action.to_owned(), op)?; - self.store.val.update(op) - match op { - Noop => raise @respo_node.RespoCommonError("TODO") - _ => () - } - } - let renderer = fn() -> @respo_node.RespoNode[_]!@respo_node.RespoCommonError { - let node = self.view!() - // @dom_ffi.log( - // @cirru.format?([node.to_cirru()], { use_inline: false }).unwrap(), - // ) - node - } - let ret = @respo.render_node?( - mount_target, - fn() { self.store }, - renderer, - dispatch_action, - Some(100), - ) - match ret { - Ok(_) => () - Err(e) => @dom_ffi.error_log(e.to_string()) - } -} diff --git a/src/main/store.mbt b/src/main/store.mbt index 2d254a8..d646d8f 100644 --- a/src/main/store.mbt +++ b/src/main/store.mbt @@ -15,15 +15,15 @@ struct Task { time : Double } derive(Default, Eq, Hash, ToJson, @json.FromJson) -enum IndentOp { - Noop - IncTwice -} +// enum IndentOp { +// Noop +// IncTwice +// } enum ActionOp { Noop StatesChange(@respo.RespoUpdateState) - Intent(IndentOp) + // Intent(IndentOp) Increment Decrement IncTwice @@ -46,7 +46,7 @@ fn ActionOp::to_string(self : ActionOp) -> String { Noop => "Noop" StatesChange(states) => "StatesChange(\{states.cursor} \{states.data.to_json()})" - Intent(intent) => "Intent(...)" + // Intent(intent) => "Intent(...)" Increment => "Increment" Decrement => "Decrement" IncTwice => "IncTwice" @@ -100,15 +100,3 @@ fn update(self : Store, op : ActionOp) -> Unit { fn to_string(self : Store) -> String { self.to_json().stringify(indent=2) } - -fn try_from_string(s : String) -> Store!@respo_node.RespoCommonError { - let j = match @json.parse?(s) { - Ok(j) => j - Err(e) => raise @respo_node.RespoCommonError(e.to_string()) - } - let store : Store = match @json.from_json?(j) { - Ok(s) => s - Err(e) => raise @respo_node.RespoCommonError(e.to_string()) - } - store -}