Rust Handlebars dynamically select child Template

36 Views Asked by At

Imagine having a Struct Div containing objects like Header or Paragraph via a trait Element

On Execution, I want handlebars to render the Div with its children recursively. Therefore, it should select the template according to the type of the Element

use std::{cell::RefCell, error::Error, rc::Rc};

use handlebars::Handlebars;
use serde::Serialize;

pub trait Element: std::fmt::Debug + erased_serde::Serialize {}
erased_serde::serialize_trait_object!(Element);

#[derive(Debug, Serialize)]
pub struct Div {
    pub content: Vec<Rc<RefCell<dyn Element>>>,
}

impl Element for Div {}

#[derive(Debug, Serialize)]
pub struct Header {
    content: String,
}

impl Element for Header {}

#[derive(Debug, Serialize)]
pub struct Paragraph {
    content: String,
}

impl Element for Paragraph {}

fn main() -> Result<(), Box<dyn Error>> {
    let mut handlebars = Handlebars::new();

    handlebars.register_template_string("Header", "<h1>{{content}}</h1>".to_string())?;
    handlebars.register_template_string("Paragraph", "<p>{{content}}</p>".to_string())?;

    handlebars
        .register_template_string("Div", "{{#each content}}{{this}}\n{{/each}}".to_string())?;

    let div = Div {
        content: vec![
            Rc::new(RefCell::new(Header {
                content: "Title".to_string(),
            })),
            Rc::new(RefCell::new(Paragraph {
                content: "Paragraph One".to_string(),
            })),
            Rc::new(RefCell::new(Div {
                content: vec![Rc::new(RefCell::new(Paragraph {
                    content: "Nested Paragraph".to_string(),
                }))],
            })),
            Rc::new(RefCell::new(Paragraph {
                content: "Paragraph Two".to_string(),
            })),
        ],
    };

    println!("{}", handlebars.render("Div", &div)?);

    Ok(())
}

Handlebars then just renders [object] instead of using the template on that object

[object]
[object]
[object]
[object]

What I want to achieve as output:

<div><h1>Title</h1><p>Paragraph One</p><div><p>Nested Paragraph</p></div><p>Paragraph Two</p></div>

How to achieve this? How to tell handlebars to automatically select the according template based on the object type/name?

1

There are 1 best solutions below

0
hypnomaki On

The only solution I came up myself up to now is to introduce a render function at trait Element level, call this initially on the root Div and rendering it down recursively.

Unfortunately, this requires me to create another data structure like for example a json or btree map which seems like a big overhead to me.

use handlebars::{Handlebars, RenderError};
use serde::Serialize;
use serde_json::json;
use std::{cell::RefCell, error::Error, rc::Rc};

pub trait Element: std::fmt::Debug + erased_serde::Serialize {
    fn render(&self, handlebars: &Handlebars) -> Result<String, RenderError>;
}
erased_serde::serialize_trait_object!(Element);

#[derive(Debug, Serialize)]
pub struct Div {
    pub content: Vec<Rc<RefCell<dyn Element>>>,
}

impl Element for Div {
    fn render(&self, handlebars: &Handlebars) -> Result<String, RenderError> {
        let mut children = Vec::new();
        for child in self.content.iter() {
            children.push(child.as_ref().borrow().render(&handlebars)?);
        }
        handlebars.render("Div", &json!({"content": children}))
    }
}

#[derive(Debug, Serialize)]
pub struct Header {
    content: String,
}

impl Element for Header {
    fn render(&self, handlebars: &Handlebars) -> Result<String, RenderError> {
        handlebars.render("Header", &json!({"content": &self.content}))
    }
}

#[derive(Debug, Serialize)]
pub struct Paragraph {
    content: String,
}

impl Element for Paragraph {
    fn render(&self, handlebars: &Handlebars) -> Result<String, RenderError> {
        handlebars.render("Paragraph", &json!({"content": &self.content}))
    }
}

fn main() -> Result<(), Box<dyn Error>> {
    let mut handlebars = Handlebars::new();
    handlebars.register_template_string("Header", "<h1>{{{content}}}</h1>".to_string())?;
    handlebars.register_template_string("Paragraph", "<p>{{{content}}}</p>".to_string())?;
    handlebars.register_template_string(
        "Div",
        "<div>{{#each content}}{{{this}}}{{/each}}</div>".to_string(),
    )?;

    let div = Div {
        content: vec![
            Rc::new(RefCell::new(Header {
                content: "Title".to_string(),
            })),
            Rc::new(RefCell::new(Paragraph {
                content: "Paragraph One".to_string(),
            })),
            Rc::new(RefCell::new(Div {
                content: vec![Rc::new(RefCell::new(Paragraph {
                    content: "Nested Paragraph".to_string(),
                }))],
            })),
            Rc::new(RefCell::new(Paragraph {
                content: "Paragraph Two".to_string(),
            })),
        ],
    };

    println!("{}", div.render(&handlebars).unwrap());

    Ok(())
}