simi 0.0.1

A framework help building wasm-front-end web application in Rust
docs.rs failed to build simi-0.0.1
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: simi-0.2.0

Simi

A framework for building front-end web app in Rust. Simi is inspired by Yew

A simple app

Everything from simi-project/simi/examples/a-counter/lib.rs:

#![feature(proc_macro_non_items)]
#![feature(custom_attribute)]
extern crate simi;
extern crate wasm_bindgen;

use simi::prelude::*;
use wasm_bindgen::prelude::*; // Because simi use wasm-bindgen internally

enum Msg {
    Up,
    Down,
}

// This attribute (use `wasm-bindgen` internally) will create
// some boilerplate code to define and export functionality
// to run the app on the JS side.
#[simi_app] 
struct Counter {
    count: i32,
}

impl SimiApp for Counter {
    type Message = Msg;
    type Context = ();
    fn init(_main: WeakMain<Self>) -> (Self, Self::Context) {
        (Self { count: 2018 }, ())
    }
    // If this `update` method return `true`, then `render` (the next method) will be executed.
    fn update(&mut self, m: Msg, _context: &mut Self::Context) -> UpdateView {
        match m {
            Msg::Up => self.count += 1,
            Msg::Down => self.count -= 1,
        }
        true
    }
    fn render(&self, context: AppRenderContext<Self>) {
        //           ^^^^^^^ Do not change this name, it is used by `application!`
        
        // This macro generated code for creating and updating both virtual and real DOM.
        // Everything need to be initialized in create-phase. But in the update-phase, only
        // things that can be changed are tracked for update.
        application! {
            // If you want to see the generated code of this macro, uncomment the next line
            //@debug
            h2 { "Counter" }
            //   ^^^^^^^^^ Literals will be create as Text node
            // and will not be checked for update.
            p { "the " b {"simplest"} " version" }
            hr
            div { "Current value: " self.count }
            //                      ^^^^^^^^^^ Expressions will also be a Text node
            // but will be check for update everytime `render` method is executed.
            button (onclick=#Msg::Down) { "Down" }
            //               ^^^^^^^^^ Sendback Msg::Down to the app when the button get clicks.
            button (onclick=#Msg::Up) { "Up" }
            //              ^ this symbol means `no update`.
            // It means that the event listener is attached when the app is initialized.
            // Simi will not generate code for updating this event.
        }
    }
}

The simi app is actually a library

Simi does not have its own event loop. It is just a library. It must be executed from JS side, simi build (a simi-cli command) will create that portion of code for you.

Virtual DOM, application! and component!

Similar to other frameworks, Simi core thing is a virtual DOM. Everything that may change in the future will be tracked in the virtual DOM. On every update, they are check against the current value in the virtual DOM, if it has been changed, both virtual and real DOM will be updated. Everything else that will not be changed, Simi will not track them, instead, Simi render them directly to real DOM when the app started. application! and component! are responsible for building and updating DOMs. Internally, these two macros have the same implementation, working in two different mode (application mode or component mode). They generated code for building/update DOMs from a simi-style HTML template. Here, we have a closer look at these two macros.

Syntax

application! {
    hr // `<hr>`
    br // `<br>`
    div (...) // With attribute block
    span {...} // With content block
    p (...) {...} // With both attribute block and content block
}

Important:

simi only works with a set of supported html tags. If simi found out that an identifier is not an html tag, it will automatically treat it as an expression. If you believe there are valid html tags that must be supported by simi please open an issue (or better: open a pull/merge request).

Text and child content

application! {
    p { "This is the first paragraph." }
    "Some text at the root level"
    p {
        "This paragraph contains"
        code {
            "some special text"
        }
        "in its content. It also contains"
        self.get_some_string() // represent as a text node
        self.field.some_funtion().count() // also represent as a text node
        ". Every expression output must have implemented trait ToString"
    }
}

Attributes

Attributes must be provided in the form of name=value pair.

Rules for name:

  • An event name: onclick, oninput...
  • An attribute name: type, checked, id, class...
  • A hyphen-separated-series of identifiers such as data-my-custom-attribute.
  • A special simi-attribute: a str literal like "class1", this is only use as a class name, see more below - in the Conditional classes section.

Rules for value:

  • A literal: "class-name", "tag-id"...
  • A bool literal: true, false
  • A Rust expression: self.get_name()
  • A Message variant: Msg::SendMessage, Msg::SetSomething(some_value), Msg::ButtonClick(?) (for events only)
application! {
    p (id="first-para" class="class1 class2")  {
        "..."
    }
    input (type="checkbox" checked=self.pull_request.is_approved())

Conditional classes

A str literal that is used in place of attribute name will be treated as a class. Its value will be the condition to add/remove itself to/from the real element.

let some_name = "my-class";
application! {
    div (
        // These classes will never change
        class="class1 class2" 
        // These classes will be on/off depend on the result value (true/false) of the expression
        "blue-text"=self.is_something_true()
        "border"=self.is_something_false()
        // Remember: it must be a str literal
        "this-must-be-a-str-literal"=other_bool_expression()
        // If you want to use the value of a variable as a conditional class name
        // You must add a question mark suffix:
        some_name?=expression
    )
    {
        "IMPORTANT: expressions must evaluate to `bool`"
    }
}

Events

Currently, Simi only support 3 types of element events:

application! {
    // This will receive the event argument sended by JS in the place of `?`.
    button (onclick=Msg::Clicked(?)) { 
        "Click me"
    }
    // This will ignore the event argument.
    button (onclick=Msg::SendMessage) { 
        "Send"
    }
    // This will also ignore the event argument.
    button (onclick=Msg::SetValue(value)) { 
        "Send"
    }
}

if ... else ... and match

application! and component! support if else and match:

application! {
    if some_expr {
        div {...}
        p {...}
    } else if another_expr {
        section {...}
    } else {
        span {...}
    }
    match some_value {
        value1 => {
            p {...}
        }
        value2 => {
            div {...}
        }
    }
}

When rendering a new if else/match arm, all nodes (both virtual and real DOM) of the previous rendered-arm will be removed from their DOM, then the new arm will be created. So, you should minimize things in if else/match construct.

A special case is if or if else if... (without the final else). Simi will create an empty <span class="simi-empty-span-element-use-in-place-of-omitted-else"> as the content of the omitted else. If you want everything in your control, you must create the phantom-element yourself.

for loop

application! and component! supports for loop. Unlike if else or match, for only supports a single item at the root of its body:

application! {
    for value in some_list.iter() {
        li { "use the value of " value }
    }
}

Currently, for loop is implemented with the most naive algorithm: If you have N items, you remove the first item, then Simi will update the first N-1 current items, then remove the last item. Simi is not (yet) smart enough to remove just the first item in the real DOM to leave other items as is.

Do not update things with #

application! and component! try to avoid building a new virtual DOM on update. It will try to update current virtual DOM, then - if it is required, updates the real DOM.

When updating, Simi always ignore literals. Simi can also ignore updating an attribute, a content or a whole sub DOM if you want.

application! {
    // Prefix the value with `#`, Simi will only create it with the value when the app start. It will not be updated when the value change
    p { "The initial value of count: " #self.count }

    // Similarly, for an attribute: `alt` will not be updated
    img (src=self.image_url alt=#self.alt_string)

    div {
        p { ... }
        #div { // This `div` and its whole sub will be ignore when updating
            p { ... }
            span { ... }
        }
    }
}

Not allow complex Rust code in macros

application!/component! do not allow a complex handler. The following piece of code will NOT work:

application! {
    button (onclick={
            if something_is_true {  // This type
                Msg::Clicked(?)     // of event handler
            } else {                // will
                Msg::SendMessage    // NOT
            }                       // work!
        }
    )
    { 
        "Click me"
    }
}

It is better to move it out of the macros. In the case of onclick above, you should only have a Msg::Clicked(?). And do the check for SendMessage in the update method.

For other attributes:

// Complex code here, outside of `application!` or ``component!`
let result = match something {
    value1 => true,
    value2 if some_expr => true,
    _ => false,
};

application! {
    //                             vvvvvv Use the result value here
    input (type="checkbox" checked=result)
}

Component

Component is a way to reuse code. I personaly do not like the way Component works right now. But it is my best effort. I don't know how to make it better!

Here is a component extract from simi-project/simi/examples/b-counter-component/src/lib.rs

struct Counter<A: SimiApp> {
    title: Option<&'static str>, // All fields in a component struct are requires to be Option<T>
    up: simi::element_events::ElementEvent<A>, // This is actually an Option<Box<Event<A>>>
    down: simi::element_events::ElementEvent<A>,
}

impl<A: SimiApp> Component<A> for Counter<A> {
//   ^ The type name for the app is A, `component!` assume it is A (do not use another name)
    type Properties = i32;
    fn render(mut self, props: &Self::Properties, context: CompRenderContext<A>) {
        //                                        ^^^^^^^ Do not change this name
        component! {
            div { #self.title props }
            button (onclick=#self.down) { "Down" }
            button (onclick=#self.up) { "Up" }
        }
    }
}

And how to use a component (in a simi app, also extracted from examples/b-counter-component/src/lib.rs):

fn render(&self, context: AppRenderContext<Self>) {
    application! {
        h2 { "Counters" }
        p { "using a " b {"simple component"} }
        hr
        
        // `Counter` is the component's struct name
        Counter (self.value1) {
            //   ^^^^^^^^^^^ Here, we provide properties for the component
            // Simi always check `props` for update

            // Next three lines are for three fields of the struct `Counter`.
            // `application!` will convert these into an Option<T> or std::cell::Cell.
            title: "First value: " // `title` is the field name of the component struct
            up: #onclick=Msg::Value1Up // for the struct field named: `up`
            down: #onclick=Msg::Value1Down // for the struct field named: `down`
        }
        hr
        // A second use of `Counter`
        Counter (self.value2) {
            title: "Second value: "
            up: #onclick=Msg::Value2Up
            down: #onclick=Msg::Value2Down
        }
    }
}

Child Component

You can use a component directly in another component, just like using it in application! in previous section. But simi also support a more flexible use case. A component can have a child-placeholder, then when using the component, you can pass in any component to be rendered at the preserved-place. See in the full example here.

You define a component that has two child-components:

struct TwoCounters<A: SimiApp> {
    child1: ComponentChild<A>,
    child2: ComponentChild<A>,
}
impl<A: SimiApp> Component<A> for TwoCounters<A> {
    type Properties = ();
    fn render(&self, _props: &Self::Properties, context: CompRenderContext<A>) {
        component!{
            div{
                p { "The first child" }
                // A placeholder for a child component must be prefix with `$` to disambiguate from an expression
                $ self.child1
                p { "The second child" }
                $ self.child2
            }
        }
    }
}

Then, in the application:

    application! {
        TwoCounters {
            // Use component `Counter` for the first child
            child1: Counter (self.value1) {
                title: "First value: "
                up: #onclick=Msg::Value1Up
                down: #onclick=Msg::Value1Down
            }
            // Also another `Counter` as the second child
            child2: Counter (self.value2) {
                title: "Second value: "
                up: #onclick=Msg::Value2Up
                down: #onclick=Msg::Value2Down
            }
        }
    }

UbuComponent (Update-by-user Component)

A Simi app change its state in SimiApp trait's update method. If update return true, then the framework execute render (it's is all so a SimiApp method). render will check the whole DOM on every run. In some cases, your app have a region that update frequently, for example, a clock must update every second, but the rest of your app only update on user's interaction. It's very nice if you can control your clock seperately. This is where to use UbuComponent. See the full example code here

Sub App

A similar use case as UbuComponent, but it has its own data state and messages to process? You may want organize your app as a simi-multi-app.

Sub App: If your app is big? And the app state can be split into smaller and disjoint group? You may want to implement your app as a multi-app. div() [sub_app:clock]