leptos_use/
use_css_var.rs

1#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
2
3use crate::core::IntoElementMaybeSignal;
4use crate::{
5    use_mutation_observer_with_options, watch_with_options, UseMutationObserverOptions,
6    WatchOptions,
7};
8use default_struct_builder::DefaultBuilder;
9use leptos::prelude::*;
10use std::marker::PhantomData;
11use std::time::Duration;
12use wasm_bindgen::JsCast;
13
14/// Manipulate CSS variables.
15///
16/// ## Demo
17///
18/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_css_var)
19///
20/// ## Usage
21///
22/// ```
23/// # use leptos::prelude::*;
24/// # use leptos_use::use_css_var;
25/// #
26/// # #[component]
27/// # fn Demo() -> impl IntoView {
28/// let (color, set_color) = use_css_var("--color");
29///
30/// set_color.set("red".to_string());
31/// #
32/// # view! { }
33/// # }
34/// ```
35///
36/// The variable name itself can be a `Signal`.
37///
38/// ```
39/// # use leptos::prelude::*;
40/// # use leptos_use::use_css_var;
41/// #
42/// # #[component]
43/// # fn Demo() -> impl IntoView {
44/// let (key, set_key) = signal("--color".to_string());
45/// let (color, set_color) = use_css_var(key);
46/// #
47/// # view! { }
48/// # }
49/// ```
50///
51/// You can specify the element that the variable is applied to as well as an initial value in case
52/// the variable is not set yet. The option to listen for changes to the variable is also available.
53///
54/// ```
55/// # use leptos::prelude::*;
56/// # use leptos::html::Div;
57/// # use leptos_use::{use_css_var_with_options, UseCssVarOptions};
58/// #
59/// # #[component]
60/// # fn Demo() -> impl IntoView {
61/// let el = NodeRef::<Div>::new();
62///
63/// let (color, set_color) = use_css_var_with_options(
64///     "--color",
65///     UseCssVarOptions::default()
66///         .target(el)
67///         .initial_value("#eee")
68///         .observe(true),
69/// );
70///
71/// view! {
72///     <div node_ref=el>"..."</div>
73/// }
74/// # }
75/// ```
76///
77/// ## Server-Side Rendering
78///
79/// On the server this simply returns `signal(options.initial_value)`.
80pub fn use_css_var(prop: impl Into<Signal<String>>) -> (ReadSignal<String>, WriteSignal<String>) {
81    use_css_var_with_options(prop, UseCssVarOptions::default())
82}
83
84/// Version of [`use_css_var`] that takes a `UseCssVarOptions`. See [`use_css_var`] for how to use.
85pub fn use_css_var_with_options<P, El, M>(
86    prop: P,
87    options: UseCssVarOptions<El, M>,
88) -> (ReadSignal<String>, WriteSignal<String>)
89where
90    P: Into<Signal<String>>,
91    El: Clone,
92    El: IntoElementMaybeSignal<web_sys::Element, M>,
93{
94    let UseCssVarOptions {
95        target,
96        initial_value,
97        observe,
98        ..
99    } = options;
100
101    let (variable, set_variable) = signal(initial_value.clone());
102
103    #[cfg(not(feature = "ssr"))]
104    {
105        let el_signal = target.into_element_maybe_signal();
106        let prop = prop.into();
107
108        let update_css_var = move || {
109            if let Some(el) = el_signal.get_untracked() {
110                if let Ok(Some(style)) = window().get_computed_style(&el) {
111                    if let Ok(value) = style.get_property_value(&prop.read_untracked()) {
112                        set_variable.update(|var| *var = value.trim().to_string());
113                        return;
114                    }
115                }
116
117                let initial_value = initial_value.clone();
118                set_variable.update(|var| *var = initial_value);
119            }
120        };
121
122        if observe {
123            let update_css_var = update_css_var.clone();
124
125            use_mutation_observer_with_options(
126                el_signal,
127                move |_, _| update_css_var(),
128                UseMutationObserverOptions::default().attribute_filter(vec!["style".to_string()]),
129            );
130        }
131
132        // To get around style attributes on node_refs that are not applied after the first render
133        set_timeout(update_css_var.clone(), Duration::ZERO);
134
135        let _ = watch_with_options(
136            move || (el_signal.get(), prop.get()),
137            move |_, _, _| update_css_var(),
138            WatchOptions::default().immediate(true),
139        );
140
141        Effect::watch(
142            move || variable.get(),
143            move |val, _, _| {
144                if let Some(el) = el_signal.get() {
145                    let el = el.unchecked_into::<web_sys::HtmlElement>();
146                    let style = el.style();
147                    let _ = style.set_property(&prop.get_untracked(), val);
148                }
149            },
150            false,
151        );
152    }
153
154    (variable, set_variable)
155}
156
157/// Options for [`use_css_var_with_options`].
158#[derive(DefaultBuilder)]
159pub struct UseCssVarOptions<El, M>
160where
161    El: IntoElementMaybeSignal<web_sys::Element, M>,
162{
163    /// The target element to read the variable from and set the variable on.
164    /// Defaults to the `document.documentElement`.
165    target: El,
166
167    /// The initial value of the variable before it is read. Also the default value
168    /// if the variable isn't defined on the target. Defaults to "".
169    #[builder(into)]
170    initial_value: String,
171
172    /// If `true` use a `MutationObserver` to monitor variable changes. Defaults to `false`.
173    observe: bool,
174
175    #[builder(skip)]
176    _marker: PhantomData<M>,
177}
178
179#[cfg(feature = "ssr")]
180impl<M> Default for UseCssVarOptions<Option<web_sys::Element>, M>
181where
182    Option<web_sys::Element>: IntoElementMaybeSignal<web_sys::Element, M>,
183{
184    fn default() -> Self {
185        Self {
186            target: None,
187            initial_value: "".into(),
188            observe: false,
189            _marker: PhantomData,
190        }
191    }
192}
193
194#[cfg(not(feature = "ssr"))]
195impl<M> Default for UseCssVarOptions<web_sys::Element, M>
196where
197    web_sys::Element: IntoElementMaybeSignal<web_sys::Element, M>,
198{
199    fn default() -> Self {
200        Self {
201            target: document().document_element().expect("No document element"),
202            initial_value: "".into(),
203            observe: false,
204            _marker: PhantomData,
205        }
206    }
207}