Skip to main content

orbital_style/
style_registry.rs

1use leptos::{
2    attr::{any_attribute::AnyAttribute, Attribute},
3    context::{Provider, ProviderProps},
4    prelude::*,
5    tachys::{
6        hydration::Cursor,
7        renderer::types,
8        view::{
9            any_view::{AnyViewState, AnyViewWithAttrs},
10            Position, PositionState,
11        },
12    },
13};
14use std::collections::HashMap;
15
16const STYLE_MARKER: &str = r#"<meta name="orbital-style""#;
17
18/// Collects component-injected styles during SSR and streams them into `<head>`.
19#[component]
20pub fn StyleRegistry<Chil>(children: TypedChildren<Chil>) -> impl IntoView
21where
22    Chil: IntoView + 'static,
23{
24    let context = StyleRegistryContext::default();
25
26    let children = Provider(
27        ProviderProps::builder()
28            .value(context.clone())
29            .children(children)
30            .build(),
31    )
32    .into_any();
33    StyleRegistryView { context, children }
34}
35
36#[derive(Debug, Clone)]
37pub struct StyleRegistryContext {
38    styles: ArcStoredValue<HashMap<String, String>>,
39}
40
41impl StyleRegistryContext {
42    pub fn use_context() -> Option<Self> {
43        use_context()
44    }
45
46    pub fn push_style(&self, k: String, v: String) {
47        self.styles.write_value().insert(k, v);
48    }
49
50    /// Upserts a collected stylesheet into `<head>` during client hydration.
51    #[cfg(feature = "hydrate")]
52    pub fn ensure_style_in_head(&self, id: &str, content: &str) {
53        use leptos::prelude::document;
54
55        let head = document().head().expect("head no exist");
56        let style = head
57            .query_selector(&format!("style#{id}"))
58            .expect("query style element error")
59            .unwrap_or_else(|| {
60                let style = document()
61                    .create_element("style")
62                    .expect("create style element error");
63                let _ = style.set_attribute("id", id);
64
65                let orbital_meta = head
66                    .query_selector(STYLE_MARKER)
67                    .expect("query orbital-style meta element error");
68
69                if let Some(orbital_meta) = orbital_meta {
70                    let _ = head.insert_before(&style, Some(&orbital_meta));
71                } else {
72                    let _ = head.prepend_with_node_1(&style);
73                }
74
75                style
76            });
77
78        style.set_text_content(Some(content));
79    }
80
81    fn default() -> Self {
82        Self {
83            styles: Default::default(),
84        }
85    }
86
87    fn html_len(&self) -> usize {
88        const TEMPLATE_LEN: usize = r#"<style id=""></style>"#.len();
89        let mut html_len = 0;
90        let styles = self.styles.write_value();
91
92        styles.iter().for_each(|(k, v)| {
93            html_len += k.len() + v.len() + TEMPLATE_LEN;
94        });
95
96        html_len
97    }
98
99    #[allow(clippy::wrong_self_convention)]
100    fn to_html(self) -> String {
101        let mut styles = self.styles.write_value();
102        styles
103            .drain()
104            .map(|(k, v)| format!(r#"<style id="{k}">{v}</style>"#))
105            .collect::<String>()
106    }
107}
108
109struct StyleRegistryView {
110    context: StyleRegistryContext,
111    children: AnyView,
112}
113
114struct StyleRegistryState {
115    state: AnyViewState,
116}
117
118impl Render for StyleRegistryView {
119    type State = StyleRegistryState;
120
121    fn build(self) -> Self::State {
122        let state = self.children.build();
123        StyleRegistryState { state }
124    }
125
126    fn rebuild(self, state: &mut Self::State) {
127        self.children.rebuild(&mut state.state);
128    }
129}
130
131impl AddAnyAttr for StyleRegistryView {
132    type Output<SomeNewAttr: Attribute> = AnyViewWithAttrs;
133
134    fn add_any_attr<NewAttr: Attribute>(self, attr: NewAttr) -> Self::Output<NewAttr>
135    where
136        Self::Output<NewAttr>: RenderHtml,
137    {
138        self.children.add_any_attr(attr)
139    }
140}
141
142impl RenderHtml for StyleRegistryView {
143    type AsyncOutput = Self;
144    type Owned = Self;
145
146    const MIN_LENGTH: usize = 0;
147
148    fn html_len(&self) -> usize {
149        self.children.html_len() + self.context.html_len()
150    }
151
152    fn dry_resolve(&mut self) {
153        self.children.dry_resolve();
154    }
155
156    async fn resolve(self) -> Self::AsyncOutput {
157        self
158    }
159
160    fn to_html_with_buf(
161        self,
162        buf: &mut String,
163        position: &mut Position,
164        escape: bool,
165        mark_branches: bool,
166        extra_attrs: Vec<AnyAttribute>,
167    ) {
168        self.children
169            .to_html_with_buf(buf, position, escape, mark_branches, extra_attrs);
170
171        let head_loc = buf
172            .find("<head>")
173            .expect("you are using StyleRegistry without a <head> tag");
174        let marker_loc = buf.find(STYLE_MARKER).unwrap_or(head_loc + 6);
175
176        buf.insert_str(marker_loc, &self.context.to_html());
177    }
178
179    fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
180        self,
181        buf: &mut leptos::tachys::ssr::StreamBuilder,
182        position: &mut Position,
183        escape: bool,
184        mark_branches: bool,
185        extra_attrs: Vec<AnyAttribute>,
186    ) where
187        Self: Sized,
188    {
189        self.children.to_html_async_with_buf::<OUT_OF_ORDER>(
190            buf,
191            position,
192            escape,
193            mark_branches,
194            extra_attrs,
195        );
196
197        buf.with_buf(|buf| {
198            let head_loc = buf
199                .find("<head>")
200                .expect("you are using StyleRegistry without a <head> tag");
201            let marker_loc = buf.find(STYLE_MARKER).unwrap_or(head_loc + 6);
202            buf.insert_str(marker_loc, &self.context.to_html());
203        });
204    }
205
206    fn hydrate<const FROM_SERVER: bool>(
207        self,
208        cursor: &Cursor,
209        position: &PositionState,
210    ) -> Self::State {
211        let state = self.children.hydrate::<FROM_SERVER>(cursor, position);
212        StyleRegistryState { state }
213    }
214
215    fn into_owned(self) -> Self::Owned {
216        self
217    }
218}
219
220impl Mountable for StyleRegistryState {
221    fn unmount(&mut self) {
222        self.state.unmount();
223    }
224
225    fn mount(&mut self, parent: &types::Element, marker: Option<&types::Node>) {
226        self.state.mount(parent, marker);
227    }
228
229    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
230        self.state.insert_before_this(child)
231    }
232
233    fn elements(&self) -> Vec<types::Element> {
234        self.state.elements()
235    }
236}