2023-10-27 12:37:41 +01:00
use crate ::{
2023-10-27 13:57:36 +01:00
core ::{ MaybeRwSignal , StorageType } ,
2023-12-12 14:53:49 +00:00
utils ::{ FilterOptions , StringCodec } ,
2023-10-27 12:37:41 +01:00
} ;
2023-10-27 14:50:29 +01:00
use cfg_if ::cfg_if ;
2023-10-26 11:46:47 +01:00
use leptos ::* ;
2023-10-28 12:19:33 +01:00
use std ::rc ::Rc ;
2023-10-26 11:46:47 +01:00
use thiserror ::Error ;
use wasm_bindgen ::JsValue ;
2023-10-28 12:13:55 +01:00
const INTERNAL_STORAGE_EVENT : & str = " leptos-use-storage " ;
2023-10-29 11:41:59 +00:00
/// Reactive [Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage).
///
2024-02-05 19:09:15 +00:00
/// The function returns a triplet `(read_signal, write_signal, delete_from_storage_fn)`.
///
2023-11-12 23:07:10 +00:00
/// ## Demo
///
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_storage)
2023-10-29 11:41:59 +00:00
///
/// ## Usage
///
2024-02-05 19:09:15 +00:00
/// Pass a [`StorageType`] to determine the kind of key-value browser storage to use.
/// The specified key is where data is stored. All values are stored as UTF-16 strings which
/// is then encoded and decoded via the given [`Codec`]. This value is synced with other calls using
/// the same key on the smae page and across tabs for local storage.
/// See [`UseStorageOptions`] to see how behaviour can be further customised.
2023-10-29 11:41:59 +00:00
///
2024-01-31 03:11:07 +00:00
/// See [`StringCodec`] for more details on how to handle versioning — dealing with data that can outlast your code.
2023-11-04 15:43:36 +00:00
///
2024-02-05 19:09:15 +00:00
/// > To use the [`JsonCodec`], you will need to add the `"serde"` feature to your project's `Cargo.toml`.
/// > To use [`ProstCodec`], add the feature `"prost"`.
2023-10-29 11:41:59 +00:00
///
/// ## Example
///
/// ```
/// # use leptos::*;
2024-01-31 16:54:25 +00:00
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage};
2023-10-29 11:41:59 +00:00
/// # use serde::{Deserialize, Serialize};
2024-01-29 21:29:39 +00:00
/// # use leptos_use::utils::{FromToStringCodec, JsonCodec, ProstCodec};
2023-10-29 11:41:59 +00:00
/// #
/// # pub fn Demo() -> impl IntoView {
/// // Binds a struct:
/// let (state, set_state, _) = use_local_storage::<MyState, JsonCodec>("my-state");
///
/// // Binds a bool, stored as a string:
2024-01-29 21:29:39 +00:00
/// let (flag, set_flag, remove_flag) = use_session_storage::<bool, FromToStringCodec>("my-flag");
2023-10-29 11:41:59 +00:00
///
/// // Binds a number, stored as a string:
2024-01-29 21:29:39 +00:00
/// let (count, set_count, _) = use_session_storage::<i32, FromToStringCodec>("my-count");
2023-10-29 11:41:59 +00:00
/// // Binds a number, stored in JSON:
/// let (count, set_count, _) = use_session_storage::<i32, JsonCodec>("my-count-kept-in-js");
///
/// // Bind string with SessionStorage stored in ProtoBuf format:
2024-01-31 16:54:25 +00:00
/// let (id, set_id, _) = use_storage::<String, ProstCodec>(
2023-10-29 11:41:59 +00:00
/// StorageType::Session,
/// "my-id",
/// );
/// # view! { }
/// # }
///
2024-02-05 19:09:15 +00:00
/// // Data stored in JSON must implement Serialize, Deserialize.
/// // And you have to add the feature "serde" to your project's Cargo.toml
2023-10-29 11:41:59 +00:00
/// #[derive(Serialize, Deserialize, Clone, PartialEq)]
/// pub struct MyState {
/// pub hello: String,
/// pub greeting: String,
/// }
///
2024-02-11 01:03:12 +01:00
/// // Default can be used to implement initial or deleted values.
2023-10-29 11:41:59 +00:00
/// // You can also use a signal via UseStorageOptions::default_value`
/// impl Default for MyState {
/// fn default() -> Self {
/// Self {
/// hello: "hi".to_string(),
/// greeting: "Hello".to_string()
/// }
/// }
/// }
/// ```
2023-12-12 14:53:49 +00:00
///
2024-01-31 03:11:07 +00:00
/// ## Create Your Own Custom Codec
2023-12-12 14:53:49 +00:00
///
2024-01-31 03:11:07 +00:00
/// All you need to do is to implement the [`StringCodec`] trait together with `Default` and `Clone`.
2024-02-05 19:09:15 +00:00
///
/// ## Server-Side Rendering
///
/// On the server the returned signals will just read/manipulate the `initial_value` without persistence.
2023-11-12 22:50:59 +00:00
#[ inline(always) ]
2024-01-31 16:54:25 +00:00
pub fn use_storage < T , C > (
2023-11-12 22:50:59 +00:00
storage_type : StorageType ,
key : impl AsRef < str > ,
2024-01-31 16:54:25 +00:00
) -> ( Signal < T > , WriteSignal < T > , impl Fn ( ) + Clone )
where
T : Default + Clone + PartialEq ,
C : StringCodec < T > + Default ,
{
use_storage_with_options ::< T , C > ( storage_type , key , UseStorageOptions ::default ( ) )
2023-11-12 22:50:59 +00:00
}
/// Version of [`use_storage`] that accepts [`UseStorageOptions`].
2023-10-27 14:15:40 +01:00
pub fn use_storage_with_options < T , C > (
2023-10-27 12:37:41 +01:00
storage_type : StorageType ,
2023-10-27 11:56:47 +01:00
key : impl AsRef < str > ,
options : UseStorageOptions < T , C > ,
2023-11-11 10:51:23 +00:00
) -> ( Signal < T > , WriteSignal < T > , impl Fn ( ) + Clone )
2023-10-26 11:46:47 +01:00
where
2023-10-27 13:57:36 +01:00
T : Clone + PartialEq ,
2024-01-31 16:54:25 +00:00
C : StringCodec < T > + Default ,
2023-10-26 11:46:47 +01:00
{
2023-10-26 12:01:04 +01:00
let UseStorageOptions {
2023-10-27 10:05:02 +01:00
codec ,
2023-10-26 12:01:04 +01:00
on_error ,
listen_to_storage_changes ,
2023-11-04 13:50:41 +00:00
initial_value ,
2023-10-29 09:45:43 +00:00
filter ,
2023-10-26 12:01:04 +01:00
} = options ;
2023-11-04 13:30:36 +00:00
2023-11-04 13:50:41 +00:00
let ( data , set_data ) = initial_value . into_signal ( ) ;
2023-11-04 13:30:36 +00:00
let default = data . get_untracked ( ) ;
2023-10-26 11:46:47 +01:00
2023-10-29 12:18:45 +00:00
cfg_if! { if #[ cfg(feature = " ssr " ) ] {
2023-11-13 00:20:58 +00:00
let _ = codec ;
let _ = on_error ;
let _ = listen_to_storage_changes ;
let _ = filter ;
let _ = storage_type ;
let _ = key ;
let _ = INTERNAL_STORAGE_EVENT ;
2023-10-29 12:18:45 +00:00
let remove = move | | {
2023-11-04 13:30:36 +00:00
set_data . set ( default . clone ( ) ) ;
2023-10-29 12:18:45 +00:00
} ;
2023-11-13 00:20:58 +00:00
2023-10-29 12:18:45 +00:00
( data . into ( ) , set_data , remove )
} else {
2023-11-13 00:20:58 +00:00
use crate ::{ use_event_listener , use_window , watch_with_options , WatchOptions } ;
2023-10-29 12:18:45 +00:00
// Get storage API
let storage = storage_type
. into_storage ( )
. map_err ( UseStorageError ::StorageNotAvailable )
. and_then ( | s | s . ok_or ( UseStorageError ::StorageReturnedNone ) ) ;
let storage = handle_error ( & on_error , storage ) ;
2023-10-27 11:56:47 +01:00
2023-10-29 12:18:45 +00:00
// Schedules a storage event microtask. Uses a queue to avoid re-entering the runtime
let dispatch_storage_event = {
let key = key . as_ref ( ) . to_owned ( ) ;
2023-10-28 12:13:55 +01:00
let on_error = on_error . to_owned ( ) ;
2023-10-29 12:18:45 +00:00
move | | {
let key = key . to_owned ( ) ;
let on_error = on_error . to_owned ( ) ;
queue_microtask ( move | | {
// Note: we cannot construct a full StorageEvent so we _must_ rely on a custom event
let mut custom = web_sys ::CustomEventInit ::new ( ) ;
custom . detail ( & JsValue ::from_str ( & key ) ) ;
let result = window ( )
. dispatch_event (
& web_sys ::CustomEvent ::new_with_event_init_dict (
INTERNAL_STORAGE_EVENT ,
& custom ,
)
. expect ( " failed to create custom storage event " ) ,
2023-10-28 12:13:55 +01:00
)
2023-10-29 12:18:45 +00:00
. map_err ( UseStorageError ::NotifyItemChangedFailed ) ;
let _ = handle_error ( & on_error , result ) ;
} )
}
} ;
2023-10-28 12:13:55 +01:00
2023-11-04 13:30:36 +00:00
// Fetches direct from browser storage and fills set_data if changed (memo)
let fetch_from_storage = {
2023-10-29 12:18:45 +00:00
let storage = storage . to_owned ( ) ;
let codec = codec . to_owned ( ) ;
let key = key . as_ref ( ) . to_owned ( ) ;
let on_error = on_error . to_owned ( ) ;
2023-11-04 13:30:36 +00:00
move | | {
let fetched = storage
2023-10-29 12:18:45 +00:00
. to_owned ( )
. and_then ( | storage | {
// Get directly from storage
let result = storage
. get_item ( & key )
. map_err ( UseStorageError ::GetItemFailed ) ;
handle_error ( & on_error , result )
} )
. unwrap_or_default ( ) // Drop handled Err(())
. map ( | encoded | {
// Decode item
let result = codec
. decode ( encoded )
. map_err ( UseStorageError ::ItemCodecError ) ;
handle_error ( & on_error , result )
} )
. transpose ( )
2023-11-04 13:30:36 +00:00
. unwrap_or_default ( ) ; // Drop handled Err(())
match fetched {
Some ( value ) = > {
// Replace data if changed
if value ! = data . get_untracked ( ) {
set_data . set ( value )
}
}
// Revert to default
None = > set_data . set ( default . clone ( ) ) ,
} ;
}
2023-10-29 12:18:45 +00:00
} ;
2023-10-26 11:46:47 +01:00
2023-11-04 13:30:36 +00:00
// Fetch initial value
fetch_from_storage ( ) ;
2023-10-28 12:13:55 +01:00
2023-11-04 13:30:36 +00:00
// Fires when storage needs to be fetched
let notify = create_trigger ( ) ;
// Refetch from storage. Keeps track of how many times we've been notified. Does not increment for calls to set_data
let notify_id = create_memo ::< usize > ( move | prev | {
notify . track ( ) ;
match prev {
None = > 1 , // Avoid async fetch of initial value
Some ( prev ) = > {
fetch_from_storage ( ) ;
prev + 1
}
}
} ) ;
// Set item on internal (non-event) page changes to the data signal
2023-10-29 12:18:45 +00:00
{
let storage = storage . to_owned ( ) ;
let codec = codec . to_owned ( ) ;
let key = key . as_ref ( ) . to_owned ( ) ;
let on_error = on_error . to_owned ( ) ;
let dispatch_storage_event = dispatch_storage_event . to_owned ( ) ;
let _ = watch_with_options (
move | | ( notify_id . get ( ) , data . get ( ) ) ,
move | ( id , value ) , prev , _ | {
// Skip setting storage on changes from external events. The ID will change on external events.
if prev . map ( | ( prev_id , _ ) | * prev_id ! = * id ) . unwrap_or_default ( ) {
return ;
}
2023-10-28 12:13:55 +01:00
2023-10-29 12:18:45 +00:00
if let Ok ( storage ) = & storage {
// Encode value
let result = codec
. encode ( value )
. map_err ( UseStorageError ::ItemCodecError )
. and_then ( | enc_value | {
// Set storage -- sends a global event
storage
. set_item ( & key , & enc_value )
. map_err ( UseStorageError ::SetItemFailed )
} ) ;
let result = handle_error ( & on_error , result ) ;
// Send internal storage event
if result . is_ok ( ) {
dispatch_storage_event ( ) ;
}
2023-10-28 12:13:55 +01:00
}
2023-10-29 12:18:45 +00:00
} ,
WatchOptions ::default ( ) . filter ( filter ) ,
) ;
2023-11-04 13:30:36 +00:00
}
2023-10-26 11:46:47 +01:00
2023-10-29 12:18:45 +00:00
if listen_to_storage_changes {
let check_key = key . as_ref ( ) . to_owned ( ) ;
// Listen to global storage events
let _ = use_event_listener ( use_window ( ) , leptos ::ev ::storage , move | ev | {
let ev_key = ev . key ( ) ;
// Key matches or all keys deleted (None)
if ev_key = = Some ( check_key . clone ( ) ) | | ev_key . is_none ( ) {
2023-10-28 12:13:55 +01:00
notify . notify ( )
2023-10-26 11:46:47 +01:00
}
2023-10-27 19:51:02 +01:00
} ) ;
2023-10-29 12:18:45 +00:00
// Listen to internal storage events
let check_key = key . as_ref ( ) . to_owned ( ) ;
let _ = use_event_listener (
use_window ( ) ,
ev ::Custom ::new ( INTERNAL_STORAGE_EVENT ) ,
move | ev : web_sys ::CustomEvent | {
if Some ( check_key . clone ( ) ) = = ev . detail ( ) . as_string ( ) {
notify . notify ( )
}
} ,
) ;
} ;
// Remove from storage fn
let remove = {
let key = key . as_ref ( ) . to_owned ( ) ;
move | | {
let _ = storage . as_ref ( ) . map ( | storage | {
// Delete directly from storage
let result = storage
. remove_item ( & key )
. map_err ( UseStorageError ::RemoveItemFailed ) ;
let _ = handle_error ( & on_error , result ) ;
notify . notify ( ) ;
dispatch_storage_event ( ) ;
} ) ;
}
} ;
2023-10-27 13:57:36 +01:00
2023-11-11 10:51:23 +00:00
( data , set_data , remove )
2023-10-29 12:18:45 +00:00
} }
2023-10-26 11:46:47 +01:00
}
2023-11-11 13:15:13 +00:00
/// Session handling errors returned by [`use_storage_with_options`].
#[ derive(Error, Debug) ]
pub enum UseStorageError < Err > {
#[ error( " storage not available " ) ]
StorageNotAvailable ( JsValue ) ,
#[ error( " storage not returned from window " ) ]
StorageReturnedNone ,
#[ error( " failed to get item " ) ]
GetItemFailed ( JsValue ) ,
#[ error( " failed to set item " ) ]
SetItemFailed ( JsValue ) ,
#[ error( " failed to delete item " ) ]
RemoveItemFailed ( JsValue ) ,
#[ error( " failed to notify item changed " ) ]
NotifyItemChangedFailed ( JsValue ) ,
#[ error( " failed to encode / decode item value " ) ]
ItemCodecError ( Err ) ,
}
/// Options for use with [`use_local_storage_with_options`], [`use_session_storage_with_options`] and [`use_storage_with_options`].
2024-01-29 21:29:39 +00:00
pub struct UseStorageOptions < T : 'static , C : StringCodec < T > > {
2023-11-11 13:15:13 +00:00
// Translates to and from UTF-16 strings
codec : C ,
// Callback for when an error occurs
on_error : Rc < dyn Fn ( UseStorageError < C ::Error > ) > ,
// Whether to continuously listen to changes from browser storage
listen_to_storage_changes : bool ,
// Initial value to use when the storage key is not set
initial_value : MaybeRwSignal < T > ,
// Debounce or throttle the writing to storage whenever the value changes
filter : FilterOptions ,
}
2023-10-26 11:46:47 +01:00
/// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling.
2023-11-13 00:20:58 +00:00
#[ cfg(not(feature = " ssr " )) ]
2023-10-26 11:46:47 +01:00
fn handle_error < T , Err > (
on_error : & Rc < dyn Fn ( UseStorageError < Err > ) > ,
result : Result < T , UseStorageError < Err > > ,
) -> Result < T , ( ) > {
2023-11-11 10:51:23 +00:00
result . map_err ( | err | ( on_error ) ( err ) )
2023-10-26 11:46:47 +01:00
}
2024-01-29 21:29:39 +00:00
impl < T : Default , C : StringCodec < T > + Default > Default for UseStorageOptions < T , C > {
2023-10-27 16:08:51 +01:00
fn default ( ) -> Self {
2023-10-26 11:46:47 +01:00
Self {
2023-10-29 11:49:03 +00:00
codec : C ::default ( ) ,
2023-10-26 11:46:47 +01:00
on_error : Rc ::new ( | _err | ( ) ) ,
2023-10-26 12:01:04 +01:00
listen_to_storage_changes : true ,
2023-11-04 13:50:41 +00:00
initial_value : MaybeRwSignal ::default ( ) ,
2023-10-29 09:45:43 +00:00
filter : FilterOptions ::default ( ) ,
2023-10-26 11:46:47 +01:00
}
}
2023-10-29 11:49:03 +00:00
}
2023-10-26 11:46:47 +01:00
2024-01-29 21:29:39 +00:00
impl < T : Default , C : StringCodec < T > > UseStorageOptions < T , C > {
2023-10-29 11:58:14 +00:00
/// Sets the codec to use for encoding and decoding values to and from UTF-16 strings.
pub fn codec ( self , codec : impl Into < C > ) -> Self {
Self {
codec : codec . into ( ) ,
.. self
}
}
2023-10-29 11:41:59 +00:00
/// Optional callback whenever an error occurs.
2023-10-27 10:05:02 +01:00
pub fn on_error ( self , on_error : impl Fn ( UseStorageError < C ::Error > ) + 'static ) -> Self {
2023-10-26 11:46:47 +01:00
Self {
on_error : Rc ::new ( on_error ) ,
2023-10-26 12:01:04 +01:00
.. self
}
}
2023-10-29 11:41:59 +00:00
/// Listen to changes to this storage key from browser and page events. Defaults to true.
2023-10-26 12:01:04 +01:00
pub fn listen_to_storage_changes ( self , listen_to_storage_changes : bool ) -> Self {
Self {
listen_to_storage_changes ,
.. self
2023-10-26 11:46:47 +01:00
}
}
2023-10-27 13:57:36 +01:00
2023-11-04 13:50:41 +00:00
/// Initial value to use when the storage key is not set. Note that this value is read once on creation of the storage hook and not updated again. Accepts a signal and defaults to `T::default()`.
pub fn initial_value ( self , initial : impl Into < MaybeRwSignal < T > > ) -> Self {
2023-10-27 13:57:36 +01:00
Self {
2023-11-04 13:50:41 +00:00
initial_value : initial . into ( ) ,
2023-10-27 13:57:36 +01:00
.. self
}
}
2023-10-29 09:45:43 +00:00
2023-10-29 11:41:59 +00:00
/// Debounce or throttle the writing to storage whenever the value changes.
2023-10-29 09:45:43 +00:00
pub fn filter ( self , filter : impl Into < FilterOptions > ) -> Self {
Self {
filter : filter . into ( ) ,
.. self
}
}
2023-10-26 11:46:47 +01:00
}