A Rust framework for creating web apps
This package requires you to install Rust - This will enable the CLI commands below:
You'll need a recent version of Rust's nightly toolchain:
rustup update
rustup default nightly
,
The wasm32-unknown-unknown target:
rustup target add wasm32-unknown-unknown --toolchain nightly
And wasm-bindgen:
cargo +nightly install wasm-bindgen-cli
To start, clone This quickstart repo,
run build.sh
or build.ps1
in a terminal, and open index.html
. (May need to use
a local server depending on the browser) Once you change your package name, you'll
need to tweak the Html file and build script, as described below.
Or, create a new lib with Cargo: cargo new --lib appname
. Here and everywhere it appears in this guide,
appname
should be replaced with the name of your app.
You need an Html file that loads your app's compiled module, and provides an element with id to load the framework into. It also needs the following code to load your WASM module - Ie, the body should contain this:
```html
```
The quickstart repo includes this file, but you will need to rename the two
occurances of appname
. (If your project name has a hyphen, use an underscore instead here) You will eventually need to modify this file to
change the page's title, add a description, favicon, stylesheet etc.
Cargo.toml
needs wasm-bindgen
, web-sys
, and seed
as depdendencies, and crate-type
of "cdylib"
. Example:
```toml [package] name = "appname" version = "0.1.0" authors = ["Your Name email@address.com"] edition = "2018"
[lib] crate-type = ["cdylib"]
[dependencies] seed = "^0.1.0" wasm-bindgen = "^0.2.29" web-sys = "^0.3.6"
serde = "^1.0.80" serdederive = "^1.0.80" serdejson = "1.0.33"
```
Here's an example demonstrating structure and syntax; it can be found in working form
under examples/counter
. Descriptions of its parts are in the
Guide section below. Its structure follows The Elm Architecture.
lib.rs: ```rust
extern crate seed; use seed::prelude::; use wasm_bindgen::prelude::;
// Model
struct Model { count: i32, whatwecount: String }
// Setup a default here, for initialization later. impl Default for Model { fn default() -> Self { Self { count: 0, whatwecount: "click".into() } } }
// Update
enum Msg { Increment, Decrement, ChangeWWC(String), }
/// The sole source of updating the model; returns a fresh one. fn update(msg: Msg, model: &Model) -> Model { match msg { Msg::Increment => { Model {count: model.count + 1, whatwecount: model.whatwecount.clone()} }, Msg::Decrement => { Model {count: model.count - 1, whatwecount: model.whatwecount.clone()} }, Msg::ChangeWWC(text) => { Model {count: model.count, whatwecount: text.clone()} } } }
// View
/// A simple component.
fn success_level(clicks: i32) -> El
/// The top-level component we pass to the virtual dom. Must accept a ref to the model as its
/// only argument, and output a single El.
fn view(model: &Model) -> El
// Attrs, Style, Events, and children may be defined separately.
let outer_style = style!{
"display" => "flex";
"flex-direction" => "column";
"text-align" => "center"
};
div![ outer_style, vec![
h1![ "The Grand Total" ],
div![
style!{
// Example of conditional logic in a style.
"color" => if model.count > 4 {"purple"} else {"gray"};
// When passing numerical values to style!, "px" is implied.
"border" => "2px solid #004422"; "padding" => 20
},
vec![
// We can use normal Rust code and comments in the view.
h3![ format!("{} {}{} so far", model.count, model.what_we_count, plural) ],
button![ vec![ simple_ev("click", Msg::Increment) ], "+" ],
button![ vec![ simple_ev("click", Msg::Decrement) ], "-" ],
// Optionally-displaying an element
if model.count >= 10 { h2![ style!{"padding" => 50}, "Nice!" ] } else { seed::empty() }
] ],
success_level(model.count), // Incorporating a separate component
h3![ "What precisely is it we're counting?" ],
input![ attrs!{"value" => model.what_we_count},
vec![ input_ev("input", |text| Msg::ChangeWWC(text)) ]
]
] ]
}
pub fn render() { seed::run(Model::default(), update, view, "main"); } ```
To build your app, run the following two commands:
cargo build --target wasm32-unknown-unknown
and
wasm-bindgen target/wasm32-unknown-unknown/debug/appname.wasm --no modules --out-dir ./pkg
where appname
is replaced with your app's name. This compiles your code in the target
folder, and populates the pkg folder with your WASM module, a Typescript definitions file,
and a Javascript file used to link your module from HTML.
You may wish to create a build script with these two lines. (build.sh
for Linux; build.ps1
for Windows).
The Quickstart repo includes these, but you'll still need to do the rename. You can then use
./build.sh
or .\build.ps1
For development, you can view your app using a dev server, or by opening the HTML file in a browser. For example, after installing the http crate, run http
.
Or with Python installed, run python -m http.server
from your crate's root.
For details, reference the wasm-bindgen documention. In the future, I'd like the build script and commands above to be replaced by wasm-pack.
To run an example located in the examples
folder, navigate to that folder in a terminal,
run the build script for your system (build.sh
or build.ps1
), then open the index.html
file
in a web browser. Note that if you copy an example to a separate folder, you'll need
to edit its Cargo.toml
to point to the package on crates.io instead of locally: Ie replace
seed = { path = "../../"
with seed = "^0.1.0"
, and in the build script, remove the leading ../../
on the second
line.
Rust: Proficiency in Rust isn't required to get started using this framework. It helps, but I think you'll be able to build a usable webapp using this guide, and example code alone. For business logic behind the GUI, more study may be required. The official Rust Book is a good place to start.
You'll be able to go with just the basic Rust syntax common to most programming languages, eg conditionals, equalities, iteration, collections, and how Rust's borrow system applies to strings. A skim through the first few chapters of the Book, and the examples here should provide what you need. Rust's advanced and specialized features like lifetimes, generics, smartpointers, and traits aren't required to build an interactive GUI.
Web fundamentals: Experience building websites using HTML/CSS or other frameworks is required. Neither this guide nor the API docs describes how web pages are structured, or what different HTML/DOM elements, attributes, styles etc do. You'll need to know these before getting started. Seed provides tools used to assemble and manipulate these fundamentals. Mozilla's MDN web docs is a good place to start.
Other frontend frameworks The design principles Seed uses are similar to those used by React, Elm, and Yew. People familiar with how to set up interactive web pages using these tools will likely have an easy time learning this.
Model
Each app must contain a model struct,
which contains the app’s state and data. It must derive Clone
, and should contain
owned data. References
with a static lifetime may work,
but may be more difficult to work with. Example:
```rust
struct Model { count: i32, whatwecount: String }
// Setup a default here, for initialization later. impl Default for Model { fn default() -> Self { Self { count: 0, whatwecount: "click".into() } } } ```
In this example, we provide
initialization via Rust’s Default
trait, in order to keep the initialization code by the
model itself. When we call Model.default()
, it initializes with these values. We could
also initialize it using a constructor method, or a struct literal. Note the use of into()
on our string literal, to convert it into an owned string.
The model holds all data used by the app, and will be replaced with updated versions when the data changes.
Use owned data in the model; eg String
instead of &'static str
.
The model may be split into sub-structs to organize it – this is especially useful as the app grows.
Sub-structs must implement Clone
:
```rust
struct FormData { name: String, age: i8, }
struct Misc { value: i8, descrip: String, }
struct Model { form_data: FormData, misc: Misc } ```
Update
The Message is an enum which
categorizes each type of interaction with the app. Its fields may hold a value, or not.
We’ve abbreviated it as Msg
here for brevity. Example:
```rust
enum Msg { Increment, Decrement, ChangeDescrip(String), } ```
The update function
you pass to seed::run
describes how the state should change, upon
receiving each type of Message. It is the only place where the model is changed. It accepts a message, and model
reference as parameters, and returns a Model instance. This function signature cannot be changed.
Note that it doesn’t update the model in place: It returns a new one.
todo: Demonstrate example patterns, eg when to clone, ..operator, refs/not etc
While the signature of the update function is fixed (Accepts a Msg and ref to the model; outputs a new model), and will usually involve a match pattern, with an arm for each Msg, there are many ways you can structure this function. Some may be easier to write (eg, cloning the model at the top, and mutating it as needed), and others may be more efficient, or appeal to specific aesthetics. (Eg the immutable design patterns).
Example:
rust
// Sole source of updating the model; returns a whole new model.
fn update(msg: Msg, model: &Model) -> Model {
match msg {
Msg::Increment => {
Model {count: model.count + 1, what_we_count: model.what_we_count.clone()}
},
Msg::Decrement => {
Model {count: model.count - 1, what_we_count: model.what_we_count.clone()}
},
Msg::ChangeWWC(text) => {
Model {count: model.count, what_we_count: text.clone()}
}
}
}
As with the model, only one update function is passed to the app, but it may be split into
sub-functions to aid code organization.
Note that you can perform updates recursively, ie have one update trigger another. For example,
here's a non-recursive approach, where functions dothings() and doother_things() each
act on an &Model, and output a Model:
rust
fn update(fn update(msg: Msg, model: &Model) -> Model {
match msg {
Msg::A => do_things(model),
Msg::B => do_other_things(do_things(model)),
}
}
Here's a recursive equivalent:
rust
fn update(fn update(msg: Msg, model: &Model) -> Model {
match msg {
Msg::A => do_things(model),
Msg::B => do_other_things(update(Msg::A, model)),
}
}
View
Visual layout (ie HTML/DOM elements) is described declaratively in Rust, but uses macros to simplify syntax.
When passing your layout to Seed, attributes for DOM elements (eg id, class, src etc), styles (eg display, color, font-size), and events (eg onclick, contextmenu, dblclick) are passed to DOM-macros (like div!{}) using unique types.
Views are described using El structs, defined in theseed::dom_types
module. They're most-easily created
with a shorthand using macros. These macros can take any combination of the following 5 argument types:
(0 or 1 of each) Attrs
, Style
, Vec<Listener>
, Vec<El>
(children), and &str
(text). Attrs, and Style
are most-easily created usign the following macros respectively: attrs!{}
, style!{}
. Attrs,
Style, and Listener are all defined in seed::dom_types
.
Attrs
, and Style
values can be owned Strings
, &str
s, or when applicable, numerical and
boolean values. Eg: input![ attrs!{"disabled" => false]
and input![ attrs!{"disabled" => "false"]
are equivalent. If a numerical value is used in a Style
, 'px' will be automatically appended.
If you don't want this behavior, use a String
or&str
. Eg: h2![ style!{"font-size" => 16} ]
, or
h2![ style!{"font-size" => "1.5em"} ]
for specifying font size in pixels or em respectively. Note that
once created, a Style
instance holds all its values as Strings
; eg that 16
above will be stored
as "16px"
; keep this in mind if editing a style that you made outside an element macro.
Additionally, setting an InputElement's checked
property is done through normal attributes:
rust
input![ attrs!{"type" => "checkbox"; "checked" => true ]
To edit Attrs or Styles you've created, you can edit their .vals HashMap. To add
a new part to them, use their .add method:
rust
let mut attributes = attrs!{};
attributes.add("class", "truckloads");
Event syntax may change in the future. Currently, events are a Vec
of dom_types::Listener'
objects, created using the following four functions exposed in the prelude: simple_ev
,
input_ev
, keyboard_ev
, and raw_ev
. The first two are demonstrated in the example in the quickstart section.
simple_ev
takes two arguments: an event trigger (eg "click", "contextmenu" etc), and an instance
of your Msg
enum. (eg Msg::Increment). The other three event-creation-funcs
take a trigger, and a closure (An anonymous function,
similar to an arrow func in JS) that returns a Msg enum.
simple_ev
does not pass any information about the event, only that it fired.
Example: simple_ev("dblclick", Msg::ClickClick)
input_ev
passes the event target's value field, eg what a user typed in an input field.
Example: simple_ev("input", |text| NewWords(text))
keyboard_ev
returns a web_sys::KeyboardEvent,
which exposes several getter methods like key_code
and key
.
Example: simple_ev("keydown", |event| PutTheHammerDown(event))
raw_ev
returns a websys::Event. It lets you access any part of any type of
event, albeit with some unpleasant syntax. Its Enum should accept a web_sys::KeyboardEvent
.
If you wish to do something like preventdefault(), or anything note listed above,
you need to take this approach.
Example syntax to handle input and keyboard events using raw_ev
instead of
input_ev
and keyboard_ev
:
```rust
use wasm_bindgen::JsCast;
// ...
(in update func)
Msg::TextEntry(event) => {
let target = event.target().unwrap();
let inputel = target.dynref::
vec![
raw_ev("input", |ev| Msg::TextEntry(ev)),
raw_ev("keydown", |ev| Msg::KeyPress(ev)),
]
} ``` It's likely you'll be able to do most of what you wish with the simpler event funcs. If there's a type of event or use you think would benefit from a similar func, submit an issue or PR. In the descriptions above for all event-creation funcs, we assumed minimal code in the closure, and more code in the update func's match arms. For example, to process a keyboard event, these two approaches are equivalent:
```rust enum Msg { KeyDown(web_sys::KeyboardEvent) }
// ... (in update) KeyDown(event) => { let code = event.key_code() // ... }
// ... In view
vec![ keyboard_ev("keydown", |ev| KeyDown(ev)]
and
rust
enum Msg {
KeyDown(u32)
}
// ... (in update) KeyDown(code) => { // ... }
// ... In view vec![ keyboardev("keydown", |ev| KeyDown(ev.keycode()))] ```
You can pass more than one variable to the Msg
enum via the closure, as long
as it's set up appropriate in Msg
's definition.
Event syntax may be improved later with the addition of a single macro that infers what the type of event
is based on the trigger, and avoids the use of manually creating a Vec
to store the
Listener
s. For examples of all of the above (except raw_ev), check out the todomvc example.
This gawky approach is caused by a conflict between Rust's type system, and the way DOM events
are handled. For example, you may wish to pull text from an input field by reading the event target's
value field. However, not all targets contain value; it may have to be represented as
an HtmlInputElement
. (See the web-sys ref,
and Mdn ref; there's no value field)) Another example:
If we wish to read the keycode of an event, we must first cast it as a KeyboardEvent; pure Events
(websys and DOM) do not contain this field.
The todomvc example has a number of event-handling examples, including use of rawev, where it handles text input triggered by a key press, and uses preventdefault().
The following code returns an El
representing a few DOM elements displayed
in a flexbox layout:
rust
div![ style!{"display" => "flex"; "flex-direction" => "column"}, vec![
h3![ "Some things" ],
button![ "Click me!" ] // todo add event example back.
] ]
The only magic parts of this are the macros used to simplify syntax for creating these
things: text are Options
of Rust borrowed Strings; Listeners
are stored in Vecs; children are Vecs of sub-elements;
Attr
s and Style
are thinly-wrapped HashMaps. They can be created independently, and
passed to the macros separately. The following code is equivalent; it uses constructors
from the El struct. Note that El
type is imported with the Prelude.
```rust use seed::dom_types::{El, Attrs, Style, Tag};
// heading and button here show two types of element constructors
let mut heading = El::new(
Tag::H2,
Attrs::empty(),
Style::empty(),
Vec::new(),
"Some things",
Vec::New()
);
let mut button = El::empty(Tag::Button);
let children = vec![heading, button];
let mut elements = El::empty(Tag::Div);
el.add_style("display", "flex");
el.add_style("flex-direction", "column");
el.children = children;
el
```
The following equivalent example shows creating the required structs without constructors, to demonstrate that the macros and constructors above represent normal Rust structs, and provides insight into what abstractions they perform:
```rust use seed::dom_types::{El, Attrs, Style, Tag};
// Rust has no built-in HashMap literal syntax.
let mut style = HashMap::new();
style.insert("display", "flex");
style.insert("flex-direction", "column");
El { tag: Tag::Div, attrs: Attrs { vals: HashMap::new() }, style, events: Events { vals: Vec::new() }, text: None, children: vec![ El { tag: Tag::H2, attrs: Attrs { vals: HashMap::new() }, style: Style { vals: HashMap::new() }, listeners: Vec::new(); text: Some(String::from("Some Things")), children: Vec::new() }, El { tag: Tag::button, attrs: Attrs { vals: HashMap::new() }, style: Style { vals: HashMap::new() }, listeners: Vec::new(); text: None, children: Vec::new(), } ] } ```
For most uses, the first example (using macros) will be the easiest to read and write. You can mix in constructors (or struct literals) in components as needed, depending on your code structure.
The analog of components in frameworks like React are normal Rust functions that that return Els.
The parameters these functions take are not treated in a way equivalent
to attributes on native DOM elements; they just provide a way to
organize your code. In practice, they feel similar to components in React, but are just
functions used to create elements that end up in the children
property of
parent elements.
For example, you could break up the above example like this:
```rust
fn text_display(text: &str) -> El
div![ style!{"display" => "flex"; flex-direction: "column"}, vec![
text_display("Some things"),
button![ vec![ simple_ev("click", Msg::SayHi) ], "Click me!" ]
] ]
```
The text_display() component returns a single El that is inserted into its parents'
children
Vec; you can use this in patterns as you would in React. You can also use
functions that return Vecs or Tuples of Els, which you can incorporate into other components
using normal Rust code. See Fragments
section below. Rust's type system
ensures that only El
s can end up as children, so if your app compiles,
you haven't violated any rules.
Note that unlike in JSX, there's a clear syntax delineation here between natural HTML elements (element macros), and custom components (function calls).
Fragments (<>...</>
syntax in React and Yew) are components that represent multiple
elements without a parent. This is useful to avoid
unecessary divs, which may be undesirable on their own, and breaks things like tables and CSS-grid.
There's no special syntax; just have your component return a Vec of El
s instead of
one, and pass them into the parent's children
parameter via Rust's Vec methods
like extend
, or pass the whole Vec if there are no other children:
```rust
fn cols() -> Vec
fn items() -> El
When performing ternary and related operations instead an element macro, all
branches must return El
s to satisfy Rust's type system. Seed provides the
empty()
function, which creates a VDOM element that will not be rendered:
rust
div![ vec![
if model.count >= 10 { h2![ style!{"padding" => 50}, "Nice!" ] } else { seed::empty() }
] ]
For more complicated construsts, you may wish to create the children
Vec separately,
push what components are needed, and pass it into the element macro.
To start your app, pass an instance of your model, the update function, the top-level component function
(not its output), and name of the div you wish to mount it to to the seed::run
function:
```rust
pub fn render() {
seed::run(Model::default(), update, view, "main");
}
This must be wrapped in a function named `render`, with the `#[wasm_bindgen]` invocation above.
(More correctly, its name must match the func in this line in your html file):
javascript
function run() { render(); } ``` Note that you don't need to pass your Msg enum; it's inferred from the update function.
The Element-creation macros used to create views are normal Rust code, you can use comments in them normally: either on their own line, or in line.
To output to the web browser's console (ie console.log()
in JS), use web_sys::console_log1
,
or the log
macro that wraps it, which is imported in the seed prelude:
log!("On the shoulders of", 5, "giants".to_string())
Use the Serde crate to serialize and deserialize data, eg
when sending and receiving data from a REST-etc. It supports most popular formats,
including JSON
, YAML
, and XML
.
(Example, and with our integration)
To send and receive data with a server, use wasm-bindgen
's web-sys
fetch methods,
described here, paired
with Serde.
Check out the server_interaction
examples for an example of how to send and receive
data from the server in JSON.
Seed will implement a high-level fetch API in the future, wrapping web-sys's.
You can store page state locally using web_sys's Storage struct
Seed provides convenience functions seed::storage::get_storage, which returns
the
websys::storageobject, and
seed::storage::storedata` to store an arbitrary
Rust data structure that implements serde's Serialize. Example use:
```rust extern crate serde;
extern crate serdederive; extern crate serdejson;
// ...
struct Data { // Arbitrary data (All sub-structs etc must also implement Serialize and Deserialize) }
let storage = seed::storage::get_storage(); seed::storage::store(storage, "my-data", Data::new());
// ...
let loadedserialized = storage.getitem("my-data").unwrap().unwrap(); let data = serdejson::fromstr(&loaded_serialized).unwrap();
```
The configuration in the Building and Running section towards the top are intended
for development: They produce large .wasm
file sizes, and unoptimized performance.
For your release version, you'll need to append --release
to the cargo build
command,
and point your wasm-bindgen
command to the release
subdirectory vice debug
.
Example:
cargo build --target wasm32-unknown-unknown --release
and
wasm-bindgen target/wasm32-unknown-unknown/release/appname.wasm --no modules --out-dir ./pkg
There are two categories of error message you can receive: I'm using a different definition than used in this section of the Rust book. Compiler errors, and panics.
1: Errors while building, which will be displayed in the terminal
where you ran cargo build
, or the build script. Rust's compiler usually provides
helpful messages, so try to work through these using the information available. Examples include
syntax errors, passing a func/struct etc the wrong type of item, and running afoul of the
borrow checker.
2: Runtime panics. These are more difficult to deal with, especially in the web browser.
Their hallmark is a message that starts with RuntimeError: "unreachable executed"
, and correspond
to a panic in the rust code. (For example, a problem while using unwrap()
). There's
currently no neat way to identify which part of the code panicked; until this is sorted out,
you may try to narrow it down using seed.log()
commands. They're usually associated with
unwrap()
or expect()
calls.
Learning the syntax, creating a project, and building it should be easy - regardless of your familiarity with Rust.
Complete documentation that always matches the current version. Getting examples working, and starting a project should be painless, and require nothing beyond this guide.
An API that's easy to read, write, and understand.
This project takes a different approach to describing how to display DOM elements than others. It neither uses completely natural (ie macro-free) Rust code, nor an HTML-like abstraction (eg JSX or templates). My intent is to make the code close to natural Rust, while streamlining the syntax in a way suited for creating a visual layout with minimal repetition. The macros used here are thin wrappers for constructors, and don't conceal much.
The relative lack of resemblance to HTML be offputting at first, but the learning curve is shallow, and I think the macro syntax used to create elements, attributes etc is close-enough to normal Rust syntax that it's easy to reason about how the code should come together, without compartmentalizing it into logic code and display code. This lack of separation in particlar is a subjective, controversial decision, but I think the benefits are worth it.
The todomvc example is an implementation of the TodoMVC project, which has example code in my frameworks that do the same thing. Compare the example in this project to one on that page that uses a framework you're familiar with.
This project is strongly influenced by Elm, React, and Redux. The overall layout of Seed apps mimicks that of The Elm Architecture.
There are already several Rust/WASM frameworks; why add another?
My goal is for this to be easy to pick up from looking at a tutorial or documentation, regardless of your
level of experience with Rust. I'm distinguising this package through clear examples
and documentation (see goals above), and using wasm-bindgen
internally. I started this
project after being unable to get existing frameworks to work
due to lack of documented examples, and inconsistency between documentation and
published versions. My intent is for anyone who's proficient in a frontend
framework to get a standalone app working in the browser within a few minutes, using just the
Quickstart guide.
Seed approaches HTML-display syntax differently from existing packages: rather than use an HTML-like markup similar to JSX, it uses Rust builtin types, thinly-wrapped by a macro for each DOM element. This decision may not appeal to everyone, but I think it integrates more naturally with the language.
Why build a frontend in Rust over Elm or Javascript-based frameworks?
You may prefer writing in Rust, and using packages from Cargo vis npm. Getting started with this framework will, in most cases be faster, and require less config and setup overhead than with JS frameworks.
You may choose this approach over Elm if you're already comfortable with Rust, want the performance benefits, or don't want to code business logic in a purely-functional langauge.
Compared to React, for example, you may appreciate the consistency of how to write apps: There's no distinction between logic and display code; no restrictions on comments; no distinction between components and normal functions. The API is flexible, and avoids the OOP boilerplate.
I also hope that config, building, and dependency-management is cleaner with Cargo and wasm-bindgen than with npm.
High-level CSS-grid/Flexbox API ?