leptos_style/
style.rs

1use std::{
2    fmt::{self, Display},
3    ops::Deref,
4};
5
6use indexmap::IndexMap;
7use leptos::{attr::IntoAttributeValue, tachys::html::style::IntoStyle};
8
9fn style_map_to_string(map: &IndexMap<String, Option<String>>) -> String {
10    map.iter()
11        .filter_map(|(key, value)| {
12            value
13                .as_ref()
14                .and_then(|value| (!value.is_empty()).then_some(format!("{key}: {value};")))
15        })
16        .collect::<Vec<_>>()
17        .join(" ")
18}
19
20#[derive(Clone, Debug, PartialEq)]
21pub enum InnerStyle {
22    String(String),
23    Structured(IndexMap<String, Option<String>>),
24}
25
26impl InnerStyle {
27    pub fn with_defaults<I: Into<InnerStyle>>(self, defaults: I) -> Self {
28        let defaults: InnerStyle = defaults.into();
29
30        match (self, defaults) {
31            (Self::String(string), Self::String(default_string)) => {
32                Self::String(format!("{default_string} {string}"))
33            }
34            (Self::String(string), Self::Structured(default_map)) => {
35                Self::String(format!("{} {}", style_map_to_string(&default_map), string))
36            }
37            (Self::Structured(map), Self::String(default_string)) => {
38                Self::String(format!("{} {}", default_string, style_map_to_string(&map)))
39            }
40            (Self::Structured(map), Self::Structured(default_map)) => {
41                InnerStyle::Structured(default_map.into_iter().chain(map).collect())
42            }
43        }
44    }
45}
46
47impl Display for InnerStyle {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Self::String(string) => write!(f, "{}", string),
51            Self::Structured(map) => write!(f, "{}", style_map_to_string(map),),
52        }
53    }
54}
55
56#[derive(Clone, Debug, Default, PartialEq)]
57pub struct Style(pub Option<InnerStyle>);
58
59impl Style {
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    pub fn with_defaults<I: Into<Self>>(self, defaults: I) -> Self {
65        let defaults: Self = defaults.into();
66
67        Style(match (self.0, defaults.0) {
68            (Some(style), Some(defaults)) => Some(style.with_defaults(defaults)),
69            (Some(style), None) => Some(style),
70            (None, Some(defaults)) => Some(defaults),
71            (None, None) => None,
72        })
73    }
74}
75
76impl Deref for Style {
77    type Target = Option<InnerStyle>;
78
79    fn deref(&self) -> &Self::Target {
80        &self.0
81    }
82}
83
84impl Display for Style {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        write!(
87            f,
88            "{}",
89            self.0
90                .as_ref()
91                .map(|inner_style| inner_style.to_string())
92                .unwrap_or_default(),
93        )
94    }
95}
96
97impl From<Option<&str>> for Style {
98    fn from(value: Option<&str>) -> Style {
99        Style(value.map(|value| InnerStyle::String(value.to_string())))
100    }
101}
102
103impl From<Option<String>> for Style {
104    fn from(value: Option<String>) -> Style {
105        Style(value.map(InnerStyle::String))
106    }
107}
108
109impl From<&str> for Style {
110    fn from(value: &str) -> Style {
111        Style(Some(InnerStyle::String(value.to_string())))
112    }
113}
114
115impl From<String> for Style {
116    fn from(value: String) -> Style {
117        Style(Some(InnerStyle::String(value)))
118    }
119}
120
121impl From<IndexMap<String, Option<String>>> for Style {
122    fn from(value: IndexMap<String, Option<String>>) -> Style {
123        Style(Some(InnerStyle::Structured(value)))
124    }
125}
126
127impl From<IndexMap<String, String>> for Style {
128    fn from(value: IndexMap<String, String>) -> Style {
129        Style(Some(InnerStyle::Structured(
130            value
131                .into_iter()
132                .map(|(key, value)| (key, Some(value)))
133                .collect(),
134        )))
135    }
136}
137
138impl<const N: usize> From<[(&str, Option<&str>); N]> for Style {
139    fn from(value: [(&str, Option<&str>); N]) -> Style {
140        Style(Some(InnerStyle::Structured(IndexMap::from_iter(
141            value.map(|(key, value)| (key.to_string(), value.map(|value| value.to_string()))),
142        ))))
143    }
144}
145
146impl<const N: usize> From<[(&str, &str); N]> for Style {
147    fn from(value: [(&str, &str); N]) -> Style {
148        Style(Some(InnerStyle::Structured(IndexMap::from_iter(
149            value.map(|(key, value)| (key.to_string(), Some(value.to_string()))),
150        ))))
151    }
152}
153
154impl<const N: usize> From<[(&str, Option<String>); N]> for Style {
155    fn from(value: [(&str, Option<String>); N]) -> Style {
156        Style(Some(InnerStyle::Structured(IndexMap::from_iter(
157            value.map(|(key, value)| (key.to_string(), value)),
158        ))))
159    }
160}
161
162impl<const N: usize> From<[(&str, String); N]> for Style {
163    fn from(value: [(&str, String); N]) -> Style {
164        Style(Some(InnerStyle::Structured(IndexMap::from_iter(
165            value.map(|(key, value)| (key.to_string(), Some(value))),
166        ))))
167    }
168}
169
170impl<const N: usize> From<[(String, Option<String>); N]> for Style {
171    fn from(value: [(String, Option<String>); N]) -> Style {
172        Style(Some(InnerStyle::Structured(IndexMap::from_iter(value))))
173    }
174}
175
176impl<const N: usize> From<[(String, String); N]> for Style {
177    fn from(value: [(String, String); N]) -> Style {
178        Style(Some(InnerStyle::Structured(IndexMap::from_iter(
179            value.map(|(key, value)| (key, Some(value))),
180        ))))
181    }
182}
183
184impl IntoAttributeValue for Style {
185    type Output = String;
186
187    fn into_attribute_value(self) -> Self::Output {
188        self.to_string()
189    }
190}
191
192impl IntoStyle for Style {
193    type AsyncOutput = Self;
194    type State = (leptos::tachys::renderer::types::Element, Self);
195    type Cloneable = Self;
196    type CloneableOwned = Self;
197
198    fn to_html(self, style: &mut String) {
199        style.push_str(&self.to_string());
200    }
201
202    fn hydrate<const FROM_SERVER: bool>(
203        self,
204        el: &leptos::tachys::renderer::types::Element,
205    ) -> Self::State {
206        (el.clone(), self)
207    }
208
209    fn build(self, el: &leptos::tachys::renderer::types::Element) -> Self::State {
210        leptos::tachys::renderer::Rndr::set_attribute(el, "style", &self.to_string());
211        (el.clone(), self)
212    }
213
214    fn rebuild(self, state: &mut Self::State) {
215        let (el, prev) = state;
216        if self != *prev {
217            leptos::tachys::renderer::Rndr::set_attribute(el, "style", &self.to_string());
218        }
219        *prev = self;
220    }
221
222    fn into_cloneable(self) -> Self::Cloneable {
223        self
224    }
225
226    fn into_cloneable_owned(self) -> Self::CloneableOwned {
227        self
228    }
229
230    fn dry_resolve(&mut self) {}
231
232    async fn resolve(self) -> Self::AsyncOutput {
233        self
234    }
235
236    fn reset(state: &mut Self::State) {
237        let (el, _prev) = state;
238        leptos::tachys::renderer::Rndr::remove_attribute(el, "style");
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_to_string() {
248        assert_eq!("", Style::default().to_string());
249
250        assert_eq!("", Style::from(None::<String>).to_string());
251
252        assert_eq!(
253            "margin: 1rem; padding: 0.5rem;",
254            Style::from(Some("margin: 1rem; padding: 0.5rem;")).to_string(),
255        );
256
257        assert_eq!(
258            "margin: 1rem; padding: 0.5rem;",
259            Style::from("margin: 1rem; padding: 0.5rem;").to_string(),
260        );
261
262        assert_eq!(
263            "color: white; border: 1px solid black;",
264            Style::from([
265                ("color", Some("white")),
266                ("background-color", None),
267                ("border", Some("1px solid black")),
268            ])
269            .to_string()
270        );
271
272        assert_eq!(
273            "color: white; background-color: gray; border: 1px solid black;",
274            Style::from([
275                ("color", "white"),
276                ("background-color", "gray"),
277                ("border", "1px solid black"),
278            ])
279            .to_string()
280        );
281    }
282
283    #[test]
284    fn test_with_defaults() {
285        // String with string defaults
286        assert_eq!(
287            Style::from("pointer-events: none; color: red;"),
288            Style::from("color: red;").with_defaults("pointer-events: none;"),
289        );
290        assert_eq!(
291            Style::from("color: blue; color: red;"),
292            Style::from("color: red;").with_defaults("color: blue;"),
293        );
294
295        // String with structured defaults
296        assert_eq!(
297            Style::from("pointer-events: none; color: red;"),
298            Style::from("color: red;").with_defaults([("pointer-events", "none")]),
299        );
300        assert_eq!(
301            Style::from("color: blue; color: red;"),
302            Style::from("color: red;").with_defaults([("color", "blue")]),
303        );
304
305        // Structured with string defaults
306        assert_eq!(
307            Style::from("pointer-events: none; color: red;"),
308            Style::from([("color", "red")]).with_defaults("pointer-events: none;"),
309        );
310        assert_eq!(
311            Style::from("color: blue; color: red;"),
312            Style::from([("color", "red")]).with_defaults("color: blue;"),
313        );
314
315        // Structured with structured defaults
316        assert_eq!(
317            Style::from([("pointer-events", "none"), ("color", "red")]),
318            Style::from([("color", "red")]).with_defaults([("pointer-events", "none")]),
319        );
320        assert_eq!(
321            Style::from([("color", "red")]),
322            Style::from([("color", "red")]).with_defaults([("color", "blue")]),
323        );
324
325        // Optional in structured
326        assert_eq!(
327            Style::from([("color", Some("red"))]),
328            Style::from([("color", Some("red"))]).with_defaults([("color", Some("blue"))]),
329        );
330        assert_eq!(
331            Style::from([("color", None::<String>)]),
332            Style::from([("color", None::<String>)]).with_defaults([("color", Some("blue"))]),
333        );
334        assert_eq!(
335            Style::from([("color", Some("red"))]),
336            Style::from([("color", Some("red"))]).with_defaults([("color", None::<String>)]),
337        );
338        assert_eq!(
339            Style::from([("color", None::<String>)]),
340            Style::from([("color", None::<String>)]).with_defaults([("color", None::<String>)]),
341        );
342    }
343
344    #[test]
345    fn test_into_attribute_value() {
346        assert_eq!(
347            Style::from("color: red; background-color: blue;").into_attribute_value(),
348            "color: red; background-color: blue;"
349        );
350
351        assert_eq!(
352            Style::from([("color", "red"), ("background-color", "blue")]).into_attribute_value(),
353            "color: red; background-color: blue;"
354        );
355    }
356}