use crate::browser::dom::virtual_dom_bridge;
use crate::browser::{
service::routing,
url,
util::{self, window, ClosureNew},
NextTick, Url,
};
use crate::virtual_dom::{patch, El, EventHandlerManager, Mailbox, Node, Tag, View};
use builder::{
init::{Init, InitFn},
IntoAfterMount, MountPointInitInitAPI, UndefinedInitAPI, UndefinedMountPoint,
};
use enclose::enclose;
use futures::future::LocalFutureObj;
use futures::FutureExt;
use std::{
cell::{Cell, RefCell},
collections::VecDeque,
rc::Rc,
};
use types::*;
use wasm_bindgen::closure::Closure;
use wasm_bindgen_futures::spawn_local;
use web_sys::Element;
pub mod builder;
pub mod cfg;
pub mod data;
pub mod effects;
pub mod message_mapper;
pub mod orders;
pub mod render_timestamp_delta;
pub mod types;
pub use builder::{
AfterMount, BeforeMount, Builder as AppBuilder, MountPoint, MountType, UrlHandling,
};
pub use cfg::{AppCfg, AppInitCfg};
pub use data::AppData;
pub use effects::Effect;
pub use message_mapper::MessageMapper;
pub use orders::{Orders, OrdersContainer, OrdersProxy};
pub use render_timestamp_delta::RenderTimestampDelta;
pub struct UndefinedGMsg;
type OptDynInitCfg<Ms, Mdl, ElC, GMs> =
Option<AppInitCfg<Ms, Mdl, ElC, GMs, dyn IntoAfterMount<Ms, Mdl, ElC, GMs>>>;
pub enum ShouldRender {
Render,
ForceRenderNow,
Skip,
}
pub struct App<Ms, Mdl, ElC, GMs = UndefinedGMsg>
where
Ms: 'static,
Mdl: 'static,
ElC: View<Ms>,
{
pub init_cfg: OptDynInitCfg<Ms, Mdl, ElC, GMs>,
pub cfg: Rc<AppCfg<Ms, Mdl, ElC, GMs>>,
pub data: Rc<AppData<Ms, Mdl>>,
}
impl<Ms: 'static, Mdl: 'static, ElC: View<Ms>, GMs> ::std::fmt::Debug for App<Ms, Mdl, ElC, GMs> {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
write!(f, "App")
}
}
impl<Ms, Mdl, ElC: View<Ms>, GMs> Clone for App<Ms, Mdl, ElC, GMs> {
fn clone(&self) -> Self {
Self {
init_cfg: None,
cfg: Rc::clone(&self.cfg),
data: Rc::clone(&self.data),
}
}
}
impl<Ms, Mdl, ElC: View<Ms> + 'static, GMs: 'static> App<Ms, Mdl, ElC, GMs> {
pub fn builder(
update: UpdateFn<Ms, Mdl, ElC, GMs>,
view: ViewFn<Mdl, ElC>,
) -> AppBuilder<Ms, Mdl, ElC, GMs, UndefinedInitAPI> {
let _ = util::document().query_selector("html");
console_error_panic_hook::set_once();
AppBuilder::new(update, view)
}
pub fn update(&self, message: Ms) {
let mut queue: VecDeque<Effect<Ms, GMs>> = VecDeque::new();
queue.push_front(message.into());
self.process_cmd_and_msg_queue(queue);
}
pub fn sink(&self, g_msg: GMs) {
let mut queue: VecDeque<Effect<Ms, GMs>> = VecDeque::new();
queue.push_front(Effect::GMsg(g_msg));
self.process_cmd_and_msg_queue(queue);
}
pub fn process_cmd_and_msg_queue(&self, mut queue: VecDeque<Effect<Ms, GMs>>) {
while let Some(effect) = queue.pop_front() {
match effect {
Effect::Msg(msg) => {
let mut new_effects = self.process_queue_message(msg);
queue.append(&mut new_effects);
}
Effect::GMsg(g_msg) => {
let mut new_effects = self.process_queue_global_message(g_msg);
queue.append(&mut new_effects);
}
Effect::Cmd(cmd) => self.process_queue_cmd(cmd),
Effect::GCmd(g_cmd) => self.process_queue_global_cmd(g_cmd),
}
}
}
pub fn patch_window_event_handlers(&self) {
if let Some(window_events) = self.cfg.window_events {
let new_event_handlers = (window_events)(self.data.model.borrow().as_ref().unwrap());
let new_manager = EventHandlerManager::with_event_handlers(new_event_handlers);
let mut old_manager = self.data.window_event_handler_manager.replace(new_manager);
let mut new_manager = self.data.window_event_handler_manager.borrow_mut();
new_manager.attach_listeners(util::window(), Some(&mut old_manager), &self.mailbox());
}
}
pub fn add_message_listener<F>(&self, listener: F)
where
F: Fn(&Ms) + 'static,
{
self.data
.msg_listeners
.borrow_mut()
.push(Box::new(listener));
}
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
update: UpdateFn<Ms, Mdl, ElC, GMs>,
sink: Option<SinkFn<Ms, Mdl, ElC, GMs>>,
view: ViewFn<Mdl, ElC>,
mount_point: Element,
routes: Option<RoutesFn<Ms>>,
window_events: Option<WindowEventsFn<Ms, Mdl>>,
init_cfg: OptDynInitCfg<Ms, Mdl, ElC, GMs>,
) -> 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,
update,
sink,
view,
window_events,
}),
data: Rc::new(AppData {
model: RefCell::new(None),
main_el_vdom: RefCell::new(None),
popstate_closure: RefCell::new(None),
hashchange_closure: RefCell::new(None),
routes: RefCell::new(routes),
window_event_handler_manager: RefCell::new(EventHandlerManager::new()),
msg_listeners: RefCell::new(Vec::new()),
scheduled_render_handle: RefCell::new(None),
after_next_render_callbacks: RefCell::new(Vec::new()),
render_timestamp: Cell::new(None),
}),
}
}
fn bootstrap_vdom(&self, mount_type: MountType) -> El<Ms> {
let mut new = El::empty(Tag::Placeholder);
if mount_type == MountType::Takeover {
let mut dom_nodes: El<Ms> = (&self.cfg.mount_point).into();
#[cfg(debug_assertions)]
dom_nodes.warn_about_script_tags();
dom_nodes.strip_ws_nodes_from_self_and_children();
new.children = dom_nodes.children;
}
if mount_type == MountType::Takeover {
virtual_dom_bridge::assign_ws_nodes_to_el(&util::document(), &mut new);
while let Some(child) = self.cfg.mount_point.first_child() {
self.cfg
.mount_point
.remove_child(&child)
.expect("No problem removing node from parent.");
}
for child in &mut new.children {
match child {
Node::Element(child_el) => {
virtual_dom_bridge::attach_el_and_children(
child_el,
&self.cfg.mount_point,
&self.mailbox(),
);
}
Node::Text(top_child_text) => {
virtual_dom_bridge::attach_text_node(top_child_text, &self.cfg.mount_point);
}
Node::Empty => (),
}
}
}
new
}
fn process_queue_message(&self, message: Ms) -> VecDeque<Effect<Ms, GMs>> {
for l in self.data.msg_listeners.borrow().iter() {
(l)(&message)
}
let mut orders = OrdersContainer::new(self.clone());
(self.cfg.update)(
message,
&mut self.data.model.borrow_mut().as_mut().unwrap(),
&mut orders,
);
self.patch_window_event_handlers();
match orders.should_render {
ShouldRender::Render => self.schedule_render(),
ShouldRender::ForceRenderNow => {
self.cancel_scheduled_render();
self.rerender_vdom();
}
ShouldRender::Skip => (),
};
orders.effects
}
fn process_queue_global_message(&self, g_message: GMs) -> VecDeque<Effect<Ms, GMs>> {
let mut orders = OrdersContainer::new(self.clone());
if let Some(sink) = self.cfg.sink {
sink(
g_message,
&mut self.data.model.borrow_mut().as_mut().unwrap(),
&mut orders,
);
}
self.patch_window_event_handlers();
match orders.should_render {
ShouldRender::Render => self.schedule_render(),
ShouldRender::ForceRenderNow => {
self.cancel_scheduled_render();
self.rerender_vdom();
}
ShouldRender::Skip => (),
};
orders.effects
}
fn process_queue_cmd(&self, cmd: LocalFutureObj<'static, Result<Ms, Ms>>) {
let lazy_schedule_cmd = enclose!((self => s) move |_| {
spawn_local(async move {
let msg_returned_from_effect = cmd.await.unwrap_or_else(|err_msg| err_msg);
s.update(msg_returned_from_effect);
})
});
spawn_local(NextTick::new().map(lazy_schedule_cmd));
}
fn process_queue_global_cmd(&self, g_cmd: LocalFutureObj<'static, Result<GMs, GMs>>) {
let lazy_schedule_cmd = enclose!((self => s) move |_| {
spawn_local(async move {
let msg_returned_from_effect = g_cmd.await.unwrap_or_else(|err_msg| err_msg);
s.sink(msg_returned_from_effect);
})
});
spawn_local(NextTick::new().map(lazy_schedule_cmd));
}
fn schedule_render(&self) {
let mut scheduled_render_handle = self.data.scheduled_render_handle.borrow_mut();
if scheduled_render_handle.is_none() {
let cb = Closure::new(enclose!((self => s) move |_| {
s.data.scheduled_render_handle.borrow_mut().take();
s.rerender_vdom();
}));
*scheduled_render_handle = Some(util::request_animation_frame(cb));
}
}
fn cancel_scheduled_render(&self) {
self.data.scheduled_render_handle.borrow_mut().take();
}
fn rerender_vdom(&self) {
let new_render_timestamp = window().performance().expect("get `Performance`").now();
let mut new = El::empty(Tag::Placeholder);
new.children = (self.cfg.view)(self.data.model.borrow().as_ref().unwrap()).els();
let old = self
.data
.main_el_vdom
.borrow_mut()
.take()
.expect("missing main_el_vdom");
patch::patch_els(
&self.cfg.document,
&self.mailbox(),
&self.clone(),
&self.cfg.mount_point,
old.children.into_iter(),
new.children.iter_mut(),
);
self.data.main_el_vdom.borrow_mut().replace(new);
let old_render_timestamp = self
.data
.render_timestamp
.replace(Some(new_render_timestamp));
let timestamp_delta = old_render_timestamp.map(|old_render_timestamp| {
RenderTimestampDelta::new(new_render_timestamp - old_render_timestamp)
});
self.process_cmd_and_msg_queue(
self.data
.after_next_render_callbacks
.replace(Vec::new())
.into_iter()
.map(|callback| Effect::Msg(callback(timestamp_delta)))
.collect(),
);
}
fn mailbox(&self) -> Mailbox<Ms> {
Mailbox::new(enclose!((self => s) move |message| {
s.update(message);
}))
}
#[deprecated(
since = "0.5.0",
note = "Use `builder` with `AppBuilder::{after_mount, before_mount}` instead."
)]
pub fn build(
init: impl FnOnce(Url, &mut OrdersContainer<Ms, Mdl, ElC, GMs>) -> Init<Mdl> + 'static,
update: UpdateFn<Ms, Mdl, ElC, GMs>,
view: ViewFn<Mdl, ElC>,
) -> InitAppBuilder<Ms, Mdl, ElC, GMs> {
Self::builder(update, view).init(Box::new(init))
}
#[deprecated(
since = "0.4.2",
note = "Please use `AppBuilder.build_and_start` instead"
)]
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`",
);
self.data
.main_el_vdom
.replace(Some(self.bootstrap_vdom(mount_type)));
let mut orders = OrdersContainer::new(self.clone());
let AfterMount {
model,
url_handling,
} = into_after_mount.into_after_mount(url::current(), &mut orders);
self.data.model.replace(Some(model));
match url_handling {
UrlHandling::PassToRoutes => {
let url = url::current();
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.patch_window_event_handlers();
if let Some(routes) = *self.data.routes.borrow() {
routing::setup_popstate_listener(
enclose!((self => s) move |msg| s.update(msg)),
enclose!((self => s) move |closure| {
s.data.popstate_closure.replace(Some(closure));
}),
routes,
);
routing::setup_hashchange_listener(
enclose!((self => s) move |msg| s.update(msg)),
enclose!((self => s) move |closure| {
s.data.hashchange_closure.replace(Some(closure));
}),
routes,
);
routing::setup_link_listener(enclose!((self => s) move |msg| s.update(msg)), routes);
}
self.process_cmd_and_msg_queue(orders.effects);
self.rerender_vdom();
self
}
}
#[deprecated(since = "0.5.0", note = "Part of the old Init API.")]
type InitAppBuilder<Ms, Mdl, ElC, GMs> = AppBuilder<
Ms,
Mdl,
ElC,
GMs,
MountPointInitInitAPI<UndefinedMountPoint, InitFn<Ms, Mdl, ElC, GMs>>,
>;