Skip to content

Commit fafdb75

Browse files
Add support for component classes in dynamic tags (#130)
1 parent 7ad3571 commit fafdb75

3 files changed

Lines changed: 1314 additions & 56 deletions

File tree

collagraph/fragment.py

Lines changed: 193 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -160,28 +160,6 @@ def set_bind_dict(self, name: str, expression: Callable[[], dict[str, Any]]):
160160
"""
161161
self._binds.append((name, expression, False))
162162

163-
def set_type(self, expression: Callable[[], str | Callable]):
164-
"""
165-
Set a dynamic type/tag based on the expression.
166-
"""
167-
168-
@weak(self)
169-
def update_type(self, tag):
170-
anchor = self.anchor()
171-
self.unmount(destroy=False)
172-
self.tag = tag
173-
self.mount(self.target, anchor)
174-
175-
# Set the tag immediately
176-
# TODO: In case of a component tag, do we maybe want to wait???
177-
# So that we can build up a reactive props object or something?
178-
self.tag = expression()
179-
self._watchers["type"] = watch(
180-
expression,
181-
update_type,
182-
immediate=False,
183-
)
184-
185163
def set_condition(self, expression: Callable[[], bool]):
186164
"""
187165
Set a expression that determines whether this fragment
@@ -301,9 +279,6 @@ def _remove(self):
301279
self.renderer.remove(self.element, self.target)
302280
self.element = None
303281

304-
def _has_content(self):
305-
return bool(self.element)
306-
307282
def unmount(self, destroy=True):
308283
self._mounted = False
309284

@@ -605,6 +580,12 @@ def register_child(self, child: Fragment) -> None:
605580
else:
606581
self.children.append(child)
607582

583+
def first(self) -> Any | None:
584+
"""Return the first element from the rendered component fragment"""
585+
if self.fragment:
586+
return self.fragment.first()
587+
return super().first()
588+
608589
def create(self):
609590
if self.tag is None:
610591
return
@@ -683,12 +664,14 @@ def _rem_attr(self, attr):
683664
def _remove(self):
684665
self.props = None
685666

686-
def _has_content(self):
687-
return bool(self.props)
688-
689667
def unmount(self, destroy=True):
690668
if self.component:
691669
self.component.before_unmount()
670+
671+
# Unmount slot contents before calling super
672+
for slot_content in self.slot_contents:
673+
slot_content.unmount(destroy=destroy)
674+
692675
super().unmount(destroy=destroy)
693676

694677

@@ -736,3 +719,185 @@ def mount(self, target: Any, anchor: Any | None = None):
736719
item.mount(target, anchor)
737720
else:
738721
super().mount(target, anchor)
722+
723+
724+
class DynamicFragment(Fragment):
725+
"""
726+
Fragment for dynamic component tags: <component :is="expression" />
727+
728+
Handles switching between different tag types (components or elements)
729+
based on a reactive expression.
730+
"""
731+
732+
def __init__(
733+
self, renderer: Renderer, expression: Callable, parent: Fragment | None = None
734+
):
735+
# Don't pass tag to parent - it will be dynamic
736+
super().__init__(renderer, tag=None, parent=parent)
737+
738+
# Store the expression that determines the tag
739+
self._expression = expression
740+
741+
# Current active fragment (ComponentFragment or regular Fragment)
742+
self._active_fragment: Fragment | None = None
743+
744+
# Watcher for the expression
745+
self._type_watcher: Watcher | None = None
746+
747+
def create(self):
748+
"""Create the initial fragment based on expression value"""
749+
# Evaluate expression to get initial tag
750+
tag = self._expression()
751+
752+
# Create fragment for this tag
753+
self._create_fragment_for_tag(tag)
754+
755+
# Set up watcher for tag changes
756+
@weak(self)
757+
def update_type(self, new_tag):
758+
# Calculate anchor before unmounting
759+
anchor = self.anchor()
760+
761+
# Unmount current fragment
762+
if self._active_fragment:
763+
# Note: _active_fragment is no longer in self.children
764+
# (it's removed in _create_fragment_for_tag)
765+
self._active_fragment.unmount(destroy=False)
766+
767+
# Create new fragment
768+
self._create_fragment_for_tag(new_tag)
769+
770+
# Mount it
771+
if self._active_fragment:
772+
self._active_fragment.mount(self.target, anchor)
773+
774+
self._type_watcher = watch(
775+
self._expression,
776+
update_type,
777+
immediate=False,
778+
)
779+
780+
def _create_fragment_for_tag(self, tag):
781+
"""Create appropriate fragment for the given tag"""
782+
# Save existing children before creating active fragment
783+
# If we had a ComponentFragment, children are in its slot_contents
784+
if (
785+
self._active_fragment
786+
and isinstance(self._active_fragment, ComponentFragment)
787+
and self._active_fragment.tag is not None
788+
):
789+
# Get children from previous ComponentFragment's slot_contents
790+
existing_children = self._active_fragment.slot_contents.copy()
791+
else:
792+
# Get children from DynamicFragment's children
793+
existing_children = self.children.copy()
794+
795+
if callable(tag):
796+
# Component class - create ComponentFragment
797+
# Don't pass parent, so that the register_child method is skipped
798+
# so that the _active_fragment won't be part of the children
799+
# which are whatever is specified in the template
800+
self._active_fragment = ComponentFragment(
801+
self.renderer,
802+
tag=tag,
803+
props=reactive({}),
804+
)
805+
# Manually set the parent
806+
self._active_fragment._parent = ref(self)
807+
808+
# Transfer existing children as slot content
809+
# ComponentFragment.register_child() adds them to slot_contents
810+
# when tag is set
811+
for child in existing_children:
812+
child._parent = ref(self._active_fragment)
813+
# Set slot_name to "default" if not already set (e.g., by v-slot:name)
814+
if not hasattr(child, "slot_name") or child.slot_name is None:
815+
child.slot_name = "default"
816+
self._active_fragment.register_child(child)
817+
else:
818+
# String tag - create regular Fragment
819+
# Also don't pass parent here
820+
self._active_fragment = Fragment(
821+
self.renderer,
822+
tag=tag,
823+
)
824+
# Manually set the parent
825+
self._active_fragment._parent = ref(self)
826+
827+
# Transfer existing children to the active fragment
828+
for child in existing_children:
829+
child._parent = ref(self._active_fragment)
830+
self._active_fragment.children.append(child)
831+
832+
# Transfer attributes, bindings, events from self to active fragment
833+
self._active_fragment._attributes.update(self._attributes)
834+
self._active_fragment._binds.extend(self._binds)
835+
self._active_fragment._events.update(self._events)
836+
837+
# Create the fragment
838+
self._active_fragment.create()
839+
840+
def mount(self, target: Any, anchor: Any | None = None):
841+
if self._mounted:
842+
return
843+
844+
self.target = target
845+
self.create()
846+
847+
# Mount the active fragment
848+
if self._active_fragment:
849+
self._active_fragment.mount(target, anchor)
850+
851+
self._mounted = True
852+
853+
def unmount(self, destroy=True):
854+
self._mounted = False
855+
856+
# Unmount active fragment
857+
if self._active_fragment:
858+
self._active_fragment.unmount(destroy=destroy)
859+
if destroy:
860+
self._active_fragment = None
861+
862+
# Standard cleanup
863+
for child in self.children:
864+
child.unmount(destroy=destroy)
865+
866+
self._remove()
867+
868+
if destroy:
869+
self.element = None
870+
self.target = None
871+
self._attributes = {}
872+
self._events = {}
873+
if self._type_watcher:
874+
self._type_watcher.fn = lambda: ()
875+
self._type_watcher.callback = None
876+
self._type_watcher = None
877+
self._condition = None
878+
self._expression = None
879+
else:
880+
self.element = None
881+
# Keep _type_watcher alive for dynamic tag switching
882+
883+
def first(self) -> Any | None:
884+
if self._active_fragment:
885+
return self._active_fragment.first()
886+
return None
887+
888+
def _set_attr(self, attr, value):
889+
# Update our attributes (for transfer to next fragment)
890+
self._attributes[attr] = value
891+
# Apply to active fragment
892+
if self._active_fragment:
893+
self._active_fragment._set_attr(attr, value)
894+
895+
def _rem_attr(self, attr):
896+
if attr in self._attributes:
897+
del self._attributes[attr]
898+
if self._active_fragment:
899+
self._active_fragment._rem_attr(attr)
900+
901+
def _remove(self):
902+
if self._active_fragment:
903+
self._active_fragment._remove()

0 commit comments

Comments
 (0)