Skip to main content

tachys/html/
islands.rs

1use super::attribute::{any_attribute::AnyAttribute, Attribute};
2use crate::{
3    hydration::Cursor,
4    prelude::{Render, RenderHtml},
5    ssr::StreamBuilder,
6    view::{add_attr::AddAnyAttr, Position, PositionState},
7};
8
9/// An island of interactivity in an otherwise-inert HTML document.
10pub struct Island<View> {
11    has_element_representation: bool,
12    component: &'static str,
13    props_json: String,
14    view: View,
15}
16const ISLAND_TAG: &str = "leptos-island";
17const ISLAND_CHILDREN_TAG: &str = "leptos-children";
18
19impl<View> Island<View> {
20    /// Creates a new island with the given component name.
21    pub fn new(component: &'static str, view: View) -> Self {
22        Island {
23            has_element_representation:
24                Self::should_have_element_representation(),
25            component,
26            props_json: String::new(),
27            view,
28        }
29    }
30
31    /// Adds serialized component props as JSON.
32    pub fn with_props(mut self, props_json: String) -> Self {
33        self.props_json = props_json;
34        self
35    }
36
37    fn open_tag(component: &'static str, props: &str, buf: &mut String) {
38        buf.push('<');
39        buf.push_str(ISLAND_TAG);
40        buf.push(' ');
41        buf.push_str("data-component=\"");
42        buf.push_str(component);
43        buf.push('"');
44        if !props.is_empty() {
45            buf.push_str(" data-props=\"");
46            buf.push_str(&html_escape::encode_double_quoted_attribute(&props));
47            buf.push('"');
48        }
49        buf.push('>');
50    }
51
52    fn close_tag(buf: &mut String) {
53        buf.push_str("</");
54        buf.push_str(ISLAND_TAG);
55        buf.push('>');
56    }
57
58    /// Whether this island should be represented by an actual HTML element
59    fn should_have_element_representation() -> bool {
60        #[cfg(feature = "reactive_graph")]
61        {
62            use reactive_graph::owner::{use_context, IsHydrating};
63            let already_hydrating =
64                use_context::<IsHydrating>().map(|h| h.0).unwrap_or(false);
65            !already_hydrating
66        }
67        #[cfg(not(feature = "reactive_graph"))]
68        {
69            true
70        }
71    }
72}
73
74impl<View> Render for Island<View>
75where
76    View: Render,
77{
78    type State = View::State;
79
80    fn build(self) -> Self::State {
81        self.view.build()
82    }
83
84    fn rebuild(self, state: &mut Self::State) {
85        self.view.rebuild(state);
86    }
87}
88
89impl<View> AddAnyAttr for Island<View>
90where
91    View: RenderHtml,
92{
93    type Output<SomeNewAttr: Attribute> =
94        Island<<View as AddAnyAttr>::Output<SomeNewAttr>>;
95
96    fn add_any_attr<NewAttr: Attribute>(
97        self,
98        attr: NewAttr,
99    ) -> Self::Output<NewAttr>
100    where
101        Self::Output<NewAttr>: RenderHtml,
102    {
103        let Island {
104            has_element_representation,
105            component,
106            props_json,
107            view,
108        } = self;
109        Island {
110            has_element_representation,
111            component,
112            props_json,
113            view: view.add_any_attr(attr),
114        }
115    }
116}
117
118impl<View> RenderHtml for Island<View>
119where
120    View: RenderHtml,
121{
122    type AsyncOutput = Island<View::AsyncOutput>;
123    type Owned = Island<View::Owned>;
124
125    const MIN_LENGTH: usize = ISLAND_TAG.len() * 2
126        + "<>".len()
127        + "</>".len()
128        + "data-component".len()
129        + View::MIN_LENGTH;
130
131    fn dry_resolve(&mut self) {
132        self.view.dry_resolve()
133    }
134
135    async fn resolve(self) -> Self::AsyncOutput {
136        let Island {
137            has_element_representation,
138            component,
139            props_json,
140            view,
141        } = self;
142        Island {
143            has_element_representation,
144            component,
145            props_json,
146            view: view.resolve().await,
147        }
148    }
149
150    fn to_html_with_buf(
151        self,
152        buf: &mut String,
153        position: &mut Position,
154        escape: bool,
155        mark_branches: bool,
156        extra_attrs: Vec<AnyAttribute>,
157    ) {
158        let has_element = self.has_element_representation;
159        if has_element {
160            Self::open_tag(self.component, &self.props_json, buf);
161        }
162        self.view.to_html_with_buf(
163            buf,
164            position,
165            escape,
166            mark_branches,
167            extra_attrs,
168        );
169        if has_element {
170            Self::close_tag(buf);
171        }
172    }
173
174    fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
175        self,
176        buf: &mut StreamBuilder,
177        position: &mut Position,
178        escape: bool,
179        mark_branches: bool,
180        extra_attrs: Vec<AnyAttribute>,
181    ) where
182        Self: Sized,
183    {
184        let has_element = self.has_element_representation;
185        // insert the opening tag synchronously
186        let mut tag = String::new();
187        if has_element {
188            Self::open_tag(self.component, &self.props_json, &mut tag);
189        }
190        buf.push_sync(&tag);
191
192        // streaming render for the view
193        self.view.to_html_async_with_buf::<OUT_OF_ORDER>(
194            buf,
195            position,
196            escape,
197            mark_branches,
198            extra_attrs,
199        );
200
201        // and insert the closing tag synchronously
202        tag.clear();
203        if has_element {
204            Self::close_tag(&mut tag);
205        }
206        buf.push_sync(&tag);
207    }
208
209    fn hydrate<const FROM_SERVER: bool>(
210        self,
211        cursor: &Cursor,
212        position: &PositionState,
213    ) -> Self::State {
214        if self.has_element_representation {
215            if position.get() == Position::FirstChild {
216                cursor.child();
217            } else if position.get() == Position::NextChild {
218                cursor.sibling();
219            }
220            position.set(Position::FirstChild);
221        }
222
223        self.view.hydrate::<FROM_SERVER>(cursor, position)
224    }
225
226    fn into_owned(self) -> Self::Owned {
227        Island {
228            has_element_representation: self.has_element_representation,
229            component: self.component,
230            props_json: self.props_json,
231            view: self.view.into_owned(),
232        }
233    }
234}
235
236/// The children that will be projected into an [`Island`].
237pub struct IslandChildren<View> {
238    view: View,
239    on_hydrate: Option<Box<dyn Fn() + Send + Sync>>,
240}
241
242impl<View> IslandChildren<View> {
243    /// Creates a new representation of the children.
244    pub fn new(view: View) -> Self {
245        IslandChildren {
246            view,
247            on_hydrate: None,
248        }
249    }
250
251    /// Creates a new representation of the children, with a function to be called whenever
252    /// a child island hydrates.
253    pub fn new_with_on_hydrate(
254        view: View,
255        on_hydrate: impl Fn() + Send + Sync + 'static,
256    ) -> Self {
257        IslandChildren {
258            view,
259            on_hydrate: Some(Box::new(on_hydrate)),
260        }
261    }
262
263    fn open_tag(buf: &mut String) {
264        buf.push('<');
265        buf.push_str(ISLAND_CHILDREN_TAG);
266        buf.push('>');
267    }
268
269    fn close_tag(buf: &mut String) {
270        buf.push_str("</");
271        buf.push_str(ISLAND_CHILDREN_TAG);
272        buf.push('>');
273    }
274}
275
276impl<View> Render for IslandChildren<View>
277where
278    View: Render,
279{
280    type State = ();
281
282    fn build(self) -> Self::State {}
283
284    fn rebuild(self, _state: &mut Self::State) {}
285}
286
287impl<View> AddAnyAttr for IslandChildren<View>
288where
289    View: RenderHtml,
290{
291    type Output<SomeNewAttr: Attribute> =
292        IslandChildren<<View as AddAnyAttr>::Output<SomeNewAttr>>;
293
294    fn add_any_attr<NewAttr: Attribute>(
295        self,
296        attr: NewAttr,
297    ) -> Self::Output<NewAttr>
298    where
299        Self::Output<NewAttr>: RenderHtml,
300    {
301        let IslandChildren { view, on_hydrate } = self;
302        IslandChildren {
303            view: view.add_any_attr(attr),
304            on_hydrate,
305        }
306    }
307}
308
309impl<View> RenderHtml for IslandChildren<View>
310where
311    View: RenderHtml,
312{
313    type AsyncOutput = IslandChildren<View::AsyncOutput>;
314    type Owned = IslandChildren<View::Owned>;
315
316    const MIN_LENGTH: usize = ISLAND_CHILDREN_TAG.len() * 2
317        + "<>".len()
318        + "</>".len()
319        + View::MIN_LENGTH;
320
321    fn dry_resolve(&mut self) {
322        self.view.dry_resolve()
323    }
324
325    async fn resolve(self) -> Self::AsyncOutput {
326        let IslandChildren { view, on_hydrate } = self;
327        IslandChildren {
328            view: view.resolve().await,
329            on_hydrate,
330        }
331    }
332
333    fn to_html_with_buf(
334        self,
335        buf: &mut String,
336        position: &mut Position,
337        escape: bool,
338        mark_branches: bool,
339        extra_attrs: Vec<AnyAttribute>,
340    ) {
341        Self::open_tag(buf);
342        self.view.to_html_with_buf(
343            buf,
344            position,
345            escape,
346            mark_branches,
347            extra_attrs,
348        );
349        Self::close_tag(buf);
350    }
351
352    fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
353        self,
354        buf: &mut StreamBuilder,
355        position: &mut Position,
356        escape: bool,
357        mark_branches: bool,
358        extra_attrs: Vec<AnyAttribute>,
359    ) where
360        Self: Sized,
361    {
362        // insert the opening tag synchronously
363        let mut tag = String::new();
364        Self::open_tag(&mut tag);
365        buf.push_sync(&tag);
366
367        // streaming render for the view
368        self.view.to_html_async_with_buf::<OUT_OF_ORDER>(
369            buf,
370            position,
371            escape,
372            mark_branches,
373            extra_attrs,
374        );
375
376        // and insert the closing tag synchronously
377        tag.clear();
378        Self::close_tag(&mut tag);
379        buf.push_sync(&tag);
380    }
381
382    fn hydrate<const FROM_SERVER: bool>(
383        self,
384        cursor: &Cursor,
385        position: &PositionState,
386    ) -> Self::State {
387        // island children aren't hydrated
388        // we update the walk to pass over them
389        // but we don't hydrate their children
390        let curr_position = position.get();
391        if curr_position == Position::FirstChild {
392            cursor.child();
393        } else if curr_position != Position::Current {
394            cursor.sibling();
395        }
396        position.set(Position::NextChild);
397
398        if let Some(on_hydrate) = self.on_hydrate {
399            use crate::{
400                hydration::failed_to_cast_element, renderer::CastFrom,
401            };
402
403            let el =
404                crate::renderer::types::Element::cast_from(cursor.current())
405                    .unwrap_or_else(|| {
406                        failed_to_cast_element(
407                            "leptos-children",
408                            cursor.current(),
409                        )
410                    });
411            let cb = wasm_bindgen::closure::Closure::wrap(
412                on_hydrate as Box<dyn Fn()>,
413            );
414            _ = js_sys::Reflect::set(
415                &el,
416                &wasm_bindgen::JsValue::from_str("$$on_hydrate"),
417                &cb.into_js_value(),
418            );
419        }
420    }
421
422    fn into_owned(self) -> Self::Owned {
423        IslandChildren {
424            view: self.view.into_owned(),
425            on_hydrate: self.on_hydrate,
426        }
427    }
428}