added use_interval_fn

This commit is contained in:
Maccesch 2023-06-14 16:15:03 +01:00
parent 0777e5b9f8
commit 935e3cd544
22 changed files with 621 additions and 3 deletions

1
.idea/leptos-use.iml generated
View file

@ -31,6 +31,7 @@
<sourceFolder url="file://$MODULE_DIR$/examples/use_mutation_observer/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/use_mutation_observer/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/on_click_outside/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/on_click_outside/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_abs/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/use_abs/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_interval_fn/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/examples/use_event_listener/target" /> <excludeFolder url="file://$MODULE_DIR$/examples/use_event_listener/target" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
<excludeFolder url="file://$MODULE_DIR$/docs/book/book" /> <excludeFolder url="file://$MODULE_DIR$/docs/book/book" />

View file

@ -3,6 +3,12 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### New Functions 🚀
- `use_interval_fn`
## [0.3.0] - 2023-06-13 ## [0.3.0] - 2023-06-13
### Braking Changes 🛠 ### Braking Changes 🛠

View file

@ -77,5 +77,5 @@ python3 post_build.py
If you only want to add the example for one function you can run for example If you only want to add the example for one function you can run for example
```shell ```shell
python3 post_build.py use_media_query python3 post_build.py use_mequery
``` ```

View file

@ -34,6 +34,10 @@
- [use_mouse](sensors/use_mouse.md) - [use_mouse](sensors/use_mouse.md)
- [use_scroll](sensors/use_scroll.md) - [use_scroll](sensors/use_scroll.md)
# Animation
- [use_interval_fn](animation/use_interval_fn.md)
# Watch # Watch
- [watch](watch/watch.md) - [watch](watch/watch.md)

View file

@ -0,0 +1,3 @@
# use_interval_fn
<!-- cmdrun python3 ../extract_doc_comment.py use_interval_fn -->

View file

@ -8,6 +8,8 @@
<!-- cmdrun python3 generate_function_overview.py sensors --> <!-- cmdrun python3 generate_function_overview.py sensors -->
<!-- cmdrun python3 generate_function_overview.py animation -->
<!-- cmdrun python3 generate_function_overview.py watch --> <!-- cmdrun python3 generate_function_overview.py watch -->
<!-- cmdrun python3 generate_function_overview.py utilities --> <!-- cmdrun python3 generate_function_overview.py utilities -->

View file

@ -12,6 +12,7 @@ members = [
"use_favicon", "use_favicon",
"use_floor", "use_floor",
"use_intersection_observer", "use_intersection_observer",
"use_interval_fn",
"use_media_query", "use_media_query",
"use_mouse", "use_mouse",
"use_mutation_observer", "use_mutation_observer",

View file

@ -0,0 +1,16 @@
[package]
name = "use_interval_fn"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.3"
console_error_panic_hook = "0.1"
console_log = "1"
log = "0.4"
leptos-use = { path = "../..", features = ["docs", "math"] }
web-sys = "0.3"
[dev-dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3.0"

View file

@ -0,0 +1,23 @@
A simple example for `use_interval_fn`.
If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation)
as well as the nightly toolchain for Rust and the wasm32-unknown-unknown target:
```bash
cargo install trunk
npm install -D tailwindcss @tailwindcss/forms
rustup toolchain install nightly
rustup target add wasm32-unknown-unknown
```
Then, open two terminals. In the first one, run:
```
npx tailwindcss -i ./input.css -o ./style/output.css --watch
```
In the second one, run:
```bash
trunk serve --open
```

View file

@ -0,0 +1,2 @@
[build]
public_url = "/demo/"

View file

@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="css" href="style/output.css">
</head>
<body></body>
</html>

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View file

@ -0,0 +1,75 @@
use leptos::*;
use leptos_use::docs::demo_or_body;
use leptos_use::use_interval_fn;
use leptos_use::utils::Pausable;
#[component]
fn Demo(cx: Scope) -> impl IntoView {
let greetings = [
"Hello",
"Hi",
"Yo!",
"Hey",
"Hola",
"こんにちは",
"Bonjour",
"Salut!",
"你好",
"Привет",
];
let (word, set_word) = create_signal(cx, greetings[0]);
let (interval, set_interval) = create_signal(cx, 500_u64);
let (index, set_index) = create_signal(cx, 0);
let Pausable {
pause,
resume,
is_active,
} = use_interval_fn(
cx,
move || {
set_index((index.get() + 1) % greetings.len());
set_word(greetings[index.get()]);
},
interval,
);
view! { cx,
<p>{word}</p>
<p>
"Interval:"
<input
prop:value=interval
on:input=move |e| set_interval(event_target_value(&e).parse().unwrap())
type="number"
placeholder="interval"
/>
</p>
<Show
when=is_active
fallback=move |cx| {
let resume = resume.clone();
view! {cx,
<button on:click=move |_| resume()>"Resume"</button>
}
}
>
{
let pause = pause.clone();
view! {cx,
<button on:click=move |_| pause()>"Pause"</button>
}
}
</Show>
}
}
fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to(demo_or_body(), |cx| {
view! { cx, <Demo /> }
})
}

View file

@ -0,0 +1,289 @@
[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
border-radius: 0px;
padding-top: 0.5rem;
padding-right: 0.75rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
font-size: 1rem;
line-height: 1.5rem;
--tw-shadow: 0 0 #0000;
}
[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
border-color: #2563eb;
}
input::-moz-placeholder, textarea::-moz-placeholder {
color: #6b7280;
opacity: 1;
}
input::placeholder,textarea::placeholder {
color: #6b7280;
opacity: 1;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-date-and-time-value {
min-height: 1.5em;
}
::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
padding-top: 0;
padding-bottom: 0;
}
select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
[multiple] {
background-image: initial;
background-position: initial;
background-repeat: unset;
background-size: initial;
padding-right: 0.75rem;
-webkit-print-color-adjust: unset;
print-color-adjust: unset;
}
[type='checkbox'],[type='radio'] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
padding: 0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
display: inline-block;
vertical-align: middle;
background-origin: border-box;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
flex-shrink: 0;
height: 1rem;
width: 1rem;
color: #2563eb;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
--tw-shadow: 0 0 #0000;
}
[type='checkbox'] {
border-radius: 0px;
}
[type='radio'] {
border-radius: 100%;
}
[type='checkbox']:focus,[type='radio']:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 2px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
[type='checkbox']:checked,[type='radio']:checked {
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
[type='checkbox']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}
[type='radio']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
}
[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
border-color: transparent;
background-color: currentColor;
}
[type='checkbox']:indeterminate {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
border-color: transparent;
background-color: currentColor;
}
[type='file'] {
background: unset;
border-color: inherit;
border-width: 0;
border-radius: 0;
padding: 0;
font-size: unset;
line-height: inherit;
}
[type='file']:focus {
outline: 1px solid ButtonText;
outline: 1px auto -webkit-focus-ring-color;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.static {
position: static;
}
.text-\[--brand-color\] {
color: var(--brand-color);
}
.text-green-600 {
--tw-text-opacity: 1;
color: rgb(22 163 74 / var(--tw-text-opacity));
}
.opacity-75 {
opacity: 0.75;
}
@media (prefers-color-scheme: dark) {
.dark\:text-green-500 {
--tw-text-opacity: 1;
color: rgb(34 197 94 / var(--tw-text-opacity));
}
}

View file

@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"],
},
theme: {
extend: {},
},
corePlugins: {
preflight: false,
},
plugins: [
require('@tailwindcss/forms'),
],
}

View file

@ -27,6 +27,7 @@ mod use_element_visibility;
mod use_event_listener; mod use_event_listener;
mod use_favicon; mod use_favicon;
mod use_intersection_observer; mod use_intersection_observer;
mod use_interval_fn;
mod use_media_query; mod use_media_query;
mod use_mouse; mod use_mouse;
mod use_mutation_observer; mod use_mutation_observer;
@ -48,6 +49,7 @@ pub use use_element_visibility::*;
pub use use_event_listener::*; pub use use_event_listener::*;
pub use use_favicon::*; pub use use_favicon::*;
pub use use_intersection_observer::*; pub use use_intersection_observer::*;
pub use use_interval_fn::*;
pub use use_media_query::*; pub use use_media_query::*;
pub use use_mouse::*; pub use use_mouse::*;
pub use use_mutation_observer::*; pub use use_mutation_observer::*;

View file

@ -41,7 +41,7 @@ static IOS_WORKAROUND: RwLock<bool> = RwLock::new(false);
/// ``` /// ```
/// ///
/// > This function uses [Event.composedPath()](https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath) /// > This function uses [Event.composedPath()](https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath)
/// which is NOT supported by IE 11, Edge 18 and below. /// which is **not** supported by IE 11, Edge 18 and below.
/// If you are targeting these browsers, we recommend you to include /// If you are targeting these browsers, we recommend you to include
/// [this code snippet](https://gist.github.com/sibbng/13e83b1dd1b733317ce0130ef07d4efd) on your project. /// [this code snippet](https://gist.github.com/sibbng/13e83b1dd1b733317ce0130ef07d4efd) on your project.
pub fn on_click_outside<El, T, F>(cx: Scope, target: El, handler: F) -> impl FnOnce() + Clone pub fn on_click_outside<El, T, F>(cx: Scope, target: El, handler: F) -> impl FnOnce() + Clone

147
src/use_interval_fn.rs Normal file
View file

@ -0,0 +1,147 @@
use crate::utils::Pausable;
use crate::watch;
use default_struct_builder::DefaultBuilder;
use leptos::leptos_dom::helpers::IntervalHandle;
use leptos::*;
use std::cell::Cell;
use std::rc::Rc;
use std::time::Duration;
/// Wrapper for `set_interval` with controls.
///
/// ## Demo
///
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_interval_fn)
///
/// ## Usage
///
/// ```
/// # use leptos::*;
/// # use leptos_use::use_interval_fn;
/// # use leptos_use::utils::Pausable;
/// #
/// # #[component]
/// # fn Demo(cx: Scope) -> impl IntoView {
/// let Pausable { pause, resume, is_active } = use_interval_fn(
/// cx,
/// || {
/// // do something
/// },
/// 1000,
/// );
/// # view! { cx, }
/// # }
/// ```
pub fn use_interval_fn<CbFn, N>(
cx: Scope,
callback: CbFn,
interval: N,
) -> Pausable<impl Fn() + Clone, impl Fn() + Clone>
where
CbFn: Fn() + Clone + 'static,
N: Into<MaybeSignal<u64>>,
{
use_interval_fn_with_options(cx, callback, interval, UseIntervalFnOptions::default())
}
/// Version of [`use_interval_fn`] that takes `UseIntervalFnOptions`. See [`use_interval_fn`] for how to use.
pub fn use_interval_fn_with_options<CbFn, N>(
cx: Scope,
callback: CbFn,
interval: N,
options: UseIntervalFnOptions,
) -> Pausable<impl Fn() + Clone, impl Fn() + Clone>
where
CbFn: Fn() + Clone + 'static,
N: Into<MaybeSignal<u64>>,
{
let UseIntervalFnOptions {
immediate,
immediate_callback,
} = options;
let timer: Rc<Cell<Option<IntervalHandle>>> = Rc::new(Cell::new(None));
let (is_active, set_active) = create_signal(cx, false);
let clean = {
let timer = Rc::clone(&timer);
move || {
if let Some(handle) = timer.take() {
handle.clear();
}
}
};
let pause = {
let clean = clean.clone();
move || {
set_active(false);
clean();
}
};
let interval = interval.into();
let resume = move || {
let interval_value = interval.get();
if interval_value == 0 {
return;
}
set_active(true);
if immediate_callback {
callback.clone()();
}
clean();
timer.set(
set_interval_with_handle(callback.clone(), Duration::from_millis(interval_value)).ok(),
);
};
if immediate {
resume();
}
if matches!(interval, MaybeSignal::Dynamic(_)) {
let resume = resume.clone();
let stop_watch = watch(cx, interval, move |_, _, _| {
if is_active.get() {
resume();
}
});
on_cleanup(cx, stop_watch);
}
on_cleanup(cx, pause.clone());
Pausable {
is_active: is_active.into(),
pause,
resume,
}
}
/// Options for [`use_interval_fn_with_options`]
#[derive(DefaultBuilder)]
pub struct UseIntervalFnOptions {
/// Start the timer immediately. Defaults to `true`.
immediate: bool,
/// Execute the callback immediate after calling this function. Defaults to `false`
immediate_callback: bool,
}
impl Default for UseIntervalFnOptions {
fn default() -> Self {
Self {
immediate: true,
immediate_callback: false,
}
}
}

View file

@ -1,7 +1,9 @@
mod clonable_fn; mod clonable_fn;
mod filters; mod filters;
mod is; mod is;
mod pausable;
pub use clonable_fn::*; pub use clonable_fn::*;
pub use filters::*; pub use filters::*;
pub use is::*; pub use is::*;
pub use pausable::*;

17
src/utils/pausable.rs Normal file
View file

@ -0,0 +1,17 @@
use leptos::Signal;
/// Pausable effect
pub struct Pausable<PauseFn, ResumeFn>
where
PauseFn: Fn() + Clone,
ResumeFn: Fn() + Clone,
{
/// A Signal that indicates whether a pausable instance is active. `false` when paused.
pub is_active: Signal<bool>,
/// Temporarily pause the effect from executing
pub pause: PauseFn,
/// Resume the effect
pub resume: ResumeFn,
}

View file

@ -197,9 +197,10 @@ pub struct WatchOptions {
/// If `immediate` is true, the `callback` will run immediately. /// If `immediate` is true, the `callback` will run immediately.
/// If it's `false, the `callback` will run only after /// If it's `false, the `callback` will run only after
/// the first change is detected of any signal that is accessed in `deps`. /// the first change is detected of any signal that is accessed in `deps`.
/// Defaults to `false`.
immediate: bool, immediate: bool,
/// Allows to debounce or throttle the callback /// Allows to debounce or throttle the callback. Defaults to no filter.
filter: FilterOptions, filter: FilterOptions,
} }