Giter Site home page Giter Site logo

sauron's Introduction

Maintenance

sauron

Latest Version Build Status MIT licensed

sauron

Sauron is a versatile web framework and library for building client-side and/or server-side web applications with strong focus on ergonomics, simplicity and elegance. This allows you to write least amount of code possible, and focus more on the business logic rather than the inner details of the framework.

Sauron is inspired by elm-lang and is following The Elm Architecture.

Features

  • server-side rendering
  • static site generation
  • progressive rendering
  • web components / custom-element
  • html syntax for writing views
  • elegant macro to write styles
  • batteries included

Devoid of unnecessary framework complexities

  • no framework specific cli needed
  • no template specific language as everything is in rust.
    • Model and update function is all in rust.
    • view? in rust
    • events handling? rust
    • styling? believe it or not: rust

In a sauron application, there is only the model, view and update. The model is your application state. The view describes how to present the model to the user. The update function describes how to update the model, this uses message which contains the data needed for updating the model.

Counter example

In your src/lib.rs

use sauron::{
    html::text, html::units::px, jss, node, wasm_bindgen, Application, Cmd, Node, Program,
};

enum Msg {
    Increment,
    Decrement,
    Reset,
}

struct App {
    count: i32,
}

impl App {
    fn new() -> Self {
        App { count: 0 }
    }
}

impl Application for App {

    type MSG = Msg;

    fn view(&self) -> Node<Msg> {
        node! {
            <main>
                <input type="button"
                    value="+"
                    on_click=|_| {
                        Msg::Increment
                    }
                />
                <button class="count" on_click=|_|{Msg::Reset} >{text(self.count)}</button>
                <input type="button"
                    value="-"
                    on_click=|_| {
                        Msg::Decrement
                    }
                />
            </main>
        }
    }

    fn update(&mut self, msg: Msg) -> Cmd<Msg> {
        match msg {
            Msg::Increment => self.count += 1,
            Msg::Decrement => self.count -= 1,
            Msg::Reset => self.count = 0,
        }
        Cmd::none()
    }

    fn stylesheet() -> Vec<String> {
        vec![jss! {
            "body":{
                font_family: "verdana, arial, monospace",
            },

            "main":{
                width: px(30),
                height: px(100),
                margin: "auto",
                text_align: "center",
            },

            "input, .count":{
                font_size: px(40),
                padding: px(30),
                margin: px(30),
            }
        }]
    }
}

#[wasm_bindgen(start)]
pub fn start() {
    Program::mount_to_body(App::new());
}

index.html

<!doctype html>
<html>
  <head>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
    <title>Counter</title>
    <script type=module>
        import init from './pkg/counter.js';
        await init().catch(console.error);
    </script>
  </head>
  <body>
  </body>
</html>

In Cargo.toml, specify the crate-type to be cdylib

[package]
name = "counter"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
sauron = "0.61.0"

Prerequisite:

cargo install wasm-pack
cargo install basic-http-server

Build using

wasm-pack build --target web --release

Serve using

basic-http-server -a 0.0.0.0:4000

Then navigate to http://localhost:4000

Head over to the getting-started.md for the full tutorial.

For more details on the commands to build and serve, look on examples on this repo, each has scripts on how to build and run them.

Demo examples

  • todomvc The todomvc example
  • data-viewer - A resizable spreadsheet CSV data viewer
  • svg-clock - A clock drawn using SVG and window tick event.
  • ultron code-editor - A web-base text-editor with syntax highlighting
  • hackernews-sauron - A hackernews clone showcasing the feature of sauron to write web applications that can work with or without javascript.

License: MIT

sauron's People

Contributors

aschampion avatar flosse avatar gbj avatar ivanceras avatar jfmengels avatar kdheepak avatar metatoaster avatar nootr avatar philip-peterson avatar stoically avatar udoprog avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sauron's Issues

Button input `onclick` not updating as expected in `view`

Disclaimer: I am very new to Rust, so it is quite likely that the behaviour that I'm describing is totally expected and I'm just getting stumped by my lack of clear understanding of the language.

I'm seeing unexpected behaviour in how a button input onclick handler functions. The closure is supposed to change during each rerender (i.e. view invocation), based on application state. However, the observed behaviour seems to indicate that the onclick handler that is first set on the input is always used. This is despite the onclick being update multiple times in view. In the same update, the input value is also updated, and this is correctly reflected.

I made a small app, based off of the minimal Sauron example, which illustrates this: https://github.com/gesterhuizen/sauron-onclick

The README from that project is duplicated below:

Steps to reproduce

  1. Clone this repo: https://github.com/gesterhuizen/sauron-onclick
  2. Build it:
. ./bootstrap.sh
make build server
  1. Open the app in the browser: http://localhost:4001/
  2. Click the button twice.

Expected

  1. After two button clicks, the VIEW_COUNT on the button and in the text field should be 3. This is expected because the button value and text field reflects the value that VIEW_COUNT held at the time that the view was rendered.
  2. After two button clicks, the number field should be 3. This is expected because, when the button is clicked, the number field is updated with the value that VIEW_COUNT held at the time that the view was rendered. We assign a new onclick closure each time the view is rendered (https://github.com/gesterhuizen/sauron-onclick/blob/master/src/lib.rs#L40):
    fn view(&self) -> Node<Msg> {
        VIEW_COUNT.fetch_add(1, Ordering::SeqCst);
        let vc = VIEW_COUNT.load(Ordering::SeqCst);

...
                    [input(
                        [
                            r#type("button"),
                            value(format!("VIEW_COUNT: {}", vc)),
                            onclick(move |_| {
                                sauron::log(format!("Button is clicked (VIEW_COUNT = {})", vc));
                                Msg::Click(vc)
                            }),
                        ],
                        [],
                    )],

Actual

  1. Expected: After two button clicks, VIEW_COUNT on the button and in the text field is 3 (as expected).
  2. Unexpected: After two button clicks, the number field displays 1 (and stays in this state, no matter how many times the button is clicked). This is unexpected. It seems like first onclick handler that was set (i.e. when VIEW_COUNT held the value 1) is used even after multiple view rerenders.

Community

Other Rust frontend frameworks like Yew and Seed have Discord channels for troubleshooting and community support. Are there any channels like this for Sauron, or could there be?

Updates to attributes of parent of node with "key" are ignored

It seems that the presence of key on a child node confuses the diff algorithm when updates are made to attributes of the parent. In the following example, clicking on the div should flip the color between red and blue but it instead stays blue. One can see that the class in the DOM does not change even though the value of self.x does change. Removing key from the child node fixes the problem.

use sauron::{jss, prelude::*};

macro_rules! log {
  ( $( $t:tt )* ) => {
    web_sys::console::log_1(&format!( $( $t )* ).into())
  }
}

type Msg = ();

pub struct App {
  x: bool,
}

impl App {
  pub fn new() -> Self {
    App { x: false }
  }
}

impl Application<Msg> for App {
  fn update(&mut self, _msg: Msg) -> Cmd<Self, Msg> {
    self.x = !self.x;
    Cmd::none()
  }

  fn view(&self) -> Node<Msg> {
    log!("x: {}", self.x);
    div(
      [class(format!("{}", self.x)), on_click(|_| ())],
      [span([key("0")], [text("abc")])],
    )
  }

  fn style(&self) -> String {
    jss! {
      ".true": {
        color: "red",
      },
      ".false": {
        color: "blue",
      },
    }
  }
}

Examples don't rub in Safari because wasm_bindgen is undefined

Running one of the examples in Safari leads to an immediate ReferenceError: Can't find variable: wasm_bindgen in the console. If you then immediately type wasm_bindgen into the console REPL, it's defined. You can then copy and paste the loading code wasm_bindgen('pkg/futuristic_ui_bg.wasm').catch(console.error); and the app runs perfectly.

My guess is that Safari is async loading the first script (which defines wasm_bindgen) and running the second script (which calls it) before the first one is parsed.

The solution may be to do a dynamic import(...).then(...) chain.

todomvc has inverted completed check input

sauron's todomvc implementation uses a checked completion toggle when the todo is not complete and vice versa.

I found this while writing a new benchmarking suite that confirms items are marked complete. sauron is the only one I've found with this inverted.

BTW apart from this little issue (which has nothing to do with performance) sauron performs really well, good job!

Correctly set value of HtmlOptionElement

At the moment, <option value="..."> does not work as I'd expect. (Sometimes it accidentally works correctly, because when there is no value field set, the browser defaults to using the innerText of the option.)

The value fields in the custom-elements-macro-syntax example, for example, are actually doing nothing; the values end up being correct because the text content of the option is identical with them, but if you change those texts (for example to "Serbian," "English (UK)" and "English (US)" you'll notice that those new values are used as the value, and the value you give is ignored.

I think you already have this as a to-do (per your comment here). There's an easy fix for HtmlOptionElement, but perhaps you'd want to add all the other elements listed there at the same time, and/or a fallback to something like else { element.set_attribute("value", value); }?

Want an example of server-side rendering?

I was messing around this afternoon and managed to put together a working setup to render my app in Warp including pre-populating it with API data, and then passing app state in on the client-side. I’d be happy to share a repo with a minimal reproduction if you want it, to use as an example.

I have loved working with Sauron. Thanks for creating it!

Panicking when using `node! {}` or `fragment([])`

Currently, Sauron is panicking quite a bit, especcially when toggling between children which exist, and children which do not.

Below is a minimal reproducible example.

use sauron::prelude::*;
use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn start() {
  console_error_panic_hook::set_once();

  let p = Program::mount_to_body(App::default());

  p.dispatch_with_delay(Msg::ToggleShow, 1000);
  p.dispatch_with_delay(Msg::ToggleShow, 3000);
}

#[derive(Default)]
struct App {
  show: bool,
}

impl Application<Msg> for App {
  fn update(&mut self, msg: Msg) -> Cmd<Self, Msg>
  where
    Self: Sized + 'static,
  {
    match msg {
      Msg::ToggleShow => self.show = !self.show,
    }

    Cmd::none()
  }

  fn view(&self) -> Node<Msg> {
    if self.show {
      node! { <h1>"Now you see me..."</h1> }
    } else {
      node! {}
    }
  }

  fn style(&self) -> String {
    Default::default()
  }
}

enum Msg {
  ToggleShow,
}

Nodte that the above also panics when substituting node! {} for fragment([]). The panic that is occurring in this case is:

panicked at 'Node list must have already been unrolled'

Is there official comparison of Yew and Sauron

They both have similar goals and both are inspired by Elm. So what's the difference?

I think Sauron's README should mention Yew explicitly and include some table outlining similarities and differences.

Component initialisation

Component init method doesn’t seems to be triggered…Is this a bug or should we need to call manually..If so where would be the best place to call manually?

Support for multiple nodes using node! macro

Issue

                ul(vec![class("todo-list")], {
                    //TODO: node! is limited to only 1 node return in each `{}` expression
                    //TODO: can not convert this part to node_macro
                    self.entries
                        .iter()
                        .filter(|entry| match self.visibility {
                            Visibility::All => true,
                            Visibility::Active => !entry.completed,
                            Visibility::Completed => entry.completed,
                        })
                        .map(|entry| self.view_entry(entry))
                        .collect::<Vec<Node<Msg>>>()
                }),

node! macro can not contain expression which returns multiple nodes expression

Benchmarks are misleading and need to be updated.

I'm sure that Sauron is fast, but I think the benchmarks are highly misleading. For example, the Vue version (0.10) used in "benchmark 2" is from 2014. I think it's very important to not misrepresent other frameworks if performance is one of the main selling points of Sauron.

JSX similar Syntax ?

This project is nice, Besides Syntax.
Do you have a plan for a new Syntax (like JSX) ?

Closures attached to similar nodes misbehave

Related to #3, for performance reasons sauron will not recreate a node when it determines via diffing that it can update it instead. In particular it cannot detect whether two closures are equal or not for obvious reasons. However, this leads to strange behavior where event handlers end up on the wrong nodes since the existing nodes were reused instead of being recreated. For an example see https://github.com/cryslith/sauron-reorder. (Use the browser console to see which messages are delivered.)

One way to handle this could be to use a special attribute like the suggested data-disambiguate to allow specifying to the node diff algorithm that certain nodes should be recreated when data changes. That is, if data-disambiguate differs between the old version and the new version of a node, then the node must be recreated rather than reused.

IE11 support

Hello, I saw reddit post that announce production ready version of sauron framework.
On production and real world business applications support of IE11 is unfortunately must have.
Is sauron supports IE11 or have this feature in plans?

nodes inserted at position 0 have broken events

the new closures created at view() don't get passed to the corresponding elements
instead, every new node acts like the original first node, or panic on keyed nodes

i made an example code for this problem, also a video preview to illustrate it better

use {
    sauron::{
        html::text,
        prelude::*,
        Cmd, Application, Node, Program,
    },
};

struct Container {
    nodes: Vec<u32>,
    last: String
}
enum Msg {
    InsertNode,
    AddNode,
    ClickNode(u32)
}

impl Application<Msg> for Container {
    fn view(&self) -> Node<Msg> {
        let buttons = self.nodes.iter()
            .map(|&x|button([on_click(move |_|Msg::ClickNode(x))],[text(x)])).collect::<Vec<_>>();
        div([],[
            text(&self.last),
            button([on_click(|_|Msg::InsertNode)],[text("add item (broken)")]),
            button([on_click(|_|Msg::AddNode)],[text("add item (working)")]),
            div([id("buttons")],buttons)
        ])
    }
    fn update(&mut self, msg: Msg) -> Cmd<Self, Msg> {
        match msg {
            Msg::InsertNode => self.nodes.insert(0,self.nodes.len() as u32),
            Msg::AddNode => self.nodes.push(self.nodes.len() as u32),
            Msg::ClickNode(pos) => self.last = pos.to_string()
        }
        Cmd::none()
    }
}

#[wasm_bindgen(start)]
pub fn main() {
    Program::mount_to_body(
        Container{
            nodes: vec![], 
            last: String::default()
        }
    );
}

video preview
only tested this for position 0 so im not sure about the rest

Error: Window is not defined [Cloudflare Worker]

Getting this error after publishing your first example to a Cloudflare Worker:

[2021-01-01 17:07:25] GET example.com/ HTTP/1.1 500 Internal Server Error
Uncaught (in promise)
ReferenceError: Window is not defined
at imports.wbg.__wbg_instanceof_Window_747b56d25bab9510 (worker.js:314:46)
at :wasm-function[554]:0x2300e
at :wasm-function[464]:0x21bf1
at :wasm-function[510]:0x22827
at :wasm-function[432]:0x21143
at :wasm-function[433]:0x21199
at :wasm-function[448]:0x216bd
at :wasm-function[421]:0x20dad
at main (:wasm-function[701]:0x23dff)
at init (worker.js:531:10)
Uncaught (in response)
ReferenceError: Window is not defined

README.md's Cargo.toml missing wasm-bindgen dependency?

I followed the examples in README.md, but wasm-pack build --target no-modules fails:

Compiling saurontest v0.1.0 (/home/ran/src/rust/sauron/saurontest)
error: cannot find attribute `wasm_bindgen` in this scope
 --> src/lib.rs:21:3
    |
    21 | #[wasm_bindgen(start)]
       |   }}}}}}}}}}}}

       error: aborting due to previous error

This can be fixed by:

  1. Adding wasm-bindgen = "0.2" to dependencies
  2. Adding use wasm_bindgen::prelude::*; or similar to the src/lib.rs file

As a beginner wasm developer, I think it would be nice if the readme would explicitly say that the main code should go in src/lib.rs and not src/main.rs (Currently the index.html contents are prefixed with index.html, the lib.rs code could also be prefixed with src/lib.rs) :)

And perhaps mentioning Sauron requiring nightly.

node!(<h1>"example"</h1>) syntax does not work on released version 0.32.4

Compiling the minimal macro syntax example fails if Cargo.toml has sauron = "0.32.4", but succeeds when using the relative path (sauron = { path = "../../"}) or the tagged version directly from Git (sauron = { git = "https://github.com/ivanceras/sauron", tag = "0.32.4" }).

Build error:

node! {
    <main>
        <h1>"Minimal example"</h1>
        <div class="some-class" id="some-id" {attr("data-id", 1)}>

    </main>
}
^ expected struct `sauron::Text`, found struct `std::string::String`

I narrowed this down to the string literal in the h1 tag: node!(<h1>"example"</h1>).
Using the full node!(<h1>{text("example")}</h1>) works with no problem.

I haven't been able to find a root cause, but I think it has to do with the latest sauron-node-macro version (0.32.0) being released before the fixes in e27c30b. If this is true then releasing a new version with bumped versions should resolve the problem.

Should we use camelCase or snake_case for non-ident attributes?

Due to the dash(-) in between the attribute name, this is not a valid function in rust.
This would have been easily decided, if it wasn't for javascript way of accessing DOM element attributes which uses camelCase for attributes containing dash such as zIndex for z-index

src/svg/attributes

declare_attributes! {

    #[allow(non_snake_case)]
    strokeWidth => "stroke-width";

Doing both could also be used, but these will lead to fragmentation of code style in rust projects.

Thoughts?

input disabled attribute had different behavior that I was expecting in node macro

I was playing around with some of the examples and was trying to use the macro opposed to the core library functions.

When attempting to update the buttons in the fetch-data example I noticed that the disabled attribute was not working as I expected.

<input 
  class="prev_page" 
  type="button" 
  disabled={self.page <= 1}
  value="<< Prev Page"
    on_click=|_| {
    trace!("button is clicked");
    Msg::PrevPage
  }
/>

After expanding and digging into the code I found that we are just passing the attribute and value directly though. Since the HTML behavior is only check if the attribute is present this makes sense.

It would be nice to see the macro have the same behavior as disabled() from the core library.

Created pull request #43

todo example: bugs and not comply to the standard template

Testing the demo at: https://ivanceras.github.io/todomvc/

  • Bugs:
  1. Create 2 todo items
  2. Double click the first item
  3. Double click the second item => The first item should be un-edit
  4. Click back to the first item,
  5. Click the second item => error unreachable in console
  • Not comply to the standard (just saw on demo, I did not read the code)
  1. Not implement persistent storage
  2. Number of "item left" not update when check/uncheck an item
  3. "Clear complete" not hidden when there is no item checked

I saw you update sauron on schell/todo-mvc-bench#44 and I think frameworks should implement standard features to compare.

windows-tab-rows demo lags

Hi - if I type quickly into a single field in the windows-tab-rows demo then it immediately lags:

image

Is this a restriction of the JS->WASM->JS bridge, or is this unexpected?

Thanks!

How does sauron compare to seed?

Hey there, nice job with the framework!

Currently I am learning Rust by doing a small API for my own hobby project. Once finished I was planning to try to write some frontend with Rust as well. I do have some prior experience with Elm and React stacks and writing production apps with both. So using similar architecture but in Rust feels natural.

While looking around I've discovered yew and seed, and was thinking to use seed for the job. However now I found out your framework and would love to know more about it. How would you compare it to seed personally?

How to get a node ref?

Hello! Just stopping by to check out this framework, hopefully for a large project. I wanted to know if there's a way of getting a reference to a raw web_sys::Node in the browser? I need it in order to interface with Tippy.js.

Adding items to a list causes unexpected duplication, and more panics

The following example adds 4 items, but at the end, we end up with 7 items.

use sauron::prelude::*;
use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn start() {
  console_error_panic_hook::set_once();

  let p = Program::mount_to_body(App::default());

  p.dispatch_with_delay(Msg::AddItem, 1000);
  p.dispatch_with_delay(Msg::AddItem, 2000);
  p.dispatch_with_delay(Msg::AddItem, 3000);
  p.dispatch_with_delay(Msg::AddItem, 4000);
}

#[derive(Default)]
struct App {
  items: Vec<Node<Msg>>,
}

impl Application<Msg> for App {
  fn update(&mut self, msg: Msg) -> Cmd<Self, Msg>
  where
    Self: Sized + 'static,
  {
    match msg {
      Msg::AddItem => self
        .items
        .push(node! { <div>{text(self.items.len() + 1)}</div> }),
    }

    Cmd::none()
  }

  fn view(&self) -> Node<Msg> {
    node! {
      <div>
        {fragment(self.items.iter().cloned().chain([node! {<span />}]))}
      </div>
    }
  }

  fn style(&self) -> String {
    Default::default()
  }
}

enum Msg {
  AddItem,
}

Now, for the two panics that are different than the ones discussed in #72, we just need to modify the view method to the following:

    node! {
      <div>
        {fragment(self.items.iter().cloned())}
      </div>
    }

In other words, starting from an empty fragment. The panic is:

panicked at 'internal error: entered unreachable code: Getting here means we didn't find the element of next node that we are supposed to patch, patch_path: TreePath { path: [0] }',

And finally, removing the surrounding <div /> gives us:

    fragment(self.items.iter().cloned())

The above codes results in:

panicked at 'must replace node: JsValue(TypeError: getObject(...).replaceWith is not a function

I'm going to be diving into the source and trying to find what's going on.

Interval / timeout / any way to execute something in background

I want to run some code / send message periodically, but I can't find a way to do this. Cmd::new() executes everything in the same thread, so I can't just write a loop, and set_interval from web-sys requires function to be static, so it's impossible to send message.

How can I achieve this?

svg tags not showing when using node! macro in the client

Svg elements is not showing when used in browser. This would be rendered correctly when used server-side.

  <svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <pattern id="Pattern" x="0" y="0" width=".25" height=".25">
        <rect x="0" y="0" width="50" height="50" fill="skyblue"/>
      </pattern>
    </defs>

    <rect fill="url(#Pattern)" stroke="black" width="200" height="200"/>
  </svg>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.