rustwasm / gloo Goto Github PK
View Code? Open in Web Editor NEWA modular toolkit for building fast, reliable Web applications and libraries with Rust and WASM
Home Page: https://gloo-rs.web.app
License: Apache License 2.0
A modular toolkit for building fast, reliable Web applications and libraries with Rust and WASM
Home Page: https://gloo-rs.web.app
License: Apache License 2.0
This RFC tries to establish an API for IndexedDB
gloo
doesn't have an API for IndexedDB
, a fundamental technology for asynchronous persistent storage.
Based on the discussions that took place in #3 and the now recently closed #59, I propose an "rust-ish" 1x1 implementation of the idb project that has a better alignment with the goals of the gloo
team.
Two APIs are going to provided, a more complex and feature-complete IndexedDbFull
and a simpler, quicker and easier IndexedDb
that builds on top of IndexedDbFull
. This way, the user has the option to choose whatever is best for his use-case.
IndexedDbFull
and relatablesimpl IndexedDbFull {
pub fn delete_db(&self, name: &str) {}
pub fn open(&mut self, database_name: &str, version: f64) -> IndexedDbFullVersionManager {}
pub fn store(&self, name: &str) -> IndexedDbFullStorageManager;
}
impl IndexedDbFullStoreManager {
pub fn all_keys(&self) {}
pub fn clear(&self) { }
pub fn delete(&self, key: &str) {}
pub fn get(&self, key: &str) {}
pub fn set(&self, key: &str, value: &str) {}
pub fn transaction(&self, mode: &str) {}
}
impl IndexedDbFullVersionManager {
pub fn upgrade(&self, cb: F) {}
pub fn blocked(&self, cb: F) {}
pub fn blocking(&self, cb: F) {}
}
let idb = IndexedDbFull::default();
let v = idb.open("foo-db", 1.0);
v.upgrade(|old, new, transaction| ... );
let s = idb.store("foo-store");
s.set("foo-key", "foo-value");
IndexedDb
Has a single hard-coded database, version and store.
// This is just a demonstration. `IndexedDb` and `IndexedDbFullStoreManager`
// are using the same trait definition for its methods.
impl IndexedDb {
pub fn all_keys(&self) {}
pub fn clear(&self) {}
pub fn delete(&self, key: &str) {}
pub fn get(&self, key: &str) {}
pub fn set(&self, key: &str, value: &str) {}
}
let idb = IndexedDb::default();
s.set("foo-key", "foo-value");
The two APIs are using callbacks and their Future
and Stream
interfaces will have a very similar, if not equal, definition.
The API focuses on simplicity but there are more complex projects like PouchDB
and dexie.js
.
Should IndexedDbFullStoreManager
, IndexedDbFullVersionManager
and IndexedDb
use the Fluent Interface pattern?
A tracking issue for one of the most opinionated kinds of libraries!
Can we make something like glium
for Web GL? Does it make sense to jump straight there, or should we build an incremental Rustier version of the Web GL APIs before trying to make an Opinionated(tm) library?
We can fix this TODO
:
Since the linked PR merged and there's been a wasm-bindgen release with the new API: https://docs.rs/wasm-bindgen/0.2.40/wasm_bindgen/closure/struct.Closure.html#method.once
Provide "mid-level" bindings to the XMLHttpRequest API. "mid-level" in this case would mean a callback-based API to cover the whole of XHR, mirroring web_sys::XmlHttpRequest
with more idiomatic Rust types and ergonomic improvements where possible without compromising fidelity to the original.
The "blessed" browser API for HTTP going forward is fetch
, but fetch does not provide all the features XMLHttpRequest does at the moment (one notable example being download/upload progress events).
This could also be used as a base for higher level bindings providing a more ergonomic interface from Rust, but I propose leaving it to a future discussion.
The bindings would be implemented inside a new gloo-xhr
crate in a callback
top-level module, to leave room for a potential future-based wrapper built on of it.
Two examples to motivate why we would benefit from "mid-level" bindings, compared to raw web_sys
:
web_sys
, but a set_body
method generic over a (for example) XhrRequestBody
trait implemented by all valid body types.The documentation should mention that fetch
is the API that is currently being pushed forward in browsers and will eventually supersede XmlHttpRequest.
Alternatively, these bindings could be grouped with the fetch
bindings in a common gloo-http
or gloo-ajax
crate.
The implementation would aim to map as close as possible to the original web API; as such few design decisions should be necessary.
Providing nice, ideally typed errors could be challenging, as the error cases in networking APIs are many.
Yosh and I started one at the 2019 Rust All Hands.
Give you a future for when the DOMContentLoaded
event has fired.
Can we use and/or take inspiration from typed-html?
postMessage
(Note: this is for when we are not using shared memory and multithreading)
We should wrap up the fetch API into a nice Rust-y utility crate!
https://github.com/tinaun/futures-native-timers
As similar as it could be within reason. This would enable building a cross-platform (native and wasm) timers crate without too much hassle. This would bring good cross-ecosystem pollination and do a tiny amount of work towards transparent server-side/client-side style applications.
Perhaps something like https://github.com/yerkopalma/choo-sse?
Probably best expressed as a futures stream.
Make an API for displaying notifications (see MDN documentation).
The event handlers in web-sys
accept a Option<&Function>
, which is ugly to work with. Also, it is missing some options.
I propose to use Rust closures instead of Function
. Also I'd like a function requesting permission to display notifications, that returns a Future
.
Notification::builder()
returns a NotificationBuilder
, whose build function is called show()
:
Notification::request_permission()
.map(|_| {
let notification = Notification::builder()
.title("Foo")
.body("Bar")
.icon("notification.png")
.badge("notification-badge.png")
.image("img.png")
.silent(true)
.show();
on(¬ification, move |e: ClickEvent| unimplemented!())
})
I don't know of any drawbacks. However, I'm not familiar with other frameworks, so please tell me if there are better alternatives.
The syntax of the event listeners, which is discussed in #43
Provide a serialization and deserialization framework for converting between javascript values (wasm_bindgen::JsValue
) and rust values.
Related proposals:
Currently, there are a few different ways to work with javascript values, but they are all sub-optimal.
extern type
& JsCast
I guess this is the low-level way of working with javascript objects. We don't use reflection like js_sys::Object
or js_sys::Reflect
. If we use dyn_into
, we will get an instanceof
check, but there is no check that the values we define in the #[wasm_bindgen]
extern block actually match the properties the javascript object contains.
js_sys::Object
and js_sys::Reflect
These types provide reflection - we can check to see if an object has a property or not. This is a good pragmatic safe way of providing a checked gateway into the typesafe rust land. If we check for all parameters we expect, there is probably some overhead, but we know the values we create in rust are correct and we can avoid panics. TODO I need to understand better what happens if you import a non-existant property in a wasm-bindgen extern block.
JsValue::{from/into}_serde
In this case we just convert our javascript type into a string and then re-parse it using the awesome serde framework. This is a reliable way to check we get exactly what we want, but has the following downsides:
JSON.stringify
. For example, we can't hold javascript functions, and we lose type information converting objects into json.This proposal aims to provide fine-grained control of parsing exactly what you want to, leaving opaque what you want to, and doing it all without going through a string.
What follows is just my early thoughts on how this might work. It needs iteration.
The serde framework provides a great template for how to generate serialization and deserialization code. I would envisage this working like
#[derive(GlooDeserialize)]
pub struct SomeJsValue {
// a plain javascript datatype
my_int: i32,
// in javascript this would be `value.point == {x: 1, y: 1}`
// unsure whether to look at Js prototype name or just properties
point: Subtype,
// a javascript function that we don't work with - we just pass it back into JS code
my_func: js_sys::Function,
// something we don't want to parse, we just check it's there
arbitrary_value: wasm_bindgen::JsValue,
// maybe we can distinguish between a property being null, and it not being defined
#[glooserde(null)]
should_be_null: (),
}
#[derive(GlooDeserialize)]
pub struct Subtype {
x: i32,
y: i32
}
There is some complexity from the fact that we do different things for different types, and special case types from wasm_bindgen/js_sys. For a JsValue, we just have to check the object has a property and then get a handle on the property, for functions, we would check in js that the object is a function, and then for actual objects we use reflection to check the properties are there and then copy them (or a handle for complex objects) into rust.
The main drawback of this is it's probably a lot of work to implement.
We also already have all the options above, so we could use them, or develop something like gloo-properties. We could possibly build this on top of something like gloo-properties - maybe that would decrease complexity.
Basically everything about this. Is it a good idea? How would we implement it? I feel its quite ambitious & complex.
Thought I already did this but I guess not >.<
I'd like to do Azure pipelines since they have a higher level of parallelism at the free tier than travis ci. Also, they're not recently acquired by a sketchy, strip-it-down-for-parts company that laid off a ton of people.
Expose an API matching gloo-timers
for using alarms in WebExtensions.
The MDN documentation describes alarms as "like setTimeout()
and setInterval()
, except that those functions don't work with background pages that are loaded on demand". Because of these similarities, gloo-timers
provides a good precedent for the design of this API.
The gloo-webextensions
crate would, like timers, expose both callback-style and futures-based APIs for alarms in submodules called callback
and future
, the latter gated on the futures
feature.
Note: one difference between timers and alarms is that the browser generates ids for timeouts and intervals, but we are responsible for generating a unique name for an alarm.
Callback API skeletons:
pub struct Alarm { .. }
impl Alarm {
// Create a new alarm that will fire once in `millis` ms
pub fn new(millis: f64, callback: F) -> Alarm
where
F: 'static + FnOnce()
{ .. }
// returns the generated name for the alarm
pub fn forget(mut self) -> AlarmId { .. }
// prevent alarm from firing
pub fn cancel(mut self) -> Closure<FnOnce()> { .. }
}
// cancels alarm
impl Drop for Alarm { .. }
pub struct RecurringAlarm { .. }
impl RecurringAlarm {
// create a new alarm that will fire every `minutes` minutes
pub fn new<F>(minutes: f64, callback: F) -> RecurringAlarm
where
F: 'static + FnMut()
{ .. }
// make the alarm un-cancelable, returning the id
pub fn forget(&mut self) -> AlarmId { .. }
// stop the alarm from continuing to fire
pub fn cancel(mut self) -> Closure<FnMut()> { .. }
}
// cancels alarm
impl Drop for RecurringAlarm { .. }
// newtype wrapper around the generated alarm name
struct AlarmId { .. }
impl AlarmId {
// cancel the alarm with this id
pub fn cancel(&self);
}
Futures API skeletons:
pub struct AlarmFuture { .. }
impl AlarmFuture {
// Create a new alarm future that will become ready in `millis` ms, once polled
pub fn new(millis: f64, callback: F) -> Alarm
where
F: 'static + FnOnce()
{ .. }
}
// cancels alarm
impl Drop for AlarmFuture { .. }
impl Future for AlarmFuture {
type Item = ();
type Error = ();
..
}
pub struct AlarmStream { .. }
impl AlarmStream {
// create a new alarm stream that will fire first in `minutes` minutes, and then again every `minutes` minutes, once polled
pub fn new<F>(minutes: f64, callback: F) -> AlarmStream
where
F: 'static + FnMut()
{ .. }
// create a new alarm stream that will fire first after `delay` minutes, and then again every `minutes` minutes, once polled
pub fn new_with_delay<F>(minutes: f64, delay: f64, callback: F) -> AlarmStream
where
F: 'static + FnMut()
{ .. }
}
// cancels alarm
impl Drop for AlarmStream { .. }
impl Stream for AlarmStream {
type Item = ();
type Error = ();
..
}
gloo-timers
tokio-timer
may also be a good source of inspiration.One alternative would be to design the API to more closely match the raw API, rather than basing this off of gloo-timers
.
Is generating a unique alarm name the right thing to do or should that be the user's responsibility as it is for the raw API?
Let's make an idiomatic, Rust-y interface to the file API!
The crates/timers
crate should follow the submodule design that we came up with in #27 and are formalizing in #34.
That is:
callbacks
submodulefutures
module
futures
crate optionalfutures
submodule when the futures
feature is enabledWe have a placeholder right now, but we should have something cool!
Ideally it would use multiple gloo
crates (probably via gloo::whatever
re-exports).
Maybe using timers and events? Suggestions welcome!
I'd like to open this issue to discuss what we want in an initial Gloo release.
We will want to have resolved our plan for versioning Gloo before we actually cut any releases. I think this is the only hard requirement.
We have a spectrum along which to choose: on one end we have "deliver some tiny value to users very soon" and on the other end we have "deliver bigger value to users later".
The more stuff we put in the 0.1 release, the more of a "wow"-factor there is with the amount of things we are shipping, but the longer it will take to be ready. The fewer stuff we put in the 0.1 release, the sooner it starts helping users and we start getting real world feedback on Gloo, but the less of a "wow"-factor the release will have.
I propose that we find a subset of the in-flight proposals that we would like to focus on for an 0.1 release. Reach consensus on their designs, get them implemented, and ship an 0.1.
I think this subset should be fairly small and conservative, so we can ship on the sooner end of the spectrum. Because we are doing so much design work up front, the implementation of each proposal is actually a fairly complete API that has more bang for its buck in terms of "wow"-factor and value for users than an equivalently-sized PR otherwise might.
Note that whatever we choose as blockers for the initial release, anything else that lands while we are still trying to finish the blockers would also make it in that 0.1 release too. But those things would be nice-to-haves and we wouldn't delay the release for them.
Here is what I propose as the 0.1 release blockers:
gloo_timers
— already implemented \o/gloo_console_timer
— already implemented \o/gloo_events
— already implemented \o/
gloo_file
— relatively self-contained and I think we are very close to finalizing the design.gloo_(properties|reflect)
— again, this seems pretty close and there seem to only be two relatively minor open questions about the design still.This list is totally up for discussion and we can add and remove items from it if we want. What do y'all think?
Let's make an idiomatic, Rust-y interface for working with Web Sockets!
All Gloo modules should have narrative documentation, perhaps as part of a centralized guide.
It's common in the Rust community to rely on API docs, and narrow-scoped examples. These are important, but not sufficient.
With some exceptions, Rust libs, including most WASM frameworks, tend to be documented through examples and API docs. While these are important, having a detailed, narrative guide is important too. For example, the Rust book. I think all Gloo components should have a maintained guide of this style. Could use the Rust Book template. Example: Vulkano
has a very extensive API, a guide demonstrating some things, and a few examples, but is missing large chunks of info, like how to use it for 3d items, or multiple objects. Example: Yew
is a popular stdweb-based framework, but has no narrative guide outside a brief readme, which until recently, was misaligned with the released API. Example: Rocket
is a backend framework with a detailed guide.
Related: In addition to feature-specific examples, and long/complex ones, I think short ones that demonstrate API coverage, and using various features together are useful... and missing in many Rust crates.
It requires more upfront effort, and maintenance work when APIs change.
Doing this would make Gloo more appealing to people who are experienced with frontend dev, but not Rust.
I'd like to ask two questions related to versioning:
gloo
crate's version and each gloo_whatever
crate's version?Note: I'm assuming that whatever the answers, we are following cargo's interpretation of semver.
On one end of the spectrum is never breaking backwards compatibility. This has the advantage of making upgrades to the latest version of crates easy for users. The disadvantage is that our APIs, once published, are set in stone: we can only add new APIs, can't ever fix flaws in existing APIs.
The Rust std
library takes this to an extreme, where backwards compatibility is guaranteed indefinitely. wasm-bindgen
is a little looser in that we haven't ruled out breaking version bumps, but we are trying pretty hard to avoid them. We will likely have a single breaking version before moving to 1.0, after which we likely won't do breaking changes anymore.
The other end of the spectrum is fixing flawed APIs whenever we notice them and releasing breaking version bumps immediately. The advantages are that we can fix flaws immediately, and don't have to maintain code that we don't believe meets our design standards. The disadvantages are more difficult upgrades for users, so they are less likely to get the latest and greatest bug fixes and perf improvements.
Do we want to keep each individual gloo_whatever
crate's version locked to the same version as the umbrella gloo
crate or not?
Imagine this scenario: we have gloo
, gloo_timers
, and gloo_events
all at version 0.1.2
:
gloo @ 0.1.2
- gloo_timers @ 0.1.2
- gloo_events @ 0.1.2
Now we fix a bug in gloo_timers
that causes us to bump its version from 0.1.2
to 0.1.3
. To get the fix in the umbrella gloo
crate, we have to bump it to 0.1.3
as well. But do we also bump gloo_events
to 0.1.3
or do we leave it at 0.1.2
?
gloo @ 0.1.3
- gloo_timers @ 0.1.3
- gloo_events @ 0.1.2 or 0.1.3?
The advantages of keeping everything in lock step is largely a lighter maintenance burden for ourselves, the authors/maintainers of Gloo. The disadvantage is unnecessary version bumps for toolkit crates. This disadvantage is magnified by breaking version bumps: in the previous example, if it was a breaking bump to 0.2.0
instead of a compatible bump to 0.1.3
, then we could force an unnecessary breaking bump on gloo_events
, which would be kind of crappy for anyone using that crate by itself without the rest of Gloo.
The advantages and disadvantages of doing the minimum cascading version bumps are basically the inverse of those for keeping everything in lock step. Upgrades are easier for users (particularly those that are using individual crates, and not the umbrella crate). However, the maintenance burden is larger for Gloo maintainers.
Note that a breaking change in a gloo_whatever
crate will always cascade as a breaking change in the umbrella gloo
crate.
Would be A++++ if someone went through https://platform.html5.org/ and made sure we had an associated tracking issue for each item there (where it makes sense). Lots of potential utility crates!
Addresses #47 in part by proposing a mid-level API. The possibility for a higher level API is left open.
The mid-level file API aims to implement File I/O on top of the raw JS apis found here: https://w3c.github.io/FileAPI/ which includes listing files, creating files, and reading files.
In general this API will mostly differ from the web_sys
interface in the following ways:
js_sys tends to be fairly "stringly" typed, so where we have known enums, the crate will provide bespoke types.
This is also true for errors which are usually exposed as JsValues. In this crate they will be exposed as well-formed Rust types.
All callbacks will be of the appropriate closure type (e.g., FnOnce).
JS provides a basic collection type over files (usually from an tag). In general FileList
will be an opaque struct that offers a rich interface to the collection through an Iterator
implementation.
Blob
is a "file like" interface. The crate would expose this as a Rust interface from which the specific types File
and RawData
inherit. These types would be roughly 1 to 1 translations of the File
and Blob
objects just with less "stringly" typed interfaces. For example, RawData
would accept a known list of mime-types
The FileReader API is a bit wonky and can easily be used in the incorrect way. For instance, if the user calls readAsArrayBuffer
or other "read" APIs before the data is loaded, the operation won't return data. The crate will expose an API that more properly models the "read" state machine.
This will be done through a futures API which takes the user through the state machine:
let file_reader = FileReader::new();
file_reader.read_as_text(&blob).map(move |contents| {
element.set_inner_html(contents);
})
The various "read" methods will take ownership of self
so that users can't try to read multiple files at once. These methods return an InProgressRead
struct that is a future and also contains an abort
method for aborting the upload.
The events "error", "load", and "loadend" are all representable through the future and thus aren't exposed on the file reader. The events "progress", "loadstart", and "abort" are still exposed as methods that take the appropriate Rust closure.
This API would mirror the future based API just in a different namespace.
FileReader exposes a very different API from its JS counterpart. We could do a more 1 to 1 translation of the API and leave the improved API for a higher-level crate. This would make this crate only a very marginal improvement on what is already exposed in js_sys
.
Is there an ergonomic way to expose FileReader "progress", "loadstart", and "abort" events as part of the future API?
Is there a better way to model the relationship between Blob
and File
? Possibilities include an enum and a File
type which contains a Blob
type.
It currently supports setTimeout
and clearTimeout
but does not support setInterval
and clearInterval
.
Let's make an idiomatic, Rust-y interface to 2D canvas!
I propose to implement an API which makes it easy to use Rust's RAII-based system for event listener registration/deregistration.
When registering an event listener, it is often needed to deregister it later.
In JavaScript this is done with the removeEventListener
method, but that is verbose and error prone. It also does not clean up Rust resources (e.g. the memory for the closure).
pub struct EventListener<'a, F> where F: ?Sized {
node: EventTarget,
kind: &'a str,
callback: Closure<F>,
}
impl<'a, F> EventListener<'a, F> where F: ?Sized + WasmClosure {
#[inline]
pub fn new<F>(node: &EventTarget, kind: &'a str, f: F) -> Self {
let callback = Closure::wrap(Box::new(f) as Box<F>);
node.add_event_listener_with_callback(kind, callback.as_ref().unchecked_ref()).unwrap();
Self {
node: node.clone(),
kind,
callback,
}
}
}
impl<'a, F> Drop for EventListener<'a, F> where F: ?Sized {
#[inline]
fn drop(&mut self) {
self.node.remove_event_listener_with_callback(self.kind, self.callback.as_ref().unchecked_ref()).unwrap();
}
}
The idea is that you can simply call EventListener::new(&node, "foo", move |e| { ... })
and when the EventListener
is dropped it will then cleanup the Rust memory for the closure and it will also deregister the event listener.
This is a general purpose idiomatic mid-level API which can work with any event on any DOM node. I expect it to be used pervasively both within Gloo and outside of Gloo.
For a mid-level API like this, there aren't really any alternatives. Some small things can be tweaked, and I haven't thought very hard about how to support FnOnce
(but I'm sure it is possible). Ideas and refinements are welcome.
It is possible to build a higher level API on top of this.
This higher level API gives full static type support for JS events, making it impossible to get the types wrong.
The way that it works is that a new trait like this is created:
pub trait StaticEvent {
const TAG: &'static str;
type Event;
fn wrap(event: Self::Event) -> Self;
}
And a new fn
is created, like this:
pub fn on<A, F>(node: &EventTarget, f: F) -> EventListener<'static, FnMut(A::Event)>
where A: StaticEvent,
F: FnMut(A) + 'static {
EventListener::new(node, A::TAG, move |e| {
f(A::wrap(e));
})
}
Now you can create various events like this:
struct ClickEvent {
event: web_sys::MouseEvent,
}
impl ClickEvent {
// Various methods specific to the `click` event
}
impl StaticEvent for ClickEvent {
const TAG: &'static str = "click";
type Event = web_sys::MouseEvent;
fn wrap(event: Self::Event) -> Self {
ClickEvent { event }
}
}
struct MouseDownEvent {
event: web_sys::MouseEvent,
}
impl MouseDownEvent {
// Various methods specific to the `mousedown` event
}
impl StaticEvent for MouseDownEvent {
const TAG: &'static str = "mousedown";
type Event = web_sys::MouseEvent;
fn wrap(event: Self::Event) -> Self {
MouseDownEvent { event }
}
}
And now finally we can actually use the API:
on(&node, move |e: ClickEvent| {
...
})
on(&node, move |e: MouseDownEvent| {
...
})
This API has some huge benefits:
You no longer need to specify a string for the event type (so no more typos).
You no longer need to do any coercions to the correct type (that's all handled automatically now).
It guarantees that the static type is correct (in other words, if you have a click
event, it is guaranteed to be matched to a ClickEvent
Rust type, and vice versa).
This technique was pioneered by stdweb, and it works out really well in practice. It may also be necessary for solving rustwasm/wasm-bindgen#1348
But that's something which can be layered on top of the mid-level EventListener
API, so I defer it to later.
An API that provides guaranteed static typing for events, which is both more convenient and much safer.
#30 lays the groundwork for a mid-level event listener system, but it only supports web_sys::Event
, and it requires passing in string event types, which are error-prone: typos can happen, and there's no guarantee that the string matches the desired type:
EventListener::new(&foo, "click", move |e| {
// Oops, "click" isn't a KeyboardEvent! Panic!
let e: KeyboardEvent = e.dyn_into().unwrap_throw();
})
This proposal completely solves that, by guaranteeing that the static types are always correct.
It also does automatic type casts, which is more convenient for the user (compared to the manual type casts of EventListener
).
First, we must create a trait which supports conversion from web_sys::Event
:
pub trait StaticEvent<'a> {
const EVENT_TYPE: &'a str;
fn unchecked_from_event(event: web_sys::Event) -> Self;
}
Now we create various structs, one struct per event type, and impl StaticEvent
for them:
// https://www.w3.org/TR/uievents/#event-type-click
pub struct ClickEvent {
event: web_sys::MouseEvent,
}
impl ClickEvent {
// Various methods go here...
}
impl StaticEvent<'static> for ClickEvent {
const EVENT_TYPE: &'static str = "click";
#[inline]
fn unchecked_from_event(event: web_sys::Event) -> Self {
Self {
event: event.unchecked_into(),
}
}
}
(And similarly for MouseUpEvent
, MouseDownEvent
, KeyDownEvent
, etc.)
If possible, the above should be automatically generated based upon WebIDL.
If that's not possible, we should create a macro that makes it easy to generate impls for StaticEvent
:
#[derive(StaticEvent("click"))]
pub struct ClickEvent {
event: web_sys::MouseEvent,
}
Lastly, there should be an on
function which allows for actually using StaticEvent
:
pub fn on<'a, A, F>(node: &EventTarget, callback: F) -> EventListener<'a>
where A: StaticEvent<'a>,
F: FnMut(A) + 'static {
EventListener::new(node, A::EVENT_TYPE, move |e| {
callback(A::unchecked_from_event(e));
})
}
And now it's possible for users to use the API:
on(&foo, move |e: ClickEvent| {
// ...
})
on(&foo, move |e: MouseUpEvent| {
// ...
})
The primary drawback is the extra complexity, and the extra work of needing to create an extra struct per event type.
However, I don't see any better way to achieve the goals (full static typing, and automatic casts to the correct type).
Perhaps the structs should accept a reference instead? In other words, it would look like this:
pub struct ClickEvent<'a> {
event: &'a web_sys::MouseEvent,
}
impl<'a> ClickEvent<'a> {
// Various methods go here...
}
impl<'a> StaticEvent<'a, 'static> for ClickEvent<'a> {
const EVENT_TYPE: &'static str = "click";
#[inline]
fn unchecked_from_event(event: &'a web_sys::Event) -> Self {
Self {
event: event.unchecked_ref(),
}
}
}
Does that buy us anything?
There is a very important bug which we need to solve: rustwasm/wasm-bindgen#1348
I have given a lot of thought to this, and my (tentative) conclusion is that we can solve this bug by modifying a select few APIs.
In particular, if we change the input
, keydown
, keypress
, and keyup
events to do unpaired surrogate checking, that might be good enough to fix everything!
So, in that case, StaticEvent
allows us to do that sort of checking:
#[derive(StaticEvent("input"))]
pub struct InputEvent {
event: web_sys::InputEvent,
}
impl InputEvent {
// Returns None if there are unpaired surrogates
fn value(&self) -> Option<String> {
// Unpaired surrogate checks...
}
}
And now users can do this:
on(&foo, move |e: InputEvent| {
if let Some(value) = e.value() {
// We know there aren't any unpaired surrogates!
}
})
So this API is not just about convenience or static type safety, it's also about fixing unpaired surrogates!
By this I mean things like
Another opinionated puzzle piece!
I'm playing with wrapping the history api, which should be easy. I've got a crate, but the test in it takes ages to run on firefox, and full-on crashes chrome. It might be a bug in what I've done - but I would appreciate other people taking a look.
The code is currently called wasm-history.
Should be similar to our existing wasm-pack test
s that run in firefox and chrome, but we need to use a macos vm image, and I'm not 100% sure how to do that in azure pipelines yet!
See .azure-pipelines.yml
and .ci/wasm-pack-test-headless.yml
for more details.
Should have a utility crate for working with Indexed DB in a Rust-y, idiomatic way.
It appears that both console_error_panic_hook and UnwrapThrowExt accomplish the same thing; ie displaying panics in the browser console via console.error
. CONTRIBUTING.md directs UnwrapThrowExt
. What do y'all think? What's panic_hook
's niche?
A mid-level API to set and query properties on arbitrary Javascript values.
The js-sys crate already provides Reflect
to get and set properties on JsValue
types, and wasm-bindgen provides Serde integration that allows to easily map an existing Rust type into Javascript land. So why do we need more?
The use case we're addressing here is ad-hoc property access. This occurs often when writing Rust WASM code that interfaces with existing userland Javascript APIs. It's expected that as the Rust WASM ecosystem matures, this situation will decline in frequency. However that kind of shift is not going to happen overnight, and in the meantime we should provide better ergonomics for users that need to interface with raw Javascript values.
gloo_properties::property
Used to get/set a single property on an existing JsValue
.
API:
/// Retrieves a single Property on target Object. This Property can be used to
/// get and set the value.
pub fn property<K>(target: &JsValue, key: K) -> Property
where K: Into<JsValue> { /* ... */ }
struct Property { /* ... */ }
impl Property {
/// Retrieves the value of this Property as a String. If the property is
/// undefined, null, or not a String, an Err is returned.
pub fn string(&self) -> Result<String, JsValue> { /* ... */ }
/// Retrieves the value of this Property as a u32. If the property is
/// undefined, null, not a Number, or overflows a u32, an Err is returned.
pub fn u32(&self) -> Result<u32, JsValue> { /* ... */ }
// And so on...
pub fn u16(&self) -> Result<u16, JsValue> { /* ... */ }
// pub fn u8...
// pub fn i32...
// pub fn i16...
// pub fn i8...
/// Retrieves the value of this Property as a bool. If the property is
/// undefined, null, or not a Boolean, an Err is returned.
pub fn bool(&self) -> Result<bool, JsValue> { /* ... */ }
/// Retrieves the value of this Property as any kind of Javascript mapped
/// type. Anything that is exported from #[wasm_bindgen] is usable. If the
/// property is undefined, null, or an incorrect type, an Err is returned.
pub fn cast<T: JsCast>(&self) -> Result<T, JsValue> { /* ... */ }
/// Retrieves a sub-property from this Property. This is used to traverse
/// arbitrary nested object structures. Note that if a property does not
/// exist, a Property will still be returned, but any attempts to get or set
/// a value will return an Error.
pub fn property<K: Into<JsValue>>(self, key: K) -> Property { /* ... */ }
/// Sets the given value on the Property. Returns an error if the set failed
/// for any reason. e.g frozen object, error in a setter on the Object, etc.
pub fn set<V: Into<JsValue>>(&self, val: V) -> Result<bool, JsValue> { /* ... */ }
}
Usage examples:
use gloo_properties::property;
// Because key is generic over Into<JsValue>, using it is very convenient:
property(&obj, "foo"); // static string literals
property(&obj, &existing_str); // existing String / &strs
property(&obj, existing_str); // Pass by ref not required.
// Symbols work too.
let sym = JsValue::symbol("secret");
property(&obj, &sym);
// Nested property access:
property(&obj, "foo").property("bar").property("quux");
// Retrieving values.
property(&obj, "foo").string().unwrap_throw();
let my_element: web_sys::Element = property(&obj, "foo").cast();
// Traversing multiple levels never fails until trying to get/set something.
property(&obj, "i").property("don't").property("exist"); // -> Property
property(&obj, "exists").property("nope").string() // -> Err("Property 'i' does not exist")
// Setter is generic over anything that can convert into a JsValue, which is
// all core Rust types, and all web_sys types.
property(&obj, "foo").set("a direct string");
property(&obj, "foo").set(42u32);
property(&obj, "foo").set(true);
let an_existing_object = Object::new();
property(&obj, "foo").set(an_existing_object);
gloo_properties::setter
Used to quickly build up a set of properties on a value.
API:
/// Returns a Setter that can be used to set multiple properties on the given
/// target. Ownership of the target is taken until Setter#finish() is called.
pub fn setter<T: JsCast>(target: T) -> Setter<T> { /* ... */ }
pub struct Setter<T: JsCast> { /* ... */ }
impl<T: JsCast> Setter<T> {
/// Sets given key/value pair on the target object.
pub fn set<K, V>(self, key: K, val: V) -> Self
where
K: Into<JsValue>,
V: Into<JsValue>,
{ /* ... */ }
/// Sets given key/value pair on the target object, and then calls the given
/// closure to set sub properties on the provided value.
pub fn with<K, V, F>(self, key: K, val: V, func: F) -> Self
where
K: Into<JsValue>,
V: JsCast,
F: FnOnce(Setter<V>) -> Setter<V>,
{ /* ... */ }
/// Finishes setting properties on target object and returns it.
pub fn finish() -> T { /* ... */ }
}
Usage:
use gloo_properties::setter;
let my_sym = JsValue::symbol("secret");
let my_obj = setter(Object::new())
.set("foo", "bar")
.set(&my_sym, "quux")
.set(123, 321)
.set("bool", false)
.set("jsval", Object::new())
.with("nested", Object::new(), |sub| sub
.set("and", "so")
.set("on", "it goes"))
.finish();
Currently, working with Reflect
(which this crate would use) is slower than working with duck-typed interfaces, and in many cases is also slower than the Serde integration.
The obvious alternatives are to use duck-typed interfaces or the Serde integration. Both of these approaches require one to write and annotate a struct
with the desired fields. The proposed API is suitable for use when the user does not wish to define explicit types in order to grab properties out of an existing value in an ad-hoc manner.
For example:
// Instead of needing to do this:
{
#[wasm_bindgen]
extern "C" {
type MyThing;
#[wasm_bindgen(method, getter, structural)]
fn foo(this: &MyThing) -> String;
}
let thing: MyThing = my_obj.dyn_into::<MyThing>().unwrap_throw();
thing.foo
}
// Or this:
{
#[derive(Deserialize)]
struct MyThing {
foo: String,
}
let thing: MyThing = JsValue::into_serde(&my_obj).unwrap_throw();
thing.foo
}
// One can just do this:
{
property(&my_obj, "foo").string().unwrap_throw();
}
Currently, none.
Right now web-sys provides very low-level access to the web APIs. My understanding is that Gloo is intended to create idiomatic high-level Rust APIs on top of those low-level APIs. However, there are different degrees of "high-level".
As an example, it's easy to wrap setTimeout
and setInterval
to make a nice convenient API that uses FnOnce
and FnMut
. But that's only slightly more high-level than using the APIs directly.
On the other hand, we could wrap setTimeout
and setInterval
into a Future
and Stream
, which is definitely much higher level (and more idiomatic for Rust).
So when we are faced with this choice between mid-level and high-level, what should we choose? I think we should have a consistent approach across the Gloo ecosystem.
I propose that we always expose the high-level APIs (Future
, Stream
, etc.) and then have a submodule which exposes the mid-level APIs.
That makes it clear that the high-level APIs are the idiomatic APIs that generally should be used, but still makes the mid-level APIs accessible for unusual circumstances.
To start the bikeshedding, I propose we consistently use the name raw
for the mid-level submodule.
Would allow frontend and backend code to communicate in an abstract manner on an async Stream. It allows code reuse and prevents every application to have to implement this on top of the existing websocket api
Add impl AsyncRead/AsyncWrite to websocket
Allow upgrading an Http connection to an AsyncRead/AsyncWrite without websockets. I have no idea what the implementation of this would look like, and if it's even possible without websockets.
That being said, this alternative would be preferable I think. It means we don't need WS on the server either, just an upgraded http connection.
I kind of need this. If need be I don't mind attempting to implement this myself. If this is deemed a good solution, is there any mentoring possible to hint the way?
RxJS is "a library for composing asynchronous and event-based programs by using observable sequences." Its core abstraction is the "observable", which is a collection of future values that has many commonalities with rust streams. I propose to create a similar API for gloo based on streams.
gloo
needs a library for interacting with DOM events that works well within the idioms of rust. The patterns in RxJS
might provide a good fit. Basing it off of streams takes advantage of rust's strong async story, and will only become more ergonomic in the future with features such as async/await and language-level support for streams.
A wrapper type would be created for each element, with a Stream
-returning method for each event that the element can emit.
For example, the RxJS smart counter example (which implements a simple odometer effect) might look something like this:
fn smart_counter() -> impl Future<Item = (), Error = ()> {
let input = gloo_dom::Input::get_by_id("range")
.expect("expected input element with id #range to exist");
let update_btn = gloo_dom::Button::get_by_id("update")
.expect("expected button element with id #update to exist");
update_btn
.clicks()
.map(move |_| input.value().parse::<i32>().unwrap())
.for_each(move |end| {
IntervalStream::new(20)
.zip(stream::iter_ok(0..=end))
.for_each(|(_, num)| {
gloo_dom::Element::get_by_id("display")
.expect("expected element with id #display to exist")
.set_inner_text(&num.to_string());
Ok(())
})
})
}
(I prototyped just enough to get this working here)
Since this would just be another crate in gloo
, I think the main alternative is to just not do this.
The major drawback is that this API is not as ergonomic as it could be with rust as it is today, but future language features might change that.
See also rustwasm/team#162
We should have an idiomatic Rust utility crate for working with
requestAnimationFrame
requestAnimationFrame
loop (reusing the same function)requestAnimationFrame
sA crate which contains idiomatic high-level Rust APIs for the WebExtensions standard, which is used to create browser extensions for Firefox, Chrome, and Edge.
I personally have made multiple Chrome extensions, so this is something I need and have a lot of experience with.
Being able to create fast and safe and maintainable browser extensions in Rust is a big deal. In my experience JavaScript just does not scale well with big extensions.
But the APIs are... well, rather terrible. So in addition to porting the APIs to Rust, we should clean them up and make them actually reasonable.
I've done this sort of work before in JavaScript, so I know a lot of the weird quirks and gotchas of the APIs.
We should have a utility crate for working with window.sessionStorage
and window.localStorage
!
Let me first start by saying that @rustwasm has changed my programmer-life for the better in more ways than I can count. I'm absolutely loving the Rust + WebAssembly story!
I say that so that anything that I say below doesn't sound like dissent. I just want to share feedback which will hopefully help push @rustwasm forwards!
I recognize that some of my thoughts will be wrong and/or misinformed - so feel free to point that out!!!
Also big disclaimer that I maintain some Rust web front-end tools although I'm hoping that that doesn't make my feedback too biased!
wasm-bindgen wasm-pack and the rest of rustwasm are so successful because they provide functionality / solve pain points in a way that is pretty much objectively correct.
You're unlikely to see a second wasm-bindgen or a second wasm-pack because they aren't trying to be much more than exactly what almost anyone would want on the low level.
Just about anyone that needs something that doesn't exist will contribute it back to wasm-bindgen or wasm-pack if it's low level - or just build in top of wasm-bindgen or wasm-pack.
They do an excellent job of focusing on problems that are solvable almost entirely without any material disagreement - and letting the community build on top of that.
I'm very afraid of Gloo breaking this mold.
I'm getting the impression that we might be heading in the direction of "Come to Gloo to get modules that you need."
This to me will just lead to "Gloo has some great modules, but there are 14 other people building similar modules because they disagree with how Gloo did it."
I think that a better direction would be for Gloo to focus on being the glue between modules that the community builds.
This would mostly look like Traits and generic methods that ... so long as a community module implements these traits it's compatible with the rest of the ecosystem.
We don't need one toolkit to rule them all IMO. I think we need Gloo to empower dozens of other developers to iterate on what they think Rust web modules should look like.
And the community can very easily try out the options since they all are compatible with these Gloo traits / generic methods.
And all of these maintainers are contributing back to Gloo as they learn.
Rust on the web is still young - so there will certainly be a lot of learning around what patterns allow us to build production grade applications the fastest.
I want to trust the community to discovery these things, and I want to trust @rustwasm to empower the community on that journey by building out the most lowest-common-denominator libraries and tooling.
Look at what people are already doing and try to be the glue between them.
What do the virtual-dom's look like today? What do the procedural macros for HTML parsing look like today? What do the diff/patch algorithms look like?
Can Gloo provide traits / generic methods that almost every single one of these libraries would build on top of?
Thereby unifying the community and giving people with different ideas the room to explore them while contributing what they find back to the super-low-level Gloo crates?
Which - I know is the goal! I just think that some of the issues that I see are far too high level. So I'll give some examples below of what I mean by low level, least common denominator.
I maintain an html-macro - and it looks like there are a few others in the community - each with their own advantages / disadvantages.
A disadvantage of my macro is that there isn't nearly as much type checking as in typed-html. But I can't switch to typed-html even when they have wasm-bindgen support because I personally prefer not having to wrap text in quotation marks.
It'd be nice if Gloo
provided the validation that typed-html
html-macro
and any other macros could build on top of.
"Is this string a valid class". "Is this href valid?". etc
People will have different thoughts and ideas on how a diff/patch algorithm should look.. But one thing that is pretty consistent is that virtual nodes all have properties ... events ... and basically look like real DOM elements.
As people explore diff/patch algorithms and their own virtual dom's it would be nice if they were all building on top of Gloo.
Don't like one diff/patch algorithm that someone provides? Switch to another one pain free since they're both generic on top of Gloo
's VirtualNode trait!
I can open issues when they come to mind if useful! But hopefully these were helpful!
In general I think that for a lot of the issues that are open we can zoom in to specific pieces of them that are un-opinionated and focus on that.
I think that the best path forwards for Gloo right now for a lot of these issues that are open is to look at unifying/being the glue between all libraries that exist now and will exist in the future.
Gloo should spur innovation by being the foundation for all future experimentation.
Gloo should focus on the bits of the stack and the problems that will have nearly zero opinion on how to do them properly - and let the community build on top of those solutions (just like wasm-bindgen and wasm-pack).
Just my thoughts! Hope it helps!
This is a proposal for a new WebSocket abstraction. One crate with a submodule for a callbacks-based type and another submodule which builds on top of the previous for futures support.
WebSockets are fundamental in the web world, and JS land has a plethora of WebSocket libraries with simple APIs. Let's build something on par with these APIs in order to provide the best possible experience.
Without the improvements proposed here:
web_sys::WebSocket
type.See the original discussion over here in the seed project.
Expand each of the sections below for more details on the two types of WebSocket abstractions being proposed. Everything here has been updated per our discussions in this thread up until this point in time.
The plan here is to build on top of the gloo_events
crate, which has not yet landed as of 2019.04.04, but which should be landing quite soon.
The essential idea is that we build a new WebSocket type which exposes a builder pattern for creating new instances. This will allow users to easily register their callbacks while building the WebSocket instance. The constructor will then wrap any given callbacks in a gloo_events::EventHandler
, register the event handler on the appropriate event coming from the web_sys::WebSocket
, and will then retain the event handler for proper cleanup when the WebSocket is dropped.
This abstraction will also enable the futures based WebSocket type (discussed below) to easily build upon this events based WebSocket type.
// Begin building a new WebSocket instance.
// The given `on*` closures will be wrapped by `gloo_events::EventHandler`s.
let ws = WebSocket::connect("/ws")
// Any protocols which the user would like this connection to use may be specified here.
.protocols(&["custom.proto.com"])
// For `onmessage`, users register an FnMut which takes the unpacked
// data from the received frame. The `WsMessage` enum instance indicates
// if the message was text or binary. See below for the reasoning behind this.
.onmessage(|event: WsMessage| ())
// The other `on*` handlers will be given the raw event object.
.onopen(|event: Event| ())
.onerror(|event: Event| ())
.onclose(|event: Event| ())
// By default, instances will be configured to reconnect with our `ReconnectConfig::default()`.
// However, for more control, users may specify customized reconnect config.
.reconnect(ReconnectConfig)
// If users want this instance to not perform reconnects at all, this must be called.
.no_reconnect()
.build(); // Finalize the build, returning a new WebSocket instance.
/// An enumeration of the different types of WebSocket messages which may be received.
///
/// The gloo WebSocket will unpack received messages and present them as the appropriate variant.
pub enum WsMessage {
Text(String),
Binary(Vec<u8>),
}
Reconnects will be handled by inserting some logic in the onclose
handler (even if a user is has not registered an onclose
callback). We will probably just use the Window.set_timeout_with_callback_and_timeout_and_arguments_0 for scheduling the reconnect events based on the backoff algorithm.
We will blow away the old web_sys::WebSocket
, build the new one and register the same original EventHandler
s. When the new connection opens, we will update the retry state as needed. If an error takes place and the new WebSocket is closed before it ever goes into an open state, we will proceed with the retry algorithm.
We will most likely just use Rc<Cell<_>>
or Rc<RefCell<_>>
on the web_sys::WebSocket
and the reconnect config instance for internally mutating them.
This type will implement the futures Stream + Sink
traits which will allow for futures-based reading and writing on the underlying WebSocket, and is built on top of the gloo_events
based type described above.
We are also planning to implement AsyncRead + AsyncWrite
on this type (per @yoshuawuyts recommendation) which will allow folks to use the tokio_codec::{Encode, Decode}
traits for more robust framing on top of the WebSocket.
A set of Rust enums are used for representing the various event types and variants, as all events will come through an internally held futures::sync::mpsc::UnboundedReceiver
.
/// An enumeration of all possible event types coming from the underlying WebSocket.
///
/// These are all sent through a futures unbounded channel which the callbacks
/// publish to from within the callbacks-based WebSocket type.
pub enum WSEvent {
Open(Event),
Message(WsMessage), // WsMessage is described in the callbacks-based section above.
Error(Event),
Close(Event),
}
A few examples of how to build an instance of this futures based type.
// Will build an instance with the default reconnect config.
let ws = WebSocket::connect("/ws");
// Will build an instance which is not configured to reconnect.
let ws = WebSocket::no_reconnect("/ws");
// Will build an instance which uses a custom reconnect config.
let ws = WebSocket::custom_reconnect("/ws", cfg);
// Use sink to send messages & stream to receive messages.
// This comes from https://docs.rs/futures/0.1.25/futures/stream/trait.Stream.html#method.split
let (sink, stream) = ws.split();
// This type also impls AsyncRead + AsyncWrite, so that users can implement
// their own framing protocols (thanks @yoshuawuyts).
use MyEncoderDecoder; // User's custom Encoder + Decoder.
let proto_stream = MyEncoderDecoder::framed(ws)?;
let (mut reader, mut writer) = proto_stream.split(); // Framed reader & writer.
Most of this work is actually done. A pull request will be open soon so that we can start looking at an actual implementation.
Reconnecting will actually be handled by the underlying events-based WebSocket instance. Nothing new should need to be implemented here.
AsyncRead + AsyncWrite
impls, inasmuch as they would need to treat all data sent and received as binary data (server side included)?Ok(AsyncSink::NotReady(msg))
when the underlying WebSocket is being rebuilt?I propose adding a crate that wraps common web_sys features with a cleaner API.
Examples:
Window
, Document
and History
in a way that handles failures automatically with expect_throw
value
of input etc elementsconsole::log
and console::error
with a more flexible APIThe web_sys
api can be verbose; high-level crates needn't each provide their own wrappers.
Some common features of web_sys
are verbose when used directly. It's likely that many Gloo
crates will make use of them - let's settle on a general wrapping API higher-level crates can use. Example
Examples:
/// Convenience function to avoid repeating expect logic.
pub fn window() -> web_sys::Window {
web_sys::window().expect_throw("Can't find the global Window")
}
/// Convenience function to access the web_sys DOM document.
pub fn document() -> web_sys::Document {
window().document().expect_throw("Can't find document")
}
/// Convenience function to access the web_sys history.
pub fn history() -> web_sys::History {
window().history().expect_throw("Can't find history")
}
/// Simplify getting the value of input elements; required due to the need to cast
/// from general nodes/elements to HTML__Elements.
pub fn get_value(target: &web_sys::EventTarget) -> String {
if let Some(input) = target.dyn_ref::<web_sys::HtmlInputElement>() {
return input.value();
}
if let Some(input) = target.dyn_ref::<web_sys::HtmlTextAreaElement>() {
return input.value();
}
if let Some(input) = target.dyn_ref::<web_sys::HtmlSelectElement>() {
return input.value();
}
"".into()
}
/// Similar to get_value
pub fn set_value(target: &web_sys::EventTarget, value: &str) {
if let Some(input) = target.dyn_ref::<web_sys::HtmlInputElement>() {
input.set_value(value);
}
if let Some(input) = target.dyn_ref::<web_sys::HtmlTextAreaElement>() {
input.set_value(value);
}
if let Some(input) = target.dyn_ref::<web_sys::HtmlSelectElement>() {
input.set_value(value);
}
}
The original web_sys
APIs aren't that clumsy - this might clutter up Gloo
without providing much benefit. Perhaps we want to use pattern matching to handle failures of web_sys::Window
etc instead of using expect_throw
.
It's easy to create a collection of helper functions that are intended to be general, but spread over a wide range of uses, some of which may not be common. It's possible that they may cover a smaller set of use-cases than intended. Thin wrappers in general can be seen as obfuscating the original, well-known API. Why build a new API that causes confusion with the one it wraps, when it's not much better?
It's unclear what the scope of this should be. There are surely grey areas about what's appropriate here, and how cohesive/comprehensive it should be. What causes web_sys::Window
etc to fail?
I'm certainly biased by my own use-cases. Do y'all think these are, in-fact useful, general wrappers that would see use in many/most Gloo
crates?
I've only scratched the surface. What else should be added?
Let's make an idiomatic, Rust-y interface to the WebAudio API!
Allow creating Timeout
s and Intervals
without attaching them.
I have the following snippet:
let game = Rc::new(RefCell::new(game));
let game_ = game.clone();
let closure_run: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
let mut game = game_.borrow_mut();
game.run();
}));
let game_ = game.clone();
let closure: Closure<dyn FnMut(_)> = Closure::wrap(Box::new(move |event: KeyboardEvent| {
if event.key() == "Enter" {
{
let mut game = game_.borrow_mut();
game.handle_input();
}
window
.set_timeout_with_callback(closure_run.as_ref().unchecked_ref())
.unwrap();
}
}));
input
.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
I tried -- perhaps not hard enough -- to port it to gloo
, but it didn't seem to work. Notice that I'm creating closure_run
outside of closure
. If I switch the closure
code to Timeout
, it doesn't compile because closure
is FnMut
, and Timeout::new
consumes the callback. And if I try to move the code inside, I get other borrow checker errors.
Needs some exploratory survey and design work!
#
fragment?pushState
and history API integration?We should also do the link checking support that mdbook has.
wasm-bindgen does this in CI, so we can probably crib a bunch of stuff from there: https://github.com/rustwasm/wasm-bindgen/blob/master/azure-pipelines.yml#L242-L254
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.