GitHub

Lib.rs Crates.io Docs.rs

Rust 1.58 CI Crates.io - License

GitHub open issues open pull requests good first issues

crev reviews Zulip Chat

Asteracea is a web application framework aiming to combine the strengths of Angular and React while fully supporting Rust's lifetime model.

Note: Asteracea is experimental software.
While it appears to work well so far, there likely will be breaking changes to the template syntax.

Installation

Please use cargo-edit to always add the latest version of this library:

cargo add asteracea

Design goals

  • Little boilerplate / Useful defaults

    Most generated boilerplate code is adjusted automatically to what is required. For example, the signature of a component's .render method changes if a Node is generated.

    There is still room for improvement here without sacrificing readability.

  • Co-location / DRY

    Intent shouldn't need to be reiterated in multiple places (split declaration, initialisation and usage).

    For now, short form captures nested in the component templates provide a way to centralise some semantics (similarly to React's Hooks but without their control flow limitations).

    Further improvements in this area are planned.

  • Robust code

    Element names are statically checked against lignin-schema by default, but other schemata can be defined similarly. Empty elements like <br> cannot contain children.

    Similar checks for attributes and event names are planned.

  • No default runtime

    Asteracea components compile to plain Rust code with few dependencies, which helps keep bundles small.

    Use lignin-dom or lignin-html to transform rendered Node trees into live user interfaces.

Examples

Additional examples can be found in the examples directory.

Empty component

The most simple (Node-rendering) component can be written like this:

asteracea::component! {
  pub Empty()() -> Sync
  [] // Empty node sequence
}
// Render into a bump allocator:
// This is generally only this explicit at the application root.
let mut bump = bumpalo::Bump::new();
let root = {
  struct Root;
  rhizome::sync::Node::new(core::any::TypeId::of::<Root>())
};
assert!(matches!(
  Box::pin(Empty::new(root.as_ref(), Empty::new_args_builder().build()).unwrap())
    .as_ref()
    .render(&mut bump, Empty::render_args_builder().build())
    .unwrap(),
  lignin::Node::Multi(&[]) // Empty node sequence
));

VDOM Sync-ness can be inferred (even transitively) at zero runtime cost by omitting -> Sync (or -> !Sync), except for components visible outside their crate.

Unit component

A return type other than Node can be specified after the render argument list:

asteracea::component! {
  Unit(/* ::new arguments */)(/* .render arguments */) -> ()
  {} // Empty Rust block
}
asteracea::component! {
  Offset(base: usize)(offset: usize) -> usize
  let pub self.base: usize = base; // ²
  { self.base + offset }
}
// This is generally only this explicit at the application root.
let mut bump = bumpalo::Bump::new();
let root = {
  struct Root;
  rhizome::sync::Node::new(core::any::TypeId::of::<Root>())
};
assert_eq!(
  Box::pin(Unit::new(root.as_ref(), Unit::new_args_builder().build()).unwrap())
    .as_ref()
    .render(&mut bump, Unit::render_args_builder().build())
    .unwrap(),
  (),
);
assert_eq!(
  Box::pin(Offset::new(root.as_ref(), Offset::new_args_builder().base(2).build()).unwrap())
    .as_ref()
    .render(&mut bump, Offset::render_args_builder().offset(3).build())
    .unwrap(),
  5,
);

² #2

Counter component

For a relatively complex example, see this parametrised counter:

"use asteracea::component; use lignin::web::Event; use std::cell::Cell; fn schedule_render() { /* ... */ } component! { pub Counter( /// The counter's starting value. initial: i32, priv step: i32, // field from argument pub enabled: bool = true, // default parameter )( // optional argument; // `class` is `Option<&'bump str>` only inside this component, not its API. class?: &'bump str, ) -> !Sync // visible across crate-boundaries, so use explicit `Sync`ness // shorthand capture; Defines a struct field. let self.value = Cell:: ::new(initial);
'bump str>` ."class"? = {class} // Anything within curlies is plain Rust. "The current value is: " !(self.value())

Read the original on github.com ↗