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))
}
fn main() {
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)))
}
fn main() {
start_app("app", root);
}
-
The function
main
is invoked automatically. -
Zoon's function
start_app
appends the element returned from theroot
function to the element with the idapp
.-
You can also pass the value
None
instead of"app"
to mount directly tobody
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, methodsColumn::new()
or.item(..)
writes to DOM.) -
Data stored in functions marked by the attribute
#[static_ref]
are lazily initialized on the first call.
-
-
The user clicks the decrement button.
-
The function
decrement
is invoked. -
counter
's value is decremented. -
counter
has typeMutable
=> it sends its updated value to all associated signals. -
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 newText
element.
- In the original example, only the content of the
Notes:
- Read the excellent tutorial for
Mutable
and signals in thefutures_signals
crate. zoon::*
reimports most needed types and you can access some of Zoon's dependencies byzoon::[library]
likezoon::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.
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 native Zoon element.
You can create custom ones the same way or make similar ones but simpler - it always depends on your requirements.
There are three sections: Element
, Abilities
and Attributes
in Button
's code.
Element
:
use crate::*; // `crate` == `zoon`
use std::{iter, marker::PhantomData};
// ------ ------
// Element
// ------ ------
make_flags!(Label, OnPress);
pub struct Button<LabelFlag, OnPressFlag, RE: RawEl> {
raw_el: RE,
flags: PhantomData<(LabelFlag, OnPressFlag)>,
}
impl Button<LabelFlagNotSet, OnPressFlagNotSet> {
pub fn new() -> Self {
run_once!(|| {
global_styles()
.style_group(
StyleGroup::new(".button > *")
.style("margin-top", "auto")
.style("margin-bottom", "auto"),
)
// ...
});
Self {
raw_el: RawHtmlEl::<web_sys::HtmlDivElement>::new("div")
.class("button")
.attr("role", "button")
.attr("tabindex", "0")
.style("cursor", "pointer")
// ...
flags: PhantomData,
}
}
}
impl<OnPressFlag, RE: RawEl> Element for Button<LabelFlagSet, OnPressFlag, RE> {
fn into_raw_element(self) -> RawElement {
self.raw_el.into()
}
}
impl<LabelFlag, OnPressFlag, RE: RawEl> IntoIterator for Button<LabelFlag, OnPressFlag, RE> {
// ...
}
impl<LabelFlag, OnPressFlag, RE: RawEl> RawElWrapper for Button<LabelFlag, OnPressFlag, RE> {
type RawEl = RE;
fn update_raw_el(mut self, updater: impl FnOnce(Self::RawEl) -> Self::RawEl) -> Self {
self.raw_el = updater(self.raw_el);
self
}
}
- The element has to implement the trait
Element
. - It's strongly recommended to implement
RawElWrapper
andIntoIterator
to allow users to customize the element and use its abilities and to use some Zoon helpers. RawHtmlEl::style
automatically adds vendor prefixes for CSS property names and values where required. E.g."user-select"
will be replaced with"-webkit-user-select"
on Safari and browsers on iOS.make_flags!
,run_once
andglobal_styles
will be explained later.
Abilities
:
// ------ ------
// Abilities
// ------ ------
impl<LabelFlag, OnPressFlag, RE: RawEl> Styleable<'_> for Button<LabelFlag, OnPressFlag, RE> {}
impl<LabelFlag, OnPressFlag, RE: RawEl> KeyboardEventAware for Button<LabelFlag, OnPressFlag, RE> {}
// ...
Abilities are traits where all functions have a default implementation. Only elements should implement abilities. Example: When you implement Styleable
for your element, 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.rs
. The Styleable
ability implementation:
pub trait Styleable<'a>: RawElWrapper + Sized {
fn s(self, style: impl Style<'a>) -> Self {
self.update_raw_el(|raw_el| {
raw_el.style_group(style.merge_with_group(StyleGroup::default()))
})
}
}
Attributes
:
// ------ ------
// Attributes
// ------ ------
impl<'a, LabelFlag, OnPressFlag, RE: RawEl> Button<LabelFlag, OnPressFlag, RE> {
pub fn label(
mut self,
label: impl IntoElement<'a> + 'a,
) -> Button<LabelFlagSet, OnPressFlag, RE>
where
LabelFlag: FlagNotSet,
{
self.raw_el = self.raw_el.child(label);
self.into_type()
}
// ...
Note: Attribute implementations look a bit verbose because of long types and generics but it's a trade-off for the user comfort and safety. Also we will improve it as soon as stable Rust has better support for const generics and other things.
--
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`
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
andafter_remove
on all elements implementing the abilityHookableLifecycle
. -
Hooks allow you to access the DOM node directly through their function argument. The code calling native DOM APIs may become 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 API will be probably revisited once Wasm fully supports multithreading. One idea is to introduce lightweight actors as an alternative to
#[static_ref]
functions.
- Note: The API will be probably revisited once Wasm fully supports multithreading. One idea is to introduce lightweight actors as an alternative to
The HookableLifecycle
ability / trait:
pub trait HookableLifecycle: RawElWrapper + Sized {
fn after_insert(
self,
handler: impl FnOnce(<Self::RawEl as RawEl>::DomElement) + 'static,
) -> Self {
self.update_raw_el(|raw_el| raw_el.after_insert(handler))
}
fn after_remove(
self,
handler: impl FnOnce(<Self::RawEl as RawEl>::DomElement) + 'static,
) -> Self {
self.update_raw_el(|raw_el| raw_el.after_remove(handler))
}
}
and its implementation for Canvas
:
impl<WidthFlag, HeightFlag, RE: RawEl> HookableLifecycle for Canvas<WidthFlag, HeightFlag, RE> {}
DomElement
is one of theweb_sys
/ DOM elements. It's web_sys::HtmlCanvasElement in this case (as defined incrates/zoon/src/element/canvas.rs
).- NOTE:
HookableLifecycle
is now automatically implemented for all items implementingRawElWrapper
.
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(hsluv!(0, 0, 32.7)))
.s(Background::new().color(hsluv!(0, 0, 0, 0.3)))
.s(Shadows::new([Shadow::new()
.inner()
.y(-2)
.blur(1)
.color(hsluv!(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(hsluv!(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 Zoon element APIs. Padding and declarative layout (columns, rows, nearby elements, spacing, etc.) are more natural alternatives.
A Paragraph element part:
impl ChoosableTag for Paragraph<EmptyFlagSet, RawHtmlEl<web_sys::HtmlElement>> {
fn with_tag(tag: Tag) -> Self {
run_once!(|| {
global_styles()
.style_group(StyleGroup::new(".paragraph > *").style_important("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(tag.as_str()).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
andstyle_group_droppable
.- Global styles are stored in one dedicated
<style>
element appended to the<head>
. StyleGroup
selector and styles are validated in the runtime - invalid ones triggerpanic!
.- Vendor prefixes are automatically attached to CSS property names and values when needed.
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 eitherRawHtmlEl
orRawSvgEl
with many useful methods includingstyle
andstyle_group
.StyleGroup
selector is prefixed by a unique element class id - e.g.._13:hover .icon
.StyleGroup
s are stored among the global styles and dropped when the associated element is removed from the DOM.
Note: Zoon Animation API is in development but you can use Transitions
as a less powerful alternative (Transitions
are Rust wrappers for CSS transition, explained below).
fn sidebar() -> impl Element {
Column::new()
.s(Width::exact_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 ofTransition
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.
UPDATE:
You can use:
hsluv!(0, 0, 32.7)
"#edc8f5"
"Black"
color!("oklch(0.6 0.182 350.53 / .7")
oklch().l(0.6).c(0.182).h(350.53).a(1)
and other color formats while calling methods like Font::new().color(..)
. See /crates/zoon/src/css_color/into_color.rs
for more info.
Definitions wrapped in the color!
macro are heavily recommended because they are parsed during compilation.
.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([
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:
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.
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 - 360s
; saturation ; 0 - 100l
; lightness ; 0 - 100a
; alpha channel / opacity ; 0 (transparent) - 100 (opaque)
The macro hsluv!
creates an HSLuv
instance and all color components are checked during compilation.
Notes/Update: There is a new color system - OKLCH. It's similar to HSLuv
but it should be a bit better. Also a color palette generator together with system-agnostic API could be introduced into Zoon in the nearer future. See the related issue.
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.)
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.
Have you ever ever tried to align an element with a text block? An example:
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:
--
- Related blog post: Font size is useless; let’s fix it by Nikita Prokopov
- https://caniuse.com/sr_leading-trim-text-edge
- Inspirations for a future MZ polyfill:
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
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 theMutableViewport
ability (e.g.Row
,Column
orEl
). -
Notes:
Viewport
'sx
andy
may be negative while the user is scrolling on the phone.x
andy
are automatically clamped. So you can write things likeviewport_y().set(i32::MAX)
and don't be afraid the viewport will be moved outside of the scene.
UpMsg
are sent from Zoon to Moon.DownMsg
in the opposite direction.UpMsg
could be buffered when the Moon server is offline. AndDownMsg
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 newCorId
. You can also send an auth token together with theUpMsg
. - A session id is automatically generated when the
Connection
is created. Then it's sent with eachUpMsg
. You can use it to simulate standard request-response mechanism. Task::start
orTask::start_droppable
spawn the givenFuture
. (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);
jump_to_bottom();
})
// .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),
}
});
}
- Could be used as a timeout or stopwatch (to set an interval between callback calls).
Timer
has methodsnew
,new_immediate
,once
andsleep
(async).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(Gap::both(20))
.item("2s Async Sleep")
.item_signal(asleep_signal.map_bool(
|| El::new().child("zZZ...").left_either(),
move || start_button(sleep.clone()).right_either(),
))
}
- You just need the struct
Router
and theroute
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 likef64
orString
. - 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
andRouter::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| async { 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
orshift
while clicking. - The user hasn't clicked by the primary button (left button for right-handed).
static STORAGE_KEY: &str = "todomvc-zoon";
#[derive(Deserialize, Serialize)]
#[serde(crate = "serde")]
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 / serde-lite's
Deserialize
andSerialize
can be stored in the local or session storage. #[serde(crate = "serde")]
is needed because Rust macros often doesn't work as expected when reimported (fromzoon
in this case).- See
examples/todomvc
orcrates/zoon/src/web_storage.rs
for more info.
-
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.) [Not implemented yet]
-
You can configure the default page title, The Open Graph Metadata and other things in the Moon app.
async fn frontend() -> Frontend { Frontend::new().title("Chat example").append_to_head( " <style> html { background-color: black; } </style>", ) }
-
"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 good SEO 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 has incorrect size on the website.
- I don't want to be afraid to refactor styles.
- I don't want to write backend code instead of the frontend code just because the frontend is 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
orundefined
? - 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.
-
-
"How are we taking care of animations?" (by None on chat)
- The API for advanced animations is in development.
- Inspiration:
- react-spring
- Framer Motion
- React UseGesture
- elm-animator
- "svelte has really good set of animation examples in their tutorial site. Incase it can help somehow [section 9 -11]" (by Ruman on chat)
- rust-dominator/examples/animation
-
"Hey Martin, what about Seed?"
- Zoon and Seed have very different features and goals. I no longer actively maintain Seed.
-
"How do I get a standalone html+wasm output? I previously used Yew + Trunk." (by
@Noir
on chat)mzoon build --release --frontend-dist netlify