Rust has its goals set on to be a primary WASM language and it would be awesome to use it both in backend and frontend web. Ruukh is one of such efforts to realise that dream. Ruukh, a frontend web framework, is inspired by both VueJS and ReactJS.

So, what does it look like?

#![feature(proc_macro_gen, proc_macro_non_items, decl_macro)]

use wasm_bindgen::prelude::*;
use ruukh::prelude::*;

#[component]
#[derive(Lifecycle)]
struct MyApp;

impl Render for MyApp {
    fn render(&self) -> Markup<Self> {
        html! {
            "Hello World!"
        }
    }
}

#[wasm_bindgen]
pub fn run() {
    App::<MyApp>::new().mount("app");
}

This snippet as you would infer requires the latest rust nightly with Rust2018 1 edition to run. The run function is a special function in this context as it is exposed to JS environment to be called after WASM intialization. Currently binary packages are unexecutable using wasm-bindgen. We’ll talk about how to setup a project to run this web app later.

Now, let’s discuss what every bit of code does here.

#![feature(proc_macro_gen, proc_macro_non_items, decl_macro)]

The project heavily depends on the proc-macro features as well as decl_macro feature to make this framework work.

#[component]
#[derive(Lifecycle)]
struct MyApp;

MyApp is a component struct onto which #[component] is marked. This attribute prepares the struct into a working implementation of a component. At a higher level understanding, it provides a reactivity mechanism to the component as well as implement Component trait on the struct. #[derive(Lifecycle)] implements Lifecycle trait which provides methods which are invoked when each of the lifecycles states are completed. You may provide a custom implementation for the Lifecycle if you desire so.

impl Render for MyApp {
    fn render(&self) -> Markup<Self> {
        html! {
            "Hello World!"
        }
    }
}

View is rendered using Render trait where a markup generated by html! macro.

#[wasm_bindgen]
pub fn run() {
    App::<MyApp>::new().mount("app");
}

Creates a new app with MyApp mounted on element with id="app".

That was a simple overview, now lets incorporate some state to the app.

#[component]
#[derive(Lifecycle)]
struct MyApp {
    #[state]
    count: i32
}

impl Render for MyApp {
    fn render(&self) -> Markup<Self> {
        html! {
            "The count is "{ self.count }"."
            <button @click={|this: &Self, _event| {
                this.set_state(|state| {
                    state.count += 1;
                });
            }}>"Increment"</button>
        }
    }
}

To mark a struct field as a state, just mark it with #[state] attribute. The initial value of the state is Default::default(). To override the default value provided by Default implementation just provide #[state(default = 5)] or some other value.

Looking at the html! markup, you may find three important bits:

First.

"The count is "{ self.count }"."

{ self.count } is any rust expression which resolves to any type that is convertible to Markup.

Second.

<button @click={|this: &Self, _event| {
    ...
}}>Increment</button>

Button is listening to click event with an event handler passed to it. You may pass event listeners with @ prepending to the event name. Also, mind the fact that the event handler signature is Fn(&self, event: Event).

Third.

this.set_state(|state| {
    state.count += 1;
});

To mutate state, call self.set_state with a closure which does all the changes required. The component intelligently reacts to the state changes when the state actually changes i.e. state.count = state.count does not do anything.

Till now everything was a single component, now lets look at other examples to get the full benefits of a component centered framework.

#[component]
#[derive(Lifecycle)]
struct MyApp;

impl Render for MyApp {
    fn render(&self) -> Markup<Self> {
        html! {
            <div>
                <Button
                    text={"Save"}
                    @save-all={Self::save_all}
                ></Button>
            </div>
        }
    }
}

// MyApp::save_all implementation

#[component]
#[derive(Lifecycle)]
#[events(
    #[optional]
    fn save_all(&self, event: Event);
)]
struct Button {
    text: &'static str
}

impl Render for Button {
    fn render(&self) -> Markup<Self> {
        html! {
            <button 
                style={"background-color: white;"} 
                @click={Self::save_all}
            >{ self.text }
            </button>
        }
    }
}

Now, we need to disect the code. The first of the tidbits:

#[events(
    #[optional]
    fn save_all(&self, event: Event);
)]

This declares that the component Button accepts the given events. The event signature requires that the first argument be &self. You also might note that the event itself is marked with #[optional]. This is to declare that the event is optional to pass an event handler. If you require the event to be absolutely required then omit this attribute.

struct Button {
    text: &'static str
}

The text field is a prop field for this component. Any field with no or #[prop] attribute is a prop field. You may also make it optional by using either Option<T> or using a default value #[prop(default = val)].

<Button
    text={"Save"}
    @save-all={Self::save_all}
></Button>

The usage of this component is done using the component name in a non-self-closing tag. The props and events are passed in a kebab-cased fashion. Event names are likewise preceded by @ as always.

Currently, there is no way to pass children to the component. So you will have to wait out on that. Likewise, this initial release is really unstable and will have breaking changes between minor releases as I would like to experiment more on the design. Also, I cannot make a promise to ever stable-y release this project as I would want it to be full proof before I make a long term commitment. So, this project has to be taken as study and experimentation in the areas of frontend development in Rust.

Along with the framework, cargo-ruukh subcommand is provided to ease app building and running. So before you start hacking on examples provided, I would recommend you to install the CLI first with cargo install cargo-ruukh. Happy Hacking!

1. The latest rust nightly by default creates a Rust2018 edition project.