mirror of https://github.com/yewstack/yew
Upstream yewtil crate (#1119)
* Upstream yewtil crate * cargo fmt * cargo clippy * cargo fmt
This commit is contained in:
parent
a91e7f6512
commit
f9a783c2ca
13
Cargo.toml
13
Cargo.toml
|
@ -13,6 +13,19 @@ members = [
|
|||
"yew-router/examples/router_component",
|
||||
"yew-router/examples/switch",
|
||||
|
||||
# Utilities
|
||||
"yewtil",
|
||||
"yewtil-macro",
|
||||
"yewtil/examples/pure_component",
|
||||
# "yewtil/examples/dsl",
|
||||
"yewtil/examples/lrc",
|
||||
"yewtil/examples/history",
|
||||
"yewtil/examples/mrc_irc",
|
||||
"yewtil/examples/effect",
|
||||
"yewtil/examples/fetch",
|
||||
"yewtil/examples/futures",
|
||||
"yewtil/examples/function_component",
|
||||
|
||||
# Examples
|
||||
"examples/counter",
|
||||
"examples/crm",
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "yewtil-macro"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT/Apache-2.0"
|
||||
description = "Macros to be re-exported from the yewtil crate"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.6"
|
||||
quote = "1.0.2"
|
||||
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
|
|
@ -0,0 +1,194 @@
|
|||
use proc_macro::TokenStream as TokenStream1;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::export::ToTokens;
|
||||
use syn::parse::{Parse, ParseBuffer};
|
||||
use syn::parse_macro_input;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::token;
|
||||
use syn::Token;
|
||||
use syn::{braced, parenthesized};
|
||||
use syn::{Block, Error, Field, Stmt, Type, VisPublic, Visibility};
|
||||
|
||||
pub fn function_component_handler(attr: TokenStream, item: TokenStream1) -> TokenStream1 {
|
||||
let component_name = attr.to_string();
|
||||
assert!(
|
||||
!component_name.is_empty(),
|
||||
"you must provide a component name. eg: function_component(MyComponent)"
|
||||
);
|
||||
let component_name = Ident::new(&component_name, Span::call_site());
|
||||
let function = parse_macro_input!(item as Function);
|
||||
TokenStream1::from(
|
||||
FunctionComponentInfo {
|
||||
component_name,
|
||||
function,
|
||||
}
|
||||
.to_token_stream(),
|
||||
)
|
||||
}
|
||||
|
||||
pub struct FunctionComponentInfo {
|
||||
component_name: Ident,
|
||||
function: Function,
|
||||
}
|
||||
|
||||
// TODO, support type parameters
|
||||
|
||||
pub struct Function {
|
||||
pub vis: Visibility,
|
||||
pub fn_token: Token![fn],
|
||||
pub name: Ident,
|
||||
pub paren_token: token::Paren,
|
||||
pub fields: Punctuated<Field, Token![,]>,
|
||||
pub returns_token: Token![->],
|
||||
pub return_ty: Ident,
|
||||
pub brace_token: token::Brace,
|
||||
pub body: Vec<Stmt>,
|
||||
}
|
||||
|
||||
impl Parse for Function {
|
||||
fn parse(input: &ParseBuffer) -> Result<Self, Error> {
|
||||
let vis = input.parse()?;
|
||||
let fn_token = input.parse()?;
|
||||
let name = input.parse()?;
|
||||
let content;
|
||||
let paren_token = parenthesized!(content in input);
|
||||
let returns_token = input.parse()?;
|
||||
let return_ty = input.parse()?;
|
||||
let content2;
|
||||
let brace_token = braced!(content2 in input);
|
||||
Ok(Function {
|
||||
vis,
|
||||
fn_token,
|
||||
name,
|
||||
paren_token,
|
||||
fields: content.parse_terminated(Field::parse_named)?,
|
||||
returns_token,
|
||||
return_ty,
|
||||
brace_token,
|
||||
body: content2.call(Block::parse_within)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Function {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let Function {
|
||||
vis,
|
||||
fn_token,
|
||||
name,
|
||||
fields,
|
||||
returns_token,
|
||||
return_ty,
|
||||
body,
|
||||
..
|
||||
} = self;
|
||||
let fields = fields
|
||||
.iter()
|
||||
.map(|field: &Field| {
|
||||
let mut new_field: Field = field.clone();
|
||||
new_field.attrs = vec![];
|
||||
new_field
|
||||
})
|
||||
.collect::<Punctuated<_, Token![,]>>();
|
||||
|
||||
tokens.extend(quote! {
|
||||
#vis #fn_token #name(#fields) #returns_token #return_ty {
|
||||
#(#body)*
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for FunctionComponentInfo {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let FunctionComponentInfo {
|
||||
component_name,
|
||||
function,
|
||||
} = self;
|
||||
// The function tokens must be re-generated in order to strip the attributes that are not allowed.
|
||||
let function_token_stream = function.to_token_stream();
|
||||
let Function {
|
||||
vis, name, fields, ..
|
||||
} = function;
|
||||
|
||||
let impl_name = format!("FuncComp{}", component_name.to_string());
|
||||
let impl_name = Ident::new(&impl_name, Span::call_site());
|
||||
|
||||
let alias = quote! {
|
||||
#vis type #component_name = ::yewtil::Pure<#impl_name>;
|
||||
};
|
||||
|
||||
// Set the fields to be public and strips references as necessary.
|
||||
// This will preserve attributes like #[props(required)], which will appear in the generated struct below.
|
||||
let new_fields = fields
|
||||
.iter()
|
||||
.map(|field: &Field| {
|
||||
let mut new_field: Field = field.clone();
|
||||
let visibility = Visibility::Public(VisPublic {
|
||||
pub_token: syn::token::Pub {
|
||||
span: Span::call_site(),
|
||||
},
|
||||
});
|
||||
// Strip references so the component can have a static lifetime.
|
||||
// TODO Handle 'static lifetimes gracefully here - allowing &'static strings instead of erroneously converting them to plain strs.
|
||||
let ty = match &field.ty {
|
||||
Type::Reference(x) => {
|
||||
let elem = x.elem.clone();
|
||||
Type::Verbatim(quote! {
|
||||
#elem
|
||||
})
|
||||
}
|
||||
x => x.clone(),
|
||||
};
|
||||
new_field.vis = visibility;
|
||||
new_field.ty = ty;
|
||||
new_field
|
||||
})
|
||||
.collect::<Punctuated<_, Token![,]>>();
|
||||
|
||||
let component_struct = quote! {
|
||||
#[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::yew::Properties)]
|
||||
#vis struct #impl_name {
|
||||
#new_fields
|
||||
}
|
||||
};
|
||||
|
||||
let arguments = fields
|
||||
.iter()
|
||||
.zip(new_fields.iter())
|
||||
.map(|(field, new_field): (&Field, &Field)| {
|
||||
let field_name = field.ident.as_ref().expect("Field must have name");
|
||||
|
||||
// If the fields differ, then a reference was removed from the function's field's type
|
||||
// to make it static.
|
||||
// Otherwise it is assumed that the type is not a reference on the function and it
|
||||
// implements clone, and that when calling the function, the type should be cloned again.
|
||||
if field.ty != new_field.ty {
|
||||
quote! {
|
||||
&self.#field_name
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
self.#field_name.clone()
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Punctuated<_, Token![,]>>();
|
||||
|
||||
let pure_component_impl = quote! {
|
||||
impl ::yewtil::PureComponent for #impl_name {
|
||||
fn render(&self) -> ::yew::Html {
|
||||
#name(#arguments)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokens.extend(quote! {
|
||||
#function_token_stream
|
||||
#alias
|
||||
#component_struct
|
||||
#pure_component_impl
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
extern crate proc_macro;
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
use crate::function_component::function_component_handler;
|
||||
|
||||
mod function_component;
|
||||
#[proc_macro_attribute]
|
||||
pub fn function_component(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
function_component_handler(attr.into(), item)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
# Changelog
|
||||
|
||||
<!-- START TEMPLATE
|
||||
|
||||
## ✨ **VERSION** *(DATE)*
|
||||
|
||||
- #### ⚡️ Features
|
||||
- Sample
|
||||
- #### 🛠 Fixes
|
||||
- Sample
|
||||
- #### 🚨 Breaking changes
|
||||
- Sample
|
||||
|
||||
END TEMPLATE-->
|
||||
|
||||
## ✨ **v0.2.0** *11/18/19*
|
||||
- #### ⚡️ Features
|
||||
- Add new `FetchRequest` trait, `fetch_resource()` function, and `FetchState` enum
|
||||
to simplify making fetch requests using futures.
|
||||
- Add `Default` implementations to `Irc` and `Mrc`.
|
|
@ -0,0 +1,68 @@
|
|||
[package]
|
||||
name = "yewtil"
|
||||
version = "0.2.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "Utility crate for Yew"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/yewstack/yewtil"
|
||||
readme = "Readme.md"
|
||||
|
||||
[features]
|
||||
default = ["stable"] # Only stable is included by default.
|
||||
all = ["stable", "experimental"]
|
||||
|
||||
# Broad features
|
||||
## All features MUST be stable or experimental
|
||||
stable = ["neq", "pure", "history", "mrc_irc", "effect", "future"]
|
||||
experimental = ["dsl", "lrc", "with_callback", "fetch" ]
|
||||
|
||||
# Some pointers are stable, some experimental.
|
||||
# This makes sure you get all the pointers
|
||||
ptr = ["lrc", "mrc_irc"]
|
||||
|
||||
# Misc features
|
||||
neq = []
|
||||
pure = ["neq", "yewtil-macro"]
|
||||
with_callback = []
|
||||
history = []
|
||||
dsl = []
|
||||
effect = []
|
||||
fetch = ["serde", "serde_json", "neq", "future"]
|
||||
future = ["wasm-bindgen-futures", "wasm-bindgen", "stdweb", "futures", "web-sys"]
|
||||
|
||||
# Ptr features
|
||||
lrc = []
|
||||
mrc_irc = []
|
||||
|
||||
[dependencies]
|
||||
futures = {version = "0.3.1", optional = true}
|
||||
log = "0.4.8"
|
||||
serde = {version= "1.0.102", optional = true}
|
||||
serde_json = { version = "1.0.41", optional = true }
|
||||
wasm-bindgen = {version = "0.2.51", features=["serde-serialize"], optional = true}
|
||||
wasm-bindgen-futures = {version = "0.4.3", optional = true}
|
||||
yew = { path = "../yew" }
|
||||
yewtil-macro = { path = "../yewtil-macro", optional = true }
|
||||
|
||||
[dependencies.stdweb]
|
||||
version = "0.4.20"
|
||||
optional = true
|
||||
features = [
|
||||
"futures-support",
|
||||
"experimental_features_which_may_break_on_minor_version_bumps",
|
||||
]
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.31"
|
||||
optional = true
|
||||
features = [
|
||||
'Headers',
|
||||
'Request',
|
||||
'RequestInit',
|
||||
'RequestMode',
|
||||
'Response',
|
||||
'Window',
|
||||
'Location',
|
||||
'Storage',
|
||||
]
|
|
@ -0,0 +1,96 @@
|
|||
# Yewtil
|
||||
Utility crate for the [Yew](https://github.com/yewstack/yew) frontend web framework.
|
||||
|
||||
## Purpose
|
||||
Provide a place for commonly used utilities for Yew to reside without them having to be included in the Yew crate itself.
|
||||
As a consequence of this, the Yew crate is free to make changes that may cause breakages in this crate.
|
||||
|
||||
## Features
|
||||
Currently, this crate supports these features in a stable capacity:
|
||||
* `NeqAssign` - makes assigning props and returning a relevant ShouldRender value easier.
|
||||
* Pure Components - implement pure components using the `PureComponent` trait and the `Pure` Component adaptor.
|
||||
This should make it much easier to define simple components that don't hold state.
|
||||
* Function components - a macro that takes a function that returns `Html` and converts it to a pure component.
|
||||
* `Mrc`/`Irc` smart pointers - Rc-like pointers that are more ergonomic to use within Yew.
|
||||
* `History` - A wrapper that holds the history of values that have been assigned to it.
|
||||
* `Effect` - A way to update component state by defining what to change inside of `html!` callbacks
|
||||
instead of handling messages in `Component::update()`.
|
||||
|
||||
This crate also has an experimental feature flag that enables the following features:
|
||||
* `Lrc` smart pointer - Rc-like pointer implemented on top of a linked list. Allows for novel state update mechanics
|
||||
and traversal over linked shared pointers. <sup><sub>(This needs to be fuzz tested to make sure it doesn't leak.)</sub></sup>
|
||||
* DSL for `Html<Self>` - A function-based domain-specific-language for Yew that can be used in a limited capacity instead of the `html!` macro. <sup><sub>(Broken by recent changes in yew. Will be rewritten from scratch eventually.)</sub></sup>
|
||||
|
||||
These experimental features are either not sufficiently vetted, may change significantly, or may be removed.
|
||||
|
||||
## Example Projects
|
||||
Examples for every stable feature exist [here](https://github.com/yewstack/yew/tree/master/yewtil/examples).
|
||||
|
||||
Check out the [Pure Components example](https://github.com/yewstack/yew/tree/master/yewtil/examples/demo) to see how Pure Components work.
|
||||
|
||||
## Example
|
||||
#### neq_assign:
|
||||
```rust
|
||||
fn change(&mut self, props: Self::Properties) -> ShouldRender {
|
||||
self.props.neq_assign(props)
|
||||
}
|
||||
```
|
||||
|
||||
-------------
|
||||
|
||||
#### Pure Component:
|
||||
```rust
|
||||
pub type Button = Pure<PureButton>;
|
||||
|
||||
#[derive(PartialEq, Clone, Properties)]
|
||||
pub struct PureButton {
|
||||
pub callback: Callback<Msg>,
|
||||
#[prop_or_default]
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl PureComponent for PureButton {
|
||||
fn render(&self) -> VNode {
|
||||
html! {
|
||||
<button onclick=&self.callback>{ &self.text }</button>
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
--------------
|
||||
|
||||
#### History
|
||||
```rust
|
||||
pub struct Model {
|
||||
text: History<String>,
|
||||
}
|
||||
|
||||
// ...
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
Msg::SetText(text) => self.text.neq_set(text),
|
||||
Msg::Reset => self.text.reset(),
|
||||
Msg::Forget => {
|
||||
self.text.forget();
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Update Schedule
|
||||
This crate will target stable Yew.
|
||||
|
||||
As new idioms are introduced to Yew, this crate may see updates, but given the rarity of those, this crate may sit unaltered for some time.
|
||||
|
||||
## Scoping
|
||||
This crate aims to be more permissive in what is allowed in than Yew, so if you have a function, type, or trait you would like to share, please open a PR or Issue.
|
||||
|
||||
Components are welcome as well, but they must not have external dependencies, should solve some problem encountered my many users of Yew, and should allow for theming if possible, like an auto-scrolling wrapper, a RecyclerView/Infinite-scrolling component, or possibly a comprehensive Input component.
|
||||
|
||||
Common UI elements like modals or dropdowns should probably best be left to CSS-framework component libraries, as they should often be coupled to the external CSS used to display them.
|
||||
|
||||
### Stability
|
||||
Since this crate aims to present a variety of helper types, traits, and functions, where the utility of each may be unknown at the time the feature is added, newer additions may be not be included in the default feature-set, and may be locked behind an `experimental` flag.
|
||||
While in early development, features marked as `experimental` may be changed more frequently or even entirely removed, while those marked as `stable` will not be removed and can be depended on to not change significantly.
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "dsl"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT/Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.8"
|
||||
web_logger = "0.2.0"
|
||||
yew = { path = "../../../yew" }
|
||||
yewtil = { path = "../..", features = ["dsl"] }
|
|
@ -0,0 +1,49 @@
|
|||
use yew::{Component, ComponentLink, Html, ShouldRender};
|
||||
|
||||
use yewtil::dsl::{list, populated_list, tag, text, BoxedVNodeProducer};
|
||||
|
||||
pub struct Model {}
|
||||
|
||||
pub enum Msg {
|
||||
DoIt,
|
||||
}
|
||||
|
||||
impl Component for Model {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
|
||||
Model {}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
Msg::DoIt => {
|
||||
log::info!("got message");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: ()) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
BoxedVNodeProducer::from(
|
||||
list()
|
||||
.child(text("Hello there"))
|
||||
.child(tag("p").child(text("Paragraph content")))
|
||||
.child(populated_list(vec![
|
||||
tag("b").child(text("Bolded")).into(),
|
||||
text("Normal text").into(),
|
||||
])),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
web_logger::init();
|
||||
yew::start_app::<Model>();
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "effect"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.8"
|
||||
web_logger = "0.2.0"
|
||||
yew = { path = "../../../yew" }
|
||||
yewtil = { path = "../..", features = ["effect"] }
|
|
@ -0,0 +1,49 @@
|
|||
use yew::{html, Component, ComponentLink, Html, ShouldRender};
|
||||
use yewtil::{effect, Effect};
|
||||
|
||||
pub struct Model {
|
||||
value: bool,
|
||||
link: ComponentLink<Self>,
|
||||
}
|
||||
|
||||
impl Component for Model {
|
||||
type Message = Effect<Self>;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
Model { value: false, link }
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
msg.call(self)
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: ()) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<div>
|
||||
{self.value}
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onclick=self.link.callback(|_| effect(|model: &mut Self| {
|
||||
model.value = !model.value;
|
||||
true
|
||||
}))
|
||||
>
|
||||
{"Toggle"}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
web_logger::init();
|
||||
yew::start_app::<Model>();
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# Allow cargo-check to work as expected.
|
||||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "fetch"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.8"
|
||||
serde = "1.0.102"
|
||||
wasm-bindgen = "0.2.51"
|
||||
web_logger = "0.2.0"
|
||||
yew = { path = "../../../yew" }
|
||||
yewtil = { path = "../..", features = ["fetch"] }
|
|
@ -0,0 +1,9 @@
|
|||
Shows off ergonomic JSON deserialization fetch abstraction.
|
||||
|
||||
Run with:
|
||||
|
||||
```shell script
|
||||
wasm-pack build --target web && rollup ./main.js --format iife --file ./pkg/bundle.js && python -m SimpleHTTPServer 8080
|
||||
```
|
||||
|
||||
It is expected that you have a setup with wasm-pack, rollup, and python installed.
|
|
@ -0,0 +1,10 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Yewtil • Fetch</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="/pkg/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,6 @@
|
|||
import init, { run_app } from './pkg/fetch.js';
|
||||
async function main() {
|
||||
await init('./pkg/fetch_bg.wasm');
|
||||
run_app();
|
||||
}
|
||||
main()
|
|
@ -0,0 +1,122 @@
|
|||
use crate::Msg::SetMarkdownFetchState;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use yew::{html, Component, ComponentLink, Html, ShouldRender};
|
||||
use yewtil::fetch::{Fetch, FetchAction, FetchRequest, FetchState, Json, MethodBody};
|
||||
use yewtil::future::LinkFuture;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn run_app() {
|
||||
yew::start_app::<Model>();
|
||||
}
|
||||
|
||||
struct Model {
|
||||
markdown: Fetch<Request, Vec<Employee>>,
|
||||
link: ComponentLink<Self>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Request;
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Employee {
|
||||
id: String,
|
||||
employee_name: String,
|
||||
employee_salary: String,
|
||||
employee_age: String,
|
||||
profile_image: String,
|
||||
}
|
||||
|
||||
impl FetchRequest for Request {
|
||||
type RequestBody = ();
|
||||
type ResponseBody = Vec<Employee>;
|
||||
type Format = Json;
|
||||
|
||||
fn url(&self) -> String {
|
||||
// Given that this is an external resource, this may fail sometime in the future.
|
||||
// Please report any regressions related to this.
|
||||
"http://dummy.restapiexample.com/api/v1/employees".to_string()
|
||||
}
|
||||
|
||||
fn method(&self) -> MethodBody<Self::RequestBody> {
|
||||
MethodBody::Get
|
||||
}
|
||||
|
||||
fn headers(&self) -> Vec<(String, String)> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn use_cors(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
enum Msg {
|
||||
SetMarkdownFetchState(FetchAction<Vec<Employee>>),
|
||||
GetMarkdown,
|
||||
}
|
||||
|
||||
impl Component for Model {
|
||||
// Some details omitted. Explore the examples to see more.
|
||||
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
Model {
|
||||
markdown: Default::default(),
|
||||
link,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
Msg::SetMarkdownFetchState(fetch_state) => {
|
||||
self.markdown.apply(fetch_state);
|
||||
true
|
||||
}
|
||||
Msg::GetMarkdown => {
|
||||
self.link
|
||||
.send_future(self.markdown.fetch(Msg::SetMarkdownFetchState));
|
||||
self.link
|
||||
.send_message(SetMarkdownFetchState(FetchAction::Fetching));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
match self.markdown.as_ref().state() {
|
||||
FetchState::NotFetching(_) => {
|
||||
html! {<button onclick=self.link.callback(|_| Msg::GetMarkdown)>{"Get employees"}</button>}
|
||||
}
|
||||
FetchState::Fetching(_) => html! {"Fetching"},
|
||||
FetchState::Fetched(data) => data.iter().map(render_employee).collect(),
|
||||
FetchState::Failed(_, err) => html! {&err},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_employee(e: &Employee) -> Html {
|
||||
html! {
|
||||
<div>
|
||||
<div>
|
||||
{"Name: "}
|
||||
{&e.employee_name}
|
||||
</div>
|
||||
<div>
|
||||
{"Salary: "}
|
||||
{&e.employee_salary}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{"Age: "}
|
||||
{&e.employee_age}
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "function_component"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.8"
|
||||
web_logger = "0.2.0"
|
||||
yew = { path = "../../../yew" }
|
||||
yewtil = { path = "../.." }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", not(cargo_web)))'.dependencies]
|
||||
wasm-bindgen = "0.2.51"
|
|
@ -0,0 +1,13 @@
|
|||
use yew::{html, Callback, Html, MouseEvent};
|
||||
use yewtil::function_component;
|
||||
|
||||
#[function_component(Button)]
|
||||
pub fn button(
|
||||
callback: &Callback<MouseEvent>,
|
||||
#[prop_or_default] text: String,
|
||||
#[prop_or_default] _num: usize,
|
||||
) -> Html {
|
||||
html! {
|
||||
<button onclick=callback>{ text }</button>
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
use yew::{html, Component, ComponentLink, Html, ShouldRender};
|
||||
|
||||
mod button;
|
||||
use crate::button::Button;
|
||||
|
||||
pub struct Model {
|
||||
link: ComponentLink<Self>,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
DoIt,
|
||||
}
|
||||
|
||||
impl Component for Model {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
Model { link }
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
Msg::DoIt => {
|
||||
log::info!("got message");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: ()) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! {
|
||||
<Button callback=self.link.callback(|_| Msg::DoIt) text = "Click me!" />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
web_logger::init();
|
||||
yew::start_app::<Model>();
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "futures-yewtil"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "web_sys fetching and futures demonstration"
|
||||
license = "MIT/Apache"
|
||||
repository = "https://github.com/yewstack/yew"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2.51"
|
||||
wasm-bindgen-futures = "0.4.3"
|
||||
yew = { path = "../../../yew" }
|
||||
yewtil = { path = "../.." }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.30"
|
||||
features = [
|
||||
'Headers',
|
||||
'Request',
|
||||
'RequestInit',
|
||||
'RequestMode',
|
||||
'Response',
|
||||
'Window',
|
||||
]
|
|
@ -0,0 +1,14 @@
|
|||
# Futures Example
|
||||
This example shows off how to make a asynchronous fetch request using web_sys and Yew's futures support.
|
||||
|
||||
Because this example uses features not allowed by cargo web, it cannot be included in the showcase, and must be built with a different toolchain instead.
|
||||
|
||||
### How to run:
|
||||
This example requires rustc v1.39.0 or above to compile due to its use of async/.await syntax.
|
||||
|
||||
```sh
|
||||
wasm-pack build --target web && rollup ./main.js --format iife --file ./pkg/bundle.js && python -m SimpleHTTPServer 8080
|
||||
```
|
||||
This will compile the project, bundle up the compiler output and static assets, and start a http server on port 8080 so you can access the example at localhost:8080.
|
||||
|
||||
It is expected that you have a setup with wasm-pack, rollup, and python installed.
|
|
@ -0,0 +1,10 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Yew • Futures</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="/pkg/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,6 @@
|
|||
import init, { run_app } from './pkg/futures.js';
|
||||
async function main() {
|
||||
await init('./pkg/futures_bg.wasm');
|
||||
run_app();
|
||||
}
|
||||
main()
|
|
@ -0,0 +1,125 @@
|
|||
use crate::Msg::SetMarkdownFetchState;
|
||||
use std::fmt::{Error, Formatter};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{Request, RequestInit, RequestMode, Response, Window};
|
||||
use yew::{html, Component, ComponentLink, Html, ShouldRender};
|
||||
use yewtil::future::LinkFuture;
|
||||
|
||||
/// Something wrong has occurred while fetching an external resource.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FetchError {
|
||||
err: JsValue,
|
||||
}
|
||||
impl std::fmt::Display for FetchError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
|
||||
std::fmt::Debug::fmt(&self.err, f)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for FetchError {}
|
||||
|
||||
impl From<JsValue> for FetchError {
|
||||
fn from(value: JsValue) -> Self {
|
||||
FetchError { err: value }
|
||||
}
|
||||
}
|
||||
|
||||
/// The possible states a fetch request can be in.
|
||||
pub enum FetchState<T> {
|
||||
NotFetching,
|
||||
Fetching,
|
||||
Success(T),
|
||||
Failed(FetchError),
|
||||
}
|
||||
|
||||
/// Fetches markdown from Yew's README.md.
|
||||
///
|
||||
/// Consult the following for an example of the fetch api by the team behind web_sys:
|
||||
/// https://rustwasm.github.io/wasm-bindgen/examples/fetch.html
|
||||
async fn fetch_markdown() -> Result<String, FetchError> {
|
||||
let mut opts = RequestInit::new();
|
||||
opts.method("GET");
|
||||
opts.mode(RequestMode::Cors);
|
||||
|
||||
let request = Request::new_with_str_and_init(
|
||||
"https://raw.githubusercontent.com/yewstack/yew/master/README.md",
|
||||
&opts,
|
||||
)?;
|
||||
|
||||
let window: Window = web_sys::window().unwrap();
|
||||
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||
assert!(resp_value.is_instance_of::<Response>());
|
||||
|
||||
let resp: Response = resp_value.dyn_into().unwrap();
|
||||
|
||||
let text = JsFuture::from(resp.text()?).await?;
|
||||
Ok(text.as_string().unwrap())
|
||||
}
|
||||
|
||||
struct Model {
|
||||
markdown: FetchState<String>,
|
||||
link: ComponentLink<Self>,
|
||||
}
|
||||
|
||||
enum Msg {
|
||||
SetMarkdownFetchState(FetchState<String>),
|
||||
GetMarkdown,
|
||||
}
|
||||
|
||||
impl Component for Model {
|
||||
// Some details omitted. Explore the examples to see more.
|
||||
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
Model {
|
||||
markdown: FetchState::NotFetching,
|
||||
link,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
Msg::SetMarkdownFetchState(fetch_state) => {
|
||||
self.markdown = fetch_state;
|
||||
true
|
||||
}
|
||||
Msg::GetMarkdown => {
|
||||
let future = async {
|
||||
match fetch_markdown().await {
|
||||
Ok(md) => Msg::SetMarkdownFetchState(FetchState::Success(md)),
|
||||
Err(err) => Msg::SetMarkdownFetchState(FetchState::Failed(err)),
|
||||
}
|
||||
};
|
||||
self.link.send_future(future);
|
||||
self.link
|
||||
.send_message(SetMarkdownFetchState(FetchState::Fetching));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: ()) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
match &self.markdown {
|
||||
FetchState::NotFetching => html! {
|
||||
<button onclick=self.link.callback(|_| Msg::GetMarkdown)>
|
||||
{"Get Markdown"}
|
||||
</button>
|
||||
},
|
||||
FetchState::Fetching => html! {"Fetching"},
|
||||
FetchState::Success(data) => html! {&data},
|
||||
FetchState::Failed(err) => html! {&err},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn run_app() {
|
||||
yew::start_app::<Model>();
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "history"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT/Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.8"
|
||||
web_logger = "0.2.0"
|
||||
yew = { path = "../../../yew" }
|
||||
yewtil = { path = "../.." }
|
|
@ -0,0 +1,64 @@
|
|||
use yew::{html, Component, ComponentLink, Html, InputData, ShouldRender};
|
||||
use yewtil::History;
|
||||
|
||||
pub struct Model {
|
||||
text: History<String>,
|
||||
link: ComponentLink<Self>,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
SetText(String),
|
||||
Reset,
|
||||
Forget,
|
||||
}
|
||||
|
||||
impl Component for Model {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
Model {
|
||||
text: History::new("".to_string()),
|
||||
link,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
Msg::SetText(text) => self.text.neq_set(text),
|
||||
Msg::Reset => self.text.reset(),
|
||||
Msg::Forget => {
|
||||
self.text.forget();
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: ()) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<div>
|
||||
{&*self.text}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type = "text"
|
||||
value = &*self.text
|
||||
oninput = self.link.callback(|x: InputData| Msg::SetText(x.value))
|
||||
/>
|
||||
<button onclick=self.link.callback(|_| Msg::Reset)>{"Reset to the oldest value"} </button>
|
||||
<button onclick=self.link.callback(|_| Msg::Forget)>{"Forget prior values"} </button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
web_logger::init();
|
||||
yew::start_app::<Model>();
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "lrc"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT/Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.8"
|
||||
web_logger = "0.2.0"
|
||||
yew = { path = "../../../yew" }
|
||||
yewtil = { path = "../..", features = ["lrc"] }
|
|
@ -0,0 +1,61 @@
|
|||
use yew::{
|
||||
events::InputData, html, Callback, Component, ComponentLink, Html, Properties, ShouldRender,
|
||||
};
|
||||
use yewtil::ptr::Lrc;
|
||||
use yewtil::NeqAssign;
|
||||
|
||||
#[derive(PartialEq, Clone, Properties)]
|
||||
pub struct Props {
|
||||
pub text: Lrc<String>,
|
||||
pub callback: Callback<()>,
|
||||
}
|
||||
|
||||
pub struct Child {
|
||||
props: Props,
|
||||
on_input: Callback<InputData>,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
UpdateText(InputData),
|
||||
}
|
||||
|
||||
impl Component for Child {
|
||||
type Message = Msg;
|
||||
type Properties = Props;
|
||||
|
||||
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
Child {
|
||||
props,
|
||||
on_input: link.callback(Msg::UpdateText),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
Msg::UpdateText(input) => {
|
||||
// Only update the Lrc if the new value is different.
|
||||
self.props.text.neq_set(input.value);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn change(&mut self, props: Self::Properties) -> ShouldRender {
|
||||
self.props.neq_assign(props)
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<input
|
||||
type = "text"
|
||||
value = self.props.text.as_ref()
|
||||
oninput = &self.on_input
|
||||
/>
|
||||
<button onclick=self.props.callback.reform(|_| ()) >
|
||||
{"Update parent"}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
use yew::{html, Callback, Component, ComponentLink, Html, ShouldRender};
|
||||
use yewtil::ptr::Lrc;
|
||||
|
||||
mod child;
|
||||
use crate::child::Child;
|
||||
|
||||
pub struct Model {
|
||||
text: Lrc<String>,
|
||||
update_text: Callback<()>,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
UpdateTextAtADistance,
|
||||
}
|
||||
|
||||
impl Component for Model {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
Model {
|
||||
text: Lrc::new("".to_string()),
|
||||
update_text: link.callback(|_| Msg::UpdateTextAtADistance),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
Msg::UpdateTextAtADistance => self.text.update(),
|
||||
}
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: ()) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<div>
|
||||
{&*self.text}
|
||||
</div>
|
||||
// Either of the children's update buttons will cause this component's text
|
||||
// to update to the most recently edited text.
|
||||
<div>
|
||||
<Child text=&self.text callback = &self.update_text />
|
||||
</div>
|
||||
<div>
|
||||
<Child text=&self.text callback = &self.update_text />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
web_logger::init();
|
||||
yew::start_app::<Model>();
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "mrc_irc"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT/Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.8"
|
||||
web_logger = "0.2.0"
|
||||
yew = { path = "../../../yew" }
|
||||
yewtil = { path = "../.." }
|
|
@ -0,0 +1,47 @@
|
|||
use yew::{
|
||||
events::InputData, html, Callback, Component, ComponentLink, Html, Properties, ShouldRender,
|
||||
};
|
||||
use yewtil::ptr::Irc;
|
||||
use yewtil::NeqAssign;
|
||||
|
||||
#[derive(Clone, PartialEq, Properties)]
|
||||
pub struct Props {
|
||||
/// This value can't be altered.
|
||||
pub text: Irc<String>,
|
||||
/// This heavily implies the only way to update the text field is to send a message back
|
||||
/// to the parent to have the parent component update it.
|
||||
pub callback: Callback<String>,
|
||||
}
|
||||
|
||||
pub struct Child {
|
||||
props: Props,
|
||||
}
|
||||
|
||||
impl Component for Child {
|
||||
type Message = ();
|
||||
type Properties = Props;
|
||||
|
||||
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
|
||||
Child { props }
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn change(&mut self, props: Self::Properties) -> ShouldRender {
|
||||
self.props.neq_assign(props)
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<input
|
||||
type = "text"
|
||||
value = &*self.props.text
|
||||
oninput = self.props.callback.reform(|i: InputData| i.value)
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
use yew::{html, Callback, Component, ComponentLink, Html, ShouldRender};
|
||||
use yewtil::ptr::Mrc;
|
||||
|
||||
mod child;
|
||||
use crate::child::Child;
|
||||
use yewtil::NeqAssign;
|
||||
|
||||
pub struct Model {
|
||||
text: Mrc<String>,
|
||||
update_text: Callback<String>,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
UpdateText(String),
|
||||
}
|
||||
|
||||
impl Component for Model {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
Model {
|
||||
text: Mrc::new("".to_string()),
|
||||
update_text: link.callback(Msg::UpdateText),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
Msg::UpdateText(text) => {
|
||||
// Because Mrc<T> implements BorrowMut<T>, neq assign can be used here.
|
||||
self.text.neq_assign(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn change(&mut self, _: ()) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<div>
|
||||
{&*self.text}
|
||||
</div>
|
||||
<div>
|
||||
// By passing an `Irc`, we strongly imply that the value should not be updated
|
||||
// by the child. An effort to modify the value downstream is easily identified
|
||||
// as subverting the contract implied by using `Irc`s.
|
||||
<Child text=&self.text.irc() callback=&self.update_text />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
web_logger::init();
|
||||
yew::start_app::<Model>();
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "pure_component"
|
||||
version = "0.1.0"
|
||||
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT/Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.8"
|
||||
web_logger = "0.2.0"
|
||||
yew = { path = "../../../yew" }
|
||||
yewtil = { path = "../.." }
|
|
@ -0,0 +1,21 @@
|
|||
use yew::virtual_dom::VNode;
|
||||
use yew::{html, Callback, MouseEvent, Properties};
|
||||
use yewtil::{Pure, PureComponent};
|
||||
|
||||
/// Alias to make usability better.
|
||||
pub type Button = Pure<PureButton>;
|
||||
|
||||
#[derive(Clone, PartialEq, Properties)]
|
||||
pub struct PureButton {
|
||||
pub callback: Callback<MouseEvent>,
|
||||
#[prop_or_default]
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl PureComponent for PureButton {
|
||||
fn render(&self) -> VNode {
|
||||
html! {
|
||||
<button onclick=&self.callback>{ &self.text }</button>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
use yew::{html, Component, ComponentLink, Html, ShouldRender};
|
||||
|
||||
mod button;
|
||||
use crate::button::Button;
|
||||
|
||||
pub struct Model {
|
||||
link: ComponentLink<Self>,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
DoIt,
|
||||
}
|
||||
|
||||
impl Component for Model {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
Model { link }
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
Msg::DoIt => {
|
||||
log::info!("got message");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: ()) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! {
|
||||
<Button callback=self.link.callback(|_| Msg::DoIt) text = "Click me!" />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
web_logger::init();
|
||||
yew::start_app::<Model>();
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
pub use crate::dsl::vcomp::VCompProducer;
|
||||
use crate::dsl::vlist::VListProducer;
|
||||
pub use crate::dsl::vtag::VTagProducer;
|
||||
pub use crate::dsl::vtext::VTextProducer;
|
||||
use yew::virtual_dom::vcomp::ScopeHolder;
|
||||
use yew::virtual_dom::VNode;
|
||||
use yew::Component;
|
||||
|
||||
mod vcomp;
|
||||
mod vlist;
|
||||
mod vtag;
|
||||
mod vtext;
|
||||
|
||||
/// Wrapper around a function that produces a vnode.
|
||||
pub struct BoxedVNodeProducer<COMP: Component>(Box<dyn FnOnce(ScopeHolder<COMP>) -> VNode<COMP>>);
|
||||
|
||||
impl<COMP: Component> BoxedVNodeProducer<COMP> {
|
||||
fn wrap(f: impl FnOnce(ScopeHolder<COMP>) -> VNode<COMP> + 'static) -> Self {
|
||||
BoxedVNodeProducer(Box::new(f))
|
||||
}
|
||||
fn execute(self, scope: &ScopeHolder<COMP>) -> VNode<COMP> {
|
||||
(self.0)(scope.clone())
|
||||
}
|
||||
pub fn build(self) -> VNode<COMP> {
|
||||
let scope = ScopeHolder::default();
|
||||
self.execute(&scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: Component> Into<VNode<COMP>> for BoxedVNodeProducer<COMP> {
|
||||
fn into(self) -> VNode<COMP> {
|
||||
self.build()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a tag node.
|
||||
pub fn tag<COMP: Component>(tag: &'static str) -> VTagProducer<COMP> {
|
||||
VTagProducer::new(tag)
|
||||
}
|
||||
|
||||
/// Creates a component (Specified by the second type parameter).
|
||||
pub fn comp<COMP: Component, CHILD: Component>(props: CHILD::Properties) -> VCompProducer<COMP> {
|
||||
VCompProducer::new::<CHILD>(props)
|
||||
}
|
||||
|
||||
/// Creates a text node
|
||||
pub fn text<COMP: Component, T: Into<String> + 'static>(text: T) -> VTextProducer<COMP> {
|
||||
VTextProducer::new::<T>(text)
|
||||
}
|
||||
|
||||
/// Creates a new vlist, populated with the provided vnodes
|
||||
pub fn populated_list<COMP: Component>(list: Vec<BoxedVNodeProducer<COMP>>) -> VListProducer<COMP> {
|
||||
VListProducer::populated_new(list)
|
||||
}
|
||||
|
||||
/// Creates a new vlist
|
||||
pub fn list<COMP: Component>() -> VListProducer<COMP> {
|
||||
VListProducer::new()
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
use crate::dsl::BoxedVNodeProducer;
|
||||
use yew::virtual_dom::vcomp::ScopeHolder;
|
||||
use yew::virtual_dom::VComp;
|
||||
use yew::{Component, NodeRef};
|
||||
|
||||
pub struct VCompProducer<COMP: Component>(Box<dyn FnOnce(ScopeHolder<COMP>) -> VComp<COMP>>);
|
||||
|
||||
impl<COMP: Component> VCompProducer<COMP> {
|
||||
pub fn new<CHILD: Component>(props: CHILD::Properties) -> Self {
|
||||
// TODO allow getting the noderef as a parameter somewhere.
|
||||
VCompProducer(Box::new(move |scope| VComp::new::<CHILD>(props, scope, NodeRef::default())))
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: Component> From<VCompProducer<COMP>> for BoxedVNodeProducer<COMP> {
|
||||
fn from(vcomp_prod: VCompProducer<COMP>) -> Self {
|
||||
BoxedVNodeProducer::wrap(move |scope| (vcomp_prod.0)(scope).into())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
use crate::dsl::BoxedVNodeProducer;
|
||||
use yew::virtual_dom::VList;
|
||||
use yew::Component;
|
||||
|
||||
pub struct VListProducer<COMP: Component> {
|
||||
children: Vec<BoxedVNodeProducer<COMP>>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> VListProducer<COMP> {
|
||||
pub fn new() -> Self {
|
||||
VListProducer { children: vec![] }
|
||||
}
|
||||
|
||||
pub fn child<T: Into<BoxedVNodeProducer<COMP>>>(mut self, child: T) -> Self {
|
||||
self.children.push(child.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn populated_new(children: Vec<BoxedVNodeProducer<COMP>>) -> Self {
|
||||
VListProducer { children }
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: Component> From<VListProducer<COMP>> for BoxedVNodeProducer<COMP> {
|
||||
fn from(vlist_prod: VListProducer<COMP>) -> Self {
|
||||
BoxedVNodeProducer(Box::new(move |scope| {
|
||||
let mut vlist = VList::new();
|
||||
for child in vlist_prod.children {
|
||||
let child = child.execute(&scope);
|
||||
vlist.add_child(child);
|
||||
}
|
||||
vlist.into()
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
use crate::dsl::BoxedVNodeProducer;
|
||||
use yew::virtual_dom::vcomp::ScopeHolder;
|
||||
use yew::virtual_dom::{Listener, VTag};
|
||||
use yew::{Classes, Component};
|
||||
|
||||
pub struct Effect<T, COMP: Component>(Box<dyn FnOnce(T, &ScopeHolder<COMP>) -> T>);
|
||||
impl<T, COMP: Component> Effect<T, COMP> {
|
||||
fn new(f: impl FnOnce(T, &ScopeHolder<COMP>) -> T + 'static) -> Self {
|
||||
Effect(Box::new(f))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VTagProducer<COMP: Component> {
|
||||
tag_type: &'static str,
|
||||
effects: Vec<Effect<VTag<COMP>, COMP>>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> VTagProducer<COMP> {
|
||||
pub fn new(tag_type: &'static str) -> Self {
|
||||
VTagProducer {
|
||||
tag_type,
|
||||
effects: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
// TODO, consider making this T: Into<VNode> - The whole dsl doesn't need to be lazy. - although being generic over an additional argument that is either () OR Scope is problematic.
|
||||
pub fn child<T: Into<BoxedVNodeProducer<COMP>> + 'static>(mut self, child: T) -> Self {
|
||||
let effect = Effect::new(move |mut vtag: VTag<COMP>, scope: &ScopeHolder<COMP>| {
|
||||
let child = child.into().execute(scope);
|
||||
vtag.add_child(child);
|
||||
vtag
|
||||
});
|
||||
self.effects.push(effect);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn attribute(mut self, name: String, value: String) -> Self {
|
||||
let effect = Effect::new(move |mut vtag: VTag<COMP>, _scope: &ScopeHolder<COMP>| {
|
||||
vtag.add_attribute(&name, &value);
|
||||
vtag
|
||||
});
|
||||
self.effects.push(effect);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn listener(mut self, listener: Box<dyn Listener<COMP>>) -> Self {
|
||||
let effect = Effect::new(move |mut vtag: VTag<COMP>, _scope: &ScopeHolder<COMP>| {
|
||||
vtag.add_listener(listener);
|
||||
vtag
|
||||
});
|
||||
self.effects.push(effect);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn classes(mut self, classes: Classes) -> Self {
|
||||
let effect = Effect::new(move |mut vtag: VTag<COMP>, _scope: &ScopeHolder<COMP>| {
|
||||
vtag.set_classes(classes);
|
||||
vtag
|
||||
});
|
||||
self.effects.push(effect);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: Component> From<VTagProducer<COMP>> for BoxedVNodeProducer<COMP> {
|
||||
fn from(vtag_prod: VTagProducer<COMP>) -> Self {
|
||||
BoxedVNodeProducer::wrap(move |scope| {
|
||||
let mut vtag = VTag::new(vtag_prod.tag_type);
|
||||
for effect in vtag_prod.effects.into_iter() {
|
||||
vtag = (effect.0)(vtag, &scope)
|
||||
}
|
||||
vtag.into()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
use crate::dsl::BoxedVNodeProducer;
|
||||
use yew::virtual_dom::VText;
|
||||
use yew::Component;
|
||||
|
||||
pub struct VTextProducer<COMP: Component>(Box<dyn FnOnce() -> VText<COMP>>);
|
||||
|
||||
impl<COMP: Component> VTextProducer<COMP> {
|
||||
pub fn new<T: Into<String> + 'static>(text: T) -> Self {
|
||||
VTextProducer(Box::new(move || VText::new(text.into())))
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: Component> From<VTextProducer<COMP>> for BoxedVNodeProducer<COMP> {
|
||||
fn from(vtext_prod: VTextProducer<COMP>) -> Self {
|
||||
BoxedVNodeProducer::wrap(move |_scope| (vtext_prod.0)().into())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
use std::rc::Rc;
|
||||
use yew::ShouldRender;
|
||||
|
||||
/// Alternative to using Message enums.
|
||||
///
|
||||
/// Using Effects instead of Messages allows you to define the mutation to your component's state
|
||||
/// from inside `html!` macros instead of from within update functions.
|
||||
pub struct Effect<COMP>(Box<dyn Fn(&mut COMP) -> ShouldRender>);
|
||||
|
||||
impl<COMP> Default for Effect<COMP> {
|
||||
fn default() -> Self {
|
||||
Effect::new(|_| false)
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP> Effect<COMP> {
|
||||
/// Wraps a function in an Effect wrapper.
|
||||
pub fn new(f: impl Fn(&mut COMP) -> ShouldRender + 'static) -> Self {
|
||||
Effect(Box::new(f))
|
||||
}
|
||||
|
||||
/// Runs the effect, causing a mutation to the component state.
|
||||
pub fn call(self, component: &mut COMP) -> ShouldRender {
|
||||
(self.0)(component)
|
||||
}
|
||||
}
|
||||
|
||||
/// Terser wrapper function to be used instead of `Effect::new()`.
|
||||
pub fn effect<COMP>(f: impl Fn(&mut COMP) -> ShouldRender + 'static) -> Effect<COMP> {
|
||||
Effect::new(f)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod wip {
|
||||
use super::*;
|
||||
|
||||
// TODO, once function_traits stabalize, this `to_effect()` will be able to be replaced with just a () call, making this more ergonomic.
|
||||
// https://github.com/rust-lang/rust/issues/29625
|
||||
// TODO, change naming of this.
|
||||
|
||||
// TODO. Consider an arbitrary state holder: Hashmap<&str, Box<dyn Any + 'static>. It might be possible to write a function that returns a &T, and a StateHook<COMP, T>
|
||||
// TODO for any given T that could be inserted into the holder. This might work well if the state holder itself is a component.
|
||||
// Actually, as long as order is preserved, a Vec<Box<dyn Any + 'static>>, might work just as well.
|
||||
// This would replicate the useState hook in react https://reactjs.org/docs/hooks-state.html
|
||||
|
||||
/// Wrapper around a mutable accessor to one of the component's (or another construct capabale of storing state's) fields.
|
||||
pub struct StateHook<STORE, T>(Rc<dyn Fn(&mut STORE) -> &mut T>);
|
||||
|
||||
impl<STORE: 'static, T: 'static> StateHook<STORE, T> {
|
||||
/// Creates a new state hook.
|
||||
pub fn new(mutable_accessor: impl Fn(&mut STORE) -> &mut T + 'static) -> Self {
|
||||
StateHook(Rc::new(mutable_accessor))
|
||||
}
|
||||
|
||||
/// Creates an effect using the wrapped accessor and a mutation function for the `T`.
|
||||
pub fn to_effect(&self, f: impl Fn(&mut T) -> ShouldRender + 'static) -> Effect<STORE> {
|
||||
let mutable_accessor = self.0.clone();
|
||||
Effect::new(move |comp| {
|
||||
let t = (mutable_accessor)(comp);
|
||||
f(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
//! Feature to enable fetching using web_sys-based fetch requests.
|
||||
//!
|
||||
//! This feature makes use of JSON for the request and response bodies.
|
||||
//!
|
||||
//! # Note
|
||||
//! Because this makes use of futures, enabling this feature will require the use of a
|
||||
//! web_sys compatible build environment and will prevent you from using `cargo web`.
|
||||
|
||||
use crate::NeqAssign; // requires "neq" feature.
|
||||
|
||||
mod action;
|
||||
mod error;
|
||||
mod request;
|
||||
mod state;
|
||||
|
||||
pub use self::action::*;
|
||||
pub use self::error::*;
|
||||
pub use self::request::*;
|
||||
pub use self::state::*;
|
||||
use std::future::Future;
|
||||
use wasm_bindgen::__rt::core::marker::PhantomData;
|
||||
|
||||
/// Indicates that a change was caused by a set function.
|
||||
pub type DidChange = bool;
|
||||
|
||||
/// A fetch type that is useful for when you don't hold any request directly.
|
||||
///
|
||||
/// This is useful for GET and DELETE requests where additional information needed to create the request object
|
||||
/// can be provided by a closure.
|
||||
pub type AcquireFetch<T> = Fetch<(), T>;
|
||||
|
||||
/// A fetch type that is useful for when the request type is the same as the response type.
|
||||
///
|
||||
/// This makes sense to use when the request and response bodies are exactly the same.
|
||||
/// Some PUT requests are amenable to this arrangement.
|
||||
pub type ModifyFetch<T> = Fetch<T, T>;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub struct Fetch<REQ, RES> {
|
||||
request: REQ,
|
||||
response: FetchState<RES>,
|
||||
}
|
||||
|
||||
impl<REQ: PartialEq, RES> Fetch<REQ, RES> {
|
||||
/// Sets the request without changing the variant.
|
||||
pub fn set_req(&mut self, request: REQ) -> DidChange {
|
||||
self.request.neq_assign(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl<REQ: Default, RES: PartialEq> Fetch<REQ, RES> {
|
||||
/// Sets the Fetch wrapper to indicate that a request was successfully fetched.
|
||||
pub fn set_fetched(&mut self, res: RES) -> DidChange {
|
||||
let will_change = match &self.response {
|
||||
FetchState::Fetched(old_res) => &res == old_res,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
let old = std::mem::take(&mut self.response);
|
||||
let new = old.fetched(res);
|
||||
std::mem::replace(&mut self.response, new);
|
||||
|
||||
will_change
|
||||
}
|
||||
|
||||
/// Apply a FetchAction to alter the Fetch wrapper to perform a state change.
|
||||
pub fn apply(&mut self, action: FetchAction<RES>) -> DidChange {
|
||||
match action {
|
||||
FetchAction::NotFetching => self.set_not_fetching(),
|
||||
FetchAction::Fetching => self.set_fetching(),
|
||||
FetchAction::Success(res) => self.set_fetched(res),
|
||||
FetchAction::Failed(err) => self.set_failed(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<REQ, RES> Fetch<REQ, RES> {
|
||||
/// Creates a new Fetch wrapper around the request.
|
||||
///
|
||||
/// It will default the response field to be put in a NotFetching state.
|
||||
pub fn new(request: REQ) -> Self {
|
||||
Self {
|
||||
request,
|
||||
response: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the response field to indicate that no fetch request is in flight.
|
||||
pub fn set_not_fetching(&mut self) -> DidChange {
|
||||
let will_change = self
|
||||
.response
|
||||
.discriminant_differs(&FetchState::NotFetching(None));
|
||||
|
||||
let old = std::mem::take(&mut self.response);
|
||||
let new = old.not_fetching();
|
||||
std::mem::replace(&mut self.response, new);
|
||||
|
||||
will_change
|
||||
}
|
||||
|
||||
/// Sets the response field to indicate that a fetch request is currently being made.
|
||||
pub fn set_fetching(&mut self) -> DidChange {
|
||||
let will_change = self
|
||||
.response
|
||||
.discriminant_differs(&FetchState::Fetching(None));
|
||||
|
||||
let old = std::mem::take(&mut self.response);
|
||||
let new = old.fetching();
|
||||
std::mem::replace(&mut self.response, new);
|
||||
|
||||
will_change
|
||||
}
|
||||
|
||||
/// Sets the response field to indicate that a fetch request failed to complete.
|
||||
pub fn set_failed(&mut self, err: FetchError) -> DidChange {
|
||||
let will_change = match &self.response {
|
||||
FetchState::Failed(_, old_err) => &err == old_err,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
let old = std::mem::take(&mut self.response);
|
||||
let new = old.failed(err);
|
||||
std::mem::replace(&mut self.response, new);
|
||||
|
||||
will_change
|
||||
}
|
||||
|
||||
// TODO need tests to make sure that this is ergonomic.
|
||||
/// Makes an asynchronous fetch request, which will produce a message that makes use of a
|
||||
/// `FetchAction` when it completes.
|
||||
pub fn fetch_convert<T: FetchRequest, Msg>(
|
||||
&self,
|
||||
to_request: impl Fn(&Self) -> &T,
|
||||
to_msg: impl Fn(FetchAction<T::ResponseBody>) -> Msg,
|
||||
) -> impl Future<Output = Msg> {
|
||||
let request: &T = to_request(self);
|
||||
let request = create_request(request);
|
||||
let req_type: PhantomData<T> = PhantomData;
|
||||
async move {
|
||||
let fetch_state = match fetch_resource(request, req_type).await {
|
||||
Ok(response) => FetchAction::Success(response),
|
||||
Err(err) => FetchAction::Failed(err),
|
||||
};
|
||||
|
||||
to_msg(fetch_state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms the type of the response held by the fetch state (if any exists).
|
||||
pub fn map<NewRes, F: Fn(Fetch<REQ, RES>) -> Fetch<REQ, NewRes>>(
|
||||
self,
|
||||
f: F,
|
||||
) -> Fetch<REQ, NewRes> {
|
||||
f(self)
|
||||
}
|
||||
|
||||
/// Unwraps the Fetch wrapper to produce the response it may contain.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the Fetch wrapper doesn't contain an instance of a response, this function will panic.
|
||||
pub fn unwrap(self) -> RES {
|
||||
// TODO, actually provide some diagnostic here.
|
||||
self.res().unwrap()
|
||||
}
|
||||
|
||||
/// Gets the response body (if present).
|
||||
pub fn res(self) -> Option<RES> {
|
||||
match self.response {
|
||||
FetchState::NotFetching(res) => res,
|
||||
FetchState::Fetching(res) => res,
|
||||
FetchState::Fetched(res) => Some(res),
|
||||
FetchState::Failed(res, _) => res,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the request body.
|
||||
pub fn req(self) -> REQ {
|
||||
self.request
|
||||
}
|
||||
|
||||
pub fn state(self) -> FetchState<RES> {
|
||||
self.response
|
||||
}
|
||||
|
||||
/// Converts the wrapped values to references.
|
||||
///
|
||||
/// # Note
|
||||
/// This may be expensive if a Failed variant made into a reference, as the FetchError is cloned.
|
||||
pub fn as_ref(&self) -> Fetch<&REQ, &RES> {
|
||||
let response = match &self.response {
|
||||
FetchState::NotFetching(res) => FetchState::NotFetching(res.as_ref()),
|
||||
FetchState::Fetching(res) => FetchState::Fetching(res.as_ref()),
|
||||
FetchState::Fetched(res) => FetchState::Fetched(res),
|
||||
FetchState::Failed(res, err) => FetchState::Failed(res.as_ref(), err.clone()),
|
||||
};
|
||||
|
||||
Fetch {
|
||||
request: &self.request,
|
||||
response,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the wrapped values to mutable references.
|
||||
///
|
||||
/// # Note
|
||||
/// This may be expensive if a Failed variant made into a reference, as the FetchError is cloned.
|
||||
pub fn as_mut(&mut self) -> Fetch<&mut REQ, &mut RES> {
|
||||
let response = match &mut self.response {
|
||||
FetchState::NotFetching(res) => FetchState::NotFetching(res.as_mut()),
|
||||
FetchState::Fetching(res) => FetchState::Fetching(res.as_mut()),
|
||||
FetchState::Fetched(res) => FetchState::Fetched(res),
|
||||
FetchState::Failed(res, err) => FetchState::Failed(res.as_mut(), err.clone()),
|
||||
};
|
||||
Fetch {
|
||||
request: &mut self.request,
|
||||
response,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<REQ: FetchRequest> Fetch<REQ, REQ::ResponseBody> {
|
||||
/// Makes an asynchronous fetch request, which will produce a message that makes use of a
|
||||
/// `FetchAction` when it completes.
|
||||
pub fn fetch<Msg>(
|
||||
&self,
|
||||
to_msg: impl Fn(FetchAction<REQ::ResponseBody>) -> Msg,
|
||||
) -> impl Future<Output = Msg> {
|
||||
let request = self.as_ref().req();
|
||||
let request = create_request(request);
|
||||
let req_type: PhantomData<REQ> = PhantomData;
|
||||
async move {
|
||||
let fetch_state = match fetch_resource(request, req_type).await {
|
||||
Ok(response) => FetchAction::Success(response),
|
||||
Err(err) => FetchAction::Failed(err),
|
||||
};
|
||||
|
||||
to_msg(fetch_state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn setting_fetching_state_doesnt_change_strong_count() {
|
||||
// This is done to detect if a leak occurred.
|
||||
let data: Arc<i32> = Arc::new(22);
|
||||
let cloned_data: Arc<i32> = data.clone();
|
||||
assert_eq!(Arc::strong_count(&data), 2);
|
||||
let mut fs: Fetch<Arc<i32>, ()> = Fetch::new(cloned_data);
|
||||
fs.set_fetching();
|
||||
|
||||
assert_eq!(Arc::strong_count(&data), 2);
|
||||
assert_eq!(FetchState::Fetching(None), fs.response)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setting_fetched_state() {
|
||||
let mut fs = Fetch {
|
||||
request: (),
|
||||
response: FetchState::Fetching(None),
|
||||
};
|
||||
assert!(fs.set_fetched("SomeValue".to_string()));
|
||||
assert_eq!(fs.response, FetchState::Fetched("SomeValue".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setting_fetching_from_fetched() {
|
||||
let mut fs = Fetch {
|
||||
request: (),
|
||||
response: FetchState::Fetched("Lorem".to_string()),
|
||||
};
|
||||
assert!(fs.set_fetching());
|
||||
assert_eq!(fs.response, FetchState::Fetching(Some("Lorem".to_string())));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
use crate::fetch::FetchError;
|
||||
use crate::NeqAssign;
|
||||
|
||||
/// Represents a state change to Fetch wrapper.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum FetchAction<T> {
|
||||
NotFetching,
|
||||
Fetching,
|
||||
Success(T), // TODO rename to Fetched(T)
|
||||
Failed(FetchError),
|
||||
}
|
||||
|
||||
impl<T> Default for FetchAction<T> {
|
||||
fn default() -> Self {
|
||||
FetchAction::NotFetching
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FetchAction<T> {
|
||||
/// Returns a reference to the Success case
|
||||
pub fn success(&self) -> Option<&T> {
|
||||
match self {
|
||||
FetchAction::Success(value) => Some(value),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the value out of the fetch state if it is a `Success` variant.
|
||||
pub fn unwrap(self) -> T {
|
||||
if let FetchAction::Success(value) = self {
|
||||
value
|
||||
} else {
|
||||
panic!("Could not unwrap value of FetchState");
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms the FetchState into another FetchState using the given function.
|
||||
pub fn map<U, F: Fn(T) -> U>(self, f: F) -> FetchAction<U> {
|
||||
match self {
|
||||
FetchAction::NotFetching => FetchAction::NotFetching,
|
||||
FetchAction::Fetching => FetchAction::NotFetching,
|
||||
FetchAction::Success(t) => FetchAction::Success(f(t)),
|
||||
FetchAction::Failed(e) => FetchAction::Failed(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a function that mutates the response if the Action is the success case.
|
||||
pub fn alter<F: Fn(&mut T)>(&mut self, f: F) {
|
||||
if let FetchAction::Success(t) = self {
|
||||
f(t)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the FetchAction to contain a reference to the success case.
|
||||
pub fn as_ref(&self) -> FetchAction<&T> {
|
||||
match self {
|
||||
FetchAction::NotFetching => FetchAction::NotFetching,
|
||||
FetchAction::Fetching => FetchAction::NotFetching,
|
||||
FetchAction::Success(t) => FetchAction::Success(t),
|
||||
FetchAction::Failed(e) => FetchAction::Failed(e.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> FetchAction<T> {
|
||||
/// Sets the fetch state to be fetching.
|
||||
/// If it wasn't already in a fetch state, it will return `true`,
|
||||
/// to indicate that the component should re-render.
|
||||
pub fn set_fetching(&mut self) -> bool {
|
||||
self.neq_assign(FetchAction::Fetching)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
use wasm_bindgen::JsValue;
|
||||
|
||||
/// A representation of an error that may occur when making a fetch request.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum FetchError {
|
||||
/// The response could not be deserialized.
|
||||
DeserializeError { error: String, content: String },
|
||||
/// The response had an error code.
|
||||
ResponseError {
|
||||
status_code: u16,
|
||||
response_body: String,
|
||||
},
|
||||
/// Text was not available on the response.
|
||||
// TODO, this might get thrown in unexpected circumstances.
|
||||
TextNotAvailable,
|
||||
/// The Fetch Future could not be created due to a misconfiguration.
|
||||
CouldNotCreateFetchFuture,
|
||||
/// The request could cont be created due to a misconfiguration.
|
||||
CouldNotCreateRequest(JsValue), // TODO, convert this to a string or more structured error - implement Hash on this and related structs.
|
||||
/// Could not serialize the request body.
|
||||
CouldNotSerializeRequestBody,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FetchError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FetchError::DeserializeError { error, content } => f.write_str(&format!(
|
||||
"Could not deserialize a successful request. With error: {}, and content: {}",
|
||||
error, content
|
||||
)),
|
||||
FetchError::ResponseError {
|
||||
status_code,
|
||||
response_body,
|
||||
} => f.write_str(&format!(
|
||||
"The server returned a response with code: {}, and body: {}",
|
||||
status_code, response_body
|
||||
)),
|
||||
FetchError::TextNotAvailable => {
|
||||
f.write_str("The text could not be extracted from the response.")
|
||||
}
|
||||
FetchError::CouldNotCreateFetchFuture => {
|
||||
f.write_str("Could not create a fetch future.")
|
||||
}
|
||||
FetchError::CouldNotCreateRequest(_) => {
|
||||
f.write_str("Could not create a fetch request.")
|
||||
}
|
||||
FetchError::CouldNotSerializeRequestBody => {
|
||||
f.write_str("Could not serialize the body in the fetch request.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for FetchError {}
|
|
@ -0,0 +1,220 @@
|
|||
use crate::fetch::FetchError;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::marker::PhantomData;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{Request, RequestInit, RequestMode, Response, Window};
|
||||
|
||||
/// An enum representing what method to use for the request,
|
||||
/// as well as a body if the method is able to have a body.
|
||||
///
|
||||
/// Connect, Options, Trace are omitted because they are unlikely to be used in this scenario.
|
||||
/// Please open an issue if their absence is a problem for you.
|
||||
pub enum MethodBody<'a, T> {
|
||||
Head,
|
||||
Get,
|
||||
Delete,
|
||||
Post(&'a T),
|
||||
Put(&'a T),
|
||||
Patch(&'a T),
|
||||
}
|
||||
|
||||
impl<'a, T> MethodBody<'a, T> {
|
||||
pub fn as_method(&self) -> &'static str {
|
||||
match self {
|
||||
MethodBody::Get => "GET",
|
||||
MethodBody::Delete => "DELETE",
|
||||
MethodBody::Post(_) => "POST",
|
||||
MethodBody::Put(_) => "PUT",
|
||||
MethodBody::Patch(_) => "PATCH",
|
||||
MethodBody::Head => "HEAD",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Serialize> MethodBody<'a, T> {
|
||||
pub fn as_body<FORMAT: Format>(&self) -> Result<Option<JsValue>, FetchError> {
|
||||
let body: Option<String> = match self {
|
||||
MethodBody::Get | MethodBody::Delete | MethodBody::Head => None,
|
||||
MethodBody::Put(data) | MethodBody::Post(data) | MethodBody::Patch(data) => {
|
||||
let body = FORMAT::serialize(data)
|
||||
.ok_or_else(|| FetchError::CouldNotSerializeRequestBody)?;
|
||||
Some(body)
|
||||
}
|
||||
};
|
||||
|
||||
let body = body.map(|data| JsValue::from_str(data.as_str()));
|
||||
Ok(body)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO, this is only works with String/&str. It would be a good addition if Vec<u8>/&[u8] were supported.
|
||||
/// Determines what format the data will be transmitted in.
|
||||
pub trait Format {
|
||||
fn serialize<T: Serialize>(t: &T) -> Option<String>;
|
||||
fn deserialize<T: DeserializeOwned>(s: &str) -> Option<T>;
|
||||
}
|
||||
|
||||
/// Transport data using the JSON format
|
||||
pub struct Json;
|
||||
impl Format for Json {
|
||||
// type Transport = Text;
|
||||
|
||||
fn serialize<T: Serialize>(t: &T) -> Option<String> {
|
||||
serde_json::to_string(t).ok()
|
||||
}
|
||||
|
||||
fn deserialize<T: DeserializeOwned>(s: &str) -> Option<T> {
|
||||
serde_json::from_str(s).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait used to declare how a fetch request shall be made using a type.
|
||||
///
|
||||
///
|
||||
/// Because it would be of little benefit to have to implement all details of this trait for every
|
||||
/// request you make, the following should provide a template for reducing the amount of boilerplate
|
||||
/// per request.
|
||||
///
|
||||
/// # Simplifying Example
|
||||
/// ```
|
||||
/// use yewtil::fetch::{FetchRequest, MethodBody, Json, Fetch};
|
||||
/// use serde::Serialize;
|
||||
/// use serde::de::DeserializeOwned;
|
||||
///
|
||||
/// pub trait MyFetchRequest {
|
||||
/// type RequestBody: Serialize;
|
||||
/// type ResponseBody: DeserializeOwned;
|
||||
/// fn path(&self) -> String;
|
||||
/// fn method(&self) -> MethodBody<Self::RequestBody>;
|
||||
/// }
|
||||
/// /// A wrapper to allow implementing a foreign trait generically for anything wrapped by it that meets
|
||||
/// /// the specified type bounds.
|
||||
/// /// This isn't ideal, and will not be required in the future after coherence rules are changed.
|
||||
/// /// https://github.com/rust-lang/rfcs/blob/master/text/2451-re-rebalancing-coherence.md
|
||||
/// pub struct LocalWrapper<T>(T);
|
||||
///
|
||||
/// impl <T: MyFetchRequest> FetchRequest for LocalWrapper<T> {
|
||||
/// type RequestBody = T::RequestBody;
|
||||
/// type ResponseBody = T::ResponseBody;
|
||||
/// type Format = Json; // Always serialize using json
|
||||
///
|
||||
/// fn url(&self) -> String {
|
||||
/// // The origin will always be the same
|
||||
/// format!("http://some_host_website.com/{}", self.0.path())
|
||||
/// }
|
||||
///
|
||||
/// fn method(&self) -> MethodBody<Self::RequestBody> {
|
||||
/// self.0.method()
|
||||
/// }
|
||||
///
|
||||
/// fn headers(&self) -> Vec<(String, String)> {
|
||||
/// // Always attach the same headers.
|
||||
/// vec![
|
||||
/// ("Content-Type".to_string(), "application/json".to_string())
|
||||
/// ]
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// pub struct ApplesRequest;
|
||||
/// impl MyFetchRequest for ApplesRequest {
|
||||
/// type RequestBody = (); type ResponseBody = ();
|
||||
/// fn path(&self) -> String { "apples".to_string()}
|
||||
/// fn method(&self) -> MethodBody<Self::RequestBody> {MethodBody::Get}
|
||||
/// }
|
||||
///
|
||||
/// pub enum Msg {
|
||||
/// Variant
|
||||
/// }
|
||||
///
|
||||
/// let fetch_wrapper = Fetch::new(LocalWrapper(ApplesRequest));
|
||||
/// fetch_wrapper.fetch(|_| Msg::Variant); // Kicks off an async request.
|
||||
/// ```
|
||||
pub trait FetchRequest {
|
||||
/// The Request Body (if any).
|
||||
type RequestBody: Serialize;
|
||||
/// The Response Body (if any).
|
||||
type ResponseBody: DeserializeOwned;
|
||||
|
||||
/// What format to use for serialization and deserialization.
|
||||
///
|
||||
/// Ideally default to serde_json::Deserializer once
|
||||
/// https://github.com/rust-lang/rust/issues/29661
|
||||
type Format: Format;
|
||||
|
||||
/// The URL of the resource to fetch.
|
||||
fn url(&self) -> String;
|
||||
|
||||
/// The HTTP method and body (if any) to be used in constructing the request.
|
||||
fn method(&self) -> MethodBody<Self::RequestBody>;
|
||||
|
||||
/// The headers to attach to the request .
|
||||
fn headers(&self) -> Vec<(String, String)>;
|
||||
|
||||
/// Use CORS for the request. By default, it will not.
|
||||
fn use_cors(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_request<T: FetchRequest>(request: &T) -> Result<Request, FetchError> {
|
||||
let method = request.method();
|
||||
let headers = request.headers();
|
||||
let headers = JsValue::from_serde(&headers).expect("Convert Headers to Tuple");
|
||||
|
||||
// configure options for the request
|
||||
let mut opts = RequestInit::new();
|
||||
opts.method(method.as_method());
|
||||
opts.body(method.as_body::<T::Format>()?.as_ref());
|
||||
opts.headers(&headers);
|
||||
|
||||
// TODO, see if there are more options that can be specified.
|
||||
if request.use_cors() {
|
||||
opts.mode(RequestMode::Cors);
|
||||
}
|
||||
|
||||
// Create the request
|
||||
// TODO make this a Rust value instead.
|
||||
Request::new_with_str_and_init(&request.url(), &opts).map_err(FetchError::CouldNotCreateRequest)
|
||||
}
|
||||
|
||||
/// Fetch a resource, returning a result of the expected response,
|
||||
/// or an error indicating what went wrong.
|
||||
pub async fn fetch_resource<T: FetchRequest>(
|
||||
request: Result<Request, FetchError>,
|
||||
_req_type: PhantomData<T>,
|
||||
) -> Result<T::ResponseBody, FetchError> {
|
||||
let request = request?;
|
||||
// Send the request, resolving it to a response.
|
||||
let window: Window = web_sys::window().unwrap();
|
||||
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
||||
.await
|
||||
.map_err(|_| FetchError::CouldNotCreateFetchFuture)?;
|
||||
debug_assert!(resp_value.is_instance_of::<Response>());
|
||||
let resp: Response = resp_value.dyn_into().unwrap();
|
||||
|
||||
// Process the response
|
||||
let text = JsFuture::from(resp.text().map_err(|_| FetchError::TextNotAvailable)?)
|
||||
.await
|
||||
.map_err(|_| FetchError::TextNotAvailable)?;
|
||||
|
||||
let text_string = text.as_string().unwrap();
|
||||
|
||||
// If the response isn't ok, then return an error without trying to deserialize.
|
||||
if !resp.ok() {
|
||||
return Err(FetchError::ResponseError {
|
||||
status_code: resp.status(),
|
||||
response_body: text_string,
|
||||
});
|
||||
}
|
||||
|
||||
let deserialized =
|
||||
<T::Format>::deserialize(&text_string).ok_or_else(|| FetchError::DeserializeError {
|
||||
error: "".to_string(),
|
||||
content: text_string,
|
||||
})?;
|
||||
|
||||
Ok(deserialized)
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
use crate::fetch::FetchError;
|
||||
|
||||
/// Holds the state of the request being made and response
|
||||
/// (if any has been made successfully at any prior point).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum FetchState<RES> {
|
||||
NotFetching(Option<RES>),
|
||||
Fetching(Option<RES>),
|
||||
Fetched(RES),
|
||||
Failed(Option<RES>, FetchError),
|
||||
}
|
||||
|
||||
impl<RES> Default for FetchState<RES> {
|
||||
fn default() -> Self {
|
||||
FetchState::NotFetching(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<RES> FetchState<RES> {
|
||||
/// Determines if there is a different discriminant between the fetch states.
|
||||
pub(crate) fn discriminant_differs(&self, other: &Self) -> bool {
|
||||
std::mem::discriminant(self) != std::mem::discriminant(other)
|
||||
}
|
||||
|
||||
pub(crate) fn not_fetching(self) -> Self {
|
||||
match self {
|
||||
FetchState::NotFetching(res) => FetchState::NotFetching(res),
|
||||
FetchState::Fetching(res) => FetchState::NotFetching(res),
|
||||
FetchState::Fetched(res) => FetchState::NotFetching(Some(res)),
|
||||
FetchState::Failed(res, _err) => FetchState::NotFetching(res),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fetching(self) -> Self {
|
||||
match self {
|
||||
FetchState::NotFetching(res) => FetchState::Fetching(res),
|
||||
FetchState::Fetching(res) => FetchState::Fetching(res),
|
||||
FetchState::Fetched(res) => FetchState::Fetching(Some(res)),
|
||||
FetchState::Failed(res, _err) => FetchState::Fetching(res),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fetched(self, res: RES) -> Self {
|
||||
match self {
|
||||
FetchState::NotFetching(_res) => FetchState::Fetched(res),
|
||||
FetchState::Fetching(_res) => FetchState::Fetched(res),
|
||||
FetchState::Fetched(_res) => FetchState::Fetched(res),
|
||||
FetchState::Failed(_res, _err) => FetchState::Fetched(res),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn failed(self, err: FetchError) -> Self {
|
||||
match self {
|
||||
FetchState::NotFetching(res) => FetchState::Failed(res, err),
|
||||
FetchState::Fetching(res) => FetchState::Failed(res, err),
|
||||
FetchState::Fetched(res) => FetchState::Failed(Some(res), err),
|
||||
FetchState::Failed(res, _err) => FetchState::Failed(res, err),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
use std::future::Future;
|
||||
use stdweb::spawn_local;
|
||||
use yew::{
|
||||
agent::{Agent, AgentLink},
|
||||
Component, ComponentLink,
|
||||
};
|
||||
|
||||
/// Trait that allows you to use `ComponentLink` and `AgentLink` to register futures.
|
||||
pub trait LinkFuture {
|
||||
type Message;
|
||||
/// This method processes a Future that returns a message and sends it back to the component's
|
||||
/// loop.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the future panics, then the promise will not resolve, and will leak.
|
||||
fn send_future<F>(&self, future: F)
|
||||
where
|
||||
F: Future<Output = Self::Message> + 'static;
|
||||
|
||||
/// Registers a future that resolves to multiple messages.
|
||||
/// # Panics
|
||||
/// If the future panics, then the promise will not resolve, and will leak.
|
||||
fn send_future_batch<F>(&self, future: F)
|
||||
where
|
||||
F: Future<Output = Vec<Self::Message>> + 'static;
|
||||
}
|
||||
|
||||
impl<COMP: Component> LinkFuture for ComponentLink<COMP> {
|
||||
type Message = COMP::Message;
|
||||
|
||||
fn send_future<F>(&self, future: F)
|
||||
where
|
||||
F: Future<Output = Self::Message> + 'static,
|
||||
{
|
||||
let link: ComponentLink<COMP> = self.clone();
|
||||
let js_future = async move {
|
||||
let message: COMP::Message = future.await;
|
||||
link.send_message(message);
|
||||
};
|
||||
spawn_local(js_future);
|
||||
}
|
||||
|
||||
fn send_future_batch<F>(&self, future: F)
|
||||
where
|
||||
F: Future<Output = Vec<Self::Message>> + 'static,
|
||||
{
|
||||
let link: ComponentLink<COMP> = self.clone();
|
||||
let js_future = async move {
|
||||
let messages: Vec<COMP::Message> = future.await;
|
||||
link.send_message_batch(messages);
|
||||
};
|
||||
spawn_local(js_future);
|
||||
}
|
||||
}
|
||||
|
||||
impl<AGN: Agent> LinkFuture for AgentLink<AGN> {
|
||||
type Message = AGN::Message;
|
||||
|
||||
fn send_future<F>(&self, future: F)
|
||||
where
|
||||
F: Future<Output = Self::Message> + 'static,
|
||||
{
|
||||
let link: AgentLink<AGN> = self.clone();
|
||||
let js_future = async move {
|
||||
let message: AGN::Message = future.await;
|
||||
let cb = link.callback(|m: AGN::Message| m);
|
||||
cb.emit(message);
|
||||
};
|
||||
spawn_local(js_future);
|
||||
}
|
||||
|
||||
fn send_future_batch<F>(&self, _future: F)
|
||||
where
|
||||
F: Future<Output = Vec<Self::Message>> + 'static,
|
||||
{
|
||||
unimplemented!("Agents don't support batching their messages.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::ops::Deref;
|
||||
|
||||
// TODO when const generics lands, it would be useful to add a usize type parameter over the max number of elements.
|
||||
|
||||
// It would also be interesting to see if a diffing history implementation could be built over types
|
||||
// that can represent themselves as diffs - where reversible transitions can be recorded instead of values
|
||||
// and the transitions can be rolled back.
|
||||
// That would probably have worse performance in exchange for smaller size.
|
||||
|
||||
/// Wrapper that keeps track of prior values that have been assigned to it.
|
||||
///
|
||||
/// It keeps values that have been `set` for it around for the duration of its lifetime,
|
||||
/// or until they are dropped by calling `reset` or `forget`.
|
||||
///
|
||||
/// Prior values can be iterated over as well.
|
||||
pub struct History<T>(VecDeque<T>);
|
||||
|
||||
impl<T> History<T> {
|
||||
/// Creates a new history wrapper.
|
||||
pub fn new(value: T) -> Self {
|
||||
let mut vec = VecDeque::new();
|
||||
vec.push_front(value);
|
||||
Self(vec)
|
||||
}
|
||||
|
||||
/// Set the value represented by the `History` struct.
|
||||
///
|
||||
/// This pushes the new value into the front of a list,
|
||||
/// where the front-most value represents the most recent value.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
///# use yewtil::History;
|
||||
/// let mut history = History::new(0);
|
||||
/// history.set(1);
|
||||
///
|
||||
/// assert_eq!(*history, 1);
|
||||
/// assert_eq!(history.count(), 2);
|
||||
/// ```
|
||||
pub fn set(&mut self, value: T) {
|
||||
self.0.push_front(value)
|
||||
}
|
||||
|
||||
/// Replaces the current value without creating a history entry.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
///# use yewtil::History;
|
||||
/// let mut history = History::new(0);
|
||||
/// history.replace(1);
|
||||
///
|
||||
/// assert_eq!(*history, 1);
|
||||
/// assert_eq!(history.count(), 1);
|
||||
/// ```
|
||||
pub fn replace(&mut self, value: T) {
|
||||
self.0[0] = value;
|
||||
}
|
||||
|
||||
/// Removes all prior values.
|
||||
///
|
||||
/// This effectively sets a new "checkpoint" that can be restored by calling `reset`.
|
||||
///
|
||||
/// The returned bool indicates if any entries were removed.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
///# use yewtil::History;
|
||||
/// let mut history = History::new(0);
|
||||
/// history.set(1);
|
||||
/// history.set(2);
|
||||
///
|
||||
/// history.forget();
|
||||
/// assert_eq!(*history, 2);
|
||||
/// assert_eq!(history.count(), 1);
|
||||
/// ```
|
||||
pub fn forget(&mut self) -> bool {
|
||||
if self.dirty() {
|
||||
self.0.drain(1..);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove all elements except the last one, making the oldest entry the "current value".
|
||||
///
|
||||
/// The returned bool indicates if any entries were removed.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
///# use yewtil::History;
|
||||
/// let mut history = History::new(0);
|
||||
/// history.set(1);
|
||||
/// history.set(2);
|
||||
///
|
||||
/// history.reset();
|
||||
/// assert_eq!(*history, 0);
|
||||
/// assert_eq!(history.count(), 1);
|
||||
/// ```
|
||||
pub fn reset(&mut self) -> bool {
|
||||
if self.dirty() {
|
||||
self.0.drain(..self.0.len() - 1);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if there is more than one entry in the history.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
///# use yewtil::History;
|
||||
/// let mut history = History::new(0);
|
||||
/// history.set(1);
|
||||
/// assert!(history.dirty());
|
||||
/// ```
|
||||
pub fn dirty(&mut self) -> bool {
|
||||
self.count() > 1
|
||||
}
|
||||
|
||||
/// Returns the number of entries in the history.
|
||||
///
|
||||
/// This will never be less than 1, as the first entry of the backing VecDeque is always occupied by the
|
||||
/// "current value" in the `History` struct.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
///# use yewtil::History;
|
||||
/// let mut history = History::new(0);
|
||||
/// assert_eq!(history.count(), 1);
|
||||
///
|
||||
/// history.set(1);
|
||||
/// assert_eq!(history.count(), 2);
|
||||
/// ```
|
||||
pub fn count(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Produces an iterator over references to history items ordered from newest to oldest.
|
||||
pub fn iter(&self) -> std::collections::vec_deque::Iter<T> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
/// Gets the current value.
|
||||
pub fn into_inner(mut self) -> T {
|
||||
self.0
|
||||
.pop_front()
|
||||
.expect("History should have at least one item")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> History<T> {
|
||||
/// Will only `set` the value if the provided value is different than the current value.
|
||||
///
|
||||
/// It returns true to indicate if the history's current value was updated to be the provided value.
|
||||
/// # Example
|
||||
/// ```
|
||||
///# use yewtil::History;
|
||||
/// let mut history = History::new(0);
|
||||
/// let did_set = history.neq_set(0);
|
||||
/// assert!(!did_set);
|
||||
///
|
||||
/// let did_set = history.neq_set(1);
|
||||
/// assert!(did_set);
|
||||
/// ```
|
||||
pub fn neq_set(&mut self, value: T) -> bool {
|
||||
if self.0[0] != value {
|
||||
self.set(value);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoIterator for History<T> {
|
||||
type Item = T;
|
||||
type IntoIter = std::collections::vec_deque::IntoIter<T>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsRef<T> for History<T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
// Get the first element
|
||||
&self.0[0]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for History<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
//! Utility library for the Yew frontend web framework.
|
||||
//!
|
||||
//! All features:
|
||||
//!
|
||||
//! * "neq" - NeqAssign trait
|
||||
//! * "pure" - Pure components and function components.
|
||||
//! * "future" - Async support for Yew Messages
|
||||
//! * "fetch" - Wrapper that holds requests and responses.
|
||||
//! * "mrc_irc" - Ergonomic Rc pointers.
|
||||
//! * "lrc" - Linked-list Rc pointer.
|
||||
//! * "history" - History tracker
|
||||
// //! * "dsl" - Use functions instead of Yew's `html!` macro.
|
||||
|
||||
//#[cfg(feature = "dsl")]
|
||||
//pub mod dsl;
|
||||
|
||||
#[cfg(feature = "neq")]
|
||||
mod not_equal_assign;
|
||||
|
||||
#[cfg(feature = "pure")]
|
||||
mod pure;
|
||||
|
||||
#[cfg(any(feature = "mrc_irc", feature = "lrc"))]
|
||||
pub mod ptr;
|
||||
|
||||
#[cfg(feature = "history")]
|
||||
mod history;
|
||||
|
||||
#[cfg(feature = "history")]
|
||||
pub use history::History;
|
||||
|
||||
#[cfg(feature = "neq")]
|
||||
pub use not_equal_assign::NeqAssign;
|
||||
|
||||
#[cfg(feature = "pure")]
|
||||
pub use pure::{Pure, PureComponent};
|
||||
|
||||
#[cfg(feature = "pure")]
|
||||
pub use yewtil_macro::function_component;
|
||||
|
||||
#[cfg(feature = "fetch")]
|
||||
pub mod fetch;
|
||||
|
||||
#[cfg(feature = "effect")]
|
||||
mod effect;
|
||||
#[cfg(feature = "effect")]
|
||||
pub use effect::{effect, Effect};
|
||||
|
||||
#[cfg(feature = "future")]
|
||||
pub mod future;
|
|
@ -0,0 +1,59 @@
|
|||
//! Module for `neq_assign` utility function.
|
||||
|
||||
use std::borrow::BorrowMut;
|
||||
use yew::html::ShouldRender;
|
||||
|
||||
/// Blanket trait to provide a convenience method for assigning props in `changed` or updating values in `update`.
|
||||
pub trait NeqAssign<NEW> {
|
||||
/// If `self` and `new` aren't equal, assigns `new` to `self` and returns true, otherwise returns false.
|
||||
///
|
||||
/// Short for "Not equal assign".
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use yew::{Component, ShouldRender, ComponentLink};
|
||||
/// # use yewtil::NeqAssign;
|
||||
/// # use yew::Properties;
|
||||
///# use yew::virtual_dom::VNode;
|
||||
/// ##[derive(Properties, PartialEq)]
|
||||
/// struct Props {
|
||||
/// field1: String,
|
||||
/// field2: usize
|
||||
/// }
|
||||
/// struct Model {
|
||||
/// props: Props
|
||||
/// }
|
||||
/// impl Component for Model {
|
||||
/// # type Message = ();
|
||||
/// type Properties = Props;
|
||||
/// // ...
|
||||
/// #
|
||||
/// # fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
/// # unimplemented!()
|
||||
/// # }
|
||||
/// # fn update(&mut self, msg: ()) -> ShouldRender {
|
||||
/// # unimplemented!()
|
||||
/// # }
|
||||
/// #
|
||||
/// fn change(&mut self, props: Self::Properties) -> ShouldRender{
|
||||
/// self.props.neq_assign(props)
|
||||
/// }
|
||||
///#
|
||||
///# fn view(&self) -> VNode {
|
||||
///# unimplemented!()
|
||||
///# }
|
||||
/// }
|
||||
/// ```
|
||||
fn neq_assign(&mut self, new: NEW) -> ShouldRender;
|
||||
}
|
||||
|
||||
impl<T: BorrowMut<U>, U: PartialEq> NeqAssign<U> for T {
|
||||
fn neq_assign(&mut self, new: U) -> bool {
|
||||
if self.borrow() != &new {
|
||||
*self.borrow_mut() = new;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
use crate::ptr::rc_box::{
|
||||
clone_impl, clone_inner, decrement_and_possibly_deallocate, get_count, get_ref_boxed_content,
|
||||
is_exclusive, try_unwrap, unwrap_clone, RcBox,
|
||||
};
|
||||
use std::borrow::Borrow;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::Deref;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
/// Immutable Reference Counted pointer.
|
||||
///
|
||||
/// The `Irc` points to a value that cannot be mutated.
|
||||
/// If a `Mrc` and `Irc` point to the same value, mutating the value via the `Mrc` will
|
||||
/// clone and allocate, _then_ mutate the value, leaving the value pointed to by the `Irc` alone.
|
||||
///
|
||||
/// Unless the `Irc` is unwrapped, that value is mutated, and another `Irc` is created using that value
|
||||
/// the `Irc` will guarantee that the value is not changed, and can be transparently passed to child components
|
||||
/// with knowledge that its value matches that of an original `Mrc` or `Irc` that it was cloned from.
|
||||
///
|
||||
/// This makes `Irc`s ideal for passing around immutable views to data through components in Yew, as
|
||||
/// cloning the `Irc` itself is cheap, and the `Irc` guarantees that its data cannot be changed by
|
||||
/// some intermediate component without obvious unwrap --> modify --> rewrap operations.
|
||||
pub struct Irc<T> {
|
||||
/// Pointer to the value and reference counter.
|
||||
pub(crate) ptr: NonNull<RcBox<T>>,
|
||||
}
|
||||
|
||||
impl<T> Irc<T> {
|
||||
/// Allocates the value behind an `Irc` pointer.
|
||||
pub fn new(value: T) -> Self {
|
||||
let rc_box = RcBox::new(value);
|
||||
let ptr = rc_box.into_non_null();
|
||||
Self { ptr }
|
||||
}
|
||||
|
||||
/// Tries to extract the value from the `Irc`, returning the `Irc` if there is one or
|
||||
/// more other smart pointers to the value.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use yewtil::ptr::Irc;
|
||||
/// let irc = Irc::new(0);
|
||||
///
|
||||
/// let clone = irc.clone();
|
||||
/// let irc = irc.try_unwrap().expect_err("Should not be able to unwrap");
|
||||
///
|
||||
/// std::mem::drop(clone);
|
||||
/// let value = irc.try_unwrap().expect("Should get value");
|
||||
/// ```
|
||||
pub fn try_unwrap(self) -> Result<T, Self> {
|
||||
try_unwrap(self.ptr).map_err(|ptr| {
|
||||
Self { ptr } // Recover the ptr
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the reference count of the `Irc`.
|
||||
///
|
||||
/// An exclusive `Irc` will have a count of `1`.
|
||||
/// The count is incremented on any cloning action and is decremented when `drop` is called.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use yewtil::ptr::Irc;
|
||||
/// let irc = Irc::new(0);
|
||||
/// assert_eq!(irc.get_count(), 1);
|
||||
///
|
||||
/// let _clone = irc.clone();
|
||||
/// assert_eq!(irc.get_count(), 2);
|
||||
///
|
||||
/// std::mem::drop(_clone);
|
||||
/// assert_eq!(irc.get_count(), 1);
|
||||
/// ```
|
||||
pub fn get_count(&self) -> usize {
|
||||
get_count(self.ptr)
|
||||
}
|
||||
|
||||
//
|
||||
/// ```
|
||||
/// use yewtil::ptr::Irc;
|
||||
/// let irc = Irc::new(0);
|
||||
/// assert!(irc.is_exclusive());
|
||||
///
|
||||
/// let _clone = irc.clone();
|
||||
/// assert!(!irc.is_exclusive());
|
||||
///
|
||||
/// std::mem::drop(_clone);
|
||||
/// assert!(irc.is_exclusive());
|
||||
/// ```
|
||||
pub fn is_exclusive(&self) -> bool {
|
||||
is_exclusive(self.ptr)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Irc<T> {
|
||||
/// Unwraps the value from the `Irc`, cloning the value instead if another `Irc` or `Mrc` points
|
||||
/// to the same value.
|
||||
pub fn unwrap_clone(self) -> T {
|
||||
unwrap_clone(self.ptr)
|
||||
}
|
||||
/// Clones the wrapped value of the `Irc`.
|
||||
pub fn clone_inner(&self) -> T {
|
||||
clone_inner(self.ptr)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for Irc<T> {
|
||||
fn drop(&mut self) {
|
||||
unsafe { decrement_and_possibly_deallocate(self.ptr) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> Default for Irc<T> {
|
||||
fn default() -> Self {
|
||||
Irc::new(T::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for Irc<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
ptr: clone_impl(self.ptr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsRef<T> for Irc<T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
get_ref_boxed_content(&self.ptr).value.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Irc<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Borrow<T> for Irc<T> {
|
||||
fn borrow(&self) -> &T {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq for Irc<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.as_ref().eq(other.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Eq> Eq for Irc<T> {}
|
||||
|
||||
impl<T: PartialOrd> PartialOrd for Irc<T> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.as_ref().partial_cmp(other.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ord> Ord for Irc<T> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.as_ref().cmp(other.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash> Hash for Irc<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.as_ref().hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug> fmt::Debug for Irc<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let rc_box = get_ref_boxed_content(&self.ptr);
|
||||
f.debug_struct("Irc")
|
||||
.field("value", rc_box.value.as_ref())
|
||||
.field("count", &rc_box.get_count())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn create_new() {
|
||||
let _irc = Irc::new(0);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,21 @@
|
|||
//! Smart pointers for use within Yew.
|
||||
//!
|
||||
//! These all offer similar semantics to `std::rc::Rc`, but offer better ergonomics within Yew,
|
||||
//! or functionality not available in `Rc`.
|
||||
#[cfg(feature = "mrc_irc")]
|
||||
mod irc;
|
||||
#[cfg(feature = "lrc")]
|
||||
mod lrc;
|
||||
#[cfg(feature = "mrc_irc")]
|
||||
mod mrc;
|
||||
mod rc_box;
|
||||
mod takeable;
|
||||
|
||||
#[cfg(feature = "mrc_irc")]
|
||||
pub use irc::Irc;
|
||||
#[cfg(feature = "lrc")]
|
||||
pub use lrc::Lrc;
|
||||
#[cfg(feature = "mrc_irc")]
|
||||
pub use mrc::Mrc;
|
||||
|
||||
pub(crate) type IsZero = bool;
|
|
@ -0,0 +1,331 @@
|
|||
use crate::ptr::rc_box::{
|
||||
clone_impl, clone_inner, decrement_and_possibly_deallocate, get_count, get_mut_boxed_content,
|
||||
get_ref_boxed_content, is_exclusive, try_unwrap, unwrap_clone, RcBox,
|
||||
};
|
||||
use crate::ptr::Irc;
|
||||
use std::borrow::{Borrow, BorrowMut};
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
/// Mutable Reference Counted pointer
|
||||
///
|
||||
/// The `Mrc` has similar semantics to `std::rc::Rc` pointer,
|
||||
/// with notable differences that it does not support `Weak` pointers,
|
||||
/// it supports `std::ops::DerefMut` via the possibly allocating `make_mut` function,
|
||||
/// and that it can create immutable handles to its data (`Irc`).
|
||||
///
|
||||
/// This should make it just slightly more size efficient and performant than `Rc`,
|
||||
/// and should be more ergonomic to use than `Rc` given that you can mutably
|
||||
/// assign to it without much ceremony.
|
||||
///
|
||||
/// Passing `Irc` pointers to children guarantee that no intermediate component can modify the value
|
||||
/// behind the pointer.
|
||||
/// This makes it ideal for passing around configuration data where some components can ergonomicly
|
||||
/// "modify" and cheaply pass the pointers back to parent components, while other components can only read it.
|
||||
///
|
||||
/// # Note
|
||||
/// Assigning to an `Mrc` within Yew when you have passed shared copies of the ptr to child components,
|
||||
/// will always end up cloning the value stored in the `Mrc`. `Rc` makes this performance cost explicit,
|
||||
/// by making you use `Rc::make_mut()`. Because cloning is unavoidable in the context it was designed for,
|
||||
/// `Mrc` opts to provide nicer ergonomics around assignment.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use yewtil::ptr::Mrc;
|
||||
///
|
||||
/// let mut mrc = Mrc::new(5);
|
||||
/// *mrc = 10; // This just replaces the value because the mrc isn't shared.
|
||||
///
|
||||
/// assert_eq!(*mrc, 10);
|
||||
///
|
||||
/// let clone = mrc.clone();
|
||||
/// *mrc = 20; // This operation clones the value and allocates space for it.
|
||||
///
|
||||
/// assert_eq!(*clone, 10);
|
||||
/// assert_eq!(*mrc, 20);
|
||||
/// ```
|
||||
pub struct Mrc<T> {
|
||||
/// Pointer to the value and reference counter.
|
||||
ptr: NonNull<RcBox<T>>,
|
||||
}
|
||||
|
||||
impl<T> Mrc<T> {
|
||||
/// Allocates a value behind a `Mrc` pointer.
|
||||
pub fn new(value: T) -> Self {
|
||||
let rc_box = RcBox::new(value);
|
||||
let ptr = rc_box.into_non_null();
|
||||
Self { ptr }
|
||||
}
|
||||
|
||||
/// Attempts to get a mutable reference to the wrapped value.
|
||||
///
|
||||
/// If the pointer is not shared, it will return `Some`,
|
||||
/// whereas if multiple `Mrc`s or `Irc`s point to the value, this will return None.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use yewtil::ptr::Mrc;
|
||||
/// let mut mrc = Mrc::new(0);
|
||||
/// assert!(mrc.get_mut().is_some());
|
||||
///
|
||||
/// let _clone = mrc.clone();
|
||||
/// assert!(mrc.get_mut().is_none());
|
||||
/// ```
|
||||
pub fn get_mut(&mut self) -> Option<&mut T> {
|
||||
if self.is_exclusive() {
|
||||
Some(get_mut_boxed_content(&mut self.ptr).value.as_mut())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to extract the value from the Mrc, returning the Mrc if there is one or
|
||||
/// more other pointers to the value.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use yewtil::ptr::Mrc;
|
||||
/// let mrc = Mrc::new(0);
|
||||
///
|
||||
/// let clone = mrc.clone();
|
||||
/// let mrc = mrc.try_unwrap().expect_err("Should not be able to unwrap");
|
||||
///
|
||||
/// std::mem::drop(clone);
|
||||
/// let value = mrc.try_unwrap().expect("Should get value");
|
||||
/// ```
|
||||
pub fn try_unwrap(self) -> Result<T, Self> {
|
||||
try_unwrap(self.ptr).map_err(|ptr| {
|
||||
Self { ptr } // Recover the ptr
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the reference count of the `Mrc`.
|
||||
///
|
||||
/// An exclusive `Mrc` will have a count of `1`.
|
||||
/// The count is incremented on any cloning action and is decremented when `drop` is called.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use yewtil::ptr::Mrc;
|
||||
/// let mrc = Mrc::new(0);
|
||||
/// assert_eq!(mrc.get_count(), 1);
|
||||
///
|
||||
/// let _clone = mrc.clone();
|
||||
/// assert_eq!(mrc.get_count(), 2);
|
||||
///
|
||||
/// std::mem::drop(_clone);
|
||||
/// assert_eq!(mrc.get_count(), 1);
|
||||
/// ```
|
||||
pub fn get_count(&self) -> usize {
|
||||
get_count(self.ptr)
|
||||
}
|
||||
|
||||
/// Returns `true` if no other pointers to the value exist.
|
||||
///
|
||||
/// ```
|
||||
/// use yewtil::ptr::Mrc;
|
||||
/// let mrc = Mrc::new(0);
|
||||
/// assert!(mrc.is_exclusive());
|
||||
///
|
||||
/// let _clone = mrc.clone();
|
||||
/// assert!(!mrc.is_exclusive());
|
||||
///
|
||||
/// std::mem::drop(_clone);
|
||||
/// assert!(mrc.is_exclusive());
|
||||
/// ```
|
||||
pub fn is_exclusive(&self) -> bool {
|
||||
is_exclusive(self.ptr)
|
||||
}
|
||||
|
||||
/// Returns an immutable reference counted pointer,
|
||||
/// pointing to the same value and reference count.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use yewtil::ptr::{Mrc, Irc};
|
||||
/// let mrc: Mrc<usize> = Mrc::new(0);
|
||||
/// let _irc: Irc<usize> = mrc.irc();
|
||||
///
|
||||
/// assert!(!mrc.is_exclusive());
|
||||
/// ```
|
||||
pub fn irc(&self) -> Irc<T> {
|
||||
get_ref_boxed_content(&self.ptr).inc_count();
|
||||
Irc { ptr: self.ptr }
|
||||
}
|
||||
|
||||
/// Converts this Mrc into an Irc.
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use yewtil::ptr::{Mrc, Irc};
|
||||
/// let mrc: Mrc<usize> = Mrc::new(0);
|
||||
/// let irc: Irc<usize> = mrc.into_irc();
|
||||
///
|
||||
/// assert!(irc.is_exclusive());
|
||||
/// ```
|
||||
pub fn into_irc(self) -> Irc<T> {
|
||||
// Because the Mrc is dropped, decrementing the count,
|
||||
// the count needs to be restored here.
|
||||
get_ref_boxed_content(&self.ptr).inc_count();
|
||||
Irc { ptr: self.ptr }
|
||||
}
|
||||
|
||||
/// Checks pointers for equality.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use yewtil::ptr::Mrc;
|
||||
/// let mrc1 = Mrc::new(0);
|
||||
/// let mrc2 = Mrc::new(0);
|
||||
/// assert_eq!(mrc1, mrc2);
|
||||
/// assert!(!Mrc::ptr_eq(&mrc1, &mrc2))
|
||||
/// ```
|
||||
pub fn ptr_eq(lhs: &Self, rhs: &Self) -> bool {
|
||||
std::ptr::eq(lhs.ptr.as_ptr(), rhs.ptr.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Mrc<T> {
|
||||
/// Returns a mutable reference to the value if it has exclusive access.
|
||||
/// If it does not have exclusive access, it will make a clone of the data to acquire exclusive access.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
///# use yewtil::ptr::Mrc;
|
||||
/// let mut mrc: Mrc<usize> = Mrc::new(0);
|
||||
///
|
||||
/// let _mut_ref: &mut usize = mrc.make_mut();
|
||||
/// assert_eq!(mrc.get_count(), 1);
|
||||
///
|
||||
/// let clone = mrc.clone();
|
||||
/// assert_eq!(mrc.get_count(), 2);
|
||||
///
|
||||
/// let _mut_ref: &mut usize = mrc.make_mut();
|
||||
/// assert_eq!(mrc.get_count(), 1);
|
||||
/// assert!(!Mrc::ptr_eq(&mrc, &clone))
|
||||
/// ```
|
||||
pub fn make_mut(&mut self) -> &mut T {
|
||||
if !self.is_exclusive() {
|
||||
let rc_box = RcBox::new(self.clone_inner());
|
||||
let ptr = rc_box.into_non_null();
|
||||
|
||||
// decrement the count for the boxed content at the current pointer
|
||||
// because this Mrc will point to a new value.
|
||||
|
||||
// This doesn't need to check to deallocate, because the count is guaranteed to be > 1.
|
||||
get_ref_boxed_content(&self.ptr).dec_count();
|
||||
|
||||
// Replace the pointers
|
||||
self.ptr = ptr;
|
||||
}
|
||||
|
||||
get_mut_boxed_content(&mut self.ptr).value.as_mut()
|
||||
}
|
||||
|
||||
/// Consumes the `Mrc` and returns the value from the `Mrc` if it is not shared
|
||||
/// or clones the value if another `Mrc` or `Irc` has access to it.
|
||||
pub fn unwrap_clone(self) -> T {
|
||||
unwrap_clone(self.ptr)
|
||||
}
|
||||
/// Clones the value wrapped by the `Mrc`..
|
||||
pub fn clone_inner(&self) -> T {
|
||||
clone_inner(self.ptr)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for Mrc<T> {
|
||||
fn drop(&mut self) {
|
||||
unsafe { decrement_and_possibly_deallocate(self.ptr) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for Mrc<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
ptr: clone_impl(self.ptr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> Default for Mrc<T> {
|
||||
fn default() -> Self {
|
||||
Mrc::new(T::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> DerefMut for Mrc<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.make_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> AsMut<T> for Mrc<T> {
|
||||
fn as_mut(&mut self) -> &mut T {
|
||||
self.make_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> BorrowMut<T> for Mrc<T> {
|
||||
fn borrow_mut(&mut self) -> &mut T {
|
||||
self.make_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsRef<T> for Mrc<T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
get_ref_boxed_content(&self.ptr).value.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Mrc<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Borrow<T> for Mrc<T> {
|
||||
fn borrow(&self) -> &T {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq for Mrc<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.as_ref().eq(other.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Eq> Eq for Mrc<T> {}
|
||||
|
||||
impl<T: PartialOrd> PartialOrd for Mrc<T> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.as_ref().partial_cmp(other.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ord> Ord for Mrc<T> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.as_ref().cmp(other.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash> Hash for Mrc<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.as_ref().hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug> fmt::Debug for Mrc<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let rc_box = get_ref_boxed_content(&self.ptr);
|
||||
f.debug_struct("Irc")
|
||||
.field("value", rc_box.value.as_ref())
|
||||
.field("count", &rc_box.get_count())
|
||||
.finish()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
use crate::ptr::takeable::Takeable;
|
||||
use crate::ptr::IsZero;
|
||||
use std::cell::Cell;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RcBox<T> {
|
||||
pub(crate) value: Takeable<T>,
|
||||
count: Cell<usize>,
|
||||
}
|
||||
|
||||
/// The boxed content used in Irc and Mrc.
|
||||
impl<T> RcBox<T> {
|
||||
#[inline]
|
||||
pub(crate) fn new(value: T) -> Self {
|
||||
Self {
|
||||
value: Takeable::new(value),
|
||||
count: Cell::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn into_non_null(self) -> NonNull<Self> {
|
||||
unsafe { NonNull::new_unchecked(Box::into_raw(Box::new(self))) }
|
||||
}
|
||||
|
||||
/// Gets the reference count of the node
|
||||
pub(crate) fn get_count(&self) -> usize {
|
||||
self.count.get()
|
||||
}
|
||||
|
||||
/// Increments the reference count of the node.
|
||||
#[inline]
|
||||
pub(crate) fn inc_count(&self) {
|
||||
let mut count = self.count.get();
|
||||
count += 1;
|
||||
self.count.set(count);
|
||||
}
|
||||
|
||||
/// Decrements the reference count of the node.
|
||||
/// It will return true if the count hits zero.
|
||||
/// This can be used to determine if the node should be deallocated.
|
||||
#[inline]
|
||||
pub(crate) fn dec_count(&self) -> IsZero {
|
||||
let mut count = self.count.get();
|
||||
count -= 1;
|
||||
self.count.set(count);
|
||||
count == 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_exclusive(&self) -> bool {
|
||||
self.get_count() == 1
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) unsafe fn decrement_and_possibly_deallocate<T>(node: NonNull<RcBox<T>>) {
|
||||
// If the ref-count becomes 0
|
||||
if node.as_ref().dec_count() {
|
||||
std::ptr::drop_in_place(node.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn get_mut_boxed_content<T>(ptr: &mut NonNull<RcBox<T>>) -> &mut RcBox<T> {
|
||||
unsafe { ptr.as_mut() }
|
||||
}
|
||||
#[inline(always)]
|
||||
pub(crate) fn get_ref_boxed_content<T>(ptr: &NonNull<RcBox<T>>) -> &RcBox<T> {
|
||||
unsafe { ptr.as_ref() }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn get_count<T>(ptr: NonNull<RcBox<T>>) -> usize {
|
||||
get_ref_boxed_content(&ptr).get_count()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_exclusive<T>(ptr: NonNull<RcBox<T>>) -> bool {
|
||||
get_ref_boxed_content(&ptr).is_exclusive()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn try_unwrap<T>(mut ptr: NonNull<RcBox<T>>) -> Result<T, NonNull<RcBox<T>>> {
|
||||
if is_exclusive(ptr) {
|
||||
Ok(get_mut_boxed_content(&mut ptr).value.take())
|
||||
} else {
|
||||
// The ptr's drop has ran, decrementing the count
|
||||
// This restores the value.
|
||||
get_ref_boxed_content(&ptr).inc_count();
|
||||
Err(ptr)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn clone_inner<T: Clone>(ptr: NonNull<RcBox<T>>) -> T {
|
||||
get_ref_boxed_content(&ptr).value.as_ref().clone()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn unwrap_clone<T: Clone>(mut ptr: NonNull<RcBox<T>>) -> T {
|
||||
if is_exclusive(ptr) {
|
||||
get_mut_boxed_content(&mut ptr).value.take()
|
||||
} else {
|
||||
clone_inner(ptr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Clones the pointer after incrementing the reference count.
|
||||
#[inline]
|
||||
pub(crate) fn clone_impl<T>(ptr: NonNull<RcBox<T>>) -> NonNull<RcBox<T>> {
|
||||
// Increment the ref count
|
||||
get_ref_boxed_content(&ptr).inc_count();
|
||||
// rerturn the ptr
|
||||
ptr
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
use std::fmt;
|
||||
/// A wrapper around Option<T> that only allows items to be taken.
|
||||
///
|
||||
/// # Panics
|
||||
/// It is expected to only take items from this structure in a way that
|
||||
/// it will never be accessed after items have been taken.
|
||||
///
|
||||
/// Accessing this via `as_ref` or `as_mut` after taking the value will cause a panic.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Takeable<T>(Option<T>);
|
||||
|
||||
impl<T> Takeable<T> {
|
||||
pub(crate) fn new(item: T) -> Self {
|
||||
Takeable(Some(item))
|
||||
}
|
||||
|
||||
/// This should only be called once.
|
||||
pub(crate) fn take(&mut self) -> T {
|
||||
self.0.take().expect("Can't take twice")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsRef<T> for Takeable<T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
self.0.as_ref().expect("Can't reference taken value")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsMut<T> for Takeable<T> {
|
||||
fn as_mut(&mut self) -> &mut T {
|
||||
self.0.as_mut().expect("Can't reference taken value")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug> fmt::Debug for Takeable<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.as_ref().expect("Can't reference taken value").fmt(f)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
//! Shortcut for terse component definitions.
|
||||
use crate::NeqAssign;
|
||||
use yew::{Component, ComponentLink, Html, Properties, ShouldRender};
|
||||
|
||||
/// Allows immutable components to be declared using a single struct and a single method.
|
||||
pub trait PureComponent: Properties + PartialEq + Sized + 'static {
|
||||
/// Renders self to `Html`.
|
||||
fn render(&self) -> Html;
|
||||
}
|
||||
|
||||
/// Wrapper component for pure components.
|
||||
///
|
||||
/// Due to constraints in Rust's coherence rules, `Component` can't be implemented for any `T` that implements
|
||||
/// `PureComponent`, so instead this struct wraps a `T: PureComponent` and `Component` is implemented
|
||||
/// for this instead.
|
||||
///
|
||||
/// # Example
|
||||
/// It is reasonable practice to use `Pure` as a prefix or `Impl` as a suffix to your pure component model
|
||||
/// and use an alias to provide a terser name to be used by other components:
|
||||
///
|
||||
/// ```
|
||||
/// use yew::Properties;
|
||||
/// use yew::Html;
|
||||
/// use yewtil::{PureComponent, Pure};
|
||||
///
|
||||
/// #[derive(Properties, PartialEq)]
|
||||
/// pub struct PureMyComponent {
|
||||
/// pub data: String
|
||||
/// }
|
||||
///
|
||||
/// impl PureComponent for PureMyComponent {
|
||||
/// fn render(&self) -> Html {
|
||||
///# unimplemented!()
|
||||
/// // ...
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// /// Use this from within `html!` macros.
|
||||
/// pub type MyComponent = Pure<PureMyComponent>;
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct Pure<T>(T);
|
||||
|
||||
impl<T: PureComponent + 'static> Component for Pure<T> {
|
||||
type Message = ();
|
||||
type Properties = T;
|
||||
|
||||
fn create(props: Self::Properties, _link: ComponentLink<Self>) -> Self {
|
||||
Pure(props)
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn change(&mut self, props: Self::Properties) -> ShouldRender {
|
||||
self.0.neq_assign(props)
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
self.0.render()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue