use crate::browser::dom::virtual_dom_bridge;
use crate::browser::{
service::routing,
util::{self, window, ClosureNew},
Url, DUMMY_BASE_URL,
};
use crate::virtual_dom::{patch, El, EventHandlerManager, IntoNodes, Mailbox, Node, Tag};
use builder::{
init::{Init, InitFn as BuilderInitFn},
IntoAfterMount, MountPointInitInitAPI, UndefinedInitAPI, UndefinedMountPoint,
};
use enclose::{enc, enclose};
use std::{
any::Any,
cell::{Cell, RefCell},
collections::VecDeque,
marker::PhantomData,
rc::Rc,
};
use types::{RoutesFn, SinkFn, UpdateFn, ViewFn, WindowEventsFn};
use wasm_bindgen::closure::Closure;
use web_sys::Element;
pub mod builder;
pub mod cfg;
pub mod cmd_manager;
pub mod cmds;
pub mod data;
pub mod effects;
pub mod get_element;
pub mod message_mapper;
pub mod orders;
pub mod render_info;
pub mod stream_manager;
pub mod streams;
pub mod sub_manager;
pub mod subs;
pub mod types;
pub use builder::{
AfterMount, BeforeMount, Builder as AppBuilder, MountPoint, MountType, UndefinedAfterMount,
UrlHandling,
};
pub use cfg::{AppCfg, AppInitCfg};
pub use cmd_manager::{CmdHandle, CmdManager};
pub use data::AppData;
pub use effects::Effect;
pub use get_element::GetElement;
pub use message_mapper::MessageMapper;
pub use orders::{Orders, OrdersContainer, OrdersProxy};
pub use render_info::RenderInfo;
pub use stream_manager::{StreamHandle, StreamManager};
pub use sub_manager::{Notification, SubHandle, SubManager};
pub struct UndefinedGMsg;
type OptDynInitCfg<Ms, Mdl, INodes, GMs> =
Option<AppInitCfg<Ms, Mdl, INodes, GMs, dyn IntoAfterMount<Ms, Mdl, INodes, GMs>>>;
pub enum ShouldRender {
Render,
ForceRenderNow,
Skip,
}
pub struct App<Ms, Mdl, INodes, GMs = UndefinedGMsg>
where
Ms: 'static,
Mdl: 'static,
INodes: IntoNodes<Ms>,
{
pub init_cfg: OptDynInitCfg<Ms, Mdl, INodes, GMs>,
pub cfg: Rc<AppCfg<Ms, Mdl, INodes, GMs>>,
pub data: Rc<AppData<Ms, Mdl>>,
}
impl<Ms: 'static, Mdl: 'static, INodes: IntoNodes<Ms>, GMs> ::std::fmt::Debug
for App<Ms, Mdl, INodes, GMs>
{
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
write!(f, "App")
}
}
impl<Ms, Mdl, INodes: IntoNodes<Ms>, GMs> Clone for App<Ms, Mdl, INodes, GMs> {
fn clone(&self) -> Self {
Self {
init_cfg: None,
cfg: Rc::clone(&self.cfg),
data: Rc::clone(&self.data),
}
}
}
impl<Ms, Mdl, INodes: IntoNodes<Ms> + 'static, GMs: 'static> App<Ms, Mdl, INodes, GMs> {
pub fn start(
root_element: impl GetElement,
init: impl FnOnce(Url, &mut OrdersContainer<Ms, Mdl, INodes, GMs>) -> Mdl + 'static,
update: UpdateFn<Ms, Mdl, INodes, GMs>,
view: ViewFn<Mdl, INodes>,
) -> Self {
let _ = util::document().query_selector("html");
console_error_panic_hook::set_once();
let root_element = root_element.get_element().expect("get root element");
let base_path: Rc<Vec<String>> = Rc::new(
util::document()
.query_selector("base")
.expect("query element with 'base' tag")
.and_then(|element| element.get_attribute("href"))
.and_then(|href| web_sys::Url::new_with_base(&href, DUMMY_BASE_URL).ok())
.map(|url| {
url.pathname()
.trim_start_matches('/')
.split('/')
.map(ToOwned::to_owned)
.collect()
})
.unwrap_or_default(),
);
let app_init_cfg = AppInitCfg {
mount_type: MountType::Takeover,
phantom: PhantomData,
into_after_mount: Box::new({
let base_path = Rc::clone(&base_path);
move |url: Url,
orders: &mut OrdersContainer<Ms, Mdl, INodes, GMs>|
-> AfterMount<Mdl> {
let url = url.skip_base_path(&base_path);
let model = init(url, orders);
AfterMount::new(model).url_handling(UrlHandling::None)
}
}) as Box<dyn IntoAfterMount<Ms, Mdl, INodes, GMs>>,
};
let app = Self::new(
update,
None,
view,
root_element,
None,
None,
Some(app_init_cfg),
base_path,
);
app.run()
}
pub fn builder(
update: UpdateFn<Ms, Mdl, INodes, GMs>,
view: ViewFn<Mdl, INodes>,
) -> AppBuilder<Ms, Mdl, INodes, 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_effect_queue(queue);
}
pub fn notify<SubMs: 'static + Any + Clone>(&self, message: SubMs) {
let mut queue: VecDeque<Effect<Ms, GMs>> = VecDeque::new();
queue.push_front(Effect::Notification(Notification::new(message)));
self.process_effect_queue(queue);
}
pub fn notify_with_notification(&self, notification: Notification) {
let mut queue: VecDeque<Effect<Ms, GMs>> = VecDeque::new();
queue.push_front(Effect::Notification(notification));
self.process_effect_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_effect_queue(queue);
}
pub fn process_effect_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::Notification(notification) => {
let mut new_effects = self.process_queue_notification(¬ification);
queue.append(&mut new_effects);
}
}
}
}
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, INodes, GMs>,
sink: Option<SinkFn<Ms, Mdl, INodes, GMs>>,
view: ViewFn<Mdl, INodes>,
mount_point: Element,
routes: Option<RoutesFn<Ms>>,
window_events: Option<WindowEventsFn<Ms, Mdl>>,
init_cfg: OptDynInitCfg<Ms, Mdl, INodes, GMs>,
base_path: Rc<Vec<String>>,
) -> Self {
let window = util::window();
let document = window.document().expect("get window's document");
Self {
init_cfg,
cfg: Rc::new(AppCfg {
document,
mount_point,
update,
sink,
view,
window_events,
base_path,
}),
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()),
sub_manager: RefCell::new(SubManager::new()),
msg_listeners: RefCell::new(Vec::new()),
scheduled_render_handle: RefCell::new(None),
after_next_render_callbacks: RefCell::new(Vec::new()),
render_info: 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_notification(&self, notification: &Notification) -> VecDeque<Effect<Ms, GMs>> {
self.data
.sub_manager
.borrow()
.notify(notification)
.into_iter()
.map(Effect::Msg)
.collect()
}
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 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()).into_nodes();
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 render_info = match self.data.render_info.take() {
Some(old_render_info) => RenderInfo {
timestamp: new_render_timestamp,
timestamp_delta: Some(new_render_timestamp - old_render_info.timestamp),
},
None => RenderInfo {
timestamp: new_render_timestamp,
timestamp_delta: None,
},
};
self.data.render_info.set(Some(render_info));
self.process_effect_queue(
self.data
.after_next_render_callbacks
.replace(Vec::new())
.into_iter()
.filter_map(|callback| callback(render_info).map(Effect::Msg))
.collect(),
);
}
pub fn mailbox(&self) -> Mailbox<Ms> {
Mailbox::new(enclose!((self => s) move |option_message| {
if let Some(message) = option_message {
s.update(message);
} else {
s.rerender_vdom();
}
}))
}
#[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, INodes, GMs>) -> Init<Mdl> + 'static,
update: UpdateFn<Ms, Mdl, INodes, GMs>,
view: ViewFn<Mdl, INodes>,
) -> InitAppBuilder<Ms, Mdl, INodes, 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();
self.notify(subs::UrlChanged(url.clone()));
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();
let routes = *self.data.routes.borrow();
routing::setup_popstate_listener(
enc!((self => s) move |msg| s.update(msg)),
enc!((self => s) move |closure| {
s.data.popstate_closure.replace(Some(closure));
}),
enc!((self => s) move |notification| s.notify_with_notification(notification)),
routes,
Rc::clone(&self.cfg.base_path),
);
routing::setup_hashchange_listener(
enc!((self => s) move |msg| s.update(msg)),
enc!((self => s) move |closure| {
s.data.hashchange_closure.replace(Some(closure));
}),
enc!((self => s) move |notification| s.notify_with_notification(notification)),
routes,
Rc::clone(&self.cfg.base_path),
);
routing::setup_link_listener(
enc!((self => s) move |msg| s.update(msg)),
enc!((self => s) move |notification| s.notify_with_notification(notification)),
routes,
);
orders.subscribe(enc!((self => s) move |url_requested| {
routing::url_request_handler(
url_requested,
Rc::clone(&s.cfg.base_path),
move |notification| s.notify_with_notification(notification),
)
}));
self.process_effect_queue(orders.effects);
self.rerender_vdom();
self
}
}
#[deprecated(since = "0.5.0", note = "Part of the old Init API.")]
type InitAppBuilder<Ms, Mdl, INodes, GMs> = AppBuilder<
Ms,
Mdl,
INodes,
GMs,
MountPointInitInitAPI<UndefinedMountPoint, BuilderInitFn<Ms, Mdl, INodes, GMs>>,
>;