A reactive DOM library for Rust in WASM

Overview

maple

A VDOM-less web library with fine-grained reactivity.

Getting started

The recommended build tool is Trunk. Start by adding maple-core to your Cargo.toml:

maple-core = "0.1.1"

Add the following to your src/main.rs file:

use maple_core::prelude::*;

fn main() {
    let root = template! {
        p {
            # "Hello World!"
        }
    };

    render(root);
}

That's it! There's your hello world program using maple. To run the app, simply run trunk serve --open and see the result in your web browser.

The template! macro

maple uses the template! macro as an ergonomic way to create complex user interfaces.

// You can create nested elements.
template! {
    div {
        p {
            span {
                # "Hello "
            }
            strong {
                # "World!"
            }
        }
    }
};

// Attributes (including classes and ids) can also be specified.
template! {
    p(class="my-class", id="my-paragraph")
};

template! {
    button(disabled="true") {
        # "My button"
    }
}

// Events are attached using the `on:*` directive.
template! {
    button(on:click=|_| { /* do something */ }) {
        # "Click me"
    }
}

Reactivity

Instead of relying on a Virtual DOM (VDOM), maple uses fine-grained reactivity to keep the DOM and state in sync. In fact, the reactivity part of maple can be used on its own without the DOM rendering part.

Reactivity is based on reactive primitives. Here is an example:

use maple_core::prelude::*;
let (state, set_state) = create_signal(0); // create an atom with an initial value of 0

If you are familiar with React hooks, this will immediately seem familiar to you.

Now, to access the state, we call the first element in the tuple (state) like this:

println!("The state is: {}", state()); // prints "The state is: 0"

To update the state, we call the second element in the tuple (set_state):

set_state(1);
println!("The state is: {}", state()); // should now print "The state is: 0"

Why would this be useful? It's useful because it provides a way to easily be notified of any state changes. For example, say we wanted to print out every state change. This can easily be accomplished like so:

let (state, set_state) = create_signal(0);
create_effect(move || {
    println!("The state changed. New value: {}", state());
});  // prints "The state changed. New value: 0" (note that the effect is always executed at least 1 regardless of state changes)

set_state(1); // prints "The state changed. New value: 1"
set_state(2); // prints "The state changed. New value: 2"
set_state(3); // prints "The state changed. New value: 3"

How does the create_effect(...) function know to execute the closure every time the state changes? That's why state is a function rather than a simple variable. Calling create_effect creates a new "reactivity scope" and calling state() inside this scope adds itself as a dependency. Now, when set_state is called, it automatically calls all its dependents, in this case, the closure passed to create_effect.

Now is when things get interesting. We can easily create a derived state using create_memo(...):

let (state, set_state) = create_signal(0);
let double = create_memo(move || *state() * 2);

assert_eq!(*double(), 0);

set_state(1);
assert_eq!(*double(), 2);

create_memo(...) automatically recomputes the derived value when any of its dependencies change.

Using reactivity with DOM updates

Reactivity is automatically built-in into the template! macro. Say we have the following code:

use maple_core::prelude::*;

let (state, set_state) = create_signal(0);

let root = template! {
    p {
        # state()
    }
}

This will expand to something approximately like:

use maple_core::prelude::*;
use maple_core::internal;

let (state, set_state) = create_signal(0);

let root = {
    let element = internal::element(p);
    let text = internal::text(String::new() /* placeholder */);
    create_effect(move || {
        // update text when state changes
        text.set_text_content(Some(&state()));
    });

    internal::append(&element, &text);

    element
}

If we call set_state somewhere else in our code, the text content will automatically be updated!

Components

Components in maple are simply functions that return HtmlElement. They receive their props through function arguments.

For components to automatically react to prop changes, they should accept a prop with type StateHandle<T> and call the function in the template! to subscribe to the state.

Here is an example of a simple component:

// This is temporary and will later be removed.
// Currently, the template! macro assumes that all components start with an uppercase character.
#![allow(non_snake_case)]

use maple_core::prelude::*;

fn Component(value: StateHandle<i32>) -> TemplateResult {
    template! {
        div(class="my-component") {
            # "Value: "
            # value()
        }
    }
}

// ...
let (state, set_state) = create_signal(0);

template! {
    Component(state)
}

set_state(1); // automatically update value in Component

Contributing

Issue reports and PRs are welcome! Get familiar with the project structure with ARCHITECTURE.md.

Comments
  • Some performance tweaks

    Some performance tweaks

    Optimizations in sycamore-reactive crate.

    Some notable changes:

    • Simplify and reduce allocations in map_keyed and map_indexed.
    • Make Signal::trigger_subscribers slightly faster.
    performance 
    opened by lukechu10 10
  • Make `map_indexed` and `map_keyed` more idiomatic

    Make `map_indexed` and `map_keyed` more idiomatic

    Right now, map_indexed and map_keyed use a lot of index manipulations. This is not very idiomatic in Rust.

    Preferably, most of it can be refactored to use iterators instead. This will improve maintainability and perhaps even performance because it will remove the need for bound checks.

    good first issue 
    opened by lukechu10 10
  • Builder Pattern API

    Builder Pattern API

    Proposal

    I have been contemplating what a builder pattern API, as mentioned in #265, might look like. I personally like the idea of having both API's, and having the interop, as macros have their place which can be much easier to read, but the primary advantages, in my opinion to a builder pattern is that, currently, formatting and type hints don't work with the current tooling (rust-analyzer), and certain more complicated logic is not possible at the moment, without further syntax extensions.

    Also, from a discord comment, @lukechu10 mentioned the aim of eventually supporting React Native, and possibly other rendering targets. This to me, presents two different routes that could be taken:

    1. Having a rendering-agnostic API
    2. Having a rendering-specific API

    The first option could be something as follows (for HTML):

    button();
    div();
    a();
    /* etc */
    
    

    Whereas the agnostic approach could be something like:

    node("button");
    node("div");
    node("a");
    
    

    These features could be held behind a feature flag, such as html, react-native, native-script, etc. for the render-specific API, or just builder-api for the agnostic API, and prefixed with experimental: until an API is stabilized.

    Main required features

    In no particular order. Also note that all of the mentioned functions would be implemented for the builder struct and take either &mut self or self and return their respective Self type, but have been omitted for brevity..

    • (Un)setting Attributes

      • A further distinction could be made (for memory and performance reasons) between static and dynamic attributes, such as:

        fn set_attribute(name: &str, value: impl Into<JsValue>);
        
        // The Option type is important here, as it can be used to automatically remove
        // the attribute when required.
        fn set_dyn_attribute(name: &str, value: StateHandle<Option<impl Into<JsValue>>>);
        
        // Also note that remove_attribute is not needed here, since it's handled in the
        // dynamic case and in static cases not needed.
        
    • (Un)setting properties

      • Same as above
    • (Un)setting classes

      • Once again, I recommend having a distinction between static and dynamic classes.

        fn add_class(class: &str);
        fn add_dyn_class(class: StateHandle<Option<&str>>);
        
    • Add/remove children

      • For setting children, we have some choices to make. We can accept Vec<GenericNode>, or Template, thought the latter is preferable. The question lies on how to handle dynamic children.

        fn add_children(children: Template<G>);
        
        // Perhaps dynamic children could be handled as follows
        fn add_dyn_children(children: StateHandle<Template<G>>);
        
    • Special shortcuts

      • fn id(id: &str)
      • fn style(name: &str, value: &str) or perhaps fn add_styles<K, V>(HashMap<K, V>) where K: Hash + AsRef<str>, V: AsRef<str> or something similar. along with their dynamic Signal accepting counterparts.
      • And possible others I've missed.
    • Event listeners

      fn add_event_listener(event: &str /* or typed Event */, handler: EventHandler);
      
      // When listen is false, the event handler won't be discarded, just not trigger until
      // listen is true again.
      fn add_dyn_event_listener(event: &str, handler: EventHandler, listen: StateHandle<bool>);
      
    • Refs

      fn ref(ref: NodeRef);
      
    • Binding

      • Each respective supported bindable property could be made, such as value, checked, and optional element specific binding can be made, such as group.

        fn bind_value(value: Signal<String>);
        fn bind_group(group: Signal<String>);
        /* etc */
        
      • Another very important note, especially with Web Components, binding to arbitrary attributes and properties is a must have, as a lot of components, it's the only way to get notified on changes witch aren't emitted as custom events.

      fn bind_custom_attribute(name: &str, Signal<impl Deserialize>);
      fn bind_custom_property(name: &str, Signal<impl Deserialize>);
      
    • Build

      • Finally, we need to build the node into a Template.

    Important note

    The above API would currently require using Template for instantiating components, but it could be mentioned in the docs that if macros would like to be completely omitted, then one could use Component::__create_component(props).

    Did I miss anything? Can anything be improved? Please don't be shy. The more the critique, the better we can make Sycamore, together.

    C-enhancement 
    opened by jquesada2016 9
  • Builder API

    Builder API

    I have not fully tested, nor do I think the current iteration has the right aesthetics, but I believe it's in the right direction! This is an implementation attempt for the builder API as mentioned in #268.

    C-enhancement A-builder 
    opened by jquesada2016 8
  • Need to be able to convert web_sys::Element to Template<G>

    Need to be able to convert web_sys::Element to Template

    This is important for interfacing with many JS libraries which create arbitrary elements or produce template strings. I was unable to implement IntoTemplate<G> due to the blanket implementation, so used instead the From trait. It should also be possible in the future to create the template node without needing a wrapper element, though could not find a way from reading through the source. Any tips would be appreciated.

    C-enhancement 
    opened by jquesada2016 8
  • Issues When Running

    Issues When Running

    I can't seem to get the Hello World example from the README running. I have updated my main.rs file and added the dependency to Cargo.toml, but when I run using trunk I get a long list of errors (see below).

    Environment

    • Maple: 0.4.x
    • WSL / Ubuntu 20.04.1 LTS

    Additional context I have updated trunk, wasm-pack-cli, Rust, and a few other dependencies I think are related, just in case.

    Here is the start of the output. The list goes on for quite a while longer.

    ❯ trunk serve --open
    Apr 01 07:16:47.612  INFO πŸ“¦ starting build
    Apr 01 07:16:47.616  INFO spawning asset pipelines
    Apr 01 07:16:47.775  INFO building maple-test
       Compiling js-sys v0.3.49
       Compiling console_error_panic_hook v0.1.6
    error: expected `,`
      --> /home/googlemac/.cargo/registry/src/github.com-1ecc6299db9ec823/console_error_panic_hook-0.1.6/src/lib.rs:80:43
       |
    80 |             #[wasm_bindgen(js_namespace = console)]
       |                                           ^^^^^^^
    
    error[E0433]: failed to resolve: use of undeclared type `Error`
       --> /home/googlemac/.cargo/registry/src/github.com-1ecc6299db9ec823/console_error_panic_hook-0.1.6/src/lib.rs:106:21
        |
    106 |             let e = Error::new();
        |                     ^^^^^ not found in this scope
        |
    help: consider importing one of these items
        |
    71  | use std::error::Error;
        |
    71  | use std::io::Error;
        |
    71  | use wasm_bindgen::__rt::core::fmt::Error;
        |
    
    error[E0425]: cannot find function `error` in this scope
       --> /home/googlemac/.cargo/registry/src/github.com-1ecc6299db9ec823/console_error_panic_hook-0.1.6/src/lib.rs:117:13
        |
    117 |             error(msg);
        |             ^^^^^ not found in this scope
    
    error: aborting due to 3 previous errors
    
    Some errors have detailed explanations: E0425, E0433.
    For more information about an error, try `rustc --explain E0425`.
    error: could not compile `console_error_panic_hook`
    
    To learn more, run the command again with --verbose.
    warning: build failed, waiting for other jobs to finish...
    error: expected `,`
       --> /home/googlemac/.cargo/registry/src/github.com-1ecc6299db9ec823/js-sys-0.3.49/src/lib.rs:639:39
        |
    639 |         #[wasm_bindgen(js_namespace = Atomics, catch)]
        |                                       ^^^^^^^
    
    error: expected `,`
       --> /home/googlemac/.cargo/registry/src/github.com-1ecc6299db9ec823/js-sys-0.3.49/src/lib.rs:649:39
    
    C-bug 
    opened by parker-codes 8
  • Backend Abstraction

    Backend Abstraction

    Fixes #66. I'm leaving for a week in 2 days, but I just spent an hour or two working on this today, and I'll probably do a little more tomorrow. Feel free to build on this or just do something else completely.

    This introduces a new trait, GenericNode. Right now, it only has one implementation, which is a real DOM node. I'm writing this in a way so that users will have to make their components generic, but will hopefully never have to specify what that generic type is because it'll be inferred from calling render().

    Right now, I have the following questions I'll need to answer to go further:

    • Handling next_sibling and parent_node functions. When those just call JS functions, it's no problem because it has a full GC that can handle cyclic references. Although that's possible in Rust with some work, it would be more efficient if we could remove calls to those functions. I could also make those return an error server-side (they're only used for modifying the state), but it would make modifying the data impossible server-sideβ€”meaning you can't have some async computation render stuff later, or if you did, it'd have to be implemented differently.
    • NodeRef: How to do this in a user-friendly way: do we force the user to downcast to a concrete type? It would be nice to not hardcode it being a web_sys::Node, for use cases like React Native. However, there is probably no need for it to work on the server.
    C-enhancement A-SSR 
    opened by lights0123 8
  • (Runtime) Templates

    (Runtime) Templates

    Closes #19

    Adds a template system where Template represents the structure of the view! with holes inside. These holes can then be binded to and filled in with the dynamic values.

    This should give a big performance boost on the client side because we are cloning templates now instead of creating each and every element individually. This could also give a performance boost on server-side by allowing us to cache the SSR-ed strings.

    This also unlocks other exciting features such as some form of hot view-reloading.

    C-enhancement performance 
    opened by lukechu10 7
  • Fix removing old nodes from parent

    Fix removing old nodes from parent

    Fixes #427

    The #427 bug was introduced in #416 inside the reconcile_fragments function.

    The quick fix was to add the negation that got dropped during that change - this seems to have caused the bug.

    ~I instead extracted the removal of nodes from the parent node part of the reconcile_fragments function to a new function and added tests for this new function to avoid any future regressions.~

    ~The tests use a pretty rough/bare bones mock of a GenericNode in order to satisfy the trait bounds required by the extracted function. I think the tests are worth the added file noise of the mock but let me know if you'd prefer I remove it (and probably the extracted function at that point).~

    opened by mc1098 7
  • An HTTP request example and a tailwindcss styling example

    An HTTP request example and a tailwindcss styling example

    I created this example (since I could not find an existing one) to get a feel for the developer experience of using a crate like reqwasm and styling with TailwindCSS when using sycamore.

    This example makes a GET request to CountAPI that simply increments everytime you visit the page or refresh the page i.e. counting page visits. I picked this example since I just wanted to create a really simple HTTP request to fetch some json data and then be able to display that data which then further updates a Signal and resulting into updating the relevant component.

    This also includes a little styling with TailwindCSS since the docs here suggested that the styling part is still under works.

    To test, you can run trunk serve --open which should build everything and the serve the page.

    A-examples 
    opened by attriaayush 7
  • use_context inside a Router can't find context when used after route change

    use_context inside a Router can't find context when used after route change

    Describe the bug When use_context is called from inside a Router after a route change it can't find the context value anymore and panics.

    To Reproduce Use code in reproduction repo and click on link with built site.

    Expected behavior use_context should return the context.

    Environment

    • Sycamore: master (the repo uses 0.6.3)
    • Browser: Edge/Firefox
    • OS: Ubuntu 21.04

    Additional context As far as I can tell, the context itself isn't being dropped, it's just not being found after the route changes.

    When it panics, there's an extra scope being searched in use_context which is not created in the sycamore_reactive::context::create_context_scope function.

    C-bug 
    opened by Zyllian 7
Releases(0.8.2)
Owner
Luke Chu
I enjoy coding and gaming.
Luke Chu
A Wasm component optimizer (mostly a wrapper around wasm-opt)

component-opt An optimizer for Wasm Components Current Status This project currently only offers one optimization and does not allow it to be configur

Robin Brown 6 Mar 4, 2024
A shared document application akin to Google Docs. Example usage of wasm-peers library.

Live Document Proof of concept application showcasing the usability of wasm-peers crate for easy and costless peer-2-peer WebRTC communication. It's a

null 6 Sep 19, 2022
BSV stdlib written in Rust and runs in WASM environments

BSV.WASM A Rust/WASM Library to interact with Bitcoin SV Installation NodeJS: npm i bsv-wasm --save Web: npm i bsv-wasm-web --save Rust: https://crate

null 56 Dec 15, 2022
A wasm interpreter written by rust

A wasm interpreter written by rust

nasa 69 Dec 6, 2022
a wasm interpreter written by rust

wai (WebAssembly interpreter) A simple wasm interpreter This is an ongoing project DEMO 2021-06-27.10.23.18.mov Install Install via Homebrew brew inst

nasa 69 Dec 6, 2022
Re-implementation of Panda Doodle in Rust targetting WASM, a mobile game originally written in C++

Description This is the source code of my game Panda Doodle, which can be played at https://pandadoodle.lucamoller.com/ (it's best playable on touch s

null 79 Dec 5, 2022
2D Predictive-Corrective Smoothed Particle Hydrodynamics (SPH) implementation in Rust with WASM + WebGL

pcisph-wasm 2D Predictive-Corrective Smoothed Particle Hydrodynamics (SPH) implementation in Rust with WASM + WebGL Reimplementation of my previous Ru

Lucas V. Schuermann 46 Dec 17, 2022
A playground for creating generative art, buit with RustπŸ¦€ and WASMπŸ•Έ

Genny A playground for creating generative art, buit with Rust ?? and WASM ?? About This is a simple playground that allows me to explore ideas around

JoΓ£o Paiva 3 Mar 12, 2022
A Conway's Game of Life implementation in Rust & WASM

Rust Wasm Game of Life This repo contains an example implementation of Conway's Game of Life in Rust and WebAssembly. How to use You should have wasm-

Pierre-Yves Aillet 1 Nov 11, 2021
A low-ish level tool for easily writing and hosting WASM based plugins.

A low-ish level tool for easily writing and hosting WASM based plugins. The goal of wasm_plugin is to make communicating across the host-plugin bounda

Alec Deason 62 Sep 20, 2022
wasm actor system based on lunatic

Wactor WASM actor system based on lunatic. Actors run on isolated green threads. They cannot share memory, and communicate only through input and outp

Noah Corona 25 Nov 8, 2022
swc node binding use wasm

node_swc swc node binding use wasm Build Make sure you have rust wasm-pack installed. $ yarn build # build wasm, node Usage import { parseSync, printS

δΌŠζ’’ε°” 23 Sep 8, 2022
🦞 wasm-pack based build tool

rsw-rs This project is in early experimental stage. # dev rsw watch # release rsw build # create crate rsw new # rsw.toml name = 'rsw' version = "0.

lencx 71 Dec 30, 2022
Bindings to the Tauri API for projects using wasm-bindgen

tauri-sys Raw bindings to the Tauri API for projects using wasm-bindgen Installation This crate is not yet published to crates.io, so you need to use

Jonas Kruckenberg 25 Jan 9, 2023
Renders typst code blocks in Obsidian into images using Typst through the power of WASM!

Obsidian Typst Renders typst code blocks into images using Typst through the power of WASM! This is still very much in development, so suggestions/bug

Jack 18 Apr 1, 2023
Adapter plugin to use Ruff in dprint's CLI and with JavaScript via Wasm

dprint-plugin-ruff Adapter for Ruff for use as a formatting plugin in dprint. Formats .py and .pyi files. Note: For formatting .ipynb files, use the J

null 3 Nov 28, 2023
A library to compile USDT probes into a Rust library

sonde sonde is a library to compile USDT probes into a Rust library, and to generate a friendly Rust idiomatic API around it. Userland Statically Defi

Ivan Enderlin 40 Jan 7, 2023
A Rust library for calculating sun positions

sun A rust port of the JS library suncalc. Install Add the following to your Cargo.toml [dependencies] sun = "0.2" Usage pub fn main() { let unixti

Markus Kohlhase 36 Dec 28, 2022
A cross-platform serial port library in Rust.

Introduction serialport-rs is a general-purpose cross-platform serial port library for Rust. It provides a blocking I/O interface and port enumeration

Bryant Mairs 143 Nov 5, 2021