diff --git a/CHANGELOG.md b/CHANGELOG.md index 94d2d4ab0..4fa2a7961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ - Fixed jumping cursor in inputs (#158) - Added method `orders.after_next_render(Option)` (#207) - Fixed a bug with back/forward routing to the landing page (#296) +- [BREAKING] Deprecated `Init` struct, replacing it with `BeforeMount` and `AfterMount` structs to +better denote state before and after mounting the `App` occurs. +- [BREAKING] Added a new function `builder` which replaces `build` as part of deprecating `Init`. +- [BREAKING] Added a new function `build` which replaces `finish` as part of deprecating `Init`. +- Added `IntoInit`, `IntoAfterMount`, and `IntoBeforeMount` traits. It is possible to use these +in place of a closure or function to produce the corresponding `Init`, `AfterMount`, and +`BeforeMount` structs. +- Messages sent from `IntoAfterMount` will now be run after the routing message. ## v0.4.2 - Added an `Init` struct, which can help with initial routing (Breaking) diff --git a/examples/animation_frame/src/lib.rs b/examples/animation_frame/src/lib.rs index f73ddbed6..241d5b29a 100644 --- a/examples/animation_frame/src/lib.rs +++ b/examples/animation_frame/src/lib.rs @@ -51,13 +51,13 @@ struct Model { car: Car, } -// Init +// AfterMount -fn init(_: Url, orders: &mut impl Orders) -> Init { +fn after_mount(_: Url, orders: &mut impl Orders) -> AfterMount { orders .send_msg(Msg::SetViewportWidth) .after_next_render(Msg::Rendered); - Init::new(Model::default()) + AfterMount::default() } // Update @@ -162,7 +162,8 @@ fn view_wheel(wheel_x: f64, car: &Car) -> Node { #[wasm_bindgen(start)] pub fn render() { - seed::App::build(init, update, view) + seed::App::builder(update, view) + .after_mount(after_mount) .window_events(|_| vec![simple_ev(Ev::Resize, Msg::SetViewportWidth)]) .build_and_start(); } diff --git a/examples/canvas/src/lib.rs b/examples/canvas/src/lib.rs index 9cc48ad43..d31b906ab 100644 --- a/examples/canvas/src/lib.rs +++ b/examples/canvas/src/lib.rs @@ -18,11 +18,11 @@ struct Model { fill_color: Color, } -// Init +// AfterMount -fn init(_: Url, orders: &mut impl Orders) -> Init { +fn after_mount(_: Url, orders: &mut impl Orders) -> AfterMount { orders.after_next_render(|_| Msg::Rendered); - Init::new(Model { + AfterMount::new(Model { fill_color: COLOR_A, }) } @@ -87,5 +87,7 @@ fn view(_model: &Model) -> impl View { #[wasm_bindgen(start)] pub fn render() { - seed::App::build(init, update, view).build_and_start(); + seed::App::builder(update, view) + .after_mount(after_mount) + .build_and_start(); } diff --git a/examples/counter/src/lib.rs b/examples/counter/src/lib.rs index 222a3a8c3..7579e0fd9 100644 --- a/examples/counter/src/lib.rs +++ b/examples/counter/src/lib.rs @@ -105,5 +105,5 @@ fn view(model: &Model) -> impl View { #[wasm_bindgen(start)] pub fn render() { - seed::App::build(|_, _| Init::new(Model::default()), update, view).build_and_start(); + seed::App::builder(update, view).build_and_start(); } diff --git a/examples/drop/README.md b/examples/drop/README.md index 3af5f6fd9..8a4db658d 100644 --- a/examples/drop/README.md +++ b/examples/drop/README.md @@ -1,6 +1,6 @@ ## Drop example -How to crate a drop-zone. +How to create a drop-zone. --- @@ -8,4 +8,4 @@ How to crate a drop-zone. cargo make start ``` -Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. \ No newline at end of file +Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. diff --git a/examples/drop/src/lib.rs b/examples/drop/src/lib.rs index db32dd66a..9d5bc4a63 100644 --- a/examples/drop/src/lib.rs +++ b/examples/drop/src/lib.rs @@ -11,15 +11,6 @@ struct Model { drop_zone_content: Vec>, } -// Init - -fn init(_: Url, _: &mut impl Orders) -> Init { - Init::new(Model { - drop_zone_active: false, - drop_zone_content: vec![div!["Drop files here"]], - }) -} - // Update #[derive(Clone, Debug)] @@ -117,5 +108,10 @@ fn view(model: &Model) -> impl View { #[wasm_bindgen(start)] pub fn start() { - seed::App::build(init, update, view).build_and_start(); + seed::App::builder(update, view) + .after_mount(AfterMount::new(Model { + drop_zone_active: false, + drop_zone_content: vec![div!["Drop files here"]], + })) + .build_and_start(); } diff --git a/examples/mathjax/src/lib.rs b/examples/mathjax/src/lib.rs index d08195450..b7880e1f8 100644 --- a/examples/mathjax/src/lib.rs +++ b/examples/mathjax/src/lib.rs @@ -226,13 +226,7 @@ fn view(model: &Model) -> impl View { ] } -// Init - -fn init(_: Url, _: &mut impl Orders) -> Init { - Init::new(Model::default()) -} - #[wasm_bindgen(start)] pub fn render() { - seed::App::build(init, update, view).build_and_start(); + seed::App::builder(update, view).build_and_start(); } diff --git a/examples/orders/src/lib.rs b/examples/orders/src/lib.rs index 9b85d1cda..d7a8d2cc8 100644 --- a/examples/orders/src/lib.rs +++ b/examples/orders/src/lib.rs @@ -93,5 +93,5 @@ fn view(model: &Model) -> impl View { #[wasm_bindgen(start)] pub fn start() { - seed::App::build(|_, _| Init::new(Model::default()), update, view).build_and_start(); + seed::App::builder(update, view).build_and_start(); } diff --git a/examples/server_integration/client/src/lib.rs b/examples/server_integration/client/src/lib.rs index 8c431f2f2..e7f3b4808 100644 --- a/examples/server_integration/client/src/lib.rs +++ b/examples/server_integration/client/src/lib.rs @@ -107,5 +107,5 @@ fn view_example_introduction(title: &str, description: &str) -> Vec> { #[wasm_bindgen(start)] pub fn start() { - seed::App::build(|_, _| Init::new(Model::default()), update, view).build_and_start(); + seed::App::builder(update, view).build_and_start(); } diff --git a/examples/server_interaction/src/lib.rs b/examples/server_interaction/src/lib.rs index 1956fae07..24500c8f5 100644 --- a/examples/server_interaction/src/lib.rs +++ b/examples/server_interaction/src/lib.rs @@ -128,12 +128,14 @@ fn view(model: &Model) -> Vec> { // Init -fn init(_: Url, orders: &mut impl Orders) -> Init { +fn after_mount(_: Url, orders: &mut impl Orders) -> AfterMount { orders.perform_cmd(fetch_repository_info()); - Init::new(Model::default()) + AfterMount::default() } #[wasm_bindgen(start)] pub fn render() { - seed::App::build(init, update, view).build_and_start(); + seed::App::builder(update, view) + .after_mount(after_mount) + .build_and_start(); } diff --git a/examples/server_interaction_detailed/reports/src/lib.rs b/examples/server_interaction_detailed/reports/src/lib.rs index db71883ad..27594b318 100644 --- a/examples/server_interaction_detailed/reports/src/lib.rs +++ b/examples/server_interaction_detailed/reports/src/lib.rs @@ -304,9 +304,7 @@ fn view(model: &Model) -> Vec> { #[wasm_bindgen] pub fn render() { - let state = seed::App::build(Model::default(), update, view) - .finish() - .run(); + let state = seed::App::builder(update, view).build_and_start(); state.update(Msg::GetData) } diff --git a/examples/todomvc/src/lib.rs b/examples/todomvc/src/lib.rs index d9b411737..0bc0d6db6 100644 --- a/examples/todomvc/src/lib.rs +++ b/examples/todomvc/src/lib.rs @@ -361,7 +361,7 @@ fn routes(url: seed::Url) -> Option { #[wasm_bindgen(start)] pub fn render() { - seed::App::build(|_, _| Init::new(Model::default()), update, view) + seed::App::builder(update, view) .routes(routes) .build_and_start(); } diff --git a/examples/update_from_js/src/lib.rs b/examples/update_from_js/src/lib.rs index 37f945f7d..8e24a08ff 100644 --- a/examples/update_from_js/src/lib.rs +++ b/examples/update_from_js/src/lib.rs @@ -12,12 +12,6 @@ struct Model { time_from_js: Option, } -// Init - -fn init(_: Url, _: &mut impl Orders) -> Init { - Init::new(Model::default()) -} - // Update #[derive(Clone)] @@ -76,7 +70,7 @@ fn view(model: &Model) -> Node { #[wasm_bindgen] // `wasm-bindgen` cannot transfer struct with public closures to JS (yet) so we have to send slice. pub fn start() -> Box<[JsValue]> { - let app = seed::App::build(init, update, view).build_and_start(); + let app = seed::App::builder(update, view).build_and_start(); create_closures_for_js(&app) } diff --git a/examples/user_media/src/lib.rs b/examples/user_media/src/lib.rs index 19137c51f..023245760 100644 --- a/examples/user_media/src/lib.rs +++ b/examples/user_media/src/lib.rs @@ -12,11 +12,11 @@ use web_sys::{HtmlMediaElement, MediaStream, MediaStreamConstraints}; struct Model {} -// Init +// AfterMount -fn init(_: Url, orders: &mut impl Orders) -> Init { +fn after_mount(_: Url, orders: &mut impl Orders) -> AfterMount { orders.perform_cmd(user_media()); - Init::new(Model {}) + AfterMount::new(Model {}) } fn user_media() -> impl Future { @@ -74,5 +74,7 @@ fn view(_: &Model) -> impl View { #[wasm_bindgen(start)] pub fn start() { - seed::App::build(init, update, view).build_and_start(); + seed::App::builder(update, view) + .after_mount(after_mount) + .build_and_start(); } diff --git a/examples/websocket/src/client.rs b/examples/websocket/src/client.rs index 8abdaab05..7340c0715 100644 --- a/examples/websocket/src/client.rs +++ b/examples/websocket/src/client.rs @@ -23,7 +23,7 @@ struct Model { // Init -fn init(_: Url, orders: &mut impl Orders) -> Init { +fn after_mount(_: Url, orders: &mut impl Orders) -> AfterMount { let ws = WebSocket::new(WS_URL).unwrap(); register_ws_handler(WebSocket::set_onopen, Msg::Connected, &ws, orders); @@ -31,7 +31,7 @@ fn init(_: Url, orders: &mut impl Orders) -> Init { register_ws_handler(WebSocket::set_onmessage, Msg::ServerMessage, &ws, orders); register_ws_handler(WebSocket::set_onerror, Msg::Error, &ws, orders); - Init::new(Model { + AfterMount::new(Model { ws, connected: false, msg_rx_cnt: 0, @@ -154,5 +154,7 @@ fn view(model: &Model) -> impl View { #[wasm_bindgen(start)] pub fn start() { - App::build(init, update, view).build_and_start(); + App::builder(update, view) + .after_mount(after_mount) + .build_and_start(); } diff --git a/examples/window_events/src/lib.rs b/examples/window_events/src/lib.rs index 3f9c8b946..48cfa911f 100644 --- a/examples/window_events/src/lib.rs +++ b/examples/window_events/src/lib.rs @@ -83,7 +83,7 @@ fn window_events(model: &Model) -> Vec> { #[wasm_bindgen(start)] pub fn render() { - seed::App::build(|_, _| Init::new(Model::default()), update, view) + seed::App::builder(update, view) .window_events(window_events) .build_and_start(); } diff --git a/src/lib.rs b/src/lib.rs index 902257106..0f605ddb9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,7 +100,7 @@ pub mod prelude { request_animation_frame, ClosureNew, RequestAnimationFrameHandle, RequestAnimationFrameTime, }, - vdom::{Init, MountType, RenderTimestampDelta, UrlHandling}, + vdom::{AfterMount, BeforeMount, Init, MountType, RenderTimestampDelta, UrlHandling}, }; pub use indexmap::IndexMap; // for attrs and style to work. pub use wasm_bindgen::prelude::*; diff --git a/src/orders.rs b/src/orders.rs index a12994426..98378373c 100644 --- a/src/orders.rs +++ b/src/orders.rs @@ -110,6 +110,11 @@ impl, GMs> OrdersContainer { app, } } + + pub(crate) fn merge(&mut self, mut other: Self) { + self.should_render = other.should_render; + self.effects.append(&mut other.effects); + } } impl + 'static, GMs> Orders diff --git a/src/vdom.rs b/src/vdom.rs index 154240fb2..aabf12e7a 100644 --- a/src/vdom.rs +++ b/src/vdom.rs @@ -1,6 +1,7 @@ use std::{ cell::{Cell, RefCell}, collections::{vec_deque::VecDeque, HashMap}, + marker::PhantomData, rc::Rc, }; @@ -14,8 +15,13 @@ use next_tick::NextTick; pub mod alias; pub use alias::*; + +// Building process. pub mod builder; -pub use builder::{Builder as AppBuilder, Init, MountType, UrlHandling}; +pub use builder::{ + AfterMount, BeforeMount, Builder as AppBuilder, Init, InitFn, MountPoint, MountType, + UrlHandling, +}; use crate::{ dom_types::{self, El, MessageMapper, Namespace, Node, View}, @@ -119,6 +125,21 @@ pub struct AppData { pub render_timestamp: Cell>, } +type OptDynInitCfg = + Option>>; + +pub struct AppInitCfg +where + Ms: 'static, + Mdl: 'static, + ElC: View, + IAM: builder::IntoAfterMount, +{ + mount_type: MountType, + into_after_mount: Box, + phantom: PhantomData<(Ms, Mdl, ElC, GMs)>, +} + pub struct AppCfg where Ms: 'static, @@ -127,12 +148,10 @@ where { document: web_sys::Document, mount_point: web_sys::Element, - mount_type: RefCell>, pub update: UpdateFn, pub sink: Option>, view: ViewFn, window_events: Option>, - initial_orders: RefCell>>, } pub struct App @@ -141,7 +160,9 @@ where Mdl: 'static, ElC: View, { - /// Stateless app configuration + /// Temporary app configuration that is removed after app begins running. + pub init_cfg: OptDynInitCfg, + /// App configuration available for the entire application lifetime. pub cfg: Rc>, /// Mutable app state pub data: Rc>, @@ -153,18 +174,38 @@ impl, GMs> ::std::fmt::Debug for App = + AppBuilder>>; + /// We use a struct instead of series of functions, in order to avoid passing /// repetitive sequences of parameters. impl + 'static, GMs: 'static> App { + #[deprecated( + since = "0.5.0", + note = "Use `builder` with `AppBuilder::{after_mount, before_mount}` instead." + )] pub fn build( init: impl FnOnce(routing::Url, &mut OrdersContainer) -> Init + 'static, update: UpdateFn, view: ViewFn, - ) -> AppBuilder { + ) -> InitAppBuilder { + Self::builder(update, view).init(Box::new(init)) + } + + pub fn builder( + update: UpdateFn, + view: ViewFn, + ) -> AppBuilder { + // @TODO: Remove as soon as Webkit is fixed and older browsers are no longer in use. + // https://github.com/David-OConnor/seed/issues/241 + // https://bugs.webkit.org/show_bug.cgi?id=202881 + let _ = util::document().query_selector("html"); + // Allows panic messages to output to the browser console.error. console_error_panic_hook::set_once(); - AppBuilder::new(Box::new(init), update, view) + AppBuilder::new(update, view) } #[allow(clippy::too_many_arguments)] @@ -175,11 +216,13 @@ impl + 'static, GMs: 'static> App { mount_point: Element, routes: Option>, window_events: Option>, + init_cfg: OptDynInitCfg, ) -> Self { let window = util::window(); let document = window.document().expect("Can't find the window's document"); Self { + init_cfg, cfg: Rc::new(AppCfg { document, mount_point, @@ -187,8 +230,6 @@ impl + 'static, GMs: 'static> App { sink, view, window_events, - initial_orders: RefCell::new(None), - mount_type: RefCell::new(None), }), data: Rc::new(AppData { model: RefCell::new(None), @@ -222,8 +263,7 @@ impl + 'static, GMs: 'static> App { /// Bootstrap the dom with the vdom by taking over all children of the mount point and /// replacing them with the vdom if requested. Will otherwise ignore the original children of /// the mount point. - fn bootstrap_vdom(&self) -> El { - let mount_type = self.cfg.mount_type.borrow().unwrap_or(MountType::Append); + fn bootstrap_vdom(&self, mount_type: MountType) -> El { // "new" name is for consistency with `update` function. // this section parent is a placeholder, so we can iterate over children // in a way consistent with patching code. @@ -243,10 +283,6 @@ impl + 'static, GMs: 'static> App { new.children = dom_nodes.children; } - self.setup_window_listeners(); - patch::setup_input_listeners(&mut new); - patch::attach_listeners(&mut new, &self.mailbox()); - // Recreate the needed nodes. Only do this if requested to takeover the mount point since // it should only be needed here. if mount_type == MountType::Takeover { @@ -288,9 +324,50 @@ impl + 'static, GMs: 'static> App { since = "0.4.2", note = "Please use `AppBuilder.build_and_start` instead" )] - pub fn run(self) -> Self { + pub fn run(mut self) -> Self { + let AppInitCfg { + mount_type, + into_after_mount, + .. + } = self.init_cfg.take().expect( + "`init_cfg` should be set in `App::new` which is called from `AppBuilder::build_and_start`", + ); + // Bootstrap the virtual DOM. - self.data.main_el_vdom.replace(Some(self.bootstrap_vdom())); + self.data + .main_el_vdom + .replace(Some(self.bootstrap_vdom(mount_type))); + + let mut orders = OrdersContainer::new(self.clone()); + let builder::AfterMount { + model, + url_handling, + } = into_after_mount.into_after_mount(routing::current_url(), &mut orders); + + self.data.model.replace(Some(model)); + + match url_handling { + UrlHandling::PassToRoutes => { + let url = routing::current_url(); + let routing_msg = self + .data + .routes + .borrow() + .as_ref() + .and_then(|routes| routes(url)); + if let Some(routing_msg) = routing_msg { + orders.effects.push_back(routing_msg.into()); + } + } + UrlHandling::None => (), + }; + + self.setup_window_listeners(); + patch::setup_input_listeners(&mut self.data.main_el_vdom.borrow_mut().as_mut().unwrap()); + patch::attach_listeners( + self.data.main_el_vdom.borrow_mut().as_mut().unwrap(), + &self.mailbox(), + ); // Update the state on page load, based // on the starting URL. Must be set up on the server as well. @@ -312,16 +389,11 @@ impl + 'static, GMs: 'static> App { routing::setup_link_listener(enclose!((self => s) move |msg| s.update(msg)), routes); } - // Our initial render. Can't initialize in new due to mailbox() requiring self. - self.process_cmd_and_msg_queue( - self.cfg - .initial_orders - .replace(None) - .expect("initial_orders should be set in AppBuilder::finish") - .effects, - ); - // TODO: In the future, only run the following line if the above statement does not - // TODO: call `rerender_vdom` for efficiency. + self.process_cmd_and_msg_queue(orders.effects); + // TODO: In the future, only run the following line if the above statement: + // - didn't force-rerender vdom + // - didn't schedule render + // - doesn't want to skip render self.rerender_vdom(); self @@ -550,6 +622,7 @@ impl + 'static, GMs: 'static> App { impl, GMs> Clone for App { fn clone(&self) -> Self { Self { + init_cfg: None, cfg: Rc::clone(&self.cfg), data: Rc::clone(&self.data), } diff --git a/src/vdom/builder.rs b/src/vdom/builder.rs index 96d535f16..4fb31a1bc 100644 --- a/src/vdom/builder.rs +++ b/src/vdom/builder.rs @@ -1,128 +1,409 @@ -use web_sys::Element; +use std::marker::PhantomData; -use super::{alias::*, App}; +use crate::{ + dom_types::View, + orders::OrdersContainer, + routing, + vdom::{alias::*, App, AppInitCfg}, +}; -use crate::{dom_types::View, orders::OrdersContainer, routing, util}; +pub mod init; +pub use init::{Init, InitFn, IntoInit}; +pub mod before_mount; +pub use before_mount::{BeforeMount, IntoBeforeMount, MountPoint, MountType}; +pub mod after_mount; +pub use after_mount::{AfterMount, IntoAfterMount, UrlHandling}; -pub type InitFn = - Box) -> Init>; +#[deprecated( + since = "0.5.0", + note = "Used for compatibility with old Init API. Use `BeforeAfterInitAPI` together with `BeforeMount` and `AfterMount` instead." +)] +pub struct MountPointInitInitAPI { + mount_point: MP, + into_init: II, +} +// TODO Remove when removing the other `InitAPI`s. +pub struct BeforeAfterInitAPI { + into_before_mount: IBM, + into_after_mount: IAM, +} +// TODO Remove when removing the other `InitAPI`s. +impl Default for BeforeAfterInitAPI<(), ()> { + fn default() -> Self { + BeforeAfterInitAPI { + into_before_mount: (), + into_after_mount: (), + } + } +} -pub trait MountPoint { - fn element(self) -> Element; +// TODO Remove when removing the other `InitAPI`s. +pub trait InitAPI, GMs> { + type Builder; + fn build(builder: Self::Builder) -> App; } +// TODO Remove when removing the other `InitAPI`s. +pub trait InitAPIData { + type IntoBeforeMount; + type IntoAfterMount; + #[deprecated( + since = "0.5.0", + note = "Used for compatibility with old Init API. Use `IntoBeforeMount` and `IntoAfterMount` instead." + )] + type IntoInit; + #[deprecated( + since = "0.5.0", + note = "Used for compatibility with old Init API. Use `IntoBeforeMount` and `IntoAfterMount` instead." + )] + type MountPoint; -impl MountPoint for &str { - fn element(self) -> Element { - util::document().get_element_by_id(self).unwrap_or_else(|| { - panic!( - "Can't find element with id={:?} - app cannot be mounted!\n\ - (Id defaults to \"app\", or can be set with the .mount() method)", - self - ) - }) - } + fn before_mount( + self, + into_before_mount: NewIBM, + ) -> BeforeAfterInitAPI; + fn after_mount< + Ms: 'static, + Mdl, + ElC: View, + GMs, + NewIAM: IntoAfterMount, + >( + self, + into_after_mount: NewIAM, + ) -> BeforeAfterInitAPI; + + #[deprecated( + since = "0.5.0", + note = "Used for compatibility with old Init API. Use `before_mount` and `after_mount` instead." + )] + fn init, GMs, NewII: IntoInit>( + self, + into_init: NewII, + ) -> MountPointInitInitAPI; + #[deprecated( + since = "0.5.0", + note = "Used for compatibility with old Init API. Use `before_mount` and `after_mount` instead." + )] + fn mount( + self, + mount_point: NewMP, + ) -> MountPointInitInitAPI; } -impl MountPoint for Element { - fn element(self) -> Element { - self +// TODO Remove when removing the other `InitAPI`s. +#[deprecated( + since = "0.5.0", + note = "Used for compatibility with old Init API. Use `BeforeAfterInitAPI` together with `BeforeMount` and `AfterMount` instead." +)] +impl< + Ms: 'static, + Mdl: 'static, + ElC: 'static + View, + GMs: 'static, + MP: MountPoint, + II: IntoInit, + > InitAPI for MountPointInitInitAPI +{ + type Builder = Builder; + fn build(builder: Self::Builder) -> App { + let MountPointInitInitAPI { + into_init, + mount_point, + } = builder.init_api; + + let mut app = App::new( + builder.update, + builder.sink, + builder.view, + mount_point.element(), + builder.routes, + builder.window_events, + None, + ); + + let mut initial_orders = OrdersContainer::new(app.clone()); + let init = into_init.into_init(routing::current_url(), &mut initial_orders); + + app.init_cfg.replace(AppInitCfg { + mount_type: init.mount_type, + into_after_mount: Box::new((init, initial_orders)), + phantom: PhantomData, + }); + + app } } +// TODO Remove when removing the other `InitAPI`s. +impl< + Ms: 'static, + Mdl: 'static, + ElC: 'static + View, + GMs: 'static, + IBM: IntoBeforeMount, + IAM: 'static + IntoAfterMount, + > InitAPI for BeforeAfterInitAPI +{ + type Builder = Builder; + fn build(builder: Self::Builder) -> App { + let BeforeAfterInitAPI { + into_before_mount, + into_after_mount, + } = builder.init_api; -impl MountPoint for web_sys::HtmlElement { - fn element(self) -> Element { - self.into() + let BeforeMount { + mount_point, + mount_type, + } = into_before_mount.into_before_mount(routing::current_url()); + + App::new( + builder.update, + builder.sink, + builder.view, + mount_point.element(), + builder.routes, + builder.window_events, + Some(AppInitCfg { + mount_type, + into_after_mount: Box::new(into_after_mount), + phantom: PhantomData, + }), + ) } } +// TODO Remove when removing the other `InitAPI`s. +impl, GMs: 'static> + InitAPI for () +{ + type Builder = Builder; + fn build(builder: Self::Builder) -> App { + BeforeAfterInitAPI::build(Builder { + update: builder.update, + view: builder.view, -/// Used for handling initial routing. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum UrlHandling { - PassToRoutes, - None, - // todo: Expand later, as-required -} + routes: builder.routes, + window_events: builder.window_events, + sink: builder.sink, -/// Describes the handling of elements already present in the mount element. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum MountType { - /// Take control of previously existing elements in the mount. This does not make guarantees of - /// elements added after the [`App`] has been mounted. - /// - /// Note that existing elements in the DOM will be recreated. This can be dangerous for script - /// tags and other, similar tags. - Takeover, - /// Leave the previously existing elements in the mount alone. This does not make guarantees of - /// elements added after the [`App`] has been mounted. - Append, + init_api: BeforeAfterInitAPI::default(), + }) + } } -/// Used as a flexible wrapper for the init function. -pub struct Init { - /// Initial model to be used when the app begins. - pub model: Mdl, - /// How to handle initial url routing. Defaults to [`UrlHandling::PassToRoutes`] in the - /// constructors. - pub url_handling: UrlHandling, - /// How to handle elements already present in the mount. Defaults to [`MountType::Append`] - /// in the constructors. - pub mount_type: MountType, +#[deprecated( + since = "0.5.0", + note = "Used for compatibility with old Init API. Use `BeforeAfterInitAPI` together with `BeforeMount` and `AfterMount` instead." +)] +impl InitAPIData for MountPointInitInitAPI { + type IntoBeforeMount = (); + type IntoAfterMount = (); + type IntoInit = II; + type MountPoint = MP; + + fn before_mount( + self, + into_before_mount: NewIBM, + ) -> BeforeAfterInitAPI { + BeforeAfterInitAPI { + into_before_mount, + into_after_mount: (), + } + } + fn after_mount< + Ms: 'static, + Mdl, + ElC: View, + GMs, + NewIAM: IntoAfterMount, + >( + self, + into_after_mount: NewIAM, + ) -> BeforeAfterInitAPI { + BeforeAfterInitAPI { + into_after_mount, + into_before_mount: (), + } + } + + fn init, GMs, NewII: IntoInit>( + self, + into_init: NewII, + ) -> MountPointInitInitAPI { + MountPointInitInitAPI { + into_init, + mount_point: self.mount_point, + } + } + fn mount( + self, + mount_point: NewMP, + ) -> MountPointInitInitAPI { + MountPointInitInitAPI { + mount_point, + into_init: self.into_init, + } + } } +// TODO Remove when removing the other `InitAPI`s. +impl InitAPIData for BeforeAfterInitAPI { + type IntoBeforeMount = IBM; + type IntoAfterMount = IAM; + type IntoInit = (); + type MountPoint = (); -impl Init { - pub const fn new(model: Mdl) -> Self { - Self { - model, - url_handling: UrlHandling::PassToRoutes, - mount_type: MountType::Append, + fn before_mount( + self, + into_before_mount: NewIBM, + ) -> BeforeAfterInitAPI { + BeforeAfterInitAPI { + into_before_mount, + into_after_mount: self.into_after_mount, + } + } + fn after_mount< + Ms: 'static, + Mdl, + ElC: View, + GMs, + NewIAM: IntoAfterMount, + >( + self, + into_after_mount: NewIAM, + ) -> BeforeAfterInitAPI { + BeforeAfterInitAPI { + into_after_mount, + into_before_mount: self.into_before_mount, } } - pub const fn new_with_url_handling(model: Mdl, url_handling: UrlHandling) -> Self { - Self { - model, - url_handling, - mount_type: MountType::Append, + fn init, GMs, NewII: IntoInit>( + self, + into_init: NewII, + ) -> MountPointInitInitAPI { + MountPointInitInitAPI { + into_init, + mount_point: (), + } + } + fn mount( + self, + mount_point: NewMP, + ) -> MountPointInitInitAPI { + MountPointInitInitAPI { + mount_point, + into_init: (), } } } +// TODO Remove when removing the other `InitAPI`s. +impl InitAPIData for () { + type IntoBeforeMount = (); + type IntoAfterMount = (); + type IntoInit = (); + type MountPoint = (); -impl Default for Init { - fn default() -> Self { - Self { - model: Mdl::default(), - url_handling: UrlHandling::PassToRoutes, - mount_type: MountType::Append, + fn before_mount( + self, + into_before_mount: NewIBM, + ) -> BeforeAfterInitAPI { + BeforeAfterInitAPI { + into_before_mount, + into_after_mount: (), + } + } + fn after_mount< + Ms: 'static, + Mdl, + ElC: View, + GMs, + NewIAM: IntoAfterMount, + >( + self, + into_after_mount: NewIAM, + ) -> BeforeAfterInitAPI { + BeforeAfterInitAPI { + into_after_mount, + into_before_mount: (), + } + } + + fn init, GMs, NewII: IntoInit>( + self, + into_init: NewII, + ) -> MountPointInitInitAPI { + MountPointInitInitAPI { + into_init, + mount_point: (), + } + } + fn mount( + self, + mount_point: NewMP, + ) -> MountPointInitInitAPI { + MountPointInitInitAPI { + mount_point, + into_init: (), } } } /// Used to create and store initial app configuration, ie items passed by the app creator -pub struct Builder, GMs> { - init: InitFn, +pub struct Builder, GMs, InitAPIType> { update: UpdateFn, - sink: Option>, view: ViewFn, - mount_point: Option, + routes: Option>, window_events: Option>, + sink: Option>, + + // TODO: Remove when removing legacy init fields. + init_api: InitAPIType, } -impl + 'static, GMs: 'static> Builder { +impl + 'static, GMs: 'static> Builder { /// Constructs the Builder. - pub(super) fn new( - init: InitFn, - update: UpdateFn, - view: ViewFn, - ) -> Self { - Self { - init, + pub(super) fn new(update: UpdateFn, view: ViewFn) -> Self { + Builder { update, - sink: None, view, - mount_point: None, + routes: None, window_events: None, + sink: None, + + init_api: (), + } + } +} + +impl< + Ms, + Mdl, + ElC: View + 'static, + GMs: 'static, + IBM, + IAM: 'static, + MP, + II, + InitAPIType: InitAPIData, + > Builder +{ + #[deprecated( + since = "0.5.0", + note = "Used for compatibility with old Init API. Use `before_mount` and `after_mount` instead." + )] + pub fn init>( + self, + new_init: NewII, + ) -> Builder> { + Builder { + update: self.update, + view: self.view, + + routes: self.routes, + window_events: self.window_events, + sink: self.sink, + + init_api: self.init_api.init(new_init), } } @@ -143,14 +424,56 @@ impl + 'static, GMs: 'static> Builder /// // argument is `Element` /// mount(seed::body().querySelector("section").unwrap().unwrap()) /// ``` - pub fn mount(mut self, mount_point: impl MountPoint) -> Self { - // @TODO: Remove as soon as Webkit is fixed and older browsers are no longer in use. - // https://github.com/seed-rs/seed/issues/241 - // https://bugs.webkit.org/show_bug.cgi?id=202881 - let _ = util::document().query_selector("html"); + #[deprecated( + since = "0.5.0", + note = "Used for compatibility with old Init API. Use `before_mount` and `after_mount` instead." + )] + pub fn mount( + self, + new_mount_point: NewMP, + ) -> Builder> { + Builder { + update: self.update, + view: self.view, - self.mount_point = Some(mount_point.element()); - self + routes: self.routes, + window_events: self.window_events, + sink: self.sink, + + init_api: self.init_api.mount(new_mount_point), + } + } + + pub fn before_mount( + self, + new_before_mount: NewIBM, + ) -> Builder> { + Builder { + update: self.update, + view: self.view, + + routes: self.routes, + window_events: self.window_events, + sink: self.sink, + + init_api: self.init_api.before_mount(new_before_mount), + } + } + + pub fn after_mount>( + self, + new_after_mount: NewIAM, + ) -> Builder> { + Builder { + update: self.update, + view: self.view, + + routes: self.routes, + window_events: self.window_events, + sink: self.sink, + + init_api: self.init_api.after_mount(new_after_mount), + } } /// Registers a function which maps URLs to messages. @@ -174,51 +497,37 @@ impl + 'static, GMs: 'static> Builder self.sink = Some(sink); self } +} +impl< + Ms: 'static, + Mdl, + ElC: View + 'static, + GMs: 'static, + InitAPIType: InitAPI, + > Builder +{ + /// Build and run the app. + pub fn build_and_start(self) -> App { + InitAPIType::build(self).run() + } +} + +impl< + Ms: 'static, + Mdl, + ElC: View + 'static, + GMs: 'static, + MP: MountPoint, + II: IntoInit, + > Builder> +{ /// Turn this [`Builder`] into an [`App`] which is ready to run. /// /// [`Builder`]: struct.Builder.html /// [`App`]: struct.App.html #[deprecated(since = "0.4.2", note = "Please use `.build_and_start` instead")] - pub fn finish(mut self) -> App { - if self.mount_point.is_none() { - self = self.mount("app") - } - - let app = App::new( - self.update, - self.sink, - self.view, - self.mount_point.unwrap(), - self.routes, - self.window_events, - ); - - let mut initial_orders = OrdersContainer::new(app.clone()); - let mut init = (self.init)(routing::current_url(), &mut initial_orders); - - match init.url_handling { - UrlHandling::PassToRoutes => { - let url = routing::current_url(); - if let Some(r) = self.routes { - if let Some(u) = r(url) { - (self.update)(u, &mut init.model, &mut initial_orders); - } - } - } - UrlHandling::None => (), - }; - - app.cfg.initial_orders.replace(Some(initial_orders)); - app.cfg.mount_type.replace(Some(init.mount_type)); - app.data.model.replace(Some(init.model)); - - app - } - - /// Build and run the app. - pub fn build_and_start(self) -> App { - let app = self.finish(); - app.run() + pub fn finish(self) -> App { + MountPointInitInitAPI::build(self) } } diff --git a/src/vdom/builder/after_mount.rs b/src/vdom/builder/after_mount.rs new file mode 100644 index 000000000..4cd3b43ff --- /dev/null +++ b/src/vdom/builder/after_mount.rs @@ -0,0 +1,90 @@ +use crate::{dom_types::View, orders::OrdersContainer, routing::Url}; + +/// Used for handling initial routing. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum UrlHandling { + PassToRoutes, + None, + // todo: Expand later, as-required +} + +impl Default for UrlHandling { + fn default() -> Self { + Self::PassToRoutes + } +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct AfterMount { + /// Initial model to be used when the app begins. + pub model: Mdl, + /// How to handle initial url routing. Defaults to [`UrlHandling::PassToRoutes`] in the + /// constructors. + pub url_handling: UrlHandling, +} + +impl AfterMount { + pub fn new(model: Mdl) -> Self { + Self { + model, + url_handling: UrlHandling::default(), + } + } + + // TODO: Change to const fn when possible. + // TODO: Relevant issue: https://github.com/rust-lang/rust/issues/60964 + #[allow(clippy::missing_const_for_fn)] + pub fn model(self, model: NewMdl) -> AfterMount { + AfterMount { + model, + url_handling: self.url_handling, + } + } + + pub const fn url_handling(mut self, url_handling: UrlHandling) -> Self { + self.url_handling = url_handling; + self + } +} + +#[allow(clippy::module_name_repetitions)] +pub trait IntoAfterMount, GMs> { + fn into_after_mount( + self: Box, + init_url: Url, + orders: &mut OrdersContainer, + ) -> AfterMount; +} + +impl, GMs> IntoAfterMount for AfterMount { + fn into_after_mount( + self: Box, + _: Url, + _: &mut OrdersContainer, + ) -> AfterMount { + *self + } +} + +impl, GMs, F> IntoAfterMount for F +where + F: FnOnce(Url, &mut OrdersContainer) -> AfterMount, +{ + fn into_after_mount( + self: Box, + init_url: Url, + orders: &mut OrdersContainer, + ) -> AfterMount { + self(init_url, orders) + } +} + +impl, GMs> IntoAfterMount for () { + fn into_after_mount( + self: Box, + _: Url, + _: &mut OrdersContainer, + ) -> AfterMount { + AfterMount::default() + } +} diff --git a/src/vdom/builder/before_mount.rs b/src/vdom/builder/before_mount.rs new file mode 100644 index 000000000..4f8a4b3bf --- /dev/null +++ b/src/vdom/builder/before_mount.rs @@ -0,0 +1,122 @@ +use web_sys::Element; + +use crate::{routing::Url, util}; + +pub trait MountPoint { + fn element(self) -> Element; +} + +impl MountPoint for &str { + fn element(self) -> Element { + util::document().get_element_by_id(self).unwrap_or_else(|| { + panic!( + "Can't find element with id={:?} - app cannot be mounted!\n\ + (Id defaults to \"app\", or can be set with the .mount() method)", + self + ) + }) + } +} + +impl MountPoint for Element { + fn element(self) -> Element { + self + } +} + +impl MountPoint for web_sys::HtmlElement { + fn element(self) -> Element { + self.into() + } +} + +impl MountPoint for () { + fn element(self) -> Element { + "app".element() + } +} + +/// Describes the handling of elements already present in the mount element. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MountType { + /// Take control of previously existing elements in the mount. This does not make guarantees of + /// elements added after the [`App`] has been mounted. + /// + /// Note that existing elements in the DOM will be recreated. This can be dangerous for script + /// tags and other, similar tags. + Takeover, + /// Leave the previously existing elements in the mount alone. This does not make guarantees of + /// elements added after the [`App`] has been mounted. + Append, +} + +impl Default for MountType { + fn default() -> Self { + Self::Append + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct BeforeMount { + pub mount_point: MP, + /// How to handle elements already present in the mount. Defaults to [`MountType::Append`] + /// in the constructors. + pub mount_type: MountType, +} + +impl BeforeMount { + pub fn new(mp: MP) -> Self { + Self { + mount_point: mp, + mount_type: MountType::default(), + } + } + + pub fn mount_point(self, new_mp: NewMP) -> BeforeMount { + BeforeMount { + mount_point: new_mp, + mount_type: self.mount_type, + } + } + + pub fn mount_type(mut self, new_mt: MountType) -> Self { + self.mount_type = new_mt; + self + } +} + +impl Default for BeforeMount<()> { + fn default() -> Self { + Self::new(()) + } +} + +#[allow(clippy::module_name_repetitions)] +pub trait IntoBeforeMount { + type MP: MountPoint; + fn into_before_mount(self, init_url: Url) -> BeforeMount; +} + +impl IntoBeforeMount for BeforeMount { + type MP = MP; + fn into_before_mount(self, _: Url) -> BeforeMount { + self + } +} + +impl IntoBeforeMount for F +where + F: FnOnce(Url) -> BeforeMount, +{ + type MP = MP; + fn into_before_mount(self, init_url: Url) -> BeforeMount { + self(init_url) + } +} + +impl IntoBeforeMount for () { + type MP = (); + fn into_before_mount(self, _: Url) -> BeforeMount { + BeforeMount::default() + } +} diff --git a/src/vdom/builder/init.rs b/src/vdom/builder/init.rs new file mode 100644 index 000000000..16ebe50dc --- /dev/null +++ b/src/vdom/builder/init.rs @@ -0,0 +1,107 @@ +use crate::{ + dom_types::View, + orders::OrdersContainer, + routing::Url, + vdom::builder::{ + after_mount::{AfterMount, IntoAfterMount, UrlHandling}, + before_mount::MountType, + }, +}; + +/// Used as a flexible wrapper for the init function. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[deprecated( + since = "0.5.0", + note = "Part of old Init API. Use a combination of `BeforeMount` and `AfterMount` instead." +)] +pub struct Init { + /// Initial model to be used when the app begins. + #[deprecated( + since = "0.5.0", + note = "Part of old Init API. Use `AfterMount` instead." + )] + pub model: Mdl, + /// How to handle initial url routing. Defaults to [`UrlHandling::PassToRoutes`] in the + /// constructors. + #[deprecated( + since = "0.5.0", + note = "Part of old Init API. Use `AfterMount` instead." + )] + pub url_handling: UrlHandling, + /// How to handle elements already present in the mount. Defaults to [`MountType::Append`] + /// in the constructors. + #[deprecated( + since = "0.5.0", + note = "Part of old Init API. Use `BeforeMount` instead." + )] + pub mount_type: MountType, +} + +impl Init { + #[deprecated( + since = "0.5.0", + note = "Part of old Init API. Use `AfterMount` instead." + )] + pub const fn new(model: Mdl) -> Self { + Self { + model, + url_handling: UrlHandling::PassToRoutes, + mount_type: MountType::Append, + } + } + + #[deprecated( + since = "0.5.0", + note = "Part of old Init API. Use `AfterMount` instead." + )] + pub const fn new_with_url_handling(model: Mdl, url_handling: UrlHandling) -> Self { + Self { + model, + url_handling, + mount_type: MountType::Append, + } + } +} + +#[allow(clippy::module_name_repetitions)] +#[deprecated( + since = "0.5.0", + note = "Part of old Init API. Use `AfterMount` instead." +)] +pub type InitFn = + Box) -> Init>; + +#[allow(clippy::module_name_repetitions)] +#[deprecated( + since = "0.5.0", + note = "Part of old Init API. Use `IntoAfterMount` and `IntoBeforeMount` instead." +)] +pub trait IntoInit, GMs> { + fn into_init(self, init_url: Url, ord: &mut OrdersContainer) -> Init; +} + +impl, GMs, F> IntoInit for F +where + F: FnOnce(Url, &mut OrdersContainer) -> Init, +{ + fn into_init(self, init_url: Url, ord: &mut OrdersContainer) -> Init { + self(init_url, ord) + } +} + +impl, GMs> IntoAfterMount + for (Init, OrdersContainer) +{ + fn into_after_mount( + self: Box, + _: Url, + ord: &mut OrdersContainer, + ) -> AfterMount { + let (init, old_ord) = *self; + ord.merge(old_ord); + AfterMount { + model: init.model, + url_handling: init.url_handling, + } + } +}