1use std::any::Any;
4use std::rc::Rc;
5
6#[cfg(feature = "csr")]
7use web_sys::Element;
8
9use super::BaseComponent;
10use super::scope::{AnyScope, Scope};
11#[cfg(feature = "hydration")]
12use crate::dom_bundle::Fragment;
13#[cfg(feature = "csr")]
14use crate::dom_bundle::{BSubtree, Bundle, DomSlot, DynamicDomSlot};
15#[cfg(feature = "hydration")]
16use crate::html::RenderMode;
17use crate::html::{Html, RenderError};
18use crate::scheduler::{self, Runnable, Shared};
19use crate::suspense::{BaseSuspense, Suspension};
20use crate::{Callback, Context, HtmlResult};
21
22pub(crate) enum ComponentRenderState {
23 #[cfg(feature = "csr")]
24 Render {
25 bundle: Bundle,
26 root: BSubtree,
27 parent: Element,
28 sibling_slot: DynamicDomSlot,
32 own_slot: DynamicDomSlot,
36 },
37 #[cfg(feature = "hydration")]
38 Hydration {
39 fragment: Fragment,
40 root: BSubtree,
41 parent: Element,
42 sibling_slot: DynamicDomSlot,
43 own_slot: DynamicDomSlot,
44 },
45 #[cfg(feature = "ssr")]
46 Ssr {
47 sender: Option<crate::platform::pinned::oneshot::Sender<Html>>,
48 },
49}
50
51impl std::fmt::Debug for ComponentRenderState {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 #[cfg(feature = "csr")]
55 Self::Render {
56 bundle,
57 root,
58 parent,
59 sibling_slot,
60 own_slot,
61 } => f
62 .debug_struct("ComponentRenderState::Render")
63 .field("bundle", bundle)
64 .field("root", root)
65 .field("parent", parent)
66 .field("sibling_slot", sibling_slot)
67 .field("own_slot", own_slot)
68 .finish(),
69
70 #[cfg(feature = "hydration")]
71 Self::Hydration {
72 fragment,
73 parent,
74 sibling_slot,
75 own_slot,
76 root,
77 } => f
78 .debug_struct("ComponentRenderState::Hydration")
79 .field("fragment", fragment)
80 .field("root", root)
81 .field("parent", parent)
82 .field("sibling_slot", sibling_slot)
83 .field("own_slot", own_slot)
84 .finish(),
85
86 #[cfg(feature = "ssr")]
87 Self::Ssr { sender } => {
88 let sender_repr = match sender {
89 Some(_) => "Some(_)",
90 None => "None",
91 };
92
93 f.debug_struct("ComponentRenderState::Ssr")
94 .field("sender", &sender_repr)
95 .finish()
96 }
97 }
98 }
99}
100
101#[cfg(feature = "csr")]
102impl ComponentRenderState {
103 pub(crate) fn shift(&mut self, next_parent: Element, next_slot: DomSlot) {
104 match self {
105 #[cfg(feature = "csr")]
106 Self::Render {
107 bundle,
108 parent,
109 sibling_slot,
110 ..
111 } => {
112 *parent = next_parent;
113 sibling_slot.reassign(next_slot);
114 bundle.shift(parent, sibling_slot.to_position());
115 }
116 #[cfg(feature = "hydration")]
117 Self::Hydration {
118 fragment,
119 parent,
120 sibling_slot,
121 ..
122 } => {
123 *parent = next_parent;
124 sibling_slot.reassign(next_slot);
125 fragment.shift(parent, sibling_slot.to_position());
126 }
127
128 #[cfg(feature = "ssr")]
129 Self::Ssr { .. } => {
130 #[cfg(debug_assertions)]
131 panic!("shifting is not possible during SSR");
132 }
133 }
134 }
135}
136
137struct CompStateInner<COMP>
138where
139 COMP: BaseComponent,
140{
141 pub(crate) component: COMP,
142 pub(crate) context: Context<COMP>,
143}
144
145pub(crate) trait Stateful {
151 fn view(&self) -> HtmlResult;
152 #[cfg(feature = "csr")]
153 fn rendered(&mut self, first_render: bool);
154 fn destroy(&mut self);
155
156 fn any_scope(&self) -> AnyScope;
157
158 fn flush_messages(&mut self) -> bool;
159 #[cfg(feature = "csr")]
160 fn props_changed(&mut self, props: Rc<dyn Any>) -> bool;
161
162 fn as_any(&self) -> &dyn Any;
163
164 #[cfg(feature = "hydration")]
165 fn creation_mode(&self) -> RenderMode;
166}
167
168impl<COMP> Stateful for CompStateInner<COMP>
169where
170 COMP: BaseComponent,
171{
172 fn view(&self) -> HtmlResult {
173 self.component.view(&self.context)
174 }
175
176 #[cfg(feature = "csr")]
177 fn rendered(&mut self, first_render: bool) {
178 self.component.rendered(&self.context, first_render)
179 }
180
181 fn destroy(&mut self) {
182 self.component.destroy(&self.context);
183 }
184
185 fn any_scope(&self) -> AnyScope {
186 self.context.link().clone().into()
187 }
188
189 #[cfg(feature = "hydration")]
190 fn creation_mode(&self) -> RenderMode {
191 self.context.creation_mode()
192 }
193
194 fn flush_messages(&mut self) -> bool {
195 let mut changed = false;
196 for msg in self.context.link().pending_messages.drain() {
197 if self.component.update(&self.context, msg) {
198 changed = true;
199 }
200 }
201 changed
202 }
203
204 #[cfg(feature = "csr")]
205 fn props_changed(&mut self, props: Rc<dyn Any>) -> bool {
206 let props = match Rc::downcast::<COMP::Properties>(props) {
207 Ok(m) => m,
208 _ => return false,
209 };
210
211 if self.context.props != props {
212 let old_props = std::mem::replace(&mut self.context.props, props);
213 self.component.changed(&self.context, &old_props)
214 } else {
215 false
216 }
217 }
218
219 fn as_any(&self) -> &dyn Any {
220 self
221 }
222}
223
224pub(crate) struct ComponentState {
225 pub(super) inner: Box<dyn Stateful>,
226
227 pub(super) render_state: ComponentRenderState,
228
229 #[cfg(feature = "csr")]
230 has_rendered: bool,
231 #[cfg(feature = "hydration")]
235 pending_props: Option<Rc<dyn Any>>,
236
237 suspension: Option<Suspension>,
238
239 pub(crate) comp_id: usize,
240}
241
242impl ComponentState {
243 #[tracing::instrument(
244 level = tracing::Level::DEBUG,
245 name = "create",
246 skip_all,
247 fields(component.id = scope.id),
248 )]
249 fn new<COMP: BaseComponent>(
250 initial_render_state: ComponentRenderState,
251 scope: Scope<COMP>,
252 props: Rc<COMP::Properties>,
253 #[cfg(feature = "hydration")] prepared_state: Option<String>,
254 ) -> Self {
255 let comp_id = scope.id;
256 #[cfg(feature = "hydration")]
257 let creation_mode = {
258 match initial_render_state {
259 ComponentRenderState::Render { .. } => RenderMode::Render,
260 ComponentRenderState::Hydration { .. } => RenderMode::Hydration,
261 #[cfg(feature = "ssr")]
262 ComponentRenderState::Ssr { .. } => RenderMode::Ssr,
263 }
264 };
265
266 let context = Context {
267 scope,
268 props,
269 #[cfg(feature = "hydration")]
270 creation_mode,
271 #[cfg(feature = "hydration")]
272 prepared_state,
273 };
274
275 let inner = Box::new(CompStateInner {
276 component: COMP::create(&context),
277 context,
278 });
279
280 Self {
281 inner,
282 render_state: initial_render_state,
283 suspension: None,
284
285 #[cfg(feature = "csr")]
286 has_rendered: false,
287 #[cfg(feature = "hydration")]
288 pending_props: None,
289
290 comp_id,
291 }
292 }
293
294 pub(crate) fn downcast_comp_ref<COMP>(&self) -> Option<&COMP>
295 where
296 COMP: BaseComponent + 'static,
297 {
298 self.inner
299 .as_any()
300 .downcast_ref::<CompStateInner<COMP>>()
301 .map(|m| &m.component)
302 }
303
304 fn resume_existing_suspension(&mut self) {
305 if let Some(m) = self.suspension.take() {
306 let comp_scope = self.inner.any_scope();
307
308 let suspense_scope = comp_scope.find_parent_scope::<BaseSuspense>().unwrap();
309 BaseSuspense::resume(&suspense_scope, m);
310 }
311 }
312}
313
314pub(crate) struct CreateRunner<COMP: BaseComponent> {
315 pub initial_render_state: ComponentRenderState,
316 pub props: Rc<COMP::Properties>,
317 pub scope: Scope<COMP>,
318 #[cfg(feature = "hydration")]
319 pub prepared_state: Option<String>,
320}
321
322impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
323 fn run(self: Box<Self>) {
324 let mut current_state = self.scope.state.borrow_mut();
325 if current_state.is_none() {
326 *current_state = Some(ComponentState::new(
327 self.initial_render_state,
328 self.scope.clone(),
329 self.props,
330 #[cfg(feature = "hydration")]
331 self.prepared_state,
332 ));
333 }
334 }
335}
336
337pub(crate) struct UpdateRunner {
338 pub state: Shared<Option<ComponentState>>,
339}
340
341impl ComponentState {
342 #[tracing::instrument(
343 level = tracing::Level::DEBUG,
344 skip(self),
345 fields(component.id = self.comp_id)
346 )]
347 fn update(&mut self) -> bool {
348 let schedule_render = self.inner.flush_messages();
349 tracing::trace!(schedule_render);
350 schedule_render
351 }
352}
353
354impl Runnable for UpdateRunner {
355 fn run(self: Box<Self>) {
356 if let Some(state) = self.state.borrow_mut().as_mut() {
357 let schedule_render = state.update();
358
359 if schedule_render {
360 scheduler::push_component_render(
361 state.comp_id,
362 Box::new(RenderRunner {
363 state: self.state.clone(),
364 }),
365 );
366 }
368 }
369 }
370}
371
372pub(crate) struct DestroyRunner {
373 pub state: Shared<Option<ComponentState>>,
374 pub parent_to_detach: bool,
375}
376
377impl ComponentState {
378 #[tracing::instrument(
379 level = tracing::Level::DEBUG,
380 skip(self),
381 fields(component.id = self.comp_id)
382 )]
383 fn destroy(mut self, parent_to_detach: bool) {
384 self.inner.destroy();
385 self.resume_existing_suspension();
386
387 match self.render_state {
388 #[cfg(feature = "csr")]
389 ComponentRenderState::Render {
390 bundle,
391 ref parent,
392 ref root,
393 ..
394 } => {
395 bundle.detach(root, parent, parent_to_detach);
396 }
397 #[cfg(feature = "hydration")]
399 ComponentRenderState::Hydration {
400 ref root,
401 fragment,
402 ref parent,
403 ..
404 } => {
405 fragment.detach(root, parent, parent_to_detach);
406 }
407
408 #[cfg(feature = "ssr")]
409 ComponentRenderState::Ssr { .. } => {
410 let _ = parent_to_detach;
411 }
412 }
413 }
414}
415
416impl Runnable for DestroyRunner {
417 fn run(self: Box<Self>) {
418 if let Some(state) = self.state.borrow_mut().take() {
419 state.destroy(self.parent_to_detach);
420 }
421 }
422}
423
424pub(crate) struct RenderRunner {
425 pub state: Shared<Option<ComponentState>>,
426}
427
428impl ComponentState {
429 #[tracing::instrument(
430 level = tracing::Level::DEBUG,
431 skip_all,
432 fields(component.id = self.comp_id)
433 )]
434 fn render(&mut self, shared_state: &Shared<Option<ComponentState>>) {
435 let view = self.inner.view();
436 tracing::trace!(?view, "render result");
437 match view {
438 Ok(vnode) => self.commit_render(shared_state, vnode),
439 Err(RenderError::Suspended(susp)) => self.suspend(shared_state, susp),
440 };
441 }
442
443 fn suspend(&mut self, shared_state: &Shared<Option<ComponentState>>, suspension: Suspension) {
444 if suspension.resumed() {
448 scheduler::push_component_render(
450 self.comp_id,
451 Box::new(RenderRunner {
452 state: shared_state.clone(),
453 }),
454 );
455 } else {
456 let comp_scope = self.inner.any_scope();
458
459 let suspense_scope = comp_scope
460 .find_parent_scope::<BaseSuspense>()
461 .expect("To suspend rendering, a <Suspense /> component is required.");
462
463 let comp_id = self.comp_id;
464 let shared_state = shared_state.clone();
465 suspension.listen(Callback::from(move |_| {
466 scheduler::push_component_render(
467 comp_id,
468 Box::new(RenderRunner {
469 state: shared_state.clone(),
470 }),
471 );
472 scheduler::start();
473 }));
474
475 if let Some(ref last_suspension) = self.suspension {
476 if &suspension != last_suspension {
477 BaseSuspense::resume(&suspense_scope, last_suspension.clone());
479 }
480 }
481 self.suspension = Some(suspension.clone());
482
483 BaseSuspense::suspend(&suspense_scope, suspension);
484 }
485 }
486
487 fn commit_render(&mut self, shared_state: &Shared<Option<ComponentState>>, new_vdom: Html) {
488 #[cfg(feature = "csr")]
491 let resuming_from_suspension = self.suspension.is_some();
492 self.resume_existing_suspension();
493
494 match self.render_state {
495 #[cfg(feature = "csr")]
496 ComponentRenderState::Render {
497 ref mut bundle,
498 ref parent,
499 ref root,
500 ref sibling_slot,
501 ref mut own_slot,
502 ..
503 } => {
504 let scope = self.inner.any_scope();
505
506 let new_node_ref =
507 bundle.reconcile(root, &scope, parent, sibling_slot.to_position(), new_vdom);
508 own_slot.reassign(new_node_ref);
509
510 let first_render = !self.has_rendered;
511 self.has_rendered = true;
512
513 if resuming_from_suspension {
514 let pending = PendingRendered::new(shared_state.clone(), first_render);
521 let suspense_scope = scope
522 .find_parent_scope::<BaseSuspense>()
523 .expect("a resuming component must have a Suspense ancestor");
524 BaseSuspense::defer_rendered(&suspense_scope, self.comp_id, pending);
525 } else {
526 scheduler::push_component_rendered(
527 self.comp_id,
528 Box::new(RenderedRunner {
529 state: shared_state.clone(),
530 first_render,
531 }),
532 first_render,
533 );
534 }
535 }
536
537 #[cfg(feature = "hydration")]
538 ComponentRenderState::Hydration {
539 ref mut fragment,
540 ref parent,
541 ref mut own_slot,
542 ref mut sibling_slot,
543 ref root,
544 } => {
545 scheduler::push_component_priority_render(
549 self.comp_id,
550 Box::new(RenderRunner {
551 state: shared_state.clone(),
552 }),
553 );
554
555 let scope = self.inner.any_scope();
556 let bundle = Bundle::hydrate(
557 root,
558 &scope,
559 parent,
560 fragment,
561 new_vdom,
562 &mut Some(own_slot.clone()),
563 );
564
565 fragment.trim_start_text_nodes();
567 assert!(fragment.is_empty(), "expected end of component, found node");
568
569 self.render_state = ComponentRenderState::Render {
570 root: root.clone(),
571 bundle,
572 parent: parent.clone(),
573 own_slot: own_slot.take(),
574 sibling_slot: sibling_slot.take(),
575 };
576 }
577
578 #[cfg(feature = "ssr")]
579 ComponentRenderState::Ssr { ref mut sender } => {
580 let _ = shared_state;
581 if let Some(tx) = sender.take() {
582 tx.send(new_vdom).unwrap();
583 }
584 }
585 };
586 }
587}
588
589impl Runnable for RenderRunner {
590 fn run(self: Box<Self>) {
591 let mut state = self.state.borrow_mut();
592 let state = match state.as_mut() {
593 None => return, Some(state) => state,
595 };
596
597 state.render(&self.state);
598 }
599}
600
601#[cfg(feature = "csr")]
602mod feat_csr {
603 use super::*;
604
605 pub(crate) struct PropsUpdateRunner {
606 pub state: Shared<Option<ComponentState>>,
607 pub props: Option<Rc<dyn Any>>,
608 pub next_sibling_slot: Option<DomSlot>,
609 }
610
611 impl ComponentState {
612 #[tracing::instrument(
613 level = tracing::Level::DEBUG,
614 skip(self),
615 fields(component.id = self.comp_id)
616 )]
617 fn changed(
618 &mut self,
619 props: Option<Rc<dyn Any>>,
620 next_sibling_slot: Option<DomSlot>,
621 ) -> bool {
622 if let Some(next_sibling_slot) = next_sibling_slot {
623 match &mut self.render_state {
627 #[cfg(feature = "csr")]
628 ComponentRenderState::Render { sibling_slot, .. } => {
629 sibling_slot.reassign(next_sibling_slot);
630 }
631
632 #[cfg(feature = "hydration")]
633 ComponentRenderState::Hydration { sibling_slot, .. } => {
634 sibling_slot.reassign(next_sibling_slot);
635 }
636
637 #[cfg(feature = "ssr")]
638 ComponentRenderState::Ssr { .. } => {
639 #[cfg(debug_assertions)]
640 panic!("properties do not change during SSR");
641 }
642 }
643 }
644
645 let should_render = |props: Option<Rc<dyn Any>>, state: &mut ComponentState| -> bool {
646 props.map(|m| state.inner.props_changed(m)).unwrap_or(false)
647 };
648
649 #[cfg(feature = "hydration")]
650 let should_render_hydration =
651 |props: Option<Rc<dyn Any>>, state: &mut ComponentState| -> bool {
652 match props.or_else(|| state.pending_props.take()) {
653 Some(props) => match state.has_rendered {
654 true => {
655 state.pending_props = None;
656 state.inner.props_changed(props)
657 }
658 false => {
659 state.pending_props = Some(props);
660 false
661 }
662 },
663 _ => false,
664 }
665 };
666
667 let schedule_render = {
669 #[cfg(feature = "hydration")]
670 {
671 if self.inner.creation_mode() == RenderMode::Hydration {
672 should_render_hydration(props, self)
673 } else {
674 should_render(props, self)
675 }
676 }
677
678 #[cfg(not(feature = "hydration"))]
679 should_render(props, self)
680 };
681
682 tracing::trace!(
683 "props_update(has_rendered={} schedule_render={})",
684 self.has_rendered,
685 schedule_render
686 );
687 schedule_render
688 }
689 }
690
691 impl Runnable for PropsUpdateRunner {
692 fn run(self: Box<Self>) {
693 let Self {
694 next_sibling_slot,
695 props,
696 state: shared_state,
697 } = *self;
698
699 if let Some(state) = shared_state.borrow_mut().as_mut() {
700 let schedule_render = state.changed(props, next_sibling_slot);
701
702 if schedule_render {
703 scheduler::push_component_render(
704 state.comp_id,
705 Box::new(RenderRunner {
706 state: shared_state.clone(),
707 }),
708 );
709 }
711 };
712 }
713 }
714
715 pub(crate) struct RenderedRunner {
716 pub state: Shared<Option<ComponentState>>,
717 pub first_render: bool,
718 }
719
720 pub(crate) struct PendingRendered {
724 pub state: Shared<Option<ComponentState>>,
725 pub first_render: bool,
726 }
727
728 impl PendingRendered {
729 pub(crate) fn new(state: Shared<Option<ComponentState>>, first_render: bool) -> Self {
730 Self {
731 state,
732 first_render,
733 }
734 }
735
736 pub(crate) fn absorb(&mut self, later: Self) {
740 self.state = later.state;
741 self.first_render |= later.first_render;
742 }
743
744 pub(crate) fn schedule(self, comp_id: usize) {
746 let PendingRendered {
747 state,
748 first_render,
749 } = self;
750 scheduler::push_component_rendered(
751 comp_id,
752 Box::new(RenderedRunner {
753 state,
754 first_render,
755 }),
756 false,
757 );
758 }
759 }
760
761 impl ComponentState {
762 #[tracing::instrument(
763 level = tracing::Level::DEBUG,
764 skip(self),
765 fields(component.id = self.comp_id)
766 )]
767 fn rendered(&mut self, first_render: bool) -> bool {
768 if self.suspension.is_none() {
769 self.inner.rendered(first_render);
770 }
771
772 #[cfg(feature = "hydration")]
773 {
774 self.pending_props.is_some()
775 }
776 #[cfg(not(feature = "hydration"))]
777 {
778 false
779 }
780 }
781 }
782
783 impl Runnable for RenderedRunner {
784 fn run(self: Box<Self>) {
785 if let Some(state) = self.state.borrow_mut().as_mut() {
786 let has_pending_props = state.rendered(self.first_render);
787
788 if has_pending_props {
789 scheduler::push_component_props_update(Box::new(PropsUpdateRunner {
790 state: self.state.clone(),
791 props: None,
792 next_sibling_slot: None,
793 }));
794 }
795 }
796 }
797 }
798}
799
800#[cfg(feature = "csr")]
801pub(crate) use feat_csr::*;
802
803#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
804#[cfg(test)]
805mod tests {
806 extern crate self as yew;
807
808 use std::cell::RefCell;
809 use std::ops::Deref;
810 use std::rc::Rc;
811
812 use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
813
814 use super::*;
815 use crate::dom_bundle::BSubtree;
816 use crate::html::*;
817 use crate::{Properties, html};
818
819 wasm_bindgen_test_configure!(run_in_browser);
820
821 #[derive(Clone, Properties, Default, PartialEq)]
822 struct ChildProps {
823 lifecycle: Rc<RefCell<Vec<String>>>,
824 }
825
826 struct Child {}
827
828 impl Component for Child {
829 type Message = ();
830 type Properties = ChildProps;
831
832 fn create(_ctx: &Context<Self>) -> Self {
833 Child {}
834 }
835
836 fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
837 ctx.props()
838 .lifecycle
839 .borrow_mut()
840 .push("child rendered".into());
841 }
842
843 fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
844 false
845 }
846
847 fn changed(&mut self, _ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
848 false
849 }
850
851 fn view(&self, _ctx: &Context<Self>) -> Html {
852 html! {}
853 }
854 }
855
856 #[derive(Clone, Properties, Default, PartialEq)]
857 struct Props {
858 lifecycle: Rc<RefCell<Vec<String>>>,
859 #[allow(dead_code)]
860 #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
861 create_message: Option<bool>,
862 update_message: RefCell<Option<bool>>,
863 view_message: RefCell<Option<bool>>,
864 rendered_message: RefCell<Option<bool>>,
865 }
866
867 struct Comp {
868 lifecycle: Rc<RefCell<Vec<String>>>,
869 }
870
871 impl Component for Comp {
872 type Message = bool;
873 type Properties = Props;
874
875 fn create(ctx: &Context<Self>) -> Self {
876 ctx.props().lifecycle.borrow_mut().push("create".into());
877 #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
878 if let Some(msg) = ctx.props().create_message {
879 ctx.link().send_message(msg);
880 }
881 Comp {
882 lifecycle: Rc::clone(&ctx.props().lifecycle),
883 }
884 }
885
886 fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
887 if let Some(msg) = ctx.props().rendered_message.borrow_mut().take() {
888 ctx.link().send_message(msg);
889 }
890 ctx.props()
891 .lifecycle
892 .borrow_mut()
893 .push(format!("rendered({})", first_render));
894 }
895
896 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
897 if let Some(msg) = ctx.props().update_message.borrow_mut().take() {
898 ctx.link().send_message(msg);
899 }
900 ctx.props()
901 .lifecycle
902 .borrow_mut()
903 .push(format!("update({})", msg));
904 msg
905 }
906
907 fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
908 self.lifecycle = Rc::clone(&ctx.props().lifecycle);
909 self.lifecycle.borrow_mut().push("change".into());
910 false
911 }
912
913 fn view(&self, ctx: &Context<Self>) -> Html {
914 if let Some(msg) = ctx.props().view_message.borrow_mut().take() {
915 ctx.link().send_message(msg);
916 }
917 self.lifecycle.borrow_mut().push("view".into());
918 html! { <Child lifecycle={self.lifecycle.clone()} /> }
919 }
920 }
921
922 impl Drop for Comp {
923 fn drop(&mut self) {
924 self.lifecycle.borrow_mut().push("drop".into());
925 }
926 }
927
928 fn test_lifecycle(props: Props, expected: &[&str]) {
929 let document = gloo::utils::document();
930 let scope = Scope::<Comp>::new(None);
931 let parent = document.create_element("div").unwrap();
932 let root = BSubtree::create_root(&parent);
933
934 let lifecycle = props.lifecycle.clone();
935
936 lifecycle.borrow_mut().clear();
937 let _ = scope.mount_in_place(root, parent, DomSlot::at_end(), Rc::new(props));
938 crate::scheduler::start_now();
939
940 assert_eq!(&lifecycle.borrow_mut().deref()[..], expected);
941 }
942
943 #[test]
944 fn lifecycle_tests() {
945 let lifecycle: Rc<RefCell<Vec<String>>> = Rc::default();
946
947 test_lifecycle(
948 Props {
949 lifecycle: lifecycle.clone(),
950 ..Props::default()
951 },
952 &["create", "view", "child rendered", "rendered(true)"],
953 );
954
955 test_lifecycle(
956 Props {
957 lifecycle: lifecycle.clone(),
958 #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
959 create_message: Some(false),
960 ..Props::default()
961 },
962 &[
963 "create",
964 "view",
965 "child rendered",
966 "rendered(true)",
967 "update(false)",
968 ],
969 );
970
971 test_lifecycle(
972 Props {
973 lifecycle: lifecycle.clone(),
974 view_message: RefCell::new(Some(true)),
975 ..Props::default()
976 },
977 &[
978 "create",
979 "view",
980 "child rendered",
981 "rendered(true)",
982 "update(true)",
983 "view",
984 "rendered(false)",
985 ],
986 );
987
988 test_lifecycle(
989 Props {
990 lifecycle: lifecycle.clone(),
991 view_message: RefCell::new(Some(false)),
992 ..Props::default()
993 },
994 &[
995 "create",
996 "view",
997 "child rendered",
998 "rendered(true)",
999 "update(false)",
1000 ],
1001 );
1002
1003 test_lifecycle(
1004 Props {
1005 lifecycle: lifecycle.clone(),
1006 rendered_message: RefCell::new(Some(false)),
1007 ..Props::default()
1008 },
1009 &[
1010 "create",
1011 "view",
1012 "child rendered",
1013 "rendered(true)",
1014 "update(false)",
1015 ],
1016 );
1017
1018 test_lifecycle(
1019 Props {
1020 lifecycle: lifecycle.clone(),
1021 rendered_message: RefCell::new(Some(true)),
1022 ..Props::default()
1023 },
1024 &[
1025 "create",
1026 "view",
1027 "child rendered",
1028 "rendered(true)",
1029 "update(true)",
1030 "view",
1031 "rendered(false)",
1032 ],
1033 );
1034
1035 test_lifecycle(
1037 Props {
1038 lifecycle,
1039 #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
1040 create_message: Some(true),
1041 update_message: RefCell::new(Some(true)),
1042 ..Props::default()
1043 },
1044 &[
1045 "create",
1046 "view",
1047 "child rendered",
1048 "rendered(true)",
1049 "update(true)",
1050 "update(true)",
1051 "view",
1052 "rendered(false)",
1053 ],
1054 );
1055 }
1056}