Skip to content

Latest commit

 

History

History
879 lines (685 loc) · 33.2 KB

frontend.md

File metadata and controls

879 lines (685 loc) · 33.2 KB

Frontend - Zoon


Basics

A Counter example:

use zoon::*;

#[static_ref]
fn counter() -> &'static Mutable<i32> {
    Mutable::new(0)
}

fn increment() {
    counter().update(|counter| counter + 1)
}

fn decrement() {
    counter().update(|counter| counter - 1)
}

fn root() -> impl Element {
    Column::new()
        .item(Button::new().label("-").on_press(decrement))
        .item(Text::with_signal(counter().signal()))
        .item(Button::new().label("+").on_press(increment))
}

#[wasm_bindgen(start)]
pub fn start() {
    start_app("app", root);
}

The alternative Counter example with a local state:

use zoon::{*, println};

fn root() -> impl Element {
    println!("I'm different.");

    let (counter, counter_signal) = Mutable::new_and_signal(0);
    let on_press = move |step: i32| *counter.lock_mut() += step;

    Column::new()
        .item(Button::new().label("-").on_press(clone!((on_press) move || on_press(-1))))
        .item_signal(counter_signal)
        .item(Button::new().label("+").on_press(move || on_press(1)))
}

#[wasm_bindgen(start)]
pub fn start() {
    start_app("app", root);
}

1. The App Initialization

  1. The function start is invoked automatically from the Javascript code.

  2. Zoon's function start_app appends the element returned from the root function to the element with the id app.

    • You can also pass the value None instead of "app" to mount directly to body but it's not recommended.

    • When the root function is invoked (note: it's invoked only once), all elements are immediately created and rendered to the browser DOM. (It means, for instance, methods Column::new() or .item(..) writes to DOM.)

    • Data stored in functions marked by the attribute #[static_ref] are lazily initialized on the first call.

2. Update

  1. The user clicks the decrement button.

  2. The function decrement is invoked.

  3. counter's value is decremented.

  4. counter has type Mutable => it sends its updated value to all associated signals.

  5. The new counter value is received through a signal and the corresponding text is updated.

    • In the original example, only the content of the Text element is changed.
    • In the alternative examples, the counter value is automatically transformed to a new Text element.

Notes:

  • Read the excellent tutorial for Mutable and signals in the futures_signals crate.
  • zoon::* reimports most needed types and you can access some of Zoon's dependencies by zoon::library like zoon::futures_signals.
  • clone! is a type alias for enclose::enc.
  • static_ref, clone! and other things can be disabled or set by Zoon's features.

Elements

A Counter example part:

Button::new().label("-").on_press(decrement)

We'll look at the Button element code (crates/zoon/src/element/button.rs). Button is a Zoon element, but you can create custom ones the same way.

There are three sections: Element, Abilities and Attributes.

use crate::{web_sys::HtmlDivElement, *}; // `crate` == `zoon`
use std::marker::PhantomData;

// ------ ------
//    Element 
// ------ ------

make_flags!(Label, OnPress);

pub struct Button<LabelFlag, OnPressFlag> {
    raw_el: RawHtmlEl,
    flags: PhantomData<(LabelFlag, OnPressFlag)>,
}

impl Button<LabelFlagNotSet, OnPressFlagNotSet> {
    pub fn new() -> Self {
        Self {
            raw_el: RawHtmlEl::new("div")
                .class("button")
                .attr("role", "button")
                .attr("tabindex", "0")
                .style("cursor", "pointer")
                .style("user-select", "none")
                .style("text-align", "center"),
            flags: PhantomData,
        }
    }
}

impl<OnPressFlag> Element for Button<LabelFlagSet, OnPressFlag> {
    fn into_raw_element(self) -> RawElement {
        self.raw_el.into()
    }
}

impl<LabelFlag, OnPressFlag> UpdateRawEl<RawHtmlEl> for Button<LabelFlag, OnPressFlag> {
    fn update_raw_el(mut self, updater: impl FnOnce(RawHtmlEl) -> RawHtmlEl) -> Self {
        self.raw_el = updater(self.raw_el);
        self
    }
}
  • The macro make_flags! will be explained later.
  • The element has to implement the trait Element.
  • It's recommended to implement UpdateRawEl to allow users to customize the element and it's required for abilities.
  • RawHtmlEl::style automatically add vendor prefixes for CSS property names when required. E.g. "user-select" will be replaced with "-webkit-user-select" on Safari and browsers on iOS. (Values aren't prefixed, let us know when it becomes a show-stopper for you.)
// ------ ------
//   Abilities
// ------ ------

impl<LabelFlag, OnPressFlag> Styleable<'_, RawHtmlEl> for Button<LabelFlag, OnPressFlag> {}
impl<LabelFlag, OnPressFlag> KeyboardEventAware<RawHtmlEl> for Button<LabelFlag, OnPressFlag> {}
impl<LabelFlag, OnPressFlag> Focusable for Button<LabelFlag, OnPressFlag> {}
impl<LabelFlag, OnPressFlag> MouseEventAware<RawHtmlEl> for Button<LabelFlag, OnPressFlag> {}
impl<LabelFlag, OnPressFlag> Hookable<RawHtmlEl> for Button<LabelFlag, OnPressFlag> {
    type WSElement = HtmlDivElement;
}
impl<LabelFlag, OnPressFlag> AddNearbyElement<'_> for Button<LabelFlag, OnPressFlag> {}

Abilities are basically simple traits. For example when you implement Styleable then users can call the .s(...) method on your element:

MyElement::new().s(Padding::new().all(6))

You can find all built-in abilities in crates/zoon/src/element/ability/styleable.rs. The Styleable ability:

pub trait Styleable<'a, T: RawEl>: UpdateRawEl<T> + Sized {
    fn s(self, style: impl Style<'a>) -> Self {
        self.update_raw_el(|raw_el| style.update_raw_el_styles(raw_el))
    }
}

Note: You can also implement your custom abilities.

// ------ ------
//  Attributes
// ------ ------

impl<'a, LabelFlag, OnPressFlag> Button<LabelFlag, OnPressFlag> {
    pub fn label(mut self, label: impl IntoElement<'a> + 'a) -> Button<LabelFlagSet, OnPressFlag>
    where
        LabelFlag: FlagNotSet,
    {
        self.raw_el = self.raw_el.child(label);
        self.into_type()
    }

    pub fn label_signal(
        mut self,
        label: impl Signal<Item = impl IntoElement<'a>> + Unpin + 'static,
    ) -> Button<LabelFlagSet, OnPressFlag>
    where
        LabelFlag: FlagNotSet,
    {
        self.raw_el = self.raw_el.child_signal(label);
        self.into_type()
    }

    pub fn on_press(
        mut self,
        on_press: impl FnOnce() + Clone + 'static,
    ) -> Button<LabelFlag, OnPressFlagSet>
    where
        OnPressFlag: FlagNotSet,
    {
        self.raw_el = self
            .raw_el
            .event_handler(move |_: events::Click| (on_press.clone())());
        self.into_type()
    }

    fn into_type<NewLabelFlag, NewOnPressFlag>(self) -> Button<NewLabelFlag, NewOnPressFlag> {
        Button {
            raw_el: self.raw_el,
            flags: PhantomData,
        }
    }
}

Note: Attribute implementations look a bit verbose because of long types and generics but it's a trade-off for the user's comfort and safety. Also we will improve it once stable Rust has better support for const generics and other things. And you can always write your custom elements without generics to make the code simpler.

--

make_flags!(Label, OnPress); generates code like:

struct LabelFlagSet;
struct LabelFlagNotSet;
impl zoon::FlagSet for LabelFlagSet {}
impl zoon::FlagNotSet for LabelFlagNotSet {}

struct OnPressFlagSet;
struct OnPressFlagNotSet;
impl zoon::FlagSet for OnPressFlagSet {}
impl zoon::FlagNotSet for OnPressFlagNotSet {}

The only purpose of flags is to enforce extra rules by the Rust compiler.

The compiler doesn't allow to call label or label_signal if the label is already set. The same rule applies for on_press handler.

Button::new()
    .label("-")
    .label("+")

fails with

error[E0277]: the trait bound `LabelFlagSet: FlagNotSet` is not satisfied
  --> frontend\src\lib.rs:20:14
   |
20 |.label("+"))
   | ^^^^^ the trait `FlagNotSet` is not implemented for `LabelFlagSet`

Lifecycle Hooks

Canvas example parts:

use zoon::{*, web_sys::{CanvasRenderingContext2d, HtmlCanvasElement}};

#[static_ref]
fn canvas_context() -> &'static Mutable<Option<SendWrapper<CanvasRenderingContext2d>>> {
    Mutable::new(None)
}

fn set_canvas_context(canvas: HtmlCanvasElement) {
    let ctx = canvas
        .get_context("2d")
        .unwrap_throw()
        .unwrap_throw()
        .unchecked_into::<CanvasRenderingContext2d>();
    canvas_context().set(Some(SendWrapper::new(ctx)));
    paint_canvas();
}

fn remove_canvas_context() {
    canvas_context().take();
}

fn canvas() -> impl Element {
    Canvas::new()
        .width(300)
        .height(300)
        .after_insert(set_canvas_context)
        .after_remove(|_| remove_canvas_context())
}
  • You can call methods (hooks) after_insert and after_remove on all elements that implement the ability Hookable.

  • Hooks allow you to access the DOM nodes directly. It may be quite verbose but you have the full power of the crate web_sys under your hands.

  • SendWrapper allows you to store non-Send types (e.g. web_sys elements) to statics.

    • Note: The Hooks API will be probably revisited once Wasm fully support multithreading and we know more use-cases.

The Hookable ability / trait:

pub trait Hookable<T: RawEl>: UpdateRawEl<T> + Sized {
    type WSElement: JsCast;

    fn after_insert(self, handler: impl FnOnce(Self::WSElement) + Clone + 'static) -> Self {
        // ...
    }

    fn after_remove(self, handler: impl FnOnce(Self::WSElement) + Clone + 'static) -> Self {
        // ...
    }
}

and the implementation for Canvas:

impl<WidthFlag, HeightFlag> Hookable<RawHtmlEl> for Canvas<WidthFlag, HeightFlag> {
    type WSElement = HtmlCanvasElement;
}
  • It means hooks invoke your callbacks with a specific web_sys element defined in the implementation.
    • In most cases it would be HtmlDivElement.
    • All web_sys elements implement JsCast. It allows you to "convert" for instance HtmlDivElement to HtmlElement with div.unchecked_ref::<HtmlElement>().

Styles

A TodoMVC example part:

fn new_todo_title() -> impl Element {
    TextInput::new()
        .s(Padding::new().y(19).left(60).right(16))
        .s(Font::new().size(24).color(hsl(0, 0, 32.7)))
        .s(Background::new().color(hsla(0, 0, 0, 0.3)))
        .s(Shadows::new(vec![
            Shadow::new().inner().y(-2).blur(1).color(hsla(0, 0, 0, 3))
        ]))
        .focus(true)
        .on_change(super::set_new_todo_title)
        .label_hidden("What needs to be done?")
        .placeholder(
            Placeholder::new("What needs to be done?")
                .s(Font::new().italic().color(hsl(0, 0, 91.3))),
        )
        .on_key_down_event(|event| event.if_key(Key::Enter, super::add_todo))
        .text_signal(super::new_todo_title().signal_cloned())
}
  • CSS concepts / events like focus, hover and breakpoints are handled directly by Rust / Zoon elements.

  • There is no such thing as CSS margins or selectors in built-in element APIs. Padding and declarative layout (columns, rows, nearby elements, spacing, etc.) are more natural alternatives.

Global styles

A Paragraph element part:

pub fn new() -> Self {
    run_once!(|| {
        global_styles()
            .style_group(StyleGroup::new(".paragraph > *").style("display", "inline"))
            .style_group(StyleGroup::new(".paragraph > .align_left").style("float", "left"))
            .style_group(StyleGroup::new(".paragraph > .align_right").style("float", "right"));
    });
    Self {
        raw_el: RawHtmlEl::new("p").class("paragraph"),
        flags: PhantomData,
    }
}
  • run_once! is a Zoon's macro leveraging std::sync::Once.
  • global_styles() returns &'static GlobalStyles with two public methods: style_group and style_group_droppable.
  • StyleGroup has 3 methods: new, style and style_signal.
  • Global styles are stored in one dedicated <style> element appended to the <head>.
  • StyleGroup selector and styles are validated in the runtime - invalid ones trigger panic!.
  • Vendor prefixes are automatically attached to CSS property names when needed.

Raw styles

fn element_with_raw_styles() -> impl Element {
    El::new().update_raw_el(|raw_el| {
        raw_el
            .style("cursor", "pointer")
            .style_group(
                StyleGroup::new(":hover .icon")
                    .style("display", "block")
            )
    })
}
  • raw_el is either RawHtmlEl or RawSvgEl with many useful methods, including style and style_group.
  • StyleGroup selector is prefixed by a unique element class id - e.g. ._13:hover .icon.
  • StyleGroups are stored among the global styles and dropped when the associated element is removed from the DOM.

Animated styles

fn sidebar() -> impl Element {
    Column::new()
        .s(Width::with_signal(sidebar_expanded().signal().map_bool(|| 180, || 48)))
        .s(Transitions::new([Transition::width().duration(500)]))
        .s(Clip::both())
        .item(toggle_button())
        .item(menu())
}
  • Use Transitions with an iterator of Transition to create basic animations.
  • There are some typed properties like Transition::width() and ::height(), but you can use also ::all() and custom property names with ::property("font-size").
  • Let us know when you want to add another typed property. The list of supported properties.

Color

.s(Font::new().size(24).color(hsluv!(0, 0, 32.7)))

.s(Font::new().size(30).center().color_signal(
    hovered_signal.map_bool(|| hsluv!(10.5, 37.7, 48.8), || hsluv!(12.2, 34.7, 68.2)),
))

.s(Background::new().color(hsluv!(0, 0, 0, 0.3)))

.s(Shadows::new(vec![
    Shadow::new().inner().y(-2).blur(1).color(hsluv!(0, 0, 0, 3))
]))

.s(Borders::new().top(Border::new().color(hsluv!(0, 0, 91.3))))

The most commonly used color code systems are:

  • HEX - #ffff00,
  • RGB - rgb(255, 255, 0)
  • HSL - hsl(60, 100%, 50%)

_

However when you want to:

  • create color palettes and themes
  • make sure the button is slightly lighter or darker on hover
  • make the text more readable

you often need to set saturation and lightness directly. Also it's nice to identify the hue on the first look when you are reading the code. These two conditions basically renders HEX and RGB unusable.

_

But there is also a problem with HSL. Let's compare these two colors:

Yellow HSL

Blue HSL

Are we sure they have the same lightness 50%? I don't think so. The human eye perceives yellow as brighter than blue. Fortunately there is a color system that takes into account this perception: HSLuv.

Yellow HSLuv

Blue HSLuv

That's why Zoon uses only HSLuv, represented in the code as hsluv!(h, s, l) or hsluv!(h, s, l, a), where:

  • h ; hue ; 0 - 360
  • s ; saturation ; 0 - 100
  • l ; lightness ; 0 - 100
  • a ; alpha channel / opacity ; 0 (transparent) - 100 (opaque)

The macro hsluv! creates an HSLuv instance and all color components are checked during compilation.

Other examples why color theory and design in general are difficult
  • The human eye recognizes differences between lighter tones better than between darker tones. This fact is important for creating color palettes.
  • Too extreme contrast weakens reading stamina - you shouldn't use pure black and white too often (unless you are creating a special theme for low vision users).
  • Relatively many people are at least slightly color blind. It means, for example:
    • Red "Stop button" has to have also a text label.
    • Do you want to show different routes on the map? Use different line styles (e.g. dashed, dottted) instead of different colors.
    • The guy over there maybe doesn't know his T-shirt isn't gray but pink. (It's a typical issue for deutan color blindness; ~5% of men.)
    • Pick colors and labels for charts carefully - some charts could become useless for color blind people or when you decide to print them in a gray-scale mode. (HSLuv mode can help here a bit because you can pick colors with different lightness values.)

Size

Units

CSS supports cm, mm, in, px, pt, pc, em, ex, ch, rem, vw, vh, vmin, vmax and %. I'm sure there were reasons for each of them, but let's just use px. Zoon may transform pixels to relative CSS units like rem or do other computations under the hood to improve accessibility.

Font Size

Have you ever ever tried to align an element with a text block? An example:

Element text alignment

How can we measure or even remove the space above the Zoon text? It's an incredibly difficult task because the space is different for each font and it's impossible in CSS without ugly error-prone hacks.

You will be able to resolve it in the future CSS with some new properties, mainly with leading-trim. One of the comments for the article Leading-Trim: The Future of Digital Typesetting:

"This has been a huge annoyance to me for decades! I hope this gets standardized and implemented quickly, thank you for setting this in motion!" - Tim Etler

_

Typography is one of the most complex parts of (web) design. But we have to somehow simplify it for our purposes.

So I suggest to make the font size an alias for the cap height. And the font size would be also equal to the line height. It means the code:

Paragraph::new()
    .s(Font::new().size(40).line_height(40 + 30))
    .content("Moon")
    .content("Zoon")

would be rendered as:

Font Size example

--


Viewport

The Viewport example parts:

#[static_ref]
fn viewport_y() -> &'static Mutable<i32> {
    Mutable::new(0)
}

fn jump_to_top() {
    viewport_y().set(0);
}

fn jump_to_bottom() {
    viewport_y().set(i32::MAX);
}

fn on_viewport_change(_scene: Scene, viewport: Viewport) {
    viewport_y().set(viewport.y());
    // ...
}

fn rectangles() -> impl Element {
    Column::new()
        // ...
        .on_viewport_location_change(on_viewport_change)
        .viewport_y_signal(viewport_y().signal())
        .items(iter::repeat_with(rectangle).take(5))
}

The concept of Scene + Viewport has been "stolen" from the Elm world. Just like the picture below. You can find them in Elm docs.

Scene and Viewport

  • Scene is the part of an element that contains other elements.

  • Viewport represents the part of the Scene currently visible by the user. It could be used for scrolling/jumping and to help with writing responsive elements.

  • You can set the Viewport location in elements that implement the MutableViewport ability (e.g. Row, Column or El).

  • Notes:

    • Viewport's x and y may be negative while the user is scrolling on the phone.
    • x and y are automatically clamped. So you can write things like viewport_y().set(i32::MAX) and don't be afraid the viewport will be moved outside of the scene.

Built-in libraries / API

Connection + Task

  • UpMsg are sent from Zoon to Moon. DownMsg in the opposite direction.
  • UpMsg could be buffered when the Moon server is offline. And DownMsg when the Zoon client is automatically reconnecting.
  • UpMsg are sent in a short-lived fetch request, DownMsg are sent in a server-sent event to provide real-time communication.
  • A correlation id is automatically generated and sent to the Moon with each request. Moon can send it back with the next DownMsg or send a new CorId. You can also send an auth token together with the UpMsg.
  • A session id is automatically generated when the Connection is created. Then it's sent with each UpMsg. You can use it to simulate standard request-response mechanism.
  • Task::start or Task::start_droppable spawn the given Future. (Note: Multithreading isn't supported yet.)
  • See examples/chat for the entire code.
#[static_ref]
fn connection() -> &'static Connection<UpMsg, DownMsg> {
    Connection::new(|DownMsg::MessageReceived(message), _cor_id| {
        messages().lock_mut().push_cloned(message);
    })
    // .auth_token_getter(|| AuthToken::new("my_auth_token"))
}

fn send_message() {
    Task::start(async {
        let result = connection()
            .send_up_msg(UpMsg::SendMessage(Message {
                username: username().get_cloned(),
                text: new_message_text().take(),
            }))
            .await;
        match result {
            Ok(cor_id) => println!("Correlation id: {}", cor_id),
            Err(error) => eprintln!("Failed to send message: {:?}", error),
        }
    });
}

Timer

  • Could be used as a timeout or stopwatch (to set an interval between callback calls).
  • Timer has methods new, new_immediate, once and sleep.
  • Timer is stopped on drop.
  • See examples/timer for the entire code.
#[static_ref]
fn timeout() -> &'static Mutable<Option<Timer>> {
    Mutable::new(None)
}

fn timeout_enabled() -> impl Signal<Item = bool> {
    timeout().signal_ref(Option::is_some)
}

fn start_timeout() {
    timeout().set(Some(Timer::once(2_000, stop_timeout)));
}

fn stop_timeout() {
    timeout().take();
}

fn sleep_panel() -> impl Element {
    let (asleep, asleep_signal) = Mutable::new_and_signal(false);
    let sleep = move || {
        Task::start(async move {
            asleep.set_neq(true);
            Timer::sleep(2_000).await;
            asleep.set_neq(false);
        })
    };
    Row::new()
        .s(Spacing::new(20))
        .item("2s Async Sleep")
        .item_signal(asleep_signal.map_bool(
            || El::new().child("zZZ...").left_either(),
            move || start_button(sleep.clone()).right_either(),
        ))
}

Routing

  • You just need the struct Router and the route macro to implement basic routing in your app.
  • The callback passed into Router::new is called when the url has been changed.
  • #[route("segment_a", "segment_b")] will be transformed to the url "/segment_a/segment_b".
  • Dynamic route segments (aka parameters / arguments) have to implement the trait RouteSegment (see the code below for an example). It has been already implemented for basic items like f64 or String.
  • Dynamic segment names have to match to the associated enum variant fields. Notice frequency in this snippet:
    #[route("report", frequency)]
    Report { frequency: report_page::Frequency },
  • Urls are automatically encoded and decoded (see encodeURIComponent() on MDN for more info).
  • There are helpers like routing::back, routing::url, Router::go and Router::replace.
  • Routes are matched against the incoming url path from the first one to the last one. The example of the generated code for matching the route #[route("report", frequency)]:
    fn route_0_from_route_segments(segments: &[String]) -> Option<Self> {
        if segments.len() != 2 { None? }
        if segments[0] != "report" { None? }
        Some(Self::ReportWithFrequency {
            frequency: RouteSegment::from_string_segment(&segments[1])?
        })
    }
  • The simplified part of the examples/pages below. See the original code to learn how to write "guards", redirect after login, etc.
// ------ router ------

#[static_ref]
pub fn router() -> &'static Router<Route> {
    Router::new(|route| match route { 
        Some(Route::Report { frequency }) => {
            app::set_page_id(PageId::Report);
            report_page::set_frequency(frequency);
        }
        Some(Route::Calc { operand_a, operator, operand_b }) => {
            app::set_page_id(PageId::Calc);
            calc_page::set_expression(
                calc_page::Expression::new(operand_a, operator, operand_b)
            );
        }
        Some(Route::Root) => {
            app::set_page_id(PageId::Home);
        }
        None => {
            app::set_page_id(PageId::Unknown);
        }
    })
}

// ------ Route ------

#[route]
pub enum Route {
    #[route("report", frequency)]
    Report { frequency: report_page::Frequency },

    #[route("calc", operand_a, operator, operand_b)]
    Calc {
        operand_a: f64,
        operator: String,
        operand_b: f64,
    },

    #[route()]
    Root,
}

//...

impl RouteSegment for Frequency {
    fn from_string_segment(segment: &str) -> Option<Self> {
        match segment {
            DAILY => Some(Frequency::Daily),
            WEEKLY => Some(Frequency::Weekly),
            _ => None,
        }
    }

    fn into_string_segment(self) -> Cow<'static, str> {
        self.as_str().into()
    }
}

--

Link handling

All urls starting with / are treated as internal. It means when you click the link like

<a href="/something">I'm a link with an internal url</a>

then the click event will be in most cases fully handled by the Zoon to prevent browser tab reloading.

Exceptions when the link click isn't intercepted even if its href starts with /:

  • The link has the download attribute.
  • The link has the target attribute with the value _blank.
  • The user holds the key ctrl, meta or shift while clicking.
  • The user hasn't clicked by the primary button (left button for right-handed).

LocalStorage & SessionStorage

static STORAGE_KEY: &str = "todomvc-zoon";

#[derive(Deserialize, Serialize)]
struct Todo {
    id: TodoId,
    title: Mutable<String>,
    completed: Mutable<bool>,
    #[serde(skip)]
    edited_title: Mutable<Option<String>>,
}

pub fn load_todos() {
    if let Some(Ok(todos)) = local_storage().get(STORAGE_KEY) {
        replace_todos(todos);
        println!("Todos loaded");
    }
}

fn save_todos() {
    if let Err(error) = local_storage().insert(STORAGE_KEY, todos()) {
        eprintln!("Saving todos failed: {:?}", error);
    }
}
  • All items implementing serde-lite's Deserialize and Serialize can be stored in the local or session storage.
  • See examples/todomvc or crates/zoon/src/web_storage.rs for more info.

SEO

  • When the request comes from a robot (e.g. Googlebot), then MoonZoon renders elements to a HTML string and sends it back to the robot. (It's basically a limited Server-Side Rendering / Dynamic Rendering.)

  • You'll be able to configure the default page title, The Open Graph Metadata and other things in the Moon app. The example below will be continuously updated.

    async fn frontend() -> Frontend {
        Frontend::new().title("Time Tracker example")
    }

FAQ

  1. "Why another frontend framework? Are you mad??"

    • Because I have some problems with the existing ones. Some examples:

      Problems with existing frontend frameworks
      • I'm not brave enough to write apps and merge pull requests written in a dynamic language.
      • I'm tired of configuring Webpack-like bundlers and fixing bugs caused by incorrectly typed JS libraries to Typescript.
      • I want to share code between the client and server and I want server-side rendering and I don't want to switch context (language, ecosystem, best practices, etc.) while I'm writing both frontend and server.
      • I don't want to read the entire stackoverflow.com and MDN docs to find out why my image on the website has incorrect size.
      • I don't want to be afraid to refactor styles.
      • I don't want to write code on the backend instead on the frontend because frontend is just too slow.
      • Who have time and energy to properly learn, write and constantly think about accessibility and write unit tests that take into account weird things like null or undefined?
      • I'm tired of searching for missing semicolons and brackets in HTML and CSS when it silently fails in the runtime.
      • I don't want to choose a CSS framework, bundler, state manager, router, bundler plugins, CSS preprocessor plugins, test framework and debug their incompatibilities and learn new apis everytime I want to create a new web project.
      • Why the layout is broken on iPhone, the app crashes on Safari, it's slow on Chrome and scrollbars don't work on Windows?
      • I just want to send a message to a server. I don't want to handle retrying, set headers, set timeout, correctly serialize everything, handle errors by their numbers, constantly think about cookies, domains, protocols, XSS, CSRF, etc.
      • What about SEO?
      • Should I use standard routing, hash routing, query parameters, custom base paths? Is everything correctly encoded and decoded?
      • etc.
  2. "How are we taking care of animations?" (by None on chat)

  3. "Hey Martin, what about Seed?"

    • Zoon and Seed have very different features and goals. I assume we will be able to implement some interesting features inspired by Zoon in Seed, if needed. I'll maintain Seed as usual.
  4. "How do I get a standalone html+wasm output? I previously used Yew + Trunk." (by @Noir on the MZ Discord)

    Longer answer with explanation

    It's possible, I have to do that to make js-framework-benchmark work: https://github.com/MartinKavik/js-framework-benchmark/tree/framework/moonzoon/frameworks/keyed/moonzoon.

    You have to create index.html similar to the HTML output from the Moon app. You can also disable cache_busting in your MoonZoon.toml to disable file name suffix generator for wasm and js files.

    mzoon uses wasm-pack under the hood so you'll find your "dist files" in pkg folder (https://github.com/MartinKavik/js-framework-benchmark/tree/framework/moonzoon/frameworks/keyed/moonzoon/frontend/pkg). Trunk uses wasm-bindgen directly (instead of indirectly through wasm-pack) so Trunk moves the files to dist.

    I can imagine you want to use a standalone Zoon app for deploying to Netlify / Render / Azure Static Web Apps / DO App Platform for free or to use it with your custom server. It makes sense and I have an official support for standalone Zoon app on my roadmap, but there are some problems:

    • Custom server integration is a Pandora's box:
      • We need to take into account CORS and other security settings for communication with the backend.
      • There is a chance you need to change base url or use hash routing to mitigate url conflicts with the server.
      • You have to be able to change urls for requests and SSE. Or disable SSE. Or many services don't support SSE at all or only Websockets - so we would need to resolve it somehow.
      • You need prerendering or SSR for SEO.
      • And other problems that I had to resolve during the Seed development.
      • There will be also conflicts between HTML generated from Moon and what Trunk expects. We need "adapters" for such cases.
    • And non-technical problems - I have some sponsors but I have to follow money to pay my rent - I hope MoonZoon Cloud or a business based on MZ will cover my expenses. It means I don't have a reason to work on standalone Zoon apps personally for now.