Feat/input comp ref (#47)

* feat: input add ComponentRef

* feat: AutoComplete add ComponentRef

* feat: AutoComplete component add prefix and suffix slot
This commit is contained in:
luoxiaozero 2023-12-15 00:57:23 +08:00 committed by GitHub
parent 2ea5087273
commit 08936c92ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 248 additions and 13 deletions

View file

@ -17,6 +17,7 @@ icondata = { version = "0.1.0", features = [
"AiCheckOutlined",
"AiGithubOutlined",
"AiUserOutlined",
"AiSearchOutlined",
] }
prisms = { git = "https://github.com/luoxiaozero/prisms", rev = "16d4d34b93fc20578ebf03137d54ecc7eafa4d4b" }

View file

@ -66,6 +66,17 @@ pub fn SiteHeader() -> impl IntoView {
let navigate = use_navigate();
navigate(&path, Default::default());
};
let auto_complete_ref = create_component_ref::<AutoCompleteRef>();
let handle = window_event_listener(ev::keydown, move |event| {
let key = event.key();
if key == *"/" {
if let Some(auto_complete_ref) = auto_complete_ref.get_untracked() {
event.prevent_default();
auto_complete_ref.focus();
}
}
});
on_cleanup(move || handle.remove());
view! {
<LayoutHeader style>
<Space>
@ -84,11 +95,16 @@ pub fn SiteHeader() -> impl IntoView {
<Space>
<AutoComplete
value=search_value
placeholder="Search"
placeholder="Type '/' to search"
options=search_options
clear_after_select=true
on_select=on_search_select
/>
comp_ref=auto_complete_ref
>
<AutoCompletePrefix slot>
<Icon icon=icondata::Icon::from(icondata::AiIcon::AiSearchOutlined) style="font-size: 18px; color: var(--thaw-placeholder-color);"/>
</AutoCompletePrefix>
</AutoComplete>
<Button
variant=ButtonVariant::Text
on_click=move |_| {

View file

@ -161,6 +161,32 @@ pub fn AutoCompletePage() -> impl IntoView {
</tr>
</tbody>
</Table>
<h3>"AutoComplete Ref"</h3>
<Table single_column=true>
<thead>
<tr>
<th>"Name"</th>
<th>"Type"</th>
<th>"Description"</th>
</tr>
</thead>
<tbody>
<tr>
<td>"focus"</td>
<td>
<Text code=true>"Fn(&self)"</Text>
</td>
<td>"Focus the input element."</td>
</tr>
<tr>
<td>"blur"</td>
<td>
<Text code=true>"Fn(&self)"</Text>
</td>
<td>"Blur the input element."</td>
</tr>
</tbody>
</Table>
</div>
}
}

View file

@ -6,6 +6,7 @@ use thaw::*;
#[component]
pub fn InputPage() -> impl IntoView {
let value = create_rw_signal(String::from("o"));
let input_ref = create_component_ref::<InputRef>();
view! {
<div style="width: 896px; margin: 0 auto;">
<h1>"Input"</h1>
@ -64,7 +65,46 @@ pub fn InputPage() -> impl IntoView {
</DemoCode>
</Demo>
<h1>"Prefix & Suffix"</h1>
<h3>"Imperative handle"</h3>
<Demo>
<Space vertical=true>
<Space>
<Button on_click=move |_| input_ref.get_untracked().unwrap().focus()>
"Focus"
</Button>
<Button on_click=move |_| input_ref.get_untracked().unwrap().blur()>
"Blur"
</Button>
</Space>
<Input value comp_ref=input_ref/>
</Space>
<DemoCode slot>
{highlight_str!(
r#"
let value = create_rw_signal(String::from("o"));
let input_ref = create_component_ref::<InputRef>();
view! {
<Space vertical=true>
<Space>
<Button on_click=move |_| input_ref.get_untracked().unwrap().focus()>
"Focus"
</Button>
<Button on_click=move |_| input_ref.get_untracked().unwrap().blur()>
"Blur"
</Button>
</Space>
<Input value comp_ref=input_ref/>
</Space>
}
"#,
"rust"
)}
</DemoCode>
</Demo>
<h2>"Prefix & Suffix"</h2>
<Demo>
<Space vertical=true>
<Input value>
@ -197,6 +237,32 @@ pub fn InputPage() -> impl IntoView {
</tr>
</tbody>
</Table>
<h3>"Input Ref"</h3>
<Table single_column=true>
<thead>
<tr>
<th>"Name"</th>
<th>"Type"</th>
<th>"Description"</th>
</tr>
</thead>
<tbody>
<tr>
<td>"focus"</td>
<td>
<Text code=true>"Fn(&self)"</Text>
</td>
<td>"Focus the input element."</td>
</tr>
<tr>
<td>"blur"</td>
<td>
<Text code=true>"Fn(&self)"</Text>
</td>
<td>"Blur the input element."</td>
</tr>
</tbody>
</Table>
</div>
}
}

View file

@ -5,7 +5,7 @@ use crate::{
components::{Binder, Follower, FollowerPlacement, FollowerWidth},
use_theme,
utils::{mount_style, StoredMaybeSignal},
Input, Theme,
ComponentRef, Input, InputPrefix, InputRef, InputSuffix, Theme,
};
use leptos::*;
pub use theme::AutoCompleteTheme;
@ -16,6 +16,16 @@ pub struct AutoCompleteOption {
pub value: String,
}
#[slot]
pub struct AutoCompletePrefix {
children: Children,
}
#[slot]
pub struct AutoCompleteSuffix {
children: Children,
}
#[component]
pub fn AutoComplete(
#[prop(optional, into)] value: RwSignal<String>,
@ -26,6 +36,9 @@ pub fn AutoComplete(
#[prop(optional, into)] disabled: MaybeSignal<bool>,
#[prop(optional, into)] invalid: MaybeSignal<bool>,
#[prop(optional, into)] class: MaybeSignal<String>,
#[prop(optional)] auto_complete_prefix: Option<AutoCompletePrefix>,
#[prop(optional)] auto_complete_suffix: Option<AutoCompleteSuffix>,
#[prop(optional)] comp_ref: ComponentRef<AutoCompleteRef>,
) -> impl IntoView {
mount_style("auto-complete", include_str!("./auto-complete.css"));
let theme = use_theme(Theme::light);
@ -108,6 +121,10 @@ pub fn AutoComplete(
}
}
};
let input_ref = ComponentRef::<InputRef>::new();
input_ref.on_load(move |_| {
comp_ref.load(AutoCompleteRef { input_ref });
});
let ssr_class = ssr_class(&class);
view! {
@ -121,7 +138,27 @@ pub fn AutoComplete(
on_focus=move |_| open_menu()
on_blur=move |_| is_show_menu.set(false)
allow_value
/>
comp_ref=input_ref
>
<InputPrefix if_=auto_complete_prefix.is_some() slot>
{
if let Some(auto_complete_prefix) = auto_complete_prefix {
Some((auto_complete_prefix.children)())
} else {
None
}
}
</InputPrefix>
<InputSuffix if_=auto_complete_suffix.is_some() slot>
{
if let Some(auto_complete_suffix) = auto_complete_suffix {
Some((auto_complete_suffix.children)())
} else {
None
}
}
</InputSuffix>
</Input>
</div>
<Follower
slot
@ -196,3 +233,22 @@ pub fn AutoComplete(
</Binder>
}
}
#[derive(Clone)]
pub struct AutoCompleteRef {
input_ref: ComponentRef<InputRef>,
}
impl AutoCompleteRef {
pub fn focus(&self) {
if let Some(input_ref) = self.input_ref.get_untracked() {
input_ref.focus();
}
}
pub fn blur(&self) {
if let Some(input_ref) = self.input_ref.get_untracked() {
input_ref.blur();
}
}
}

View file

@ -2,7 +2,7 @@ mod theme;
use crate::{
theme::{use_theme, Theme},
utils::mount_style,
utils::{mount_style, ComponentRef},
};
use leptos::*;
pub use theme::InputTheme;
@ -25,11 +25,15 @@ impl InputVariant {
#[slot]
pub struct InputPrefix {
#[prop(default = true)]
if_: bool,
children: Children,
}
#[slot]
pub struct InputSuffix {
#[prop(default = true)]
if_: bool,
children: Children,
}
@ -45,6 +49,7 @@ pub fn Input(
#[prop(optional, into)] invalid: MaybeSignal<bool>,
#[prop(optional)] input_prefix: Option<InputPrefix>,
#[prop(optional)] input_suffix: Option<InputSuffix>,
#[prop(optional)] comp_ref: ComponentRef<InputRef>,
) -> impl IntoView {
let theme = use_theme(Theme::light);
mount_style("input", include_str!("./input.css"));
@ -114,6 +119,10 @@ pub fn Input(
});
css_vars
});
let input_ref = create_node_ref::<html::Input>();
input_ref.on_load(move |_| {
comp_ref.load(InputRef { input_ref });
});
view! {
<div
class="thaw-input"
@ -122,7 +131,7 @@ pub fn Input(
class=("thaw-input--invalid", move || invalid.get())
style=move || css_vars.get()
>
{if let Some(prefix) = input_prefix {
{if let Some(prefix) = input_prefix.map(|prefix| prefix.if_.then(|| prefix)).flatten() {
view! { <div class="thaw-input__prefix">{(prefix.children)()}</div> }.into()
} else {
None
@ -141,9 +150,10 @@ pub fn Input(
class="thaw-input__input-el"
disabled=move || disabled.get()
placeholder=move || placeholder.get()
ref=input_ref
/>
{if let Some(suffix) = input_suffix {
{if let Some(suffix) = input_suffix.map(|suffix| suffix.if_.then(|| suffix)).flatten() {
view! { <div class="thaw-input__suffix">{(suffix.children)()}</div> }.into()
} else {
None
@ -152,3 +162,22 @@ pub fn Input(
</div>
}
}
#[derive(Clone)]
pub struct InputRef {
input_ref: NodeRef<html::Input>,
}
impl InputRef {
pub fn focus(&self) {
if let Some(input_el) = self.input_ref.get_untracked() {
_ = input_el.focus();
}
}
pub fn blur(&self) {
if let Some(input_el) = self.input_ref.get_untracked() {
_ = input_el.blur();
}
}
}

View file

@ -77,4 +77,4 @@ pub use theme::*;
pub use time_picker::*;
pub use typography::*;
pub use upload::*;
pub use utils::SignalWatch;
pub use utils::{create_component_ref, ComponentRef, SignalWatch};

View file

@ -1,4 +1,8 @@
use leptos::{create_rw_signal, RwSignal, SignalGetUntracked, SignalSet};
use leptos::{
create_render_effect, create_rw_signal, logging::debug_warn, RwSignal, SignalGet,
SignalGetUntracked, SignalUpdate,
};
use std::cell::Cell;
pub struct ComponentRef<T: 'static>(RwSignal<Option<T>>);
@ -17,6 +21,17 @@ impl<T> Clone for ComponentRef<T> {
impl<T: 'static> Copy for ComponentRef<T> {}
impl<T> ComponentRef<T> {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self) -> Option<T>
where
T: Clone,
{
self.0.get()
}
pub fn get_untracked(&self) -> Option<T>
where
T: Clone,
@ -24,7 +39,33 @@ impl<T> ComponentRef<T> {
self.0.get_untracked()
}
pub fn load(&self, comp_ref: T) {
self.0.set(Some(comp_ref));
pub fn load(&self, comp: T) {
self.0.update(|current| {
if current.is_some() {
debug_warn!(
"You are setting a ComponentRef that has already been filled. \
Its possible this is intentional."
);
}
*current = Some(comp);
});
}
pub fn on_load<F>(self, f: F)
where
T: Clone,
F: FnOnce(T) + 'static,
{
let f = Cell::new(Some(f));
create_render_effect(move |_| {
if let Some(comp) = self.get() {
f.take().unwrap()(comp);
}
});
}
}
pub fn create_component_ref<T>() -> ComponentRef<T> {
ComponentRef::default()
}

View file

@ -7,7 +7,7 @@ mod signal;
mod stored_maybe_signal;
// pub use callback::AsyncCallback;
pub(crate) use component_ref::ComponentRef;
pub use component_ref::{create_component_ref, ComponentRef};
pub(crate) use dyn_classes::*;
pub(crate) use event_listener::*;
pub(crate) use mount_style::mount_style;