orbital_style/
style_registry.rs1use 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#[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 #[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}