Docs: fleshed out custom renderer doc

This commit is contained in:
Jonathan Kelley 2021-06-24 00:18:29 -04:00
parent de1535ddac
commit 17f6576e98
5 changed files with 211 additions and 30 deletions

View file

@ -57,3 +57,4 @@ dom
textarea
noderefs
wasm
7ns

View file

@ -0,0 +1,168 @@
# Custom Renderer
Dioxus is an incredibly portable framework for UI development. The lessons, knowledge, hooks, and components you acquire over time can always be used for future projects. However, sometimes those projects cannot leverage a supported renderer or you need to implement your own better renderer.
Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but provide no trait or explicit interface to follow.
## The specifics:
Implementing the renderer is fairly straightforward. The renderer needs to:
1. Handle the stream of edit events generated by updates to the virtual DOM
2. Register listeners and pass events into the virtual DOM's event system
3. Progress the virtual DOM with an async executor (or disable the suspense API and use `progress_sync`)
Essentially, your renderer needs to implement the `RealDom` trait and generate `EventTrigger` objects to update the VirtualDOM. From there, you'll have everything needed to render the VirtualDOM to the screen.
Internally, Dioxus handles the tree relationship, diffing, memory management, and the event system, leaving as little as possible required for renderers to implement themselves.
For reference, check out the WebSys renderer as a starting point for your custom renderer.
## Trait implementation
The current `RealDom` trait lives in `dioxus_core/diff`. A version of it is provided here:
```rust
pub trait RealDom {
// Navigation
fn push_root(&mut self, root: RealDomNode);
// Add Nodes to the dom
fn append_child(&mut self);
fn replace_with(&mut self);
// Remove Nodesfrom the dom
fn remove(&mut self);
fn remove_all_children(&mut self);
// Create
fn create_text_node(&mut self, text: &str) -> RealDomNode;
fn create_element(&mut self, tag: &str) -> RealDomNode;
fn create_element_ns(&mut self, tag: &str, namespace: &str) -> RealDomNode;
// Events
fn new_event_listener(
&mut self,
event: &str,
scope: ScopeIdx,
element_id: usize,
realnode: RealDomNode,
);
fn remove_event_listener(&mut self, event: &str);
// modify
fn set_text(&mut self, text: &str);
fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool);
fn remove_attribute(&mut self, name: &str);
// node ref
fn raw_node_as_any_mut(&self) -> &mut dyn Any;
}
```
This trait defines what the Dioxus VirtualDOM expects a "RealDom" abstraction to implement for the Dioxus diffing mechanism to function properly. The Dioxus diffing mechanism operates as a [stack machine](https://en.wikipedia.org/wiki/Stack_machine) where the "push_root" method pushes a new "real" DOM node onto the stack and "append_child" and "replace_with" both remove nodes from the stack. When the RealDOM creates new nodes, it must return the `RealDomNode` type... which is just an abstraction over u32. We strongly recommend the use of `nohash-hasher`'s IntMap for managing the mapping of `RealDomNode` (ids) to their corresponding real node. For an IntMap of 1M+ nodes, an index time takes about 7ns which is very performant when compared to the traditional hasher.
## Event loop
Like most GUIs, Dioxus relies on an event loop to progress the VirtualDOM. The VirtualDOM itself can produce events as well, so it's important that your custom renderer can handle those too.
The code for the WebSys implementation is straightforward, so we'll add it here to demonstrate how simple an event loop is:
```rust
pub async fn run(&mut self) -> dioxus_core::error::Result<()> {
// Push the body element onto the WebsysDom's stack machine
let mut websys_dom = crate::new::WebsysDom::new(prepare_websys_dom().first_child().unwrap());
websys_dom.stack.push(root_node);
// Rebuild or hydrate the virtualdom
self.internal_dom.rebuild(&mut websys_dom)?;
// Wait for updates from the real dom and progress the virtual dom
while let Some(trigger) = websys_dom.wait_for_event().await {
websys_dom.stack.push(body_element.first_child().unwrap());
self.internal_dom
.progress_with_event(&mut websys_dom, trigger)?;
}
}
```
It's important that you decode the real events from your event system into Dioxus' synthetic event system (synthetic meaning abstracted). This simply means matching your event type and creating a Dioxus `VirtualEvent` type. Your custom event must implement the corresponding event trait:
```rust
fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
match event.type_().as_str() {
"keydown" | "keypress" | "keyup" => {
struct CustomKeyboardEvent(web_sys::KeyboardEvent);
impl dioxus::events::KeyboardEvent for CustomKeyboardEvent {
fn char_code(&self) -> usize { self.0.char_code() }
fn ctrl_key(&self) -> bool { self.0.ctrl_key() }
fn key(&self) -> String { self.0.key() }
fn key_code(&self) -> usize { self.0.key_code() }
fn locale(&self) -> String { self.0.locale() }
fn location(&self) -> usize { self.0.location() }
fn meta_key(&self) -> bool { self.0.meta_key() }
fn repeat(&self) -> bool { self.0.repeat() }
fn shift_key(&self) -> bool { self.0.shift_key() }
fn which(&self) -> usize { self.0.which() }
fn get_modifier_state(&self, key_code: usize) -> bool { self.0.get_modifier_state() }
}
VirtualEvent::KeyboardEvent(Rc::new(event.clone().dyn_into().unwrap()))
}
_ => todo!()
```
## Custom raw elements
If you need to go as far as relying on custom elements for your renderer - you totally can. This still enables you to use Dioxus' reactive nature, component system, shared state, and other features, but will ultimately generate different nodes. All attributes and listeners for the HTML and SVG namespace are shuttled through helper structs that essentially compile away (pose no runtime overhead). You can drop in your own elements any time you want, with little hassle. However, you must be absolutely sure your renderer can handle the new type, or it will crash and burn.
For example, the `div` element is (approximately!) defined as such:
```rust
struct div(NodeBuilder);
impl<'a> div {
#[inline]
fn new(factory: &NodeFactory<'a>) -> Self {
Self(factory.new_element("div"))
}
#[inline]
fn onclick(mut self, callback: impl Fn(MouseEvent) + 'a) -> Self {
self.0.add_listener("onclick", |evt: VirtualEvent| match evt {
MouseEvent(evt) => callback(evt),
_ => {}
});
self
}
// etc
}
```
The rsx! and html! macros just use the `div` struct as a compile-time guard around the NodeFactory.
## Compatibility
Forewarning: not every hook and service will work on your platform. Dioxus wraps things that need to be cross-platform in "synthetic" types. However, downcasting to a native type might fail if the types don't match.
There are three opportunities for platform incompatibilities to break your program:
1. When downcasting elements via `Ref.to_native<T>()`
2. When downcasting events via `Event.to_native<T>()`
3. Calling platform-specific APIs that don't exist
The best hooks will properly detect the target platform and still provide functionality, failing gracefully when a platform is not supported. We encourage - and provide - an indication to the user on what platforms a hook supports. For issues 1 and 2, these return a result as to not cause panics on unsupported platforms. When designing your hooks, we recommend propagating this error upwards into user facing code, making it obvious that this particular service is not supported.
This particular code _will panic_ due to the unwrap. Try to avoid these types of patterns.
```rust
let div_ref = use_node_ref(&cx);
cx.render(rsx!{
div { ref: div_ref, class: "custom class",
button { "click me to see my parent's class"
onclick: move |_| if let Some(div_ref) = div_ref {
panic!("Div class is {}", div_ref.to_native::<web_sys::Element>().unwrap().class())
}
}
}
})
```

View file

@ -1,30 +0,0 @@
# Custom Renderer
Dioxus is an incredibly portable framework for UI development. The lessons, knowledge, hooks, and components you acquire over time can always be used for future projects. However, sometimes those projects cannot leverage a supported renderer or you need to implement your own better renderer.
Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but provide no trait or explicit interface to follow.
Implementing the renderer is fairly straightforward. The renderer needs to:
1. Handle the stream of edit events generated by updates to the virtual DOM
2. Register listeners and pass events into the virtual DOM's event system
3. Progress the virtual DOM with an async executor (or disable the suspense API and use `progress_sync`)
Essentially, your renderer needs to understand the EditEvent type and provide a callback for injecting events. From there, you'll have everything needed to render the VirtualDOM to the screen.
Internally, Dioxus handles the tree relationship, diffing, memory management, and the event system, leaving as little as possible required for renderers to implement themselves.
For inspiration, check out the source code for the various renderers we support:
- WebSys
- Morph
## Compatibility
Forewarning: not every hook and service will work on your platform. Dioxus wraps things that need to be cross-platform in "synthetic" types. However, downcasting to a native type might fail if the types don't match.
There are three opportunities for platform incompatibilities to break your program:
1. When downcasting elements via `Ref.to_native<T>()`
2. When downcasting events via `Event.to_native<T>()`
3. Calling platform-specific APIs that don't exist
The best hooks will properly detect the target platform and still provide functionality, failing gracefully when a platform is not supported. We encourage - and provide - an indication to the user on what platforms a hook supports. For issues 1 and 2, these return a result as to not cause panics on unsupported platforms. When designing your hooks, we recommend propagating this error upwards into user facing code, making it obvious that this particular service is not supported.

View file

@ -132,3 +132,45 @@ static AntipatternMisusedHooks: FC<MisuedHooksProps> = |cx| {
rsx!(in cx, div { "Not rendering state" })
}
};
/// Antipattern: Downcasting refs and panicing
/// ------------------------------------------
///
/// Occassionally it's useful to get the ref of an element to handle it directly. Elements support downcasting to
/// Dioxus's virtual element types as well as their true native counterparts. Downcasting to Dioxus' virtual elements
/// will never panic, but downcasting to native elements will fail if on an unsupported platform. We recommend avoiding
/// publishing hooks and components that deply rely on control over elements using their native `ref`, preferring to
/// use their Dioxus Virtual Element counterpart instead.
// This particular code *will panic* due to the unwrap. Try to avoid these types of patterns.
/// ---------------------------------
/// TODO: Get this to compile properly
/// let div_ref = use_node_ref(&cx);
///
/// cx.render(rsx!{
/// div { ref: div_ref, class: "custom class",
/// button { "click me to see my parent's class"
/// onclick: move |_| if let Some(div_ref) = div_ref {
/// panic!("Div class is {}", div_ref.to_native::<web_sys::Element>().unwrap().class())
/// }
/// }
/// }
/// })
static _example: FC<()> = |cx| todo!();
/// Antipattern: publishing components and hooks with all features enabled
/// ----------------------------------------------------------------------
///
/// The `dioxus` crate combines a bunch of useful utilities together (like the rsx! and html! macros, hooks, and more).
/// However, when publishing your custom hook or component, we highly advise using only the `core` feature on the dioxus
/// crate. This makes your crate compile faster, makes it more stable, and avoids bringing in incompatible libraries that
/// might make it not compile on unsupported platforms.
///
/// We don't have a code snippet for this, but just prefer to use this line:
/// dioxus = { version = "*", features = ["core"]}
/// instead of this one:
/// dioxus = { version = "*", features = ["web", "desktop", "full"]}
/// in your Cargo.toml
///
/// This will only include the `core` dioxus crate which is relatively slim and fast to compile and avoids target-specific
/// libraries.
static __example: FC<()> = |cx| todo!();