lib.rs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. //!
  2. //!
  3. //!
  4. //!
  5. //! This crate demonstrates how to implement a custom renderer for Dioxus VNodes via the `TextRenderer` renderer.
  6. //! The `TextRenderer` consumes a Dioxus Virtual DOM, progresses its event queue, and renders the VNodes to a String.
  7. //!
  8. //! While `VNode` supports "to_string" directly, it renders child components as the RSX! macro tokens. For custom components,
  9. //! an external renderer is needed to progress the component lifecycles. The `TextRenderer` shows how to use the Virtual DOM
  10. //! API to progress these lifecycle events to generate a fully-mounted Virtual DOM instance which can be renderer in the
  11. //! `render` method.
  12. use std::fmt::{Display, Formatter};
  13. use dioxus_core::*;
  14. pub fn render_vnode(vnode: &VNode, string: &mut String) {}
  15. pub fn render_vdom(dom: &VirtualDom, cfg: impl FnOnce(SsrConfig) -> SsrConfig) -> String {
  16. format!(
  17. "{:}",
  18. TextRenderer::from_vdom(dom, cfg(SsrConfig::default()))
  19. )
  20. }
  21. pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
  22. Some(format!(
  23. "{:}",
  24. TextRenderer {
  25. cfg: SsrConfig::default(),
  26. root: vdom.get_scope(scope).unwrap().root(),
  27. vdom: Some(vdom)
  28. }
  29. ))
  30. }
  31. /// A configurable text renderer for the Dioxus VirtualDOM.
  32. ///
  33. ///
  34. /// ## Details
  35. ///
  36. /// This uses the `Formatter` infrastructure so you can write into anything that supports `write_fmt`. We can't accept
  37. /// any generic writer, so you need to "Display" the text renderer. This is done through `format!` or `format_args!`
  38. ///
  39. /// ## Example
  40. /// ```ignore
  41. /// static App: FC<()> = |cx| cx.render(rsx!(div { "hello world" }));
  42. /// let mut vdom = VirtualDom::new(App);
  43. /// vdom.rebuild();
  44. ///
  45. /// let renderer = TextRenderer::new(&vdom);
  46. /// let output = format!("{}", renderer);
  47. /// assert_eq!(output, "<div>hello world</div>");
  48. /// ```
  49. pub struct TextRenderer<'a> {
  50. vdom: Option<&'a VirtualDom>,
  51. root: &'a VNode<'a>,
  52. cfg: SsrConfig,
  53. }
  54. impl Display for TextRenderer<'_> {
  55. fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
  56. self.html_render(self.root, f, 0)
  57. }
  58. }
  59. impl<'a> TextRenderer<'a> {
  60. pub fn from_vdom(vdom: &'a VirtualDom, cfg: SsrConfig) -> Self {
  61. Self {
  62. cfg,
  63. root: vdom.base_scope().root(),
  64. vdom: Some(vdom),
  65. }
  66. }
  67. fn html_render(&self, node: &VNode, f: &mut std::fmt::Formatter, il: u16) -> std::fmt::Result {
  68. match &node {
  69. VNode::Text(text) => {
  70. if self.cfg.indent {
  71. for _ in 0..il {
  72. write!(f, " ")?;
  73. }
  74. }
  75. write!(f, "{}", text.text)?
  76. }
  77. VNode::Anchor(anchor) => {
  78. //
  79. if self.cfg.indent {
  80. for _ in 0..il {
  81. write!(f, " ")?;
  82. }
  83. }
  84. write!(f, "<!-- -->")?;
  85. }
  86. VNode::Element(el) => {
  87. if self.cfg.indent {
  88. for _ in 0..il {
  89. write!(f, " ")?;
  90. }
  91. }
  92. write!(f, "<{}", el.tag_name)?;
  93. let mut attr_iter = el.attributes.iter().peekable();
  94. while let Some(attr) = attr_iter.next() {
  95. match attr.namespace {
  96. None => write!(f, " {}=\"{}\"", attr.name, attr.value)?,
  97. Some(ns) => {
  98. // write the opening tag
  99. write!(f, " {}=\"", ns)?;
  100. let mut cur_ns_el = attr;
  101. 'ns_parse: loop {
  102. write!(f, "{}:{};", cur_ns_el.name, cur_ns_el.value)?;
  103. match attr_iter.peek() {
  104. Some(next_attr) if next_attr.namespace == Some(ns) => {
  105. cur_ns_el = attr_iter.next().unwrap();
  106. }
  107. _ => break 'ns_parse,
  108. }
  109. }
  110. // write the closing tag
  111. write!(f, "\"")?;
  112. }
  113. }
  114. }
  115. // we write the element's id as a data attribute
  116. //
  117. // when the page is loaded, the `querySelectorAll` will be used to collect all the nodes, and then add
  118. // them interpreter's stack
  119. match (self.cfg.pre_render, node.try_direct_id()) {
  120. (true, Some(id)) => {
  121. write!(f, " dio_el=\"{}\"", id)?;
  122. //
  123. for listener in el.listeners {
  124. // write the listeners
  125. }
  126. }
  127. _ => {}
  128. }
  129. match self.cfg.newline {
  130. true => write!(f, ">\n")?,
  131. false => write!(f, ">")?,
  132. }
  133. for child in el.children {
  134. self.html_render(child, f, il + 1)?;
  135. }
  136. if self.cfg.newline {
  137. write!(f, "\n")?;
  138. }
  139. if self.cfg.indent {
  140. for _ in 0..il {
  141. write!(f, " ")?;
  142. }
  143. }
  144. write!(f, "</{}>", el.tag_name)?;
  145. if self.cfg.newline {
  146. write!(f, "\n")?;
  147. }
  148. }
  149. VNode::Fragment(frag) => {
  150. for child in frag.children {
  151. self.html_render(child, f, il + 1)?;
  152. }
  153. }
  154. VNode::Component(vcomp) => {
  155. let idx = vcomp.ass_scope.get().unwrap();
  156. match (self.vdom, self.cfg.skip_components) {
  157. (Some(vdom), false) => {
  158. let new_node = vdom.get_scope(idx).unwrap().root();
  159. self.html_render(new_node, f, il + 1)?;
  160. }
  161. _ => {
  162. // render the component by name
  163. }
  164. }
  165. }
  166. VNode::Suspended { .. } => {
  167. // we can't do anything with suspended nodes
  168. }
  169. }
  170. Ok(())
  171. }
  172. }
  173. pub struct SsrConfig {
  174. // currently not supported - control if we indent the HTML output
  175. indent: bool,
  176. // Control if elements are written onto a new line
  177. newline: bool,
  178. // Choose to write ElementIDs into elements so the page can be re-hydrated later on
  179. pre_render: bool,
  180. // Currently not implemented
  181. // Don't proceed onto new components. Instead, put the name of the component.
  182. // TODO: components don't have names :(
  183. skip_components: bool,
  184. }
  185. impl Default for SsrConfig {
  186. fn default() -> Self {
  187. Self {
  188. indent: false,
  189. pre_render: false,
  190. newline: false,
  191. skip_components: false,
  192. }
  193. }
  194. }
  195. impl SsrConfig {
  196. pub fn indent(mut self, a: bool) -> Self {
  197. self.indent = a;
  198. self
  199. }
  200. pub fn newline(mut self, a: bool) -> Self {
  201. self.newline = a;
  202. self
  203. }
  204. pub fn pre_render(mut self, a: bool) -> Self {
  205. self.pre_render = a;
  206. self
  207. }
  208. pub fn skip_components(mut self, a: bool) -> Self {
  209. self.skip_components = a;
  210. self
  211. }
  212. }
  213. #[cfg(test)]
  214. mod tests {
  215. use super::*;
  216. use dioxus_core as dioxus;
  217. use dioxus_core::prelude::*;
  218. use dioxus_html as dioxus_elements;
  219. static SIMPLE_APP: FC<()> = |cx| {
  220. cx.render(rsx!(div {
  221. "hello world!"
  222. }))
  223. };
  224. static SLIGHTLY_MORE_COMPLEX: FC<()> = |cx| {
  225. cx.render(rsx! {
  226. div {
  227. title: "About W3Schools"
  228. {(0..20).map(|f| rsx!{
  229. div {
  230. title: "About W3Schools"
  231. style: "color:blue;text-align:center"
  232. class: "About W3Schools"
  233. p {
  234. title: "About W3Schools"
  235. "Hello world!: {f}"
  236. }
  237. }
  238. })}
  239. }
  240. })
  241. };
  242. static NESTED_APP: FC<()> = |cx| {
  243. cx.render(rsx!(
  244. div {
  245. SIMPLE_APP {}
  246. }
  247. ))
  248. };
  249. static FRAGMENT_APP: FC<()> = |cx| {
  250. cx.render(rsx!(
  251. div { "f1" }
  252. div { "f2" }
  253. div { "f3" }
  254. div { "f4" }
  255. ))
  256. };
  257. #[test]
  258. fn to_string_works() {
  259. let mut dom = VirtualDom::new(SIMPLE_APP);
  260. dom.rebuild().expect("failed to run virtualdom");
  261. dbg!(render_vdom(&dom, |c| c));
  262. }
  263. #[test]
  264. fn hydration() {
  265. let mut dom = VirtualDom::new(NESTED_APP);
  266. dom.rebuild().expect("failed to run virtualdom");
  267. dbg!(render_vdom(&dom, |c| c.pre_render(true)));
  268. }
  269. #[test]
  270. fn nested() {
  271. let mut dom = VirtualDom::new(NESTED_APP);
  272. dom.rebuild().expect("failed to run virtualdom");
  273. dbg!(render_vdom(&dom, |c| c));
  274. }
  275. #[test]
  276. fn fragment_app() {
  277. let mut dom = VirtualDom::new(FRAGMENT_APP);
  278. dom.rebuild().expect("failed to run virtualdom");
  279. dbg!(render_vdom(&dom, |c| c));
  280. }
  281. #[test]
  282. fn write_to_file() {
  283. use std::fs::File;
  284. use std::io::Write;
  285. let mut file = File::create("index.html").unwrap();
  286. let mut dom = VirtualDom::new(SLIGHTLY_MORE_COMPLEX);
  287. dom.rebuild().expect("failed to run virtualdom");
  288. file.write_fmt(format_args!(
  289. "{}",
  290. TextRenderer::from_vdom(&dom, SsrConfig::default())
  291. ))
  292. .unwrap();
  293. }
  294. #[test]
  295. fn styles() {
  296. static STLYE_APP: FC<()> = |cx| {
  297. cx.render(rsx! {
  298. div { style: { color: "blue", font_size: "46px" } }
  299. })
  300. };
  301. let mut dom = VirtualDom::new(STLYE_APP);
  302. dom.rebuild().expect("failed to run virtualdom");
  303. dbg!(render_vdom(&dom, |c| c));
  304. }
  305. }