Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
439 lines (379 sloc) 14.7 KB

View

Visual layout (ie HTML/DOM elements) is described declaratively in Rust, and uses macros to simplify syntax. Each element is represented by a macro, eg div![]. These act as functions that accept an arbitrary number of parameters, in any order: They handle the parameters based exclusively on type.

The view's defined by a function that's passed to seed::App::build. This takes a &Model as its parameter, and outputs something that implements the View trait, which is imported in the prelude. Usually, this is a Node, or Vec<Node>, representing all nodes that will be inserted as children on the top-level one. (The top-level Node is in the html file, and specified with seed::App::build.mount(), or as a default, the element with id app). It may composed into sub-functions, which can be thought of like components in other frameworks.

Examples:

fn view(model: &Model) -> Node<Msg> {
    h1![ "Let there be light" ],
}
fn view(model: &Model) -> Vec<Node<Msg>> {
    vec![
        h1![ "Let there be light" ],
        h2![ "Let it be both a particle and a wave" ]
    ]
}

In either of those examples, you could use the signature: fn view(model: &Model) -> impl View<Msg> instead. This allows you to change between them without changing the function signature.

The Node Enum

The Virtual DOM is represnted by nested Nodes. Node has 3 variants:

  • Text holds a Text struct. Mostly for internal use, but can be created with Node::new_text().
  • Element wraps an El, which is the main component of our VDOM. Created using macros, described below.
  • Empty is a placeholder that doens't render anything; useful in conditional/ternary logic. Created using the empty![] macro, or seed::empty().

Elements, attributes, styles

Elements are created using macros, named by the lowercase name of each element, and imported into the global namespace. Eg div! above. We use this code to import them:

#[macro_use]
extern crate seed;

These macros accept any combination 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 Node structs, representing a child
  • One or more Vecs of Node structs, representing multiple children
  • A Map, ie the result of map(), yielding Nodes or Listeners, without having to explicitly collect.

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.

Attrs and Style are thinly-wrapped hashmaps created with their own macros: attrs!{} and style!{} respectively.

Example:

fn view(model: &Model) -> impl View<Msg> {
    let things = vec![ h4![ "thing1" ], h4![ "thing2" ] ];
    
    let other_things = vec![1, 2];

    div![ attrs!{At::Class => "hardly-any"}, 
        things,  // Vec<Node<Msg>
        other_things.map(|t| h4![t.to_string()]),  // Map
        h4![ "thing3?" ],  // El
    ]
}

Note that you can create any of the above items inside an element macro, or create it separately, and pass it in. You can separate different items by comma, semicolon, or space.

Keys passed to attrs! can be Seed::Ats, Strings, or &strs. Keys passed to style! can be Seed::Sts, Strings, or &strs. Values passed to attrs!, and style! macros can be owned Strings, &strs, or for style!, units.

You use the unit! macro to apply units. There's a px function for the special case where the unit is pixels:

style!{St::Width => unit!(20, px);}
style!{St::Width => px(20);}  // equivalent

Some types, like Options, implement a trait allowing them to be used directly in style!:

let display: &str = "flex";
let direction: String = "column".to_string();
let order: Option<u32> = None;
let gap: Option<&str> = Some("8px");

let style = style![
    St::Display => display,
    St::FlexDirection => direction,
    St::Order => order,
    St::Gap => gap,
];

We can set multiple values for an attribute using Attribute.add_multiple. This is useful for setting multiple classes. Note that we must set this up outside of the view macro, since it involves modifying a variable:

fn a_component() -> Node<Msg> {
    let mut attributes = attrs!{};
    attributes.add_multiple(At::Class, vec!["A-modicum-of", "hardly-any"]);

    div![ attributes ]
}

Seed validates attributes against this list; The At enum includes only these values, and &strs passed are converted into Ats. If you wish to use a custom attribute, use At::Custom , eg At::Custom(name), where name is a String of your attribute's name. In attrs! when using &strs, inserting an unrecognized attribute will do the same. Similar Custom methods exist for Style, Namespace, Tag, and Category.

The class! and id! convenience macros allow settings attributes as a list of classes, or a single id, if no other attributes are required. Do not mix and match these with each other, or with attrs!; all but the last-passed will be thrown out.

fn a_component() -> Node<Msg> {
    // ...
    span![ class!["calculus", "chemistry", "literature"] ],
    span![ id!("unique-element") ],
    // ...
}

You can conditionally add classes with the class! macro:

let active = true;

class![
    "blue",
    "highlighted" => active,
    "confusing" => 0.99999 == 1
    
]

Styles and Attrs can be passed as refs as well, which is useful if you need to pass the same one more than once:

fn a_component() -> Node<Msg> {
    let item_style = style!{
        St::MarginTop => px(10);
        St::FontSize => unit!(1.2, em)
    };

    div![
        ul![
            li![ &item_style, "Item 1", ],
            li![ &item_style, "Item 2", ],
        ]
    ]
}

For boolean attributes that are handled by presense or absense, like disabled, checked, autofocus etc, use .as_at_value: input![ attrs!{At::Disabled => false.as_at_value() ]:

fn a_component() -> Node<Msg> {
    // ...
    input![ attrs!{At::Typed => "checkbox"; At::Checked => true.as_at_value()} ]
    input![ attrs!{At::Autofocus => true.as_at_value()} ]
    // ...
}

At::Checked => true.as_at_value() is equivalent to the presense of a checked attribute, and At::Checked => false.as_at_value() is equivalent to ommitting it.

To change Attrs or Styles you've created, edit their .vals HashMap. To add a new part to them, use their .add method:

let mut attributes = attrs!{};
attributes.add(At::Class, "truckloads");

Example of the style tag, and how you can use pattern-matching in views:

fn view(model: &Model) -> impl View<Msg> {
    div![ style!{
        St::Display => "grid";
        St::GridTemplateColumns => "auto";
        St::GridTemplateRows => "100px auto 100px"
        },
        section![ style!{St::GridRow => "1 / 2"},
            header(),
        ],
        section![ attrs!{St::GridRow => "2 / 3"},
            match model.page {
                Page::Guide => guide(),
                Page::Changelog => changelog(),
            },
        ],
        section![ style!{St::GridRow => "3 / 4"},
            footer()
        ]
    ]
}

We can combine Attrs and Style instances using their merge methods, which take an &Attrs and &Style respectively. This can be used to compose styles from reusable parts. Example:

fn a_component() -> Node<Msg> {
    let base_style = style!{"color" => "lavender"};

    div![
        h1![ &base_style.merge(&style!{St::GridRow => "1 / 2"}) "First row" ],
        h1![ &base_style.merge(&style!{St::GridRow => "2 / 3"}) "Second row" ],
    ]
}

Perhaps more cleanly, we can use multiple Styles together, to merge their entries:

fn a_component() -> Node<Msg> {
    let base_style = style!{"color" => "lavender"};

    div![
        h1![ 
            &base_style, 
            style!{St::GridRow => "1 / 2"},
            "First row" ],
        h1![ 
            &base_style, 
            style!{St::GridRow => "2 / 3"}, 
            "Second row" ],
    ]
}

Overall: we leverage of Rust's strict type system to flexibly-create the view using normal Rust code.W

El has several helper methods which can be chained together:

let my_el = div![]
    .add_text("Words")
    .add_class("complete")
    .add_attr("alt".to_string(), "a description".to_string())
    .add_style(St::Height, "20px".to_string())
    .replace_text("Oops, not complete");oo

Svg

You can create SVG elements in the same way as normal Html elements. Setting the xmlns attribute isn't required; it's set automatically when using the macro.

Example using macros:

svg![
    rect![
        attrs!{
            At::X => "5",
            At::Y =>"5",
            At::Width => "20",
            At::Height => "20",
            At::Stroke => "green",
            At::StrokeWidth => "4",
        }
    ]
]

The same exmaple using from_html:

Node::from_html(
r#"
<svg>
    <rect x="#5" y="5" width="20" height="20" stroke="green" stroke-width="4" />
</svg>
"#)

Another example, showing it in the View fn:

fn view(model: &Model) -> Vec<Node<Msg>> {
    vec![
        svg![
            attrs!{
                At::Width => "100%";
                At::Height => "100%";
                At::ViewBox => "0 0 512 512";
            },
            path![ 
                attrs!{
                    At::Fill => "lightgrey";
                    At::D => "M345.863,281.853c19.152-8.872,38.221-15.344,56.1"  // etc
                }
            ],
            // More elements as required, eg mesh, polyline, circle
        ]
    ]
}

Canvas (unreleased; for now, you can use web_sys directly.

Seed provides helper functions for use with Canvas:

fn draw() {
    let canvas = seed::canvas("canvas").unwrap();
    let ctx = seed::canvas_context_2d(&canvas);

    ctx.move_to(0., 0.);
    ctx.line_to(200., 100.);
    ctx.stroke();
}

#[wasm_bindgen(start)] pub fn render() { seed::App::build(|_, _| Init::new(Model {}), update, view).build_and_start(); draw(); }

Components

The analog of components in frameworks like React are normal Rust functions that that return Node s. These functions take parameters that 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're used in a way similar to components in React.

For example, you could organize one of the examples in the Structure section of the guide like this:

    fn text_display(text: &str) -> Node<Msg> {
        h3![ text ]
    }  
    
    div![ style!{St::Display => "flex"; St::FlexDirection => "column"},
        text_display("Some things"),
        button![ simple_ev("click", Msg::SayHi), "Click me!" ]
    ]

The text_display component returns a single Node 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 ofNodes, which you can incorporate into other Nodes using normal Rust code. See the Fragments section below. Rust's type system ensures that only Nodes can end up as children, so if your app compiles, you haven't violated any rules.

Unlike in JSX, there's a clear syntax delineation between natural DOM elements (element macros), and custom components (function calls): We called text_display above as text_display("Some things"), not text_display![ "Some things" ].

Fragments

Fragments (<>...</> syntax in React and Yew) are components that represent multiple elements without a parent. They're useful to avoid unecessary divs, which clutter the DOM, and breaks things like tables and CSS-grid. There's no special fragment syntax: have your component return a Vec of Nodes instead of one. Add it to the parent's element macro:

fn cols() -> Vec<Node<Msg>> {
    vec![
        td![ "1" ],
        td![ "2" ],
        td![ "3" ]
    ]
}

fn items() -> Node<Msg> {
    table![
        tr![ cols() ]
    ]
}

You can mix Node Vecs with Nodes in macros:

fn items() -> Node<Msg> {
    // You may wish to keep complicated or dynamic logic separate.
    let mut more_cols = vec![ td![ "another col" ], td![ "and another" ] ];
    more_cols.push(td![ "yet another" ]);

    table![
        tr![
            td![ "first col" ],  // A lone element
            cols(),  // A "fragment" component.
            td![ "an extra col" ], // A element after the fragment
            // A Vec of Els, not in a separate func
            vec![ td![ "another col" ], td![ "and another" ] ],
            more_cols  // A vec of Els created separately.
        ]
    ]
}

Dummy elements

When performing ternary operations inside an element macro, all branches must return an Node (Or Vec of Nodes) to satisfy Rust's type system. Seed provides the empty function, which creates a Node that will not be rendered, and its empty![] macro alias, which is more concise and consistent:

div![
    if model.count >= 10 { h2![ style!{St::Padding => 50}, "Nice!" ] } else { empty![]) }
]
You can’t perform that action at this time.