Git Product home page Git Product logo

silkenweb's Introduction

Silkenweb

tests crates.io Documentation MIT/Apache-2 licensed Discord

A library for building reactive web apps.

Features

Example: A Simple Counter

use futures_signals::signal::{Mutable, SignalExt};
use silkenweb::{elements::html::*, prelude::*, value::Sig};

fn main() {
    let count = Mutable::new(0);
    let count_text = count.signal().map(|i| format!("{i}"));
    let inc = move |_, _| {
        count.replace_with(|i| *i + 1);
    };

    let app = div()
        .child(button().on_click(inc).text("+"))
        .child(p().text(Sig(count_text)));

    mount("app", app);
}

Quick Start

rustup target add wasm32-unknown-unknown
cargo install --locked trunk
cd examples/counter
trunk serve --open

Book

The Silkenweb book provides a high level introduction to Silkenweb's main concepts.

Comparison With Other Frameworks

Sycamore and Leptos are 2 other signals based Rust frameworks. They are evolving quickly at the time of writing this comparison, as is Silkenweb. Also bear in mind I'm not that familiar with Sycamore or Leptos.

  • Silkenweb uses plain, non-macro Rust as much as possible, and a lot of effort has been put into making this ergonomic. Sycamore and Leptos primarily use a macro DSL to define components. I believe Sycamore also has a builder API.
  • Ecosystem: Leptos and Sycamore have cargo-leptos and Perseus respectively, whereas Silkenweb doesn't have an ecosystem at this point.
  • CSS Scoping: Silkenweb supports CSS Modules. See the CSS modules example. CSS Modules support is integrated with SSR and Hydration so that only the CSS required to render the initial page is sent from the server, then progressively enhanced as required on the client. I'm not aware of any CSS scoping support in Leptos or Sycamore.
  • Server Functions: Leptos supports server functions to seamlessly divide your app between client and server. Silkenweb doesn't directly support anything like this, but similar functionality is provided with Arpy.
  • Sycamore and Leptos both go to some effort to make cloning signals into closures more ergonomic. Silkenweb provides a clone! macro to make things a little easier, but otherwise doesn't address the problem. I'm not sure what the tradeoffs are for the Sycamore/Leptos approaches. Do they make cleaning up after derived signals harder? Do they mean more complex lifetime annotations? Do contexts need to be passed around everywhere?
  • Silkenweb has support for using third party web components. I'm not sure about Sycamore or Leptos.
  • Silkenweb has support for shadow roots, including Hydration and SSR support with the experimental Declarative Shadow DOM. It also has a simple Component wrapper to manage slots. Again, I'm not sure about Leptos and Sycamore here.
  • Silkenweb doesn't use any unsafe Rust directly. Some of the underlying Crates do use unsafe, but at least you don't have to put as much trust in my coding skills!
  • All of these frameworks support:
    • Static site generation.
    • Progressive enhancement using SSR and hydration.

Design Tradeoffs

No VDOM

The use of a signals-based approach can provide better performance because it allows the compiler to know the data dependencies within your application at compile time. This allows changes to be efficiently calculated at runtime. On the other hand, with a basic VDOM based approach, the changes need to be identified at runtime.

The drawback of a signals-based approach is that the code tends to be more complicated. However, in actual implementation, VDOM-based approaches often implement mechanisms to prevent complete re-rendering of the VDOM every time a change occurs, which adds some level of complexity to code using the VDOM approach.

No Macro DSL

Using plain Rust syntax has numerous benefits, such as:

  • No need to learn a macro Domain Specific Language (DSL).
  • Improved code completion through rust-analyser.
  • Familiar documentation structure, thanks to rustdoc.
  • Code formatting with rustfmt. While macro DSLs can be formatted with rustfmt if designed with care, the syntax is limited by rustfmt's capabilities.
  • Exceptional compiler errors from rustc: Although macro DSLs can produce informative errors, a lot of work has been put into making rustc error messsages great.
  • The ability to utilize Rust's composable, well-designed abstractions.

While a macro DSL could be developed to work with Silkenweb, the syntax in Rust is already well suited for defining document structure.

Learning

Pre Built Examples

silkenweb's People

Contributors

akesson avatar dependabot[bot] avatar simon-bourne 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

silkenweb's Issues

Cached Elements

Elements should have a .cache() method that globally caches the node by call site (and key?) and uses clone_node to generate the elements. Will this work for reactive elements?

set_attribute: Taking `ToString` instead of `AsRef<str>`

Hi,

Thanks for creating SilkenWeb โ€“ so far I really enjoy the nice macro-less API!

(It's smooth as silk... ๐Ÿ˜…)

While looking at the source code, I noticed the following function:

fn set_attribute(dom_element: &dom::Element, name: impl AsRef<str>, value: impl AsRef<str>) {
    clone!(dom_element);
    let name = name.as_ref().to_string();
    let value = value.as_ref().to_string();

    queue_update(move || dom_element.set_attribute(&name, &value).unwrap());
}

Basically, you:

  1. request something that can be turned into &str
  2. convert it (potentially a String) into a &str
  3. and then you turn that &str into a String.

That doesn't seem to make sense to me โ€“ is there a reason for that?

I believe, ToString would be the right trait to request:

fn foo<T: ToString>(s: T) {
    println!("{}", s.to_string())
}

fn main() {
    foo("bar");
    foo("bar".to_string())
}

Differences to Sycamore/Leptos/etc.

@simon-bourne it is nice to see that there is another fast framework existed by Silkenweb ๐Ÿ‘
As a curious new visitor, I wonder what is the main difference with other FRP based frameworks like sycamore or leptops.
I think it would be helpful to mention this in a short approach in the README.

BTW:
I really like the "No Macro DSL" approach ๐Ÿ’ฏ

Animation

Come up with a plan for animations

Input value stops updating from signal

Once a text input field has been edited by hand (i.e. type something into the field), the displayed input value will no longer be updated from a read signal. The input value is update just fine before the edit, it is only after the edit that it stops.

Example reproduction:

fn main() {
    let message = Signal::new("".to_owned());
    let message_read = message.read();
    let message_write = message.write();

    let app = div()
        .child(input().value(message_read))
        .child(button().text("Overwrite Message").on_click(move |_,_| {
            message_write.replace(|_| "Overwrite worked!".to_owned())
        }));

    mount("app", app);
}

In the example above, the overwrite button only appears to work if you haven't already typed something into the input.

P.S. I'm a big fan. Of all the rust web frameworks I've tried, this is definitely my favorite. I hope you keep up the great work!

Optimizing DOM Tree Representation

Currently Element mirrors the real DOM tree. It only really needs to store a tree of reactive nodes and their reactive children. Is this even worth implementing?

Tauri support

Look into what's required to support Tauri. Hopefully it just needs an example.

Event types

Currently, only a few event types are handled. We need to make a complete list and handle all of them.

Server Side Microtask Queue Emulation

Currently we just use a thread local queue to emulate the browsers microtask queue on the server. This isn't ideal when calling render_now for 2 reasons:

  1. Multiple request handlers could run on the same thread at the same time, so render_now would process tasks from all requests.
  2. A request handler could run on multiple threads so render_now could miss some tasks in multi-threaded runtimes.

One possible solution is to use tokios LocalSet and spawn_local. LocalSet implements Future, so we could keep blocking on it until it can't make any more progress.

Provide a way to only respond to signal changes

Currently we don't distinguish between signal initialization and signal changes when mapping over them. TodoMVC storage needs a way to distinguish the 2 conditions. One option is to have a map alternative that's passed an initializing flag.

Timers

Investigate how to de-bounce signals in a platform (client/server) agnostic way.

  • Using tokio::time on the server?
  • Using gloo-timers on the client.
  • Provide an example.

Routing

Routing is a bit basic at the moment. Currently it just provides a URL string signal. We should provide a more structured URL with methods to access different components. The url library from servo looks like a great parser for server side apps, but would bloat client side apps too much. We need a lightweight wrapper around the browser's URL parser on the client side and a wrapper around the url library on the server side.

Any benchmark comparison

I am new to silkenweb, excited with the non-vdom idea.

Are there any simple and quick performance benchmark compared to other rust-wasm frameworks, such as yew, seed, sycamore?

thank you

Server Side Routing

There should be an implementation of routing on the server. Url should be wrapped in our own struct with a client and server side implementation provided via a trait. The implementation should be selected at compile time with cfg attrs.

Dom child element lists

What should the primitive operations on child lists be? We should be able to implement filtering, sorting, mapping etc in terms of the primitives.

ReadSignal Products

(signal0, signal1).map(...) for larger tuples. Is it easier if we have signal flattening (#4)

0.7 build issue

I created a super-simple project based on "silkenweb-example-counter" in the examples/counter directory, but it doesn't compile due to:

โฏ cargo build
   Compiling silkenweb-macros v0.7.0
   Compiling silkenweb-signals-ext v0.7.0
error[E0603]: trait `CustomToken` is private
  --> /Users/hakesson/.cargo/registry/src/index.crates.io-6f17d22bba15001f/silkenweb-macros-0.7.0/src/parse.rs:10:26
   |
10 |     token::{self, Comma, CustomToken},
   |                          ^^^^^^^^^^^ private trait
   |
note: the trait `CustomToken` is defined here
  --> /Users/hakesson/.cargo/registry/src/index.crates.io-6f17d22bba15001f/syn-2.0.39/src/token.rs:92:16
   |
92 | pub(crate) use self::private::CustomToken;
   |                ^^^^^^^^^^^^^^^^^^^^^^^^^^

Investigate HTMX integration

htmx would be an ideal companion for SSR. Investigate how easy it would be to write the server side component using Silkenweb.

Implementation ideas:

  • Integrate directly with a server framework (initially just Axum). Arpy isn't suitable because:
    • it uses it's own RPC protocol.
    • there's no HTML encoder for Serde.
    • htmx handles errors using HTTP status codes.
  • Implement Axum IntoResponse for elements, wrapped in a HtmxResponse newtype.
  • Implement Axum FromRequest for all Serde DeserializeOwned wrapped in a HtmxRequest newtype.
  • Error handling. We should be able to return a Result<impl IntoResponse, impl IntoResponse> from our Axum handler.
  • Produce some examples.

Flattening Signals

Can we implement a flatten method on nested ReadSignals?

    impl<T> ReadSignal<ReadSignal<T>> {
        fn flatten(&self) -> ReadSignal<T>;
    }

Log panics to console

Add a log_panics function that defers to console_error_panic_hook::set_once(). Examples should call this. In debug mode, mount and hydrate should also call it.

Generalize Accumulators

Accumulators should be generalized in terms of actions and inverses. This enables And and Or accumulators which keep a running count of how many items are true.

Code re-organization to improve build times

Current dependency graph

graph TD;
  silkenweb[<b>silkenweb</b>];
  silkenweb --> base;
  silkenweb --> macros;
  silkenweb --> signals-ext;
  macros --base::css::Source--> base;
  test --base::document--> base;
  test --> silkenweb;

Proposal

  1. move base::document and base::window to the silkenweb crate.
  2. create a new styling crate with all the CSS-related stuff.
  3. create a new styling-macros crate with CSS-related macros.
  4. remove base and move its base::css code to the styling crate.
  5. create a new runtime crate that contains the runtime (task:: etc) code.
  6. remove the signals-ext and move it's code into runtime.
graph TD;
  silkenweb[<b>silkenweb</b>];
  styling --> silkenweb;
  styling --> styling-macros;
  silkenweb --> macros;
  silkenweb --> runtime;
  test --> silkenweb;

The following is achieved with this:

  • the runtime crate can be used by business logic so that testing of it can be done without a full silkenweb dependency. This guarantees that the business logic doesn't mix in any UI-related things. I.e. the app-state stuff can be added to the runtime crate.
  • styling is separated into its own crate which depends on and extends silkenweb. Like this the styling is optional.

Let me know what you think. I'll create a PR for it.

Note

I'm working on reducing the silkenweb compilation times and the styling (lightningcss, grass, cssparser) is responsible for 40% of it. For my own project, I will use a simple approach of having a parallel css file to each component and at build just appending all these css files into a single main one.

In my_component.rs:

fn my_component<D: Dom>() -> Button<D> {
    button().class("my-component")
}

In my_component.css:

.my-component {
   component specific standard css
}

Sig value not showing

I might be doing something wrong here. But to reproduce this, we can take counter-list as an example.

If we change the main function to the following:

fn main() {
    let list = Rc::new(MutableVec::new());
    list.lock_mut().push_cloned(Counter);
    mount(
        "app",
        div()
            .text("How many counters would you like?")
            // .child(
            //     div()
            //         .child(pop_button(list.clone()))
            //         .text(Sig(list.signal_vec().len().map(|len| format!("{len}"))))
            //         .child(push_button(list.clone())),
            // )
            .child(hr())
            .child(div().children_signal(list.signal_vec().map(|_| define_counter()))),
    );
}

I would assume there should be a counter that is defined and initialized with 0. but it ends up only showing two buttons and the counter value between is missing.

Thank you!

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.