mirror of https://github.com/yewstack/yew
Remove context & job agent (#2295)
* Remove context and job agent. * Revert "Remove context and job agent." This reverts commit408f6fcdf0
. * Revert "Revert "Remove context and job agent."" This reverts commit44a42dfb31
. * Update example. * Fix docs. * Fix test. * Rename examples. * Fix examples & docs. * Update website/docs/concepts/function-components/custom-hooks.mdx Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com> * Examples in alphabetical order. Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com>
This commit is contained in:
parent
b761487bac
commit
7d52858d01
|
@ -8,7 +8,9 @@ members = [
|
|||
"packages/yew-router-macro",
|
||||
|
||||
# Examples
|
||||
"examples/agents",
|
||||
"examples/boids",
|
||||
"examples/contexts",
|
||||
"examples/counter",
|
||||
"examples/dyn_create_destroy_apps",
|
||||
"examples/file_upload",
|
||||
|
@ -19,14 +21,11 @@ members = [
|
|||
"examples/js_callback",
|
||||
"examples/keyed_list",
|
||||
"examples/mount_point",
|
||||
"examples/multi_thread",
|
||||
"examples/nested_list",
|
||||
"examples/node_refs",
|
||||
"examples/password_strength",
|
||||
"examples/portals",
|
||||
"examples/pub_sub",
|
||||
"examples/router",
|
||||
"examples/store",
|
||||
"examples/timer",
|
||||
"examples/todomvc",
|
||||
"examples/two_apps",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "multi_thread"
|
||||
name = "agents"
|
||||
version = "0.1.0"
|
||||
authors = ["Denis Kolodin <deniskolodin@gmail.com>"]
|
||||
edition = "2018"
|
|
@ -0,0 +1,18 @@
|
|||
# Warning
|
||||
|
||||
The agents example is a conceptual WIP and is currently blocked on [a future Trunk feature](https://github.com/thedodd/trunk/issues/46)
|
||||
|
||||
There is an alternate agent example [here](https://github.com/yewstack/yew/tree/master/examples/web_worker_fib).
|
||||
|
||||
|
||||
# Multi-Thread Example
|
||||
|
||||
[](https://examples.yew.rs/agents)
|
||||
|
||||
|
||||
## Concepts
|
||||
|
||||
Uses an [Agent] that runs in a [Web Worker].
|
||||
|
||||
[agent]: https://yew.rs/docs/concepts/agents/
|
||||
[web worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
|
|
@ -1,4 +1,4 @@
|
|||
fn main() {
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
yew::start_app::<multi_thread::Model>();
|
||||
yew::start_app::<agents::Model>();
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use multi_thread::native_worker::Worker;
|
||||
use agents::native_worker::Worker;
|
||||
use yew_agent::Threaded;
|
||||
|
||||
fn main() {
|
|
@ -0,0 +1,49 @@
|
|||
pub mod native_worker;
|
||||
|
||||
use yew::{html, Component, Context, Html};
|
||||
use yew_agent::{Bridge, Bridged};
|
||||
|
||||
pub enum Msg {
|
||||
SendToWorker,
|
||||
DataReceived,
|
||||
}
|
||||
|
||||
pub struct Model {
|
||||
worker: Box<dyn Bridge<native_worker::Worker>>,
|
||||
}
|
||||
|
||||
impl Component for Model {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let link = ctx.link();
|
||||
let callback = link.callback(|_| Msg::DataReceived);
|
||||
let worker = native_worker::Worker::bridge(callback);
|
||||
|
||||
Self { worker }
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::SendToWorker => {
|
||||
self.worker.send(native_worker::Request::GetDataFromServer);
|
||||
false
|
||||
}
|
||||
Msg::DataReceived => {
|
||||
log::info!("DataReceived");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<div>
|
||||
<nav class="menu">
|
||||
<button onclick={ctx.link().callback(|_| Msg::SendToWorker)}>{ "Send to Thread" }</button>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "pub_sub"
|
||||
name = "contexts"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
license = "MIT OR Apache-2.0"
|
|
@ -0,0 +1,10 @@
|
|||
# Context Example
|
||||
|
||||
[](https://examples.yew.rs/contexts)
|
||||
|
||||
This is currently a technical demonstration of Context API.
|
||||
|
||||
## Concepts
|
||||
|
||||
The example has two components, which communicates through a context
|
||||
as opposed to the traditional method using component links.
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Yew • Pub Sub</title>
|
||||
<title>Yew • Context</title>
|
||||
</head>
|
||||
|
||||
<body></body>
|
|
@ -0,0 +1,23 @@
|
|||
mod msg_ctx;
|
||||
mod producer;
|
||||
mod subscriber;
|
||||
|
||||
use producer::Producer;
|
||||
use subscriber::Subscriber;
|
||||
use yew::prelude::*;
|
||||
|
||||
use msg_ctx::MessageProvider;
|
||||
|
||||
#[function_component]
|
||||
pub fn Model() -> Html {
|
||||
html! {
|
||||
<MessageProvider>
|
||||
<Producer />
|
||||
<Subscriber />
|
||||
</MessageProvider>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<Model>();
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Message {
|
||||
pub inner: String,
|
||||
}
|
||||
|
||||
impl Reducible for Message {
|
||||
type Action = String;
|
||||
|
||||
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
|
||||
Message { inner: action }.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub type MessageContext = UseReducerHandle<Message>;
|
||||
|
||||
#[derive(Properties, Debug, PartialEq)]
|
||||
pub struct MessageProviderProps {
|
||||
#[prop_or_default]
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn MessageProvider(props: &MessageProviderProps) -> Html {
|
||||
let msg = use_reducer(|| Message {
|
||||
inner: "No message yet.".to_string(),
|
||||
});
|
||||
|
||||
html! {
|
||||
<ContextProvider<MessageContext> context={msg}>
|
||||
{props.children.clone()}
|
||||
</ContextProvider<MessageContext>>
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
use super::msg_ctx::MessageContext;
|
||||
|
||||
#[function_component]
|
||||
pub fn Producer() -> Html {
|
||||
let msg_ctx = use_context::<MessageContext>().unwrap();
|
||||
|
||||
let onclick = Callback::from(move |_| msg_ctx.dispatch("Message Received.".to_string()));
|
||||
|
||||
html! {
|
||||
<button {onclick}>
|
||||
{"PRESS ME"}
|
||||
</button>
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
use super::msg_ctx::MessageContext;
|
||||
|
||||
use yew::prelude::*;
|
||||
|
||||
#[function_component]
|
||||
pub fn Subscriber() -> Html {
|
||||
let msg_ctx = use_context::<MessageContext>().unwrap();
|
||||
|
||||
let message = msg_ctx.inner.to_owned();
|
||||
|
||||
html! {
|
||||
<h1>{ message }</h1>
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
# Warning
|
||||
|
||||
The multi-thread example is a conceptual WIP and is currently blocked on [a future Trunk feature](https://github.com/thedodd/trunk/issues/46)
|
||||
|
||||
There is an alternate multi-thread example [here](https://github.com/yewstack/yew/tree/master/examples/web_worker_fib).
|
||||
|
||||
|
||||
# Multi-Thread Example
|
||||
|
||||
[](https://examples.yew.rs/multi_thread)
|
||||
|
||||
|
||||
## Concepts
|
||||
|
||||
Uses an [Agent] that runs in a [Web Worker].
|
||||
|
||||
[agent]: https://yew.rs/docs/concepts/agents/
|
||||
[web worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
|
|
@ -1,61 +0,0 @@
|
|||
use gloo_timers::callback::Interval;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use yew_agent::{Agent, AgentLink, Context, HandlerId};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Request {
|
||||
GetDataFromServer,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Response {
|
||||
DataFetched,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
Updating,
|
||||
}
|
||||
|
||||
pub struct Worker {
|
||||
link: AgentLink<Worker>,
|
||||
_interval: Interval,
|
||||
}
|
||||
|
||||
impl Agent for Worker {
|
||||
type Reach = Context<Self>;
|
||||
type Message = Msg;
|
||||
type Input = Request;
|
||||
type Output = Response;
|
||||
|
||||
fn create(link: AgentLink<Self>) -> Self {
|
||||
let duration = 3;
|
||||
|
||||
let interval = {
|
||||
let link = link.clone();
|
||||
Interval::new(duration, move || link.send_message(Msg::Updating))
|
||||
};
|
||||
|
||||
Self {
|
||||
link,
|
||||
_interval: interval,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) {
|
||||
match msg {
|
||||
Msg::Updating => {
|
||||
log::info!("Tick...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, msg: Self::Input, who: HandlerId) {
|
||||
log::info!("Request: {:?}", msg);
|
||||
match msg {
|
||||
Request::GetDataFromServer => {
|
||||
// TODO fetch actual data
|
||||
self.link.respond(who, Response::DataFetched);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
use gloo_timers::callback::Interval;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use yew_agent::{Agent, AgentLink, HandlerId, Job};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Request {
|
||||
GetDataFromServer,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Response {
|
||||
DataFetched,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
Initialized,
|
||||
Updating,
|
||||
DataFetched,
|
||||
}
|
||||
|
||||
pub struct Worker {
|
||||
link: AgentLink<Worker>,
|
||||
_interval: Interval,
|
||||
}
|
||||
|
||||
impl Agent for Worker {
|
||||
type Reach = Job<Self>;
|
||||
type Message = Msg;
|
||||
type Input = Request;
|
||||
type Output = Response;
|
||||
|
||||
fn create(link: AgentLink<Self>) -> Self {
|
||||
let duration = 3;
|
||||
|
||||
let interval = {
|
||||
let link = link.clone();
|
||||
Interval::new(duration, move || link.send_message(Msg::Updating))
|
||||
};
|
||||
|
||||
link.send_message(Msg::Initialized);
|
||||
Self {
|
||||
link,
|
||||
_interval: interval,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) {
|
||||
match msg {
|
||||
Msg::Initialized => {
|
||||
log::info!("Initialized!");
|
||||
}
|
||||
Msg::Updating => {
|
||||
log::info!("Tick...");
|
||||
}
|
||||
Msg::DataFetched => {
|
||||
log::info!("Data was fetched");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, msg: Self::Input, who: HandlerId) {
|
||||
log::info!("Request: {:?}", msg);
|
||||
match msg {
|
||||
Request::GetDataFromServer => {
|
||||
// TODO fetch actual data
|
||||
self.link.respond(who, Response::DataFetched);
|
||||
self.link.send_message(Msg::DataFetched);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
pub mod context;
|
||||
pub mod job;
|
||||
pub mod native_worker;
|
||||
|
||||
use yew::{html, Component, Context, Html};
|
||||
use yew_agent::{Bridge, Bridged};
|
||||
|
||||
pub enum Msg {
|
||||
SendToWorker,
|
||||
SendToJob,
|
||||
SendToContext,
|
||||
DataReceived,
|
||||
}
|
||||
|
||||
pub struct Model {
|
||||
worker: Box<dyn Bridge<native_worker::Worker>>,
|
||||
job: Box<dyn Bridge<job::Worker>>,
|
||||
context: Box<dyn Bridge<context::Worker>>,
|
||||
context_2: Box<dyn Bridge<context::Worker>>,
|
||||
}
|
||||
|
||||
impl Component for Model {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let link = ctx.link();
|
||||
let callback = link.callback(|_| Msg::DataReceived);
|
||||
let worker = native_worker::Worker::bridge(callback);
|
||||
|
||||
let callback = link.callback(|_| Msg::DataReceived);
|
||||
let job = job::Worker::bridge(callback);
|
||||
|
||||
let callback = link.callback(|_| Msg::DataReceived);
|
||||
let context = context::Worker::bridge(callback);
|
||||
|
||||
let callback = link.callback(|_| Msg::DataReceived);
|
||||
let context_2 = context::Worker::bridge(callback);
|
||||
|
||||
Self {
|
||||
worker,
|
||||
job,
|
||||
context,
|
||||
context_2,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::SendToWorker => {
|
||||
self.worker.send(native_worker::Request::GetDataFromServer);
|
||||
false
|
||||
}
|
||||
Msg::SendToJob => {
|
||||
self.job.send(job::Request::GetDataFromServer);
|
||||
false
|
||||
}
|
||||
Msg::SendToContext => {
|
||||
self.context.send(context::Request::GetDataFromServer);
|
||||
self.context_2.send(context::Request::GetDataFromServer);
|
||||
false
|
||||
}
|
||||
Msg::DataReceived => {
|
||||
log::info!("DataReceived");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<div>
|
||||
<nav class="menu">
|
||||
<button onclick={ctx.link().callback(|_| Msg::SendToWorker)}>{ "Send to Thread" }</button>
|
||||
<button onclick={ctx.link().callback(|_| Msg::SendToJob)}>{ "Send to Job" }</button>
|
||||
<button onclick={ctx.link().callback(|_| Msg::SendToContext)}>{ "Send to Context" }</button>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
# Pub Sub Example
|
||||
|
||||
[](https://examples.yew.rs/pub_sub)
|
||||
|
||||
This is currently a technical demonstration of agents.
|
||||
|
||||
## Concepts
|
||||
|
||||
The example has two components, which communicate through a "broker" agent
|
||||
as opposed to the traditional method using component links.
|
||||
|
||||
## Improvements
|
||||
|
||||
As it stands, this example uses a great amount of code to do very little.
|
||||
The concept should be applied to a more elaborate use case.
|
||||
|
||||
This could also be merged into the [nested_list](../nested_list) example to remove the need for `WeakComponentLink`.
|
|
@ -1,47 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use yew_agent::{Agent, AgentLink, Context, HandlerId};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Request {
|
||||
EventBusMsg(String),
|
||||
}
|
||||
|
||||
pub struct EventBus {
|
||||
link: AgentLink<EventBus>,
|
||||
subscribers: HashSet<HandlerId>,
|
||||
}
|
||||
|
||||
impl Agent for EventBus {
|
||||
type Reach = Context<Self>;
|
||||
type Message = ();
|
||||
type Input = Request;
|
||||
type Output = String;
|
||||
|
||||
fn create(link: AgentLink<Self>) -> Self {
|
||||
Self {
|
||||
link,
|
||||
subscribers: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) {}
|
||||
|
||||
fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) {
|
||||
match msg {
|
||||
Request::EventBusMsg(s) => {
|
||||
for sub in self.subscribers.iter() {
|
||||
self.link.respond(*sub, s.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn connected(&mut self, id: HandlerId) {
|
||||
self.subscribers.insert(id);
|
||||
}
|
||||
|
||||
fn disconnected(&mut self, id: HandlerId) {
|
||||
self.subscribers.remove(&id);
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
mod event_bus;
|
||||
mod producer;
|
||||
mod subscriber;
|
||||
|
||||
use producer::Producer;
|
||||
use subscriber::Subscriber;
|
||||
use yew::{html, Component, Context, Html};
|
||||
|
||||
pub struct Model;
|
||||
|
||||
impl Component for Model {
|
||||
type Message = ();
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<Producer />
|
||||
<Subscriber />
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<Model>();
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
use crate::event_bus::{EventBus, Request};
|
||||
use yew::prelude::*;
|
||||
use yew_agent::{Dispatched, Dispatcher};
|
||||
|
||||
pub enum Msg {
|
||||
Clicked,
|
||||
}
|
||||
|
||||
pub struct Producer {
|
||||
event_bus: Dispatcher<EventBus>,
|
||||
}
|
||||
|
||||
impl Component for Producer {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
event_bus: EventBus::dispatcher(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::Clicked => {
|
||||
self.event_bus
|
||||
.send(Request::EventBusMsg("Message received".to_owned()));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<button onclick={ctx.link().callback(|_| Msg::Clicked)}>
|
||||
{ "PRESS ME" }
|
||||
</button>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
use super::event_bus::EventBus;
|
||||
use yew::{html, Component, Context, Html};
|
||||
use yew_agent::{Bridge, Bridged};
|
||||
|
||||
pub enum Msg {
|
||||
NewMessage(String),
|
||||
}
|
||||
|
||||
pub struct Subscriber {
|
||||
message: String,
|
||||
_producer: Box<dyn Bridge<EventBus>>,
|
||||
}
|
||||
|
||||
impl Component for Subscriber {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
message: "No message yet.".to_owned(),
|
||||
_producer: EventBus::bridge(ctx.link().callback(Msg::NewMessage)),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::NewMessage(s) => {
|
||||
self.message = s;
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<h1>{ &self.message }</h1>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
[package]
|
||||
name = "store"
|
||||
version = "0.1.0"
|
||||
authors = ["Michał Kawalec <michal@monad.cat>"]
|
||||
edition = "2018"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew-agent = { path = "../../packages/yew-agent" }
|
||||
wasm-bindgen = "0.2"
|
||||
gloo-console = "0.2"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"HtmlInputElement",
|
||||
]
|
|
@ -1,16 +0,0 @@
|
|||
# Store Example
|
||||
|
||||
[](https://examples.yew.rs/store)
|
||||
|
||||
A timeline of posts that can be independently updated.
|
||||
|
||||
## Concepts
|
||||
|
||||
Uses the [`yew_agent`] API to keep track of posts.
|
||||
|
||||
## Improvements
|
||||
|
||||
- This example desperately needs some styling.
|
||||
- Posts should persist across sessions.
|
||||
|
||||
[`yewtil::store`]: https://docs.rs/yew_agent/latest/
|
|
@ -1,9 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Yew • Store</title>
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
</html>
|
|
@ -1 +0,0 @@
|
|||
pub mod posts;
|
|
@ -1,76 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
use yew_agent::utils::store::{Store, StoreWrapper};
|
||||
use yew_agent::AgentLink;
|
||||
|
||||
pub type PostId = u32;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PostRequest {
|
||||
Create(String),
|
||||
Update(PostId, String),
|
||||
Remove(PostId),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Action {
|
||||
SetPost(Option<PostId>, String),
|
||||
RemovePost(PostId),
|
||||
}
|
||||
|
||||
pub struct PostStore {
|
||||
pub posts: HashMap<PostId, String>,
|
||||
// Stores can have private state too
|
||||
id_counter: PostId,
|
||||
}
|
||||
|
||||
impl Store for PostStore {
|
||||
type Action = Action;
|
||||
type Input = PostRequest;
|
||||
|
||||
fn new() -> Self {
|
||||
let mut posts = HashMap::new();
|
||||
|
||||
// We insert one post to show the initial send of state
|
||||
// when a bridge is opened.
|
||||
posts.insert(0, "Magic first post".to_owned());
|
||||
|
||||
PostStore {
|
||||
posts,
|
||||
id_counter: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_input(&self, link: AgentLink<StoreWrapper<Self>>, msg: Self::Input) {
|
||||
match msg {
|
||||
PostRequest::Create(text) => {
|
||||
link.send_message(Action::SetPost(None, text));
|
||||
}
|
||||
PostRequest::Update(id, text) => {
|
||||
link.send_message(Action::SetPost(Some(id), text));
|
||||
}
|
||||
PostRequest::Remove(id) => {
|
||||
link.send_message(Action::RemovePost(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reduce(&mut self, msg: Self::Action) {
|
||||
match msg {
|
||||
Action::SetPost(id, text) => {
|
||||
let id = id.unwrap_or_else(|| self.next_id());
|
||||
self.posts.insert(id, text);
|
||||
}
|
||||
Action::RemovePost(id) => {
|
||||
self.posts.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PostStore {
|
||||
fn next_id(&mut self) -> PostId {
|
||||
let tmp = self.id_counter;
|
||||
self.id_counter += 1;
|
||||
tmp
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
mod agents;
|
||||
mod post;
|
||||
mod text_input;
|
||||
|
||||
use agents::posts::{PostId, PostRequest, PostStore};
|
||||
use gloo_console as console;
|
||||
use post::Post;
|
||||
use text_input::TextInput;
|
||||
use yew::prelude::*;
|
||||
use yew_agent::utils::store::{Bridgeable, ReadOnly, StoreWrapper};
|
||||
use yew_agent::Bridge;
|
||||
|
||||
pub enum Msg {
|
||||
CreatePost(String),
|
||||
PostStoreMsg(ReadOnly<PostStore>),
|
||||
}
|
||||
|
||||
pub struct Model {
|
||||
post_ids: Vec<PostId>,
|
||||
post_store: Box<dyn Bridge<StoreWrapper<PostStore>>>,
|
||||
}
|
||||
|
||||
impl Component for Model {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let callback = ctx.link().callback(Msg::PostStoreMsg);
|
||||
Self {
|
||||
post_ids: Vec::new(),
|
||||
post_store: PostStore::bridge(callback),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::CreatePost(text) => {
|
||||
self.post_store.send(PostRequest::Create(text));
|
||||
false
|
||||
}
|
||||
Msg::PostStoreMsg(state) => {
|
||||
// We can see this is logged once before we click any button.
|
||||
// The state of the store is sent when we open a bridge.
|
||||
console::log!("Received update");
|
||||
|
||||
let state = state.borrow();
|
||||
if state.posts.len() != self.post_ids.len() {
|
||||
self.post_ids = state.posts.keys().copied().collect();
|
||||
self.post_ids.sort_unstable();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<TextInput value="New post" onsubmit={ctx.link().callback(Msg::CreatePost)} />
|
||||
|
||||
<div>
|
||||
{ for self.post_ids.iter().map(|&id| html!{ <Post key={id} {id} /> }) }
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
fn main() {
|
||||
yew::start_app::<Model>();
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
use crate::agents::posts::{PostId, PostRequest, PostStore};
|
||||
use crate::text_input::TextInput;
|
||||
use yew::prelude::*;
|
||||
use yew_agent::utils::store::{Bridgeable, ReadOnly, StoreWrapper};
|
||||
use yew_agent::Bridge;
|
||||
|
||||
pub enum Msg {
|
||||
UpdateText(String),
|
||||
Delete,
|
||||
PostStore(ReadOnly<PostStore>),
|
||||
}
|
||||
|
||||
#[derive(Properties, Clone, PartialEq)]
|
||||
pub struct Props {
|
||||
pub id: PostId,
|
||||
}
|
||||
|
||||
pub struct Post {
|
||||
id: PostId,
|
||||
text: Option<String>,
|
||||
post_store: Box<dyn Bridge<StoreWrapper<PostStore>>>,
|
||||
}
|
||||
|
||||
impl Component for Post {
|
||||
type Message = Msg;
|
||||
type Properties = Props;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let callback = ctx.link().callback(Msg::PostStore);
|
||||
Self {
|
||||
id: ctx.props().id,
|
||||
text: None,
|
||||
post_store: PostStore::bridge(callback),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::UpdateText(text) => {
|
||||
self.post_store.send(PostRequest::Update(self.id, text));
|
||||
false
|
||||
}
|
||||
Msg::Delete => {
|
||||
self.post_store.send(PostRequest::Remove(self.id));
|
||||
false
|
||||
}
|
||||
Msg::PostStore(state) => {
|
||||
let state = state.borrow();
|
||||
|
||||
// Only update if the post changed.
|
||||
if let Some(text) = state.posts.get(&self.id) {
|
||||
if self.text.as_ref().map(|it| *it != *text).unwrap_or(false) {
|
||||
self.text = Some(text.clone());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn changed(&mut self, ctx: &Context<Self>) -> bool {
|
||||
self.id = ctx.props().id;
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let text = self.text.as_deref().unwrap_or("<pending>");
|
||||
|
||||
html! {
|
||||
<div>
|
||||
<h2>{ format!("Post #{}", self.id) }</h2>
|
||||
<p>{text}</p>
|
||||
|
||||
<TextInput value={text.to_owned()} onsubmit={ctx.link().callback(Msg::UpdateText)} />
|
||||
<button onclick={ctx.link().callback(|_| Msg::Delete)}>
|
||||
{ "Delete" }
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
use web_sys::HtmlInputElement;
|
||||
use yew::prelude::*;
|
||||
|
||||
pub enum Msg {
|
||||
Submit(String),
|
||||
}
|
||||
|
||||
#[derive(Properties, Clone, PartialEq)]
|
||||
pub struct Props {
|
||||
pub value: String,
|
||||
pub onsubmit: Callback<String>,
|
||||
}
|
||||
|
||||
pub struct TextInput {
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl Component for TextInput {
|
||||
type Message = Msg;
|
||||
type Properties = Props;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
text: ctx.props().value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::Submit(text) => {
|
||||
ctx.props().onsubmit.emit(text);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn changed(&mut self, ctx: &Context<Self>) -> bool {
|
||||
self.text = ctx.props().value.clone();
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let onkeydown = ctx.link().batch_callback(|e: KeyboardEvent| {
|
||||
e.stop_propagation();
|
||||
if e.key() == "Enter" {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
let value = input.value();
|
||||
input.set_value("");
|
||||
Some(Msg::Submit(value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<input
|
||||
placeholder={ctx.props().value.clone()}
|
||||
type="text"
|
||||
{onkeydown}
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,15 +2,12 @@
|
|||
|
||||
mod hooks;
|
||||
mod link;
|
||||
mod local;
|
||||
mod pool;
|
||||
pub mod utils;
|
||||
mod worker;
|
||||
|
||||
pub use hooks::{use_bridge, UseBridgeHandle};
|
||||
pub use link::AgentLink;
|
||||
pub(crate) use link::*;
|
||||
pub use local::{Context, Job};
|
||||
pub(crate) use pool::*;
|
||||
pub use pool::{Dispatched, Dispatcher};
|
||||
pub use worker::{Private, Public, Threaded};
|
||||
|
|
|
@ -1,141 +0,0 @@
|
|||
use super::*;
|
||||
use anymap2::{self, AnyMap};
|
||||
use slab::Slab;
|
||||
use std::cell::RefCell;
|
||||
use std::marker::PhantomData;
|
||||
use std::rc::Rc;
|
||||
use yew::callback::Callback;
|
||||
use yew::scheduler::Shared;
|
||||
|
||||
thread_local! {
|
||||
static LOCAL_AGENTS_POOL: RefCell<AnyMap> = RefCell::new(AnyMap::new());
|
||||
}
|
||||
|
||||
/// Create a single instance in the current thread.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Context<AGN> {
|
||||
_agent: PhantomData<AGN>,
|
||||
}
|
||||
|
||||
impl<AGN> Discoverer for Context<AGN>
|
||||
where
|
||||
AGN: Agent,
|
||||
{
|
||||
type Agent = AGN;
|
||||
|
||||
fn spawn_or_join(callback: Option<Callback<AGN::Output>>) -> Box<dyn Bridge<AGN>> {
|
||||
let mut scope_to_init = None;
|
||||
let bridge = LOCAL_AGENTS_POOL.with(|pool| {
|
||||
let mut pool = pool.borrow_mut();
|
||||
match pool.entry::<LocalAgent<AGN>>() {
|
||||
anymap2::Entry::Occupied(mut entry) => entry.get_mut().create_bridge(callback),
|
||||
anymap2::Entry::Vacant(entry) => {
|
||||
let scope = AgentScope::<AGN>::new();
|
||||
let launched = LocalAgent::new(&scope);
|
||||
let responder = SlabResponder {
|
||||
slab: launched.slab(),
|
||||
};
|
||||
scope_to_init = Some((scope, responder));
|
||||
entry.insert(launched).create_bridge(callback)
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Some((scope, responder)) = scope_to_init {
|
||||
let agent_link = AgentLink::connect(&scope, responder);
|
||||
let upd = AgentLifecycleEvent::Create(agent_link);
|
||||
scope.send(upd);
|
||||
}
|
||||
let upd = AgentLifecycleEvent::Connected(bridge.id);
|
||||
bridge.scope.send(upd);
|
||||
Box::new(bridge)
|
||||
}
|
||||
}
|
||||
|
||||
struct SlabResponder<AGN: Agent> {
|
||||
slab: Shared<Slab<Option<Callback<AGN::Output>>>>,
|
||||
}
|
||||
|
||||
impl<AGN: Agent> Responder<AGN> for SlabResponder<AGN> {
|
||||
fn respond(&self, id: HandlerId, output: AGN::Output) {
|
||||
locate_callback_and_respond::<AGN>(&self.slab, id, output);
|
||||
}
|
||||
}
|
||||
|
||||
impl<AGN: Agent> Dispatchable for Context<AGN> {}
|
||||
|
||||
struct ContextBridge<AGN: Agent> {
|
||||
scope: AgentScope<AGN>,
|
||||
id: HandlerId,
|
||||
}
|
||||
|
||||
impl<AGN: Agent> Bridge<AGN> for ContextBridge<AGN> {
|
||||
fn send(&mut self, msg: AGN::Input) {
|
||||
let upd = AgentLifecycleEvent::Input(msg, self.id);
|
||||
self.scope.send(upd);
|
||||
}
|
||||
}
|
||||
|
||||
impl<AGN: Agent> Drop for ContextBridge<AGN> {
|
||||
fn drop(&mut self) {
|
||||
let terminate_worker = LOCAL_AGENTS_POOL.with(|pool| {
|
||||
let mut pool = pool.borrow_mut();
|
||||
let terminate_worker = {
|
||||
if let Some(launched) = pool.get_mut::<LocalAgent<AGN>>() {
|
||||
launched.remove_bridge(self)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if terminate_worker {
|
||||
pool.remove::<LocalAgent<AGN>>();
|
||||
}
|
||||
|
||||
terminate_worker
|
||||
});
|
||||
|
||||
let upd = AgentLifecycleEvent::Disconnected(self.id);
|
||||
self.scope.send(upd);
|
||||
|
||||
if terminate_worker {
|
||||
let upd = AgentLifecycleEvent::Destroy;
|
||||
self.scope.send(upd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalAgent<AGN: Agent> {
|
||||
scope: AgentScope<AGN>,
|
||||
slab: SharedOutputSlab<AGN>,
|
||||
}
|
||||
|
||||
impl<AGN: Agent> LocalAgent<AGN> {
|
||||
pub fn new(scope: &AgentScope<AGN>) -> Self {
|
||||
let slab = Rc::new(RefCell::new(Slab::new()));
|
||||
LocalAgent {
|
||||
scope: scope.clone(),
|
||||
slab,
|
||||
}
|
||||
}
|
||||
|
||||
fn slab(&self) -> SharedOutputSlab<AGN> {
|
||||
self.slab.clone()
|
||||
}
|
||||
|
||||
fn create_bridge(&mut self, callback: Option<Callback<AGN::Output>>) -> ContextBridge<AGN> {
|
||||
let respondable = callback.is_some();
|
||||
let mut slab = self.slab.borrow_mut();
|
||||
let id: usize = slab.insert(callback);
|
||||
let id = HandlerId::new(id, respondable);
|
||||
ContextBridge {
|
||||
scope: self.scope.clone(),
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_bridge(&mut self, bridge: &ContextBridge<AGN>) -> Last {
|
||||
let mut slab = self.slab.borrow_mut();
|
||||
let _ = slab.remove(bridge.id.raw_id());
|
||||
slab.is_empty()
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
use super::*;
|
||||
use std::marker::PhantomData;
|
||||
use yew::callback::Callback;
|
||||
|
||||
const SINGLETON_ID: HandlerId = HandlerId(0, true);
|
||||
|
||||
/// Create an instance in the current thread.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Job<AGN> {
|
||||
_agent: PhantomData<AGN>,
|
||||
}
|
||||
|
||||
impl<AGN> Discoverer for Job<AGN>
|
||||
where
|
||||
AGN: Agent,
|
||||
{
|
||||
type Agent = AGN;
|
||||
|
||||
fn spawn_or_join(callback: Option<Callback<AGN::Output>>) -> Box<dyn Bridge<AGN>> {
|
||||
let callback = callback.expect("Callback required for Job");
|
||||
let scope = AgentScope::<AGN>::new();
|
||||
let responder = CallbackResponder { callback };
|
||||
let agent_link = AgentLink::connect(&scope, responder);
|
||||
let upd = AgentLifecycleEvent::Create(agent_link);
|
||||
scope.send(upd);
|
||||
let upd = AgentLifecycleEvent::Connected(SINGLETON_ID);
|
||||
scope.send(upd);
|
||||
let bridge = JobBridge { scope };
|
||||
Box::new(bridge)
|
||||
}
|
||||
}
|
||||
|
||||
struct JobBridge<AGN: Agent> {
|
||||
scope: AgentScope<AGN>,
|
||||
}
|
||||
|
||||
impl<AGN: Agent> Bridge<AGN> for JobBridge<AGN> {
|
||||
fn send(&mut self, msg: AGN::Input) {
|
||||
let upd = AgentLifecycleEvent::Input(msg, SINGLETON_ID);
|
||||
self.scope.send(upd);
|
||||
}
|
||||
}
|
||||
|
||||
impl<AGN: Agent> Drop for JobBridge<AGN> {
|
||||
fn drop(&mut self) {
|
||||
let upd = AgentLifecycleEvent::Disconnected(SINGLETON_ID);
|
||||
self.scope.send(upd);
|
||||
let upd = AgentLifecycleEvent::Destroy;
|
||||
self.scope.send(upd);
|
||||
}
|
||||
}
|
||||
|
||||
struct CallbackResponder<AGN: Agent> {
|
||||
callback: Callback<AGN::Output>,
|
||||
}
|
||||
|
||||
impl<AGN: Agent> Responder<AGN> for CallbackResponder<AGN> {
|
||||
fn respond(&self, id: HandlerId, output: AGN::Output) {
|
||||
assert_eq!(id.raw_id(), SINGLETON_ID.raw_id());
|
||||
self.callback.emit(output);
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
mod context;
|
||||
mod job;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub use context::Context;
|
||||
pub use job::Job;
|
|
@ -1 +0,0 @@
|
|||
pub mod store;
|
|
@ -1,162 +0,0 @@
|
|||
use crate::{Agent, AgentLink, Bridge, Context, Discoverer, Dispatched, Dispatcher, HandlerId};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
|
||||
/// A functional state wrapper, enforcing a unidirectional
|
||||
/// data flow and consistent state to the observers.
|
||||
///
|
||||
/// `handle_input` receives incoming messages from components,
|
||||
/// `reduce` applies changes to the state
|
||||
///
|
||||
/// The state is sent once whenever a bridge is opened and then once
|
||||
/// for each `Action` sent by the `handle_input` function. This means
|
||||
/// the initial state of the store must be valid for the consumers.
|
||||
///
|
||||
/// Once created with a first bridge, a Store will never be destroyed
|
||||
/// for the lifetime of the application.
|
||||
pub trait Store: Sized + 'static {
|
||||
/// Messages instructing the store to do somethin
|
||||
type Input;
|
||||
/// State updates to be consumed by `reduce`
|
||||
type Action;
|
||||
|
||||
/// Create a new Store
|
||||
fn new() -> Self;
|
||||
|
||||
/// Receives messages from components and other agents. Use the `link`
|
||||
/// to send actions to itself in order to notify `reduce` once your
|
||||
/// operation completes. This is the place to do side effects, like
|
||||
/// talking to the server, or asking the user for input.
|
||||
///
|
||||
/// Note that you can look at the state of your Store, but you
|
||||
/// cannot modify it here. If you want to modify it, send a Message
|
||||
/// to the reducer
|
||||
fn handle_input(&self, link: AgentLink<StoreWrapper<Self>>, msg: Self::Input);
|
||||
|
||||
/// A pure function, with no side effects. Receives a message,
|
||||
/// and applies it to the state as it sees fit.
|
||||
fn reduce(&mut self, msg: Self::Action);
|
||||
}
|
||||
|
||||
/// Hides the full context Agent from a Store and does
|
||||
/// the boring data wrangling logic
|
||||
#[derive(Debug)]
|
||||
pub struct StoreWrapper<S: Store> {
|
||||
/// Currently subscribed components and agents
|
||||
pub handlers: HashSet<HandlerId>,
|
||||
/// Link to itself so Store::handle_input can send actions to reducer
|
||||
pub link: AgentLink<Self>,
|
||||
|
||||
/// The actual Store
|
||||
pub state: Shared<S>,
|
||||
|
||||
/// A circular dispatcher to itself so the store is not removed
|
||||
pub self_dispatcher: Dispatcher<Self>,
|
||||
}
|
||||
|
||||
type Shared<T> = Rc<RefCell<T>>;
|
||||
|
||||
/// A wrapper ensuring state observers can only
|
||||
/// borrow the state immutably
|
||||
#[derive(Debug)]
|
||||
pub struct ReadOnly<S> {
|
||||
state: Shared<S>,
|
||||
}
|
||||
|
||||
impl<S> ReadOnly<S> {
|
||||
/// Allow only immutable borrows to the underlying data
|
||||
pub fn borrow(&self) -> impl Deref<Target = S> + '_ {
|
||||
self.state.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a wrapper, intended to be used as an opaque
|
||||
/// machinery allowing the Store to do it's things.
|
||||
impl<S: Store> Agent for StoreWrapper<S> {
|
||||
type Reach = Context<Self>;
|
||||
type Message = S::Action;
|
||||
type Input = S::Input;
|
||||
type Output = ReadOnly<S>;
|
||||
|
||||
fn create(link: AgentLink<Self>) -> Self {
|
||||
let state = Rc::new(RefCell::new(S::new()));
|
||||
let handlers = HashSet::new();
|
||||
|
||||
// Link to self to never go out of scope
|
||||
let self_dispatcher = Self::dispatcher();
|
||||
|
||||
StoreWrapper {
|
||||
handlers,
|
||||
link,
|
||||
state,
|
||||
self_dispatcher,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) {
|
||||
{
|
||||
self.state.borrow_mut().reduce(msg);
|
||||
}
|
||||
|
||||
for handler in self.handlers.iter() {
|
||||
self.link.respond(
|
||||
*handler,
|
||||
ReadOnly {
|
||||
state: self.state.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn connected(&mut self, id: HandlerId) {
|
||||
self.handlers.insert(id);
|
||||
self.link.respond(
|
||||
id,
|
||||
ReadOnly {
|
||||
state: self.state.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) {
|
||||
self.state.borrow().handle_input(self.link.clone(), msg);
|
||||
}
|
||||
|
||||
fn disconnected(&mut self, id: HandlerId) {
|
||||
self.handlers.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
// This instance is quite unfortunate, as the Rust compiler
|
||||
// does not support mutually exclusive trait bounds (https://github.com/rust-lang/rust/issues/51774),
|
||||
// we have to create a new trait with the same function as in the original one.
|
||||
|
||||
/// Allows us to communicate with a store
|
||||
pub trait Bridgeable: Sized + 'static {
|
||||
/// A wrapper for the store we want to bridge to,
|
||||
/// which serves as a communication intermediary
|
||||
type Wrapper: Agent;
|
||||
|
||||
/// Creates a messaging bridge between a worker and the component.
|
||||
fn bridge(
|
||||
callback: Callback<<Self::Wrapper as Agent>::Output>,
|
||||
) -> Box<dyn Bridge<Self::Wrapper>>;
|
||||
}
|
||||
|
||||
/// Implementation of bridge creation
|
||||
impl<T> Bridgeable for T
|
||||
where
|
||||
T: Store,
|
||||
{
|
||||
/// The hiding wrapper
|
||||
type Wrapper = StoreWrapper<T>;
|
||||
|
||||
fn bridge(
|
||||
callback: Callback<<Self::Wrapper as Agent>::Output>,
|
||||
) -> Box<dyn Bridge<Self::Wrapper>> {
|
||||
<Self::Wrapper as Agent>::Reach::spawn_or_join(Some(callback))
|
||||
}
|
||||
}
|
|
@ -11,8 +11,7 @@ yew-agent = { path = "../../packages/yew-agent/" }
|
|||
[dev-dependencies]
|
||||
boolinator = "2.4"
|
||||
derive_more = "0.99"
|
||||
gloo-events = "0.1"
|
||||
gloo-utils = "0.1"
|
||||
gloo = "0.5"
|
||||
js-sys = "0.3"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
//! Agent types that compile to be used by website code snippets
|
||||
|
||||
use yew_agent::{Agent, AgentLink, Context, HandlerId};
|
||||
|
||||
pub struct EventBus;
|
||||
|
||||
impl Agent for EventBus {
|
||||
type Reach = Context<Self>;
|
||||
type Message = ();
|
||||
type Input = ();
|
||||
type Output = String;
|
||||
|
||||
fn create(_link: AgentLink<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) {
|
||||
// impl
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, _msg: Self::Input, _id: HandlerId) {
|
||||
// impl
|
||||
}
|
||||
}
|
||||
|
||||
pub enum WorkerMsg {
|
||||
Process,
|
||||
}
|
||||
|
||||
pub struct MyWorker;
|
||||
|
||||
impl Agent for MyWorker {
|
||||
type Reach = Context<Self>;
|
||||
|
||||
type Message = ();
|
||||
type Input = WorkerMsg;
|
||||
type Output = ();
|
||||
|
||||
fn create(_link: AgentLink<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) {
|
||||
// impl
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, _msg: Self::Input, _id: HandlerId) {
|
||||
// impl
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
pub mod agents;
|
||||
pub mod tutorial;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/website_tests.rs"));
|
||||
|
|
|
@ -26,7 +26,7 @@ pub struct ModalProps {
|
|||
|
||||
#[function_component(Modal)]
|
||||
fn modal(props: &ModalProps) -> Html {
|
||||
let modal_host = gloo_utils::document()
|
||||
let modal_host = gloo::utils::document()
|
||||
.get_element_by_id("modal_host")
|
||||
.expect("a #modal_host element");
|
||||
|
||||
|
|
|
@ -6,13 +6,7 @@ description: "Yew's Actor System"
|
|||
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||
import ThemedImage from '@theme/ThemedImage';
|
||||
|
||||
Agents are similar to Angular's [Services](https://angular.io/guide/architecture-services)
|
||||
\(but without dependency injection\), and provide Yew with an
|
||||
[Actor Model](https://en.wikipedia.org/wiki/Actor_model). Agents can be used to route messages
|
||||
between components independently of where they sit in the component hierarchy, or they can be used
|
||||
to create shared state between different components. Agents can also be used to offload
|
||||
computationally expensive tasks from the main thread which renders the UI. There is also planned
|
||||
support for using agents to allow Yew applications to communicate across tabs \(in the future\).
|
||||
Agents are a way to offload tasks to web workers or achieve inter-tab communication(WIP).
|
||||
|
||||
In order for agents to run concurrently, Yew uses
|
||||
[web-workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers).
|
||||
|
@ -37,19 +31,14 @@ The code can be found in the <desc> tag of the svgs.
|
|||
|
||||
### Reaches
|
||||
|
||||
* Context - There will exist at most one instance of a Context Agent at any given time. Bridges will
|
||||
spawn or connect to an already spawned agent on the UI thread. This can be used to coordinate
|
||||
state between components or other agents. When no bridges are connected to this agent, the agent
|
||||
will disappear.
|
||||
* Public - There will exist at most one instance of a Public Agent at any given time. Bridges will
|
||||
spawn or connect to an already spawned agent in a web worker.
|
||||
When no bridges are connected to this agent, the agent will disappear.
|
||||
|
||||
* Job - Spawn a new agent on the UI thread for every new bridge. This is good for moving shared but
|
||||
* Private - Spawn a new agent in a web worker for every new bridge. This is good for moving shared but
|
||||
independent behavior that communicates with the browser out of components. \(TODO verify\) When
|
||||
the task is done, the agent will disappear.
|
||||
|
||||
* Public - Same as Context, but runs on its own web worker.
|
||||
|
||||
* Private - Same as Job, but runs on its own web worker.
|
||||
|
||||
* Global \(WIP\)
|
||||
|
||||
## Communication between Agents and Components
|
||||
|
@ -66,13 +55,11 @@ A dispatcher allows uni-directional communication between a component and an age
|
|||
|
||||
## Overhead
|
||||
|
||||
Agents that use web workers \(i.e. Private and Public\) will incur a serialization overhead on the
|
||||
messages they send and receive. They use [bincode](https://github.com/servo/bincode) to communicate
|
||||
with other threads, so the cost is substantially higher than just calling a function. Unless the
|
||||
cost of computation will outweigh the cost of message passing, you should use agents running on the
|
||||
UI thread \(i.e. Job or Context\).
|
||||
Agents use web workers \(i.e. Private and Public\). They incur a serialization overhead on the
|
||||
messages they send and receive. Agents use [bincode](https://github.com/servo/bincode) to communicate
|
||||
with other threads, so the cost is substantially higher than just calling a function.
|
||||
|
||||
## Further reading
|
||||
|
||||
* The [pub\_sub](https://github.com/yewstack/yew/tree/master/examples/pub_sub) example shows how
|
||||
components can use agents to communicate with each other.
|
||||
* The [multi\_thread](https://github.com/yewstack/yew/tree/master/examples/multi_thread) example shows how
|
||||
components can send message to and receive message from agents.
|
||||
|
|
|
@ -5,84 +5,114 @@ description: "Defining your own Hooks "
|
|||
|
||||
## Defining custom Hooks
|
||||
|
||||
Component's stateful logic can be extracted into usable function by creating custom Hooks.
|
||||
Component's stateful logic can be extracted into usable function by creating custom Hooks
|
||||
|
||||
Consider that we wish to create an event listener that listens to an event on the `window`
|
||||
object.
|
||||
|
||||
Consider that we have a component which subscribes to an agent and displays the messages sent to it.
|
||||
```rust
|
||||
use yew::{function_component, html, use_effect, use_state, Callback};
|
||||
use yew_agent::Bridged;
|
||||
// EventBus is an implementation yew_agent::Agent
|
||||
use website_test::agents::EventBus;
|
||||
use yew::prelude::*;
|
||||
use gloo::events::EventListener;
|
||||
use gloo::utils::window;
|
||||
use std::mem::drop;
|
||||
|
||||
|
||||
#[function_component(ShowMessages)]
|
||||
pub fn show_messages() -> Html {
|
||||
let state = use_state(Vec::new);
|
||||
#[function_component(ShowStorageChanged)]
|
||||
pub fn show_storage_changed() -> Html {
|
||||
let state_storage_changed = use_state(|| false);
|
||||
|
||||
{
|
||||
let state = state.clone();
|
||||
use_effect(move || {
|
||||
let producer = EventBus::bridge(Callback::from(move |msg| {
|
||||
let mut messages = (*state).clone();
|
||||
messages.push(msg);
|
||||
state.set(messages)
|
||||
}));
|
||||
let state_storage_changed = state_storage_changed.clone();
|
||||
use_effect(|| {
|
||||
let listener = EventListener::new(&window(), "storage", move |_| state_storage_changed.set(true));
|
||||
|
||||
|| drop(producer)
|
||||
move || { drop(listener); }
|
||||
});
|
||||
}
|
||||
|
||||
let output = state.iter().map(|it| html! { <p>{ it }</p> });
|
||||
html! { <div>{ for output }</div> }
|
||||
html! { <div>{"Storage Event Fired: "}{*state_storage_changed}</div> }
|
||||
}
|
||||
```
|
||||
|
||||
There's one problem with this code: the logic can't be reused by another component.
|
||||
If we build another component which keeps track of the messages, instead of copying the code we can move the logic into a custom hook.
|
||||
If we build another component which keeps track of the an event,
|
||||
instead of copying the code we can move the logic into a custom hook.
|
||||
|
||||
We'll start by creating a new function called `use_subscribe`.
|
||||
We'll start by creating a new function called `use_event`.
|
||||
The `use_` prefix conventionally denotes that a function is a hook.
|
||||
This function will take no arguments and return `Rc<RefCell<Vec<String>>>`.
|
||||
This function will take an event target, a event type and a callback.
|
||||
```rust
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use web_sys::{Event, EventTarget};
|
||||
use std::borrow::Cow;
|
||||
use gloo::events::EventListener;
|
||||
|
||||
fn use_subscribe() -> Rc<RefCell<Vec<String>>> {
|
||||
pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F)
|
||||
where
|
||||
E: Into<Cow<'static, str>>,
|
||||
F: Fn(&Event) + 'static,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
This is a simple hook which can be created by combining other hooks. For this example, we'll two pre-defined hooks.
|
||||
We'll use `use_state` hook to store the `Vec` for messages, so they persist between component re-renders.
|
||||
We'll also use `use_effect` to subscribe to the `EventBus` `Agent` so the subscription can be tied to component's lifecycle.
|
||||
This is a simple hook which can be created by using built-in hooks. For this example, we'll use the `use_effect_with_deps` hook,
|
||||
which subscribes to the dependencies so an event listener can be recreated when hook arguments change.
|
||||
|
||||
```rust
|
||||
use std::collections::HashSet;
|
||||
use yew::{use_effect, use_state, Callback};
|
||||
use yew_agent::Bridged;
|
||||
// EventBus is an implementation yew_agent::Agent
|
||||
use website_test::agents::EventBus;
|
||||
use yew::prelude::*;
|
||||
use web_sys::{Event, EventTarget};
|
||||
use std::borrow::Cow;
|
||||
use std::rc::Rc;
|
||||
use gloo::events::EventListener;
|
||||
|
||||
fn use_subscribe() -> Vec<String> {
|
||||
let state = use_state(Vec::new);
|
||||
pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F)
|
||||
where
|
||||
E: Into<Cow<'static, str>>,
|
||||
F: Fn(&Event) + 'static,
|
||||
{
|
||||
#[derive(Clone)]
|
||||
struct EventDependents {
|
||||
target: EventTarget,
|
||||
event_type: Cow<'static, str>,
|
||||
callback: Rc<dyn Fn(&Event)>,
|
||||
}
|
||||
|
||||
let effect_state = state.clone();
|
||||
#[allow(clippy::vtable_address_comparisons)]
|
||||
impl PartialEq for EventDependents {
|
||||
fn eq(&self, rhs: &Self) -> bool {
|
||||
self.target == rhs.target
|
||||
&& self.event_type == rhs.event_type
|
||||
&& Rc::ptr_eq(&self.callback, &rhs.callback)
|
||||
}
|
||||
}
|
||||
|
||||
use_effect(move || {
|
||||
let producer = EventBus::bridge(Callback::from(move |msg| {
|
||||
let mut messages = (*effect_state).clone();
|
||||
messages.push(msg);
|
||||
effect_state.set(messages)
|
||||
}));
|
||||
|| drop(producer)
|
||||
let deps = EventDependents {
|
||||
target: target.clone(),
|
||||
event_type: event_type.into(),
|
||||
callback: Rc::new(callback) as Rc<dyn Fn(&Event)>,
|
||||
};
|
||||
|
||||
use_effect_with_deps(
|
||||
|deps| {
|
||||
let EventDependents {
|
||||
target,
|
||||
event_type,
|
||||
callback,
|
||||
} = deps.clone();
|
||||
|
||||
let listener = EventListener::new(&target, event_type, move |e| {
|
||||
callback(e);
|
||||
});
|
||||
|
||||
(*state).clone()
|
||||
move || {
|
||||
drop(listener);
|
||||
}
|
||||
},
|
||||
deps,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Although this approach works in almost all cases, it can't be used to write primitive hooks like the pre-defined hooks we've been using already
|
||||
Although this approach works in almost all cases, it can't be used to write primitive hooks like the pre-defined hooks we've been using already.
|
||||
|
||||
### Writing primitive hooks
|
||||
|
||||
`use_hook` function is used to write such hooks. View the docs on [docs.rs](https://docs.rs/yew/0.18.0/yew-functional/use_hook.html) for the documentation
|
||||
and `hooks` directory to see implementations of pre-defined hooks.
|
||||
View the docs on [docs.rs](https://docs.rs/yew) for documentation and `hooks` directory to see implementations of pre-defined hooks.
|
||||
|
|
|
@ -75,25 +75,15 @@ If you need a mutable reference, consider using [`use_mut_ref`](#use_mut_ref).
|
|||
If you need the component to be re-rendered on state change, consider using [`use_state`](#use_state).
|
||||
|
||||
```rust
|
||||
// EventBus is an implementation of yew_agent::Agent
|
||||
use website_test::agents::EventBus;
|
||||
use yew::{function_component, html, use_ref, use_state, Callback};
|
||||
use yew_agent::Bridged;
|
||||
|
||||
#[function_component(UseRef)]
|
||||
fn ref_hook() -> Html {
|
||||
let greeting = use_state(|| "No one has greeted me yet!".to_owned());
|
||||
|
||||
{
|
||||
let greeting = greeting.clone();
|
||||
use_ref(|| EventBus::bridge(Callback::from(move |msg| {
|
||||
greeting.set(msg);
|
||||
})));
|
||||
}
|
||||
let message = use_ref(|| "Some Expensive State.".to_string());
|
||||
|
||||
html! {
|
||||
<div>
|
||||
<span>{ (*greeting).clone() }</span>
|
||||
<span>{ (*message).clone() }</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
@ -122,7 +112,7 @@ fn mut_ref_hook() -> Html {
|
|||
let message_count = use_mut_ref(|| 0);
|
||||
|
||||
let onclick = Callback::from(move |_| {
|
||||
let window = gloo_utils::window();
|
||||
let window = gloo::utils::window();
|
||||
|
||||
if *message_count.borrow_mut() > 3 {
|
||||
window.alert_with_message("Message limit reached").unwrap();
|
||||
|
@ -316,10 +306,10 @@ fn effect() -> Html {
|
|||
let counter = counter.clone();
|
||||
use_effect(move || {
|
||||
// Make a call to DOM API after component is rendered
|
||||
gloo_utils::document().set_title(&format!("You clicked {} times", *counter));
|
||||
gloo::utils::document().set_title(&format!("You clicked {} times", *counter));
|
||||
|
||||
// Perform the cleanup
|
||||
|| gloo_utils::document().set_title("You clicked 0 times")
|
||||
|| gloo::utils::document().set_title("You clicked 0 times")
|
||||
});
|
||||
}
|
||||
let onclick = {
|
||||
|
|
|
@ -17,7 +17,7 @@ used as a `Html` value using `VRef`:
|
|||
```rust
|
||||
use web_sys::{Element, Node};
|
||||
use yew::{Component, Context, html, Html};
|
||||
use gloo_utils::document;
|
||||
use gloo::utils::document;
|
||||
|
||||
struct Comp;
|
||||
|
||||
|
@ -161,45 +161,6 @@ impl Component for MyComponent {
|
|||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="Agent Handler" label="Agent Handler">
|
||||
|
||||
```rust
|
||||
use yew::{html, Component, Context, Html};
|
||||
use yew_agent::{Dispatcher, Dispatched};
|
||||
use website_test::agents::{MyWorker, WorkerMsg};
|
||||
|
||||
struct MyComponent {
|
||||
worker: Dispatcher<MyWorker>,
|
||||
}
|
||||
|
||||
impl Component for MyComponent {
|
||||
type Message = WorkerMsg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
MyComponent {
|
||||
worker: MyWorker::dispatcher(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
self.worker.send(msg);
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
// Create a callback from a worker to handle it in another context
|
||||
let click_callback = ctx.link().callback(|_| WorkerMsg::Process);
|
||||
html! {
|
||||
<button onclick={click_callback}>
|
||||
{ "Click me!" }
|
||||
</button>
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
|
|
@ -630,7 +630,7 @@ use yew::{
|
|||
Component, Context, Html, NodeRef,
|
||||
};
|
||||
|
||||
use gloo_events::EventListener;
|
||||
use gloo::events::EventListener;
|
||||
|
||||
pub struct Comp {
|
||||
my_div: NodeRef,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
title: "From 0.1.0 to 0.2.0"
|
||||
---
|
||||
|
||||
The `Context` and `Job` Agents have been removed in favour of Yew's Context API.
|
||||
|
||||
You can see the updated [`pub_sub`](https://github.com/yewstack/yew/tree/master/examples/pub_sub) example about how to use Context API.
|
||||
|
||||
For users of `yew_agent::utils::store`, you may switch to third party solutions like: [Yewdux](https://github.com/intendednull/yewdux) or [Bounce](https://github.com/futursolo/bounce).
|
Loading…
Reference in New Issue