feat: improved <For/> algorithm (#1146)

Rewrites the algorithm behind the `<For/>` component to create a more robust keyed list implementation, with the potential for future additional optimizations related to grouping moved ranges.

Closes #533.
This commit is contained in:
jquesada2016 2023-06-11 08:20:14 -06:00 committed by GitHub
parent bb10b32200
commit b6d9060152
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 966 additions and 363 deletions

View file

@ -1,2 +1,2 @@
[build] # [build]
# target = "wasm32-unknown-unknown" # target = "wasm32-unknown-unknown"

View file

@ -9,5 +9,6 @@ gloo = { version = "0.8", features = ["futures"] }
leptos = { path = "../../../leptos", features = ["tracing"] } leptos = { path = "../../../leptos", features = ["tracing"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
tracing-subscriber-wasm = "0.1"
[workspace] [workspace]

View file

@ -1,102 +1,142 @@
#![feature(iter_intersperse)]
#![allow(warnings)] #![allow(warnings)]
#[macro_use] #[macro_use]
extern crate tracing; extern crate tracing;
mod utils;
use leptos::*; use leptos::*;
use tracing::field::debug; use tracing::field::debug;
use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::util::SubscriberInitExt;
fn main() { fn main() {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_max_level(tracing::Level::TRACE) .with_max_level(tracing::Level::TRACE)
.without_time() .without_time()
.with_file(true) .with_file(true)
.with_line_number(true) .with_line_number(true)
.with_target(false) .with_target(false)
.with_writer(utils::MakeConsoleWriter) .with_writer(tracing_subscriber_wasm::MakeConsoleWriter::default())
.with_ansi(false) .with_ansi(false)
.pretty() .pretty()
.finish() .finish()
.init(); .init();
mount_to_body(view_fn); mount_to_body(view_fn);
} }
// fn view_fn(cx: Scope) -> impl IntoView {
// view! { cx,
// <h2>"Passing Tests"</h2>
// <ul>
// /* These work! */
// <Test from=[1] to=[] />
// <Test from=[1, 2] to=[] />
// <Test from=[1, 2, 3] to=[] />
// <hr/>
// <Test from=[] to=[1] />
// <Test from=[1, 2] to=[1] />
// <Test from=[2, 1] to=[1] />
// <hr/>
// <Test from=[1, 2, 3] to=[1, 2] />
// <Test from=[2] to=[1, 2] />
// <Test from=[1] to=[1, 2] />
// <Test from=[] to=[1, 2, 3] />
// <Test from=[2] to=[1, 2, 3] />
// <Test from=[1] to=[1, 2, 3] />
// <Test from=[1, 3, 2] to=[1, 2, 3] />
// <Test from=[2, 1, 3] to=[1, 2, 3] />
// </ul>
// <h2>"Broken Tests"</h2>
// <ul>
// <Test from=[3] to=[1, 2, 3] />
// <Test from=[3, 1] to=[1, 2, 3] />
// <Test from=[3, 2, 1] to=[1, 2, 3] />
// <hr/>
// <Test from=[1, 4, 2, 3] to=[1, 2, 3, 4] />
// <hr/>
// <Test from=[1, 4, 3, 2, 5] to=[1, 2, 3, 4, 5] />
// <Test from=[4, 5, 3, 1, 2] to=[1, 2, 3, 4, 5] />
// </ul>
// }
// }
// #[component]
// fn Test<From, To>(cx: Scope, from: From, to: To) -> impl IntoView
// where
// From: IntoIterator<Item = usize>,
// To: IntoIterator<Item = usize>,
// {
// let from = from.into_iter().collect::<Vec<_>>();
// let to = to.into_iter().collect::<Vec<_>>();
// let (list, set_list) = create_signal(cx, from.clone());
// request_animation_frame({
// let to = to.clone();
// move || {
// set_list(to);
// }
// });
// view! { cx,
// <li>
// "from: [" {move ||
// from
// .iter()
// .map(ToString::to_string)
// .intersperse(", ".to_string())
// .collect::<String>()
// } "]"
// <br />
// "to: [" {move ||
// to
// .iter()
// .map(ToString::to_string)
// .intersperse(", ".to_string())
// .collect::<String>()
// } "]"
// <br />
// "result: ["
// <For
// each=list
// key=|i| *i
// view=|cx, i| {
// view! { cx, <span>{i} ", "</span> }
// }
// /> "]"
// /* <p>
// "Pre | "
// <For
// each=list
// key=|i| *i
// view=|cx, i| {
// view! { cx, <span>{i}</span> }
// }
// />
// " | Post"
// </p> */
// </li>
// }
// }
fn view_fn(cx: Scope) -> impl IntoView { fn view_fn(cx: Scope) -> impl IntoView {
let view = view! { cx, let (should_show_a, sett_should_show_a) = create_signal(cx, true);
<For
each=|| vec![0, 1, 2, 3, 4, 5, 6, 7]
key=|i| *i
view=|cx, i| view! { cx, {i} }
/>
}
.into_view(cx);
let (a, set_a) = create_signal(cx, view.clone()); let a = vec![1, 2, 3, 4];
let (b, set_b) = create_signal(cx, view); let b = vec![1, 2, 3];
let (is_a, set_is_a) = create_signal(cx, true); view! { cx,
<button on:click=move |_| sett_should_show_a.update(|show| *show = !*show)>"Toggle"</button>
let handle_toggle = move |_| { <For
trace!("toggling"); each={move || if should_show_a.get() {
if is_a() { a.clone()
set_b(a()); } else {
b.clone()
set_is_a(false); }}
} else { key=|i| *i
set_a(a()); view=|cx, i| view! { cx, <h1>{i}</h1> }
/>
set_is_a(true);
} }
};
let a_tag = view! { cx, <svg::a/> };
view! { cx,
<>
<div>
<button on:click=handle_toggle>"Toggle"</button>
</div>
<svg>{a_tag}</svg>
<Example/>
<A child=Signal::from(a) />
<A child=Signal::from(b) />
</>
}
}
#[component]
fn A(cx: Scope, child: Signal<View>) -> impl IntoView {
move || child()
}
#[component]
fn Example(cx: Scope) -> impl IntoView {
trace!("rendering <Example/>");
let (value, set_value) = create_signal(cx, 10);
let memo = create_memo(cx, move |_| value() * 2);
let derived = Signal::derive(cx, move || value() * 3);
create_effect(cx, move |_| {
trace!("logging value of derived..., {}", derived.get());
});
set_timeout(
move || set_value.update(|v| *v += 1),
std::time::Duration::from_millis(50),
);
view! { cx,
<h1>"Example"</h1>
<button on:click=move |_| set_value.update(|value| *value += 1)>
"Click me"
</button>
}
} }

View file

@ -1,47 +0,0 @@
pub struct MakeConsoleWriter;
use std::io::{self, Write};
use tracing_subscriber::fmt::MakeWriter;
impl<'a> MakeWriter<'a> for MakeConsoleWriter {
type Writer = ConsoleWriter;
fn make_writer(&'a self) -> Self::Writer {
unimplemented!("use make_writer_for instead");
}
fn make_writer_for(&'a self, meta: &tracing::Metadata<'_>) -> Self::Writer {
ConsoleWriter(*meta.level(), Vec::with_capacity(256))
}
}
pub struct ConsoleWriter(tracing::Level, Vec<u8>);
impl io::Write for ConsoleWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.1.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
use gloo::console;
use tracing::Level;
let data = String::from_utf8(self.1.to_owned())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "data not UTF-8"))?;
match self.0 {
Level::TRACE => console::debug!(&data),
Level::DEBUG => console::debug!(&data),
Level::INFO => console::log!(&data),
Level::WARN => console::warn!(&data),
Level::ERROR => console::error!(&data),
}
Ok(())
}
}
impl Drop for ConsoleWriter {
fn drop(&mut self) {
let _ = self.flush();
}
}

View file

@ -402,7 +402,7 @@ cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] { if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use web_sys::Node; use web_sys::Node;
trait NonViewMarkerSibling { pub(crate) trait NonViewMarkerSibling {
fn next_non_view_marker_sibling(&self) -> Option<Node>; fn next_non_view_marker_sibling(&self) -> Option<Node>;
fn previous_non_view_marker_sibling(&self) -> Option<Node>; fn previous_non_view_marker_sibling(&self) -> Option<Node>;

File diff suppressed because it is too large Load diff