Seed
A Rust framework for creating web apps
Quickstart
Setup
This package requires you to install Rust - This will enable the CLI commands below:
You'll need a recent version of Rust: rustup update
The wasm32-unknown-unknown target: rustup target add wasm32-unknown-unknown
And wasm-bindgen: cargo install wasm-bindgen-cli
The theoretical minimum
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.
A little deeper
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:
The delete WebAssembly.instantiateStreaming;
line is an unsavory hack, but without it,
Most dev servers, and opening the html file directly won't work. If you'd like to avoid this,
delete that line, install
Python, and run python server.py
. This
file is included in the quickstart repo, and in each example folder. It's a shim
that allows Python's dev server to work with the WASM mime type. Linux users may have
to run python3 server.py
.
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:
[]
= "appname"
= "0.1.0"
= ["Your Name <email@address.com>"]
= "2018"
[]
= ["cdylib"]
[]
= "^0.1.1"
= "^0.2.29"
= "^0.3.6"
# For serialization, eg sending requests to a server. Otherwise, not required.
= "^1.0.80"
= "^1.0.80"
= "1.0.33"
A short example
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:
extern crate seed;
use *;
use *;
// Model
// Setup a default here, for initialization later.
// Update
/// The sole source of updating the model; returns a fresh one.
// View
/// A simple component.
/// 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.
Building and running
To build your app, create a pkg
subdirectory, and 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 shimmed Python dev server described above,
(Run python server.py
) or by opening the HTML file in a browser.
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.
Running included examples
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, or use the Python dev server. 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.
Guide
Prerequisites
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.
App structure
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:
// Setup a default here, for initialization later.
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
:
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:
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 as parameters, and returns a model. This function signature cannot be changed.
Note that it doesn’t update the model in place: It returns a new one.
Example:
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, and others may be more efficient, or appeal to specific aesthetics. While the example above it straightforward, this becomes import with more complicated updates.
The signature suggests taking an immutable-design/functional approach. This can be verbose when modifying collections, but is a common pattern in Elm and Redux. Unlike in a pure functional language, side-effects (ie things other that happen other than updating the model) don't require special handling. Example, from the todomvc example:
In this example, we avoid mutating data. In the first two Msgs, we filter the todos, then pass them to a new model using struct update syntax . In the third Msg, we mutate todos, but don't mutate the model itself. In the fourth, we build a new todo list using a functional technique. The docs for Rust Iterators show helpful methods for functional iterator manipulation.
Alternatively, we could write the same update function like this:
This approach, where we mutate the model directly, is much more concise when
handling collections. How-to: Reassign model
as mutable at the start of update
.
Return model
at the end. Mutate it during the match legs.
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 do_things() and do_other_things() each act on an Model, and output a Model:
,
}
}
Here's a recursive equivalent:
,
}
}
View
Visual layout (ie HTML/DOM elements) is described declaratively in Rust, but uses macros to simplify syntax.
Elements, attributes, styles
Elements are created using macros, named by the lowercase name of each element, and imported into the global namespace:
extern crate seed;
// ...
div!
These macros accept any combination (0 or 1 per) of the following parameters:
- One Attrs struct
- One Style struct
- One or more Listener structs, which handle events
- One or more Vecs of Listener structs
- One String or &str representing a node text
- One or more El structs, representing a child
- One or more Vecs of El structs, representing multiple children
The parameters can be passed in any order; the compiler knows how to handle them based on their types. Children are rendered in the order passed.
Views are described using El structs, defined in the seed::dom_types module. They're most-easily created with a shorthand using macros.
Attrs
and Style
are thinly-wrapped hashmaps created with their own macros: attrs{}
and style!{}
respectively.
Example:
let things = vec!;
div!
Note that you can create any of the above items inside an element macro, or create it separately, and pass it in.
Values passed to attrs
, and style
macros 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.
Styles and Attrs can be passed as refs as well, which is useful if you need to pass the same one more than once:
let item_style = style!;
div!
Setting an InputElement's checked
property is done through normal attributes:
input!
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:
let mut attributes = attrs!;
attributes.add;
Example of the style tag, and how you can use pattern-matching in views:
Events
Events are created by passing a a Listener,
, or vec of Listeners, created using the following four functions exposed in the prelude: simple_ev
,
input_ev
, keyboard_ev
, and raw_ev
. The first is demonstrated in the example in the quickstart section,
and all are demonstrated in the todomvc example.
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 `
input_ev
passes the event target's value field, eg what a user typed in an input field.
Example:
// ...
input_ev
keyboard_ev
returns a web_sys::KeyboardEvent,
which exposes several getter methods like key_code
and key
.
Example:
// ...
keyboard_ev_ev
Note that in the examples for input_ev and keyboard_ev, the syntax is simplified since
we're only passing the field text, and keyboard event respectively to the Msg. The input_ev
example is Rust shorthand for input_ev("input, |text| Msg::NewWords(text)
. If you were
to pass something other than, or more than just the input text (Or KeyboardEvent for keyboard_ev,
or Event for raw_ev described below),
you can't use this shorthand, and would have to do something like this intead,
explicitly writing the closure:
// ...
input_ev
raw_ev
returns a web_sys::Event.
It lets you access any part of any type of
event, albeit with more verbose syntax.
If you wish to do something like prevent_default(), or anything not listed above,
you need to take this approach. Note that for many common operations, like taking
the value of an input element after an input
or change
event, you have to deal with
casting from a generic event or target to the specific one. Seed provides convenience
functions to handle this. They wrap wasm-bindgen's .dyn_ref() and .dyn_into(), from its
JsCast trait.
Example syntax showing how you might use raw_ev; processing an input and handling a keyboard event, while using prevent_default:
// (in update func)
KeyPress =>
Seed also provides to_textarea
and to_select
functions, which you'd use as
to_input
.
This extra step 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 key_code of an event, we must first cast it as a KeyboardEvent; pure Events
(web_sys and DOM) do not contain this field.
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:
// ... (in update)
KeyDown =>
// ... In view
keyboard_ev
and
// ... (in update)
KeyDown =>
// ... In view
keyboard_ev
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. Note that if you pass a value to the enum
other than what's between ||, you may receive an error about lifetimes. This is corrected by
making the closure a move type. Eg:
keyboard_ev
Where id
is a value defined earlier.
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.
The todomvc example has a number of event-handling examples, including use of raw_ev, where it handles text input triggered by a key press, and uses prevent_default().
Element-creation macros, under the hood
The following code returns an El
representing a few DOM elements displayed
in a flexbox layout:
div!
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 elements and/or Vecs of;
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.
use ;
// heading and button here show two types of element constructors
let mut heading = new;
let mut button = empty;
let children = vec!;
let mut elements = empty;
elements.add_style;
elements.add_style;
elements.children = children;
elements
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:
use ;
// Rust has no built-in HashMap literal syntax.
let mut style = new;
style.insert;
style.insert;
El
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.
Components
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 one of the above examples like this:
div!
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 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
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 add it to the parent's element macro; on its own like in the example below,
or with other children, or Vecs of children.
Dummy elements
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:
div!
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.
Initializing your app
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 element (Usually a Div or Section) you wish to mount it to to the seed::run
function:
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):
Note that you don't need to pass your Msg enum; it's inferred from the update function.
Comments in the view
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.
Logging in the web browser
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())
Serialization and deserialization
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)
Querying servers using fetch
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.
Local storage
You can store page state locally using web_sys's Storage struct
Seed provides convenience functions seed::storage::get_storage
, which returns
the web_sys::storage
object, and seed::storage::store_data
to store an arbitrary
Rust data structure that implements serde's Serialize. Example use:
extern crate serde;
extern crate serde_derive;
extern crate serde_json;
// ...
let storage = get_storage;
store;
// ...
let loaded_serialized = storage.get_item.unwrap.unwrap;
let data = from_str.unwrap;
Building a release version
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
Debugging
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 show up as console errors in the web browser. Example:
panicked at 'assertion failed: index < len',
, and provide a traceback. (For example, a problem while using unwrap()
).
They're often associated withunwrap()
or expect()
calls. Try to use expect(), with a useful
error message instead of unwrap(): It your message will show in the console.
Reference
- wasm-bindgen guide
- Mozilla MDN web docs
- web-sys api (A good partner for the MDN docs - most DOM items have web-sys equivalents used internally)
- Rust book
- Rust standard library api
- Seed's API docs
- Learn Rust
About
Goals
-
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.
A note on view syntax
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. Specifically, the element-creation macros allow for accepting a variable number of arguments, and the attrs/style marcros are essentially HashMap literals, with wrappers that let el macros know to distinguish them.
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.
Where to start if you're familiar with existing frontend frameworks
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.
Suggestions? Critique? Submit an issue or pull request on Github
Influences
This project is strongly influenced by Elm, React, and Redux. The overall layout of Seed apps mimicks that of The Elm Architecture.
Why another entry in a saturated field?
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 like the advantages of compile-time error-checking.
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.
Shoutouts
- The WASM-Bindgen team: For building the tools this project relies on
- Alex Chrichton: For being extraodinarily helpful in the Rust / WASM community
- The Elm team: For creating and standardizing the Elm architecture
- Denis Kolodin: for creating the inspirational Yew framework
- Utkarsh Kukreti, for through his Draco repo, helping me understand how wasm-bindgen's closure system can be used to update state.
Features to add
- Router
- High-level fetch API
- Cleaner event syntax
- Virtual DOM optimization
- Docs/tutorial website example to replace this readme
- High-level CSS-grid/Flexbox API ?
Bugs to fix
- Events do not patch properly
- Other hard-to-pin patching bugs
- Text renders above children instead of below