Skip to content

Commit

Permalink
Feature: History/Undo/Redo (#54)
Browse files Browse the repository at this point in the history
* updated a usage of use_memo

* added yewdux utils

* added history implementation and example
  • Loading branch information
wainwrightmark authored May 26, 2023
1 parent b046f6b commit 51718ad
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 1 deletion.
16 changes: 16 additions & 0 deletions crates/yewdux-utils/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "yewdux-utils"
version = "0.1.0"
authors = ["Noah <[email protected]>"]
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/yewdux/yewdux-utils"
readme = "../../README.md"
description = "Ergonomic state management for Yew applications"
keywords = ["yew", "state", "redux", "shared", "container"]
categories = ["wasm", "web-programming", "rust-patterns"]


[dependencies]
yewdux = { path = "../yewdux" }

139 changes: 139 additions & 0 deletions crates/yewdux-utils/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use std::{marker::PhantomData, rc::Rc};
use yewdux::prelude::*;

#[derive(Default)]
pub struct HistoryListener<T: Store + PartialEq>(PhantomData<T>);

struct HistoryChangeMessage<T: Store + PartialEq>(Rc<T>);

impl<T: Store + PartialEq> Reducer<HistoryStore<T>> for HistoryChangeMessage<T> {
fn apply(self, mut state: Rc<HistoryStore<T>>) -> Rc<HistoryStore<T>> {
if state.matches_current(&self.0) {
return state;
}

let mut mut_state = Rc::make_mut(&mut state);
mut_state.index += 1;
mut_state.vector.truncate(mut_state.index);
mut_state.vector.push(self.0);

state
}
}

impl<T: Store + PartialEq> Listener for HistoryListener<T> {
type Store = T;

fn on_change(&mut self, state: Rc<Self::Store>) {
Dispatch::<HistoryStore<T>>::new().apply(HistoryChangeMessage::<T>(state))
}
}

#[derive(Debug, Store, PartialEq)]
pub struct HistoryStore<T: Store + PartialEq> {
vector: Vec<Rc<T>>,
index: usize,
}

impl<T: Store + PartialEq> Clone for HistoryStore<T> {
fn clone(&self) -> Self {
Self {
vector: self.vector.clone(),
index: self.index,
}
}
}

impl<T: Store + PartialEq> HistoryStore<T> {
pub fn can_apply(&self, message: &HistoryMessage) -> bool {
match message {
HistoryMessage::Undo => self.index > 0,
HistoryMessage::Redo => self.index + 1 < self.vector.len(),
HistoryMessage::Clear => self.vector.len() > 1,
HistoryMessage::JumpTo(index) => index != &self.index && index < &self.vector.len(),
}
}

fn matches_current(&self, state: &Rc<T>) -> bool {
let c = self.current();
Rc::ptr_eq(c, state)
}

fn current(&self) -> &Rc<T> {
&self.vector[self.index]
}

pub fn index(&self) -> usize {
self.index
}

pub fn states(&self) -> &[Rc<T>] {
self.vector.as_slice()
}
}

impl<T: Store + PartialEq> Default for HistoryStore<T> {
fn default() -> Self {
let s1 = Dispatch::<T>::new().get();
Self {
vector: vec![s1],
index: 0,
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HistoryMessage {
Undo,
Redo,
Clear,
JumpTo(usize),
}

impl<T: Store + PartialEq + Clone> Reducer<HistoryStore<T>> for HistoryMessage {
fn apply(self, mut state: Rc<HistoryStore<T>>) -> Rc<HistoryStore<T>> {
let mut_state = Rc::make_mut(&mut state);

let state_changed = match self {
HistoryMessage::Undo => {
if let Some(new_index) = mut_state.index.checked_sub(1) {
mut_state.index = new_index;
true
} else {
false
}
}
HistoryMessage::Redo => {
let new_index = mut_state.index + 1;
if new_index < mut_state.vector.len() {
mut_state.index = new_index;
true
} else {
false
}
}
HistoryMessage::Clear => {
let current = mut_state.vector[mut_state.index].clone();
mut_state.vector.clear();
mut_state.vector.push(current);
mut_state.index = 0;
false
}
HistoryMessage::JumpTo(index) => {
if index < mut_state.vector.len() {
mut_state.index = index;

true
} else {
false
}
}
};

if state_changed {
Dispatch::<T>::new().reduce(|_| mut_state.current().clone());
}

state
}
}
3 changes: 2 additions & 1 deletion crates/yewdux/src/functional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ where
let _dispatch = {
let selected = selected.clone();
use_memo(
deps,
move |deps| {
let deps = deps.clone();
Dispatch::subscribe(move |val: Rc<S>| {
Expand All @@ -188,7 +189,7 @@ where
}
})
},
deps,

)
};

Expand Down
11 changes: 11 additions & 0 deletions examples/history/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "history"
version = "0.1.0"
authors = ["Noah <[email protected]>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
yew = { git = "https://github.com/yewstack/yew.git", features = ["csr"] }
yewdux = { path = "../../crates/yewdux" }
yewdux-utils = { path = "../../crates/yewdux-utils" }
4 changes: 4 additions & 0 deletions examples/history/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<html>
<head>
</head>
</html>
82 changes: 82 additions & 0 deletions examples/history/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use yew::prelude::*;
use yewdux::prelude::*;
use yewdux_utils::*;

#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct State {
count: u32,
}

impl Store for State {
fn new() -> Self {
init_listener(HistoryListener::<State>::default());
Self::default()
}

fn should_notify(&self, old: &Self) -> bool {
self != old
}
}

#[function_component]
fn App() -> Html {
let (state, dispatch) = use_store::<State>();
let on_increment_click = dispatch.reduce_mut_callback(|state| state.count += 1);
let on_decrement_click = dispatch.reduce_mut_callback(|state| state.count -= 1);

html! {
<>
<p>{ state.count }</p>
<button onclick={on_increment_click}>{"+1"}</button>
<button onclick={on_decrement_click}>{"-1"}</button>

<br/>
<br/>
<Controls />
</>
}
}

#[function_component]
fn Controls() -> Html {
let (state, dispatch) = use_store::<HistoryStore<State>>();

let on_undo_click = dispatch.apply_callback(|_| HistoryMessage::Undo);
let on_redo_click = dispatch.apply_callback(|_| HistoryMessage::Redo);
let on_clear_click = dispatch.apply_callback(|_| HistoryMessage::Clear);

let undo_disabled = !state.can_apply(&HistoryMessage::Undo);
let redo_disabled = !state.can_apply(&HistoryMessage::Redo);
let clear_disabled = !state.can_apply(&HistoryMessage::Clear);

let rows: Html = state
.states()
.iter()
.enumerate()
.map(|(i, x)| {
let matches = i == state.index();
let match_text = matches.then(|| "<<<");
let text = format!("{x:?}");

let onclick = dispatch.apply_callback(move |_| HistoryMessage::JumpTo(i));

html!(<tr><td><button {onclick}>{text}</button></td> <td>{match_text}</td> </tr>)
})
.collect();

html!(
<div>
<button onclick={on_undo_click} disabled={undo_disabled}>{"Undo"}</button>
<button onclick={on_redo_click} disabled={redo_disabled}>{"Redo"}</button>
<button onclick={on_clear_click} disabled={clear_disabled}>{"Clear History"}</button>

<table>
{rows}
</table>
</div>
)
}

fn main() {
yew::Renderer::<App>::new().render();
}

0 comments on commit 51718ad

Please sign in to comment.