change: add `Scope` to view function in `<For/>` to avoid memory "leak" (#492)
This commit is contained in:
parent
4759dfcb60
commit
c24874d9c8
|
@ -66,9 +66,8 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
|||
<For
|
||||
each=counters
|
||||
key=|counter| counter.0
|
||||
view=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
|
||||
view! {
|
||||
cx,
|
||||
view=move |cx, (id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
|
||||
view! { cx,
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
|||
<For
|
||||
each={move || counters.get()}
|
||||
key={|counter| counter.0}
|
||||
view=move |(id, (value, set_value))| {
|
||||
view=move |cx, (id, (value, set_value))| {
|
||||
view! {
|
||||
cx,
|
||||
<Counter id value set_value/>
|
||||
|
|
|
@ -49,7 +49,7 @@ pub fn ErrorTemplate(
|
|||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
view= move |error| {
|
||||
view=move |cx, error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! { cx,
|
||||
|
|
|
@ -91,7 +91,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
|||
<For
|
||||
each=move || stories.clone()
|
||||
key=|story| story.id
|
||||
view=move |story: api::Story| {
|
||||
view=move |cx, story: api::Story| {
|
||||
view! { cx,
|
||||
<Story story/>
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
|
|||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move |comment| view! { cx, <Comment comment /> }
|
||||
view=move |cx, comment| view! { cx, <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -98,7 +98,7 @@ pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
|
|||
<For
|
||||
each=move || comments.clone()
|
||||
key=|comment| comment.id
|
||||
view=move |comment: api::Comment| view! { cx, <Comment comment /> }
|
||||
view=move |cx, comment: api::Comment| view! { cx, <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
|
|||
// a unique key for each item as a reference
|
||||
key=|error| error.0.clone()
|
||||
// renders each item to a view
|
||||
view= move |error| {
|
||||
view= move |cx, error| {
|
||||
let error_string = error.1.to_string();
|
||||
view! {
|
||||
cx,
|
||||
|
|
|
@ -91,7 +91,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
|||
<For
|
||||
each=move || stories.clone()
|
||||
key=|story| story.id
|
||||
view=move |story: api::Story| {
|
||||
view=move |cx, story: api::Story| {
|
||||
view! { cx,
|
||||
<Story story/>
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
|
|||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move |comment| view! { cx, <Comment comment /> }
|
||||
view=move |cx, comment| view! { cx, <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -98,7 +98,7 @@ pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
|
|||
<For
|
||||
each=move || comments.clone()
|
||||
key=|comment| comment.id
|
||||
view=move |comment: api::Comment| view! { cx, <Comment comment /> }
|
||||
view=move |cx, comment: api::Comment| view! { cx, <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ pub fn ErrorTemplate(
|
|||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
view= move |error| {
|
||||
view= move |cx, error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! {
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
use leptos::{
|
||||
web_sys::HtmlInputElement,
|
||||
*,
|
||||
};
|
||||
use leptos::{web_sys::HtmlInputElement, *};
|
||||
use storage::TodoSerialized;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -14,111 +11,104 @@ const STORAGE_KEY: &str = "todos-leptos";
|
|||
|
||||
// Basic operations to manipulate the todo list: nothing really interesting here
|
||||
impl Todos {
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
let starting_todos = if let Ok(Some(storage)) = window().local_storage() {
|
||||
storage
|
||||
.get_item(STORAGE_KEY)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|value| {
|
||||
serde_json::from_str::<Vec<TodoSerialized>>(&value).ok()
|
||||
})
|
||||
.map(|values| {
|
||||
values
|
||||
.into_iter()
|
||||
.map(|stored| stored.into_todo(cx))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
Self(starting_todos)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, todo: Todo) {
|
||||
self.0.push(todo);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: Uuid) {
|
||||
self.0.retain(|todo| todo.id != id);
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> usize {
|
||||
// `todo.completed` is a signal, so we call .get() to access its value
|
||||
self.0.iter().filter(|todo| !todo.completed.get()).count()
|
||||
}
|
||||
|
||||
pub fn completed(&self) -> usize {
|
||||
// `todo.completed` is a signal, so we call .get() to access its value
|
||||
self.0.iter().filter(|todo| todo.completed.get()).count()
|
||||
}
|
||||
|
||||
pub fn toggle_all(&self) {
|
||||
// if all are complete, mark them all active
|
||||
if self.remaining() == 0 {
|
||||
for todo in &self.0 {
|
||||
todo.completed.update(|completed| {
|
||||
if *completed {
|
||||
*completed = false
|
||||
}
|
||||
});
|
||||
}
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
let starting_todos = if let Ok(Some(storage)) = window().local_storage() {
|
||||
storage
|
||||
.get_item(STORAGE_KEY)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|value| serde_json::from_str::<Vec<TodoSerialized>>(&value).ok())
|
||||
.map(|values| {
|
||||
values
|
||||
.into_iter()
|
||||
.map(|stored| stored.into_todo(cx))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
Self(starting_todos)
|
||||
}
|
||||
// otherwise, mark them all complete
|
||||
else {
|
||||
for todo in &self.0 {
|
||||
todo.completed.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_completed(&mut self) {
|
||||
self.0.retain(|todo| !todo.completed.get());
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, todo: Todo) {
|
||||
self.0.push(todo);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: Uuid) {
|
||||
self.0.retain(|todo| todo.id != id);
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> usize {
|
||||
// `todo.completed` is a signal, so we call .get() to access its value
|
||||
self.0.iter().filter(|todo| !todo.completed.get()).count()
|
||||
}
|
||||
|
||||
pub fn completed(&self) -> usize {
|
||||
// `todo.completed` is a signal, so we call .get() to access its value
|
||||
self.0.iter().filter(|todo| todo.completed.get()).count()
|
||||
}
|
||||
|
||||
pub fn toggle_all(&self) {
|
||||
// if all are complete, mark them all active
|
||||
if self.remaining() == 0 {
|
||||
for todo in &self.0 {
|
||||
todo.completed.update(|completed| {
|
||||
if *completed {
|
||||
*completed = false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// otherwise, mark them all complete
|
||||
else {
|
||||
for todo in &self.0 {
|
||||
todo.completed.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_completed(&mut self) {
|
||||
self.0.retain(|todo| !todo.completed.get());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Todo {
|
||||
pub id: Uuid,
|
||||
pub title: RwSignal<String>,
|
||||
pub completed: RwSignal<bool>,
|
||||
pub id: Uuid,
|
||||
pub title: RwSignal<String>,
|
||||
pub completed: RwSignal<bool>,
|
||||
}
|
||||
|
||||
impl Todo {
|
||||
pub fn new(cx: Scope, id: Uuid, title: String) -> Self {
|
||||
Self::new_with_completed(cx, id, title, false)
|
||||
}
|
||||
|
||||
pub fn new_with_completed(
|
||||
cx: Scope,
|
||||
id: Uuid,
|
||||
title: String,
|
||||
completed: bool,
|
||||
) -> Self {
|
||||
// RwSignal combines the getter and setter in one struct, rather than separating
|
||||
// the getter from the setter. This makes it more convenient in some cases, such
|
||||
// as when we're putting the signals into a struct and passing it around. There's
|
||||
// no real difference: you could use `create_signal` here, or use `create_rw_signal`
|
||||
// everywhere.
|
||||
let title = create_rw_signal(cx, title);
|
||||
let completed = create_rw_signal(cx, completed);
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
completed,
|
||||
pub fn new(cx: Scope, id: Uuid, title: String) -> Self {
|
||||
Self::new_with_completed(cx, id, title, false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self) {
|
||||
// A signal's `update()` function gives you a mutable reference to the current value
|
||||
// You can use that to modify the value in place, which will notify any subscribers.
|
||||
self.completed.update(|completed| *completed = !*completed);
|
||||
}
|
||||
pub fn new_with_completed(cx: Scope, id: Uuid, title: String, completed: bool) -> Self {
|
||||
// RwSignal combines the getter and setter in one struct, rather than separating
|
||||
// the getter from the setter. This makes it more convenient in some cases, such
|
||||
// as when we're putting the signals into a struct and passing it around. There's
|
||||
// no real difference: you could use `create_signal` here, or use `create_rw_signal`
|
||||
// everywhere.
|
||||
let title = create_rw_signal(cx, title);
|
||||
let completed = create_rw_signal(cx, completed);
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
completed,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self) {
|
||||
// A signal's `update()` function gives you a mutable reference to the current value
|
||||
// You can use that to modify the value in place, which will notify any subscribers.
|
||||
self.completed.update(|completed| *completed = !*completed);
|
||||
}
|
||||
}
|
||||
|
||||
const ESCAPE_KEY: u32 = 27;
|
||||
|
@ -126,231 +116,230 @@ const ENTER_KEY: u32 = 13;
|
|||
|
||||
#[component]
|
||||
pub fn TodoMVC(cx: Scope) -> impl IntoView {
|
||||
// The `todos` are a signal, since we need to reactively update the list
|
||||
let (todos, set_todos) = create_signal(cx, Todos::new(cx));
|
||||
// The `todos` are a signal, since we need to reactively update the list
|
||||
let (todos, set_todos) = create_signal(cx, Todos::new(cx));
|
||||
|
||||
// We provide a context that each <Todo/> component can use to update the list
|
||||
// Here, I'm just passing the `WriteSignal`; a <Todo/> doesn't need to read the whole list
|
||||
// (and shouldn't try to, as that would cause each individual <Todo/> to re-render when
|
||||
// a new todo is added! This kind of hygiene is why `create_signal` defaults to read-write
|
||||
// segregation.)
|
||||
provide_context(cx, set_todos);
|
||||
// We provide a context that each <Todo/> component can use to update the list
|
||||
// Here, I'm just passing the `WriteSignal`; a <Todo/> doesn't need to read the whole list
|
||||
// (and shouldn't try to, as that would cause each individual <Todo/> to re-render when
|
||||
// a new todo is added! This kind of hygiene is why `create_signal` defaults to read-write
|
||||
// segregation.)
|
||||
provide_context(cx, set_todos);
|
||||
|
||||
// Handle the three filter modes: All, Active, and Completed
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
window_event_listener("hashchange", move |_| {
|
||||
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
});
|
||||
// Handle the three filter modes: All, Active, and Completed
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
window_event_listener("hashchange", move |_| {
|
||||
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
});
|
||||
|
||||
// Callback to add a todo on pressing the `Enter` key, if the field isn't empty
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
let title = event_target_value(&ev);
|
||||
let title = title.trim();
|
||||
if !title.is_empty() {
|
||||
let new = Todo::new(cx, Uuid::new_v4(), title.to_string());
|
||||
set_todos.update(|t| t.add(new));
|
||||
target.set_value("");
|
||||
}
|
||||
// Callback to add a todo on pressing the `Enter` key, if the field isn't empty
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
let title = event_target_value(&ev);
|
||||
let title = title.trim();
|
||||
if !title.is_empty() {
|
||||
let new = Todo::new(cx, Uuid::new_v4(), title.to_string());
|
||||
set_todos.update(|t| t.add(new));
|
||||
target.set_value("");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// A derived signal that filters the list of the todos depending on the filter mode
|
||||
// This doesn't need to be a `Memo`, because we're only reading it in one place
|
||||
let filtered_todos = move || {
|
||||
todos.with(|todos| match mode.get() {
|
||||
Mode::All => todos.0.to_vec(),
|
||||
Mode::Active => todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| !todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
Mode::Completed => todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
})
|
||||
};
|
||||
|
||||
// Serialization
|
||||
//
|
||||
// the effect reads the `todos` signal, and each `Todo`'s title and completed
|
||||
// status, so it will automatically re-run on any change to the list of tasks
|
||||
//
|
||||
// this is the main point of `create_effect`: to synchronize reactive state
|
||||
// with something outside the reactive system (like localStorage)
|
||||
create_effect(cx, move |_| {
|
||||
if let Ok(Some(storage)) = window().local_storage() {
|
||||
let objs = todos
|
||||
.get()
|
||||
.0
|
||||
.iter()
|
||||
.map(TodoSerialized::from)
|
||||
.collect::<Vec<_>>();
|
||||
let json = serde_json::to_string(&objs).expect("couldn't serialize Todos");
|
||||
if storage.set_item(STORAGE_KEY, &json).is_err() {
|
||||
log::error!("error while trying to set item in localStorage");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input
|
||||
class="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
autofocus
|
||||
on:keydown=add_todo
|
||||
/>
|
||||
</header>
|
||||
<section
|
||||
class="main"
|
||||
class:hidden={move || todos.with(|t| t.is_empty())}
|
||||
>
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox"
|
||||
prop:checked={move || todos.with(|t| t.remaining() > 0)}
|
||||
on:input=move |_| todos.with(|t| t.toggle_all())
|
||||
/>
|
||||
<label for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |cx, todo: Todo| view! { cx, <Todo todo /> }
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
<footer
|
||||
class="footer"
|
||||
class:hidden={move || todos.with(|t| t.is_empty())}
|
||||
>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 {
|
||||
" item"
|
||||
} else {
|
||||
" items"
|
||||
}}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
|
||||
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
|
||||
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden={move || todos.with(|t| t.completed() == 0)}
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}
|
||||
};
|
||||
|
||||
// A derived signal that filters the list of the todos depending on the filter mode
|
||||
// This doesn't need to be a `Memo`, because we're only reading it in one place
|
||||
let filtered_todos = move || {
|
||||
todos.with(|todos| match mode.get() {
|
||||
Mode::All => todos.0.to_vec(),
|
||||
Mode::Active => todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| !todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
Mode::Completed => todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
})
|
||||
};
|
||||
|
||||
// Serialization
|
||||
//
|
||||
// the effect reads the `todos` signal, and each `Todo`'s title and completed
|
||||
// status, so it will automatically re-run on any change to the list of tasks
|
||||
//
|
||||
// this is the main point of `create_effect`: to synchronize reactive state
|
||||
// with something outside the reactive system (like localStorage)
|
||||
create_effect(cx, move |_| {
|
||||
if let Ok(Some(storage)) = window().local_storage() {
|
||||
let objs = todos
|
||||
.get()
|
||||
.0
|
||||
.iter()
|
||||
.map(TodoSerialized::from)
|
||||
.collect::<Vec<_>>();
|
||||
let json =
|
||||
serde_json::to_string(&objs).expect("couldn't serialize Todos");
|
||||
if storage.set_item(STORAGE_KEY, &json).is_err() {
|
||||
log::error!("error while trying to set item in localStorage");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input
|
||||
class="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
autofocus
|
||||
on:keydown=add_todo
|
||||
/>
|
||||
</header>
|
||||
<section
|
||||
class="main"
|
||||
class:hidden={move || todos.with(|t| t.is_empty())}
|
||||
>
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox"
|
||||
prop:checked={move || todos.with(|t| t.remaining() > 0)}
|
||||
on:input=move |_| todos.with(|t| t.toggle_all())
|
||||
/>
|
||||
<label for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |todo: Todo| view! { cx, <Todo todo /> }
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
<footer
|
||||
class="footer"
|
||||
class:hidden={move || todos.with(|t| t.is_empty())}
|
||||
>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 {
|
||||
" item"
|
||||
} else {
|
||||
" items"
|
||||
}}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
|
||||
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
|
||||
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden={move || todos.with(|t| t.completed() == 0)}
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
let (editing, set_editing) = create_signal(cx, false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
let (editing, set_editing) = create_signal(cx, false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
|
||||
// this will be filled by _ref=input below
|
||||
let todo_input = NodeRef::<Input>::new(cx);
|
||||
// this will be filled by _ref=input below
|
||||
let todo_input = NodeRef::<Input>::new(cx);
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
set_todos.update(|t| t.remove(todo.id));
|
||||
} else {
|
||||
todo.title.set(value.to_string());
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
set_todos.update(|t| t.remove(todo.id));
|
||||
} else {
|
||||
todo.title.set(value.to_string());
|
||||
}
|
||||
set_editing(false);
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li
|
||||
class="todo"
|
||||
class:editing={editing}
|
||||
class:completed={move || todo.completed.get()}
|
||||
>
|
||||
<div class="view">
|
||||
<input
|
||||
_ref=todo_input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
prop:checked={move || (todo.completed)()}
|
||||
on:input={move |ev| {
|
||||
let checked = event_target_checked(&ev);
|
||||
todo.completed.set(checked);
|
||||
}}
|
||||
/>
|
||||
<label on:dblclick=move |_| {
|
||||
set_editing(true);
|
||||
|
||||
if let Some(input) = todo_input.get() {
|
||||
_ = input.focus();
|
||||
}
|
||||
}>
|
||||
{move || todo.title.get()}
|
||||
</label>
|
||||
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
|
||||
</div>
|
||||
{move || editing().then(|| view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden={move || !(editing)()}
|
||||
prop:value={move || todo.title.get()}
|
||||
on:focusout=move |ev: web_sys::FocusEvent| save(&event_target_value(&ev))
|
||||
on:keyup={move |ev: web_sys::KeyboardEvent| {
|
||||
let key_code = ev.key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</li>
|
||||
}
|
||||
set_editing(false);
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li
|
||||
class="todo"
|
||||
class:editing={editing}
|
||||
class:completed={move || todo.completed.get()}
|
||||
>
|
||||
<div class="view">
|
||||
<input
|
||||
_ref=todo_input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
prop:checked={move || (todo.completed)()}
|
||||
on:input={move |ev| {
|
||||
let checked = event_target_checked(&ev);
|
||||
todo.completed.set(checked);
|
||||
}}
|
||||
/>
|
||||
<label on:dblclick=move |_| {
|
||||
set_editing(true);
|
||||
|
||||
if let Some(input) = todo_input.get() {
|
||||
_ = input.focus();
|
||||
}
|
||||
}>
|
||||
{move || todo.title.get()}
|
||||
</label>
|
||||
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
|
||||
</div>
|
||||
{move || editing().then(|| view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden={move || !(editing)()}
|
||||
prop:value={move || todo.title.get()}
|
||||
on:focusout=move |ev: web_sys::FocusEvent| save(&event_target_value(&ev))
|
||||
on:keyup={move |ev: web_sys::KeyboardEvent| {
|
||||
let key_code = ev.key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
Active,
|
||||
Completed,
|
||||
All,
|
||||
Active,
|
||||
Completed,
|
||||
All,
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Mode::All
|
||||
}
|
||||
fn default() -> Self {
|
||||
Mode::All
|
||||
}
|
||||
}
|
||||
|
||||
pub fn route(hash: &str) -> Mode {
|
||||
match hash {
|
||||
"/active" => Mode::Active,
|
||||
"/completed" => Mode::Completed,
|
||||
_ => Mode::All,
|
||||
}
|
||||
match hash {
|
||||
"/active" => Mode::Active,
|
||||
"/completed" => Mode::Completed,
|
||||
_ => Mode::All,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ use std::hash::Hash;
|
|||
/// // a unique key for each item
|
||||
/// key=|counter| counter.id
|
||||
/// // renders each item to a view
|
||||
/// view=move |counter: Counter| {
|
||||
/// view=move |cx, counter: Counter| {
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <button>"Value: " {move || counter.count.get()}</button>
|
||||
|
@ -54,7 +54,7 @@ pub fn For<IF, I, T, EF, N, KF, K>(
|
|||
where
|
||||
IF: Fn() -> I + 'static,
|
||||
I: IntoIterator<Item = T>,
|
||||
EF: Fn(T) -> N + 'static,
|
||||
EF: Fn(Scope, T) -> N + 'static,
|
||||
N: IntoView,
|
||||
KF: Fn(&T) -> K + 'static,
|
||||
K: Eq + Hash + 'static,
|
||||
|
|
|
@ -32,7 +32,7 @@ fn view_fn(cx: Scope) -> impl IntoView {
|
|||
<For
|
||||
each=|| vec![0, 1, 2, 3, 4, 5, 6, 7]
|
||||
key=|i| *i
|
||||
view=|i| view! { cx, {i} }
|
||||
view=|cx, i| view! { cx, {i} }
|
||||
/>
|
||||
}
|
||||
.into_view(cx);
|
||||
|
|
|
@ -38,6 +38,7 @@ cfg_if! {
|
|||
use crate::hydration::HydrationKey;
|
||||
}
|
||||
}
|
||||
use leptos_reactive::Scope;
|
||||
use smallvec::SmallVec;
|
||||
use std::{borrow::Cow, cell::RefCell, fmt, hash::Hash, ops::Deref, rc::Rc};
|
||||
|
||||
|
@ -159,6 +160,7 @@ impl Mountable for EachRepr {
|
|||
/// The internal representation of an [`Each`] item.
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub(crate) struct EachItem {
|
||||
cx: Scope,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment: web_sys::DocumentFragment,
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -184,7 +186,7 @@ impl fmt::Debug for EachItem {
|
|||
}
|
||||
|
||||
impl EachItem {
|
||||
fn new(child: View) -> Self {
|
||||
fn new(cx: Scope, child: View) -> Self {
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
let markers = (
|
||||
|
@ -214,6 +216,7 @@ impl EachItem {
|
|||
};
|
||||
|
||||
Self {
|
||||
cx,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment,
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -226,6 +229,13 @@ impl EachItem {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Drop for EachItem {
|
||||
fn drop(&mut self) {
|
||||
self.cx.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Mountable for EachItem {
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
|
@ -272,7 +282,7 @@ pub struct Each<IF, I, T, EF, N, KF, K>
|
|||
where
|
||||
IF: Fn() -> I + 'static,
|
||||
I: IntoIterator<Item = T>,
|
||||
EF: Fn(T) -> N + 'static,
|
||||
EF: Fn(Scope, T) -> N + 'static,
|
||||
N: IntoView,
|
||||
KF: Fn(&T) -> K + 'static,
|
||||
K: Eq + Hash + 'static,
|
||||
|
@ -287,7 +297,7 @@ impl<IF, I, T, EF, N, KF, K> Each<IF, I, T, EF, N, KF, K>
|
|||
where
|
||||
IF: Fn() -> I + 'static,
|
||||
I: IntoIterator<Item = T>,
|
||||
EF: Fn(T) -> N + 'static,
|
||||
EF: Fn(Scope, T) -> N + 'static,
|
||||
N: IntoView,
|
||||
KF: Fn(&T) -> K,
|
||||
K: Eq + Hash + 'static,
|
||||
|
@ -307,7 +317,7 @@ impl<IF, I, T, EF, N, KF, K> IntoView for Each<IF, I, T, EF, N, KF, K>
|
|||
where
|
||||
IF: Fn() -> I + 'static,
|
||||
I: IntoIterator<Item = T>,
|
||||
EF: Fn(T) -> N + 'static,
|
||||
EF: Fn(Scope, T) -> N + 'static,
|
||||
N: IntoView,
|
||||
KF: Fn(&T) -> K + 'static,
|
||||
K: Eq + Hash + 'static,
|
||||
|
@ -317,7 +327,7 @@ where
|
|||
debug_assertions,
|
||||
instrument(level = "trace", name = "<Each />", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
|
||||
fn into_view(self, cx: Scope) -> crate::View {
|
||||
let Self {
|
||||
items_fn,
|
||||
each_fn,
|
||||
|
@ -370,7 +380,7 @@ where
|
|||
*children_borrow = Vec::with_capacity(items.len());
|
||||
|
||||
for item in items {
|
||||
let each_item = EachItem::new(each_fn(item).into_view(cx));
|
||||
let (each_item, _) = cx.run_child_scope(|cx| EachItem::new(cx, each_fn(cx, item).into_view(cx)));
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
mount_child(MountKind::Before(&closing), &each_item);
|
||||
|
@ -384,7 +394,7 @@ where
|
|||
} else {
|
||||
*component.children.borrow_mut() = (items_fn)()
|
||||
.into_iter()
|
||||
.map(|child| Some(EachItem::new((each_fn)(child).into_view(cx))))
|
||||
.map(|child| cx.run_child_scope(|cx| Some(EachItem::new(cx, (each_fn)(cx, child).into_view(cx)))).0)
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
@ -568,7 +578,7 @@ impl Default for DiffOpAddMode {
|
|||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
fn apply_cmds<T, EF, N>(
|
||||
cx: leptos_reactive::Scope,
|
||||
cx: Scope,
|
||||
opening: &web_sys::Node,
|
||||
closing: &web_sys::Node,
|
||||
mut cmds: Diff,
|
||||
|
@ -576,7 +586,7 @@ fn apply_cmds<T, EF, N>(
|
|||
mut items: SmallVec<[Option<T>; 128]>,
|
||||
each_fn: &EF,
|
||||
) where
|
||||
EF: Fn(T) -> N,
|
||||
EF: Fn(Scope, T) -> N,
|
||||
N: IntoView,
|
||||
{
|
||||
let range = RANGE.with(|range| (*range).clone());
|
||||
|
@ -647,9 +657,10 @@ fn apply_cmds<T, EF, N>(
|
|||
for DiffOpAdd { at, mode } in cmds.added {
|
||||
let item = items[at].take().unwrap();
|
||||
|
||||
let child = each_fn(item).into_view(cx);
|
||||
|
||||
let each_item = EachItem::new(child);
|
||||
let (each_item, _) = cx.run_child_scope(|cx| {
|
||||
let child = each_fn(cx, item).into_view(cx);
|
||||
EachItem::new(cx, child)
|
||||
});
|
||||
|
||||
match mode {
|
||||
DiffOpAddMode::Normal => {
|
||||
|
|
Loading…
Reference in New Issue