Skip to main content

leptos_use/
use_css_var.rs

1#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
2
3use crate::core::IntoElementMaybeSignal;
4use crate::{
5    UseMutationObserverOptions, WatchOptions, use_mutation_observer_with_options,
6    watch_with_options,
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/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
80///
81/// On the server this simply returns `signal(options.initial_value)`.
82pub fn use_css_var(prop: impl Into<Signal<String>>) -> (ReadSignal<String>, WriteSignal<String>) {
83    use_css_var_with_options(prop, UseCssVarOptions::default())
84}
85
86/// Version of [`use_css_var`] that takes a `UseCssVarOptions`. See [`use_css_var`] for how to use.
87pub fn use_css_var_with_options<P, El, M>(
88    prop: P,
89    options: UseCssVarOptions<El, M>,
90) -> (ReadSignal<String>, WriteSignal<String>)
91where
92    P: Into<Signal<String>>,
93    El: Clone,
94    El: IntoElementMaybeSignal<web_sys::Element, M>,
95{
96    let UseCssVarOptions {
97        target,
98        initial_value,
99        observe,
100        ..
101    } = options;
102
103    let (variable, set_variable) = signal(initial_value.clone());
104
105    #[cfg(not(feature = "ssr"))]
106    {
107        let el_signal = target.into_element_maybe_signal();
108        let prop = prop.into();
109
110        let update_css_var = move || {
111            if let Some(el) = el_signal.get_untracked() {
112                if let Ok(Some(style)) = window().get_computed_style(&el)
113                    && let Ok(value) = style.get_property_value(&prop.read_untracked())
114                {
115                    set_variable.update(|var| *var = value.trim().to_string());
116                    return;
117                }
118
119                let initial_value = initial_value.clone();
120                set_variable.update(|var| *var = initial_value);
121            }
122        };
123
124        if observe {
125            let update_css_var = update_css_var.clone();
126
127            use_mutation_observer_with_options(
128                el_signal,
129                move |_, _| update_css_var(),
130                UseMutationObserverOptions::default().attribute_filter(vec!["style".to_string()]),
131            );
132        }
133
134        // To get around style attributes on node_refs that are not applied after the first render
135        set_timeout(update_css_var.clone(), Duration::ZERO);
136
137        let _ = watch_with_options(
138            move || (el_signal.get(), prop.get()),
139            move |_, _, _| update_css_var(),
140            WatchOptions::default().immediate(true),
141        );
142
143        Effect::watch(
144            move || variable.get(),
145            move |val, _, _| {
146                if let Some(el) = el_signal.get() {
147                    let el = el.unchecked_ref::<web_sys::HtmlElement>();
148                    let style = el.style();
149                    let _ = style.set_property(&prop.get_untracked(), val);
150                }
151            },
152            false,
153        );
154    }
155
156    (variable, set_variable)
157}
158
159/// Options for [`use_css_var_with_options`].
160#[derive(DefaultBuilder)]
161pub struct UseCssVarOptions<El, M>
162where
163    El: IntoElementMaybeSignal<web_sys::Element, M>,
164{
165    /// The target element to read the variable from and set the variable on.
166    /// Defaults to the `document.documentElement`.
167    target: El,
168
169    /// The initial value of the variable before it is read. Also the default value
170    /// if the variable isn't defined on the target. Defaults to "".
171    #[builder(into)]
172    initial_value: String,
173
174    /// If `true` use a `MutationObserver` to monitor variable changes. Defaults to `false`.
175    observe: bool,
176
177    #[builder(skip)]
178    _marker: PhantomData<M>,
179}
180
181#[cfg(feature = "ssr")]
182impl<M> Default for UseCssVarOptions<Option<web_sys::Element>, M>
183where
184    Option<web_sys::Element>: IntoElementMaybeSignal<web_sys::Element, M>,
185{
186    fn default() -> Self {
187        Self {
188            target: None,
189            initial_value: "".into(),
190            observe: false,
191            _marker: PhantomData,
192        }
193    }
194}
195
196#[cfg(not(feature = "ssr"))]
197impl<M> Default for UseCssVarOptions<web_sys::Element, M>
198where
199    web_sys::Element: IntoElementMaybeSignal<web_sys::Element, M>,
200{
201    fn default() -> Self {
202        Self {
203            target: document().document_element().expect("No document element"),
204            initial_value: "".into(),
205            observe: false,
206            _marker: PhantomData,
207        }
208    }
209}