Skip to content

Commit 9e2ffaf

Browse files
Widen value types for Carousel, Stepper, Tabs, and TabPanels
Accept element objects (CarouselSlide, Step, Tab, TabPanel) in the ValueElement type parameter so .value typing is honest. This removes the need for set_value overrides and __init__ pre-conversions since _value_to_model_value handles all conversion to strings for the frontend. Split CarouselSlide, Step, StepperNavigation, Tab, TabPanel, and TabPanels into separate files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f54647f commit 9e2ffaf

11 files changed

Lines changed: 181 additions & 199 deletions

File tree

nicegui/elements/carousel.py

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
1-
from __future__ import annotations
1+
from typing import Any
22

3-
from typing import Any, cast
4-
5-
from ..context import context
63
from ..defaults import DEFAULT_PROP, DEFAULT_PROPS, resolve_defaults
74
from ..events import Handler, ValueChangeEventArguments
8-
from .mixins.disableable_element import DisableableElement
5+
from .carousel_slide import CarouselSlide
96
from .mixins.value_element import ValueElement
107

118

12-
class Carousel(ValueElement[str | None]):
9+
class Carousel(ValueElement[str | CarouselSlide | None]):
1310

1411
@resolve_defaults
1512
def __init__(self, *,
1613
value: str | CarouselSlide | None = DEFAULT_PROPS['model-value'] | None,
17-
on_value_change: Handler[ValueChangeEventArguments[str | None]] | None = None,
14+
on_value_change: Handler[ValueChangeEventArguments[str | CarouselSlide | None]] | None = None,
1815
animated: bool = DEFAULT_PROP | False,
1916
arrows: bool = DEFAULT_PROP | False,
2017
navigation: bool = DEFAULT_PROP | False,
@@ -30,18 +27,11 @@ def __init__(self, *,
3027
:param arrows: whether to show arrows for manual slide navigation (default: `False`)
3128
:param navigation: whether to show navigation dots for manual slide navigation (default: `False`)
3229
"""
33-
super().__init__(tag='q-carousel', value=self._value_to_model_value(value), on_value_change=on_value_change)
30+
super().__init__(tag='q-carousel', value=value, on_value_change=on_value_change)
3431
self._props['animated'] = animated
3532
self._props['arrows'] = arrows
3633
self._props['navigation'] = navigation
3734

38-
def set_value(self, value: str | CarouselSlide | None) -> None:
39-
"""Set the value of this element.
40-
41-
:param value: slide name or `CarouselSlide` element
42-
"""
43-
super().set_value(self._value_to_model_value(value))
44-
4535
def _value_to_model_value(self, value: Any) -> Any:
4636
return value.props['name'] if isinstance(value, CarouselSlide) else value
4737

@@ -59,21 +49,3 @@ def next(self) -> None:
5949
def previous(self) -> None:
6050
"""Show the previous slide."""
6151
self.run_method('previous')
62-
63-
64-
class CarouselSlide(DisableableElement, default_classes='nicegui-carousel-slide'):
65-
66-
def __init__(self, name: str | None = None) -> None:
67-
"""Carousel Slide
68-
69-
This element represents `Quasar's QCarouselSlide <https://quasar.dev/vue-components/carousel#qcarouselslide-api>`_ component.
70-
It is a child of a `ui.carousel` element.
71-
72-
:param name: name of the slide (will be the value of the `ui.carousel` element, auto-generated if `None`)
73-
"""
74-
super().__init__(tag='q-carousel-slide')
75-
self.carousel = cast(ValueElement, context.slot.parent)
76-
name = name or f'slide_{len(self.carousel.default_slot.children)}'
77-
self._props['name'] = name
78-
if self.carousel.value is None:
79-
self.carousel.value = name

nicegui/elements/carousel_slide.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import cast
2+
3+
from ..context import context
4+
from .mixins.disableable_element import DisableableElement
5+
from .mixins.value_element import ValueElement
6+
7+
8+
class CarouselSlide(DisableableElement, default_classes='nicegui-carousel-slide'):
9+
10+
def __init__(self, name: str | None = None) -> None:
11+
"""Carousel Slide
12+
13+
This element represents `Quasar's QCarouselSlide <https://quasar.dev/vue-components/carousel#qcarouselslide-api>`_ component.
14+
It is a child of a `ui.carousel` element.
15+
16+
:param name: name of the slide (will be the value of the `ui.carousel` element, auto-generated if `None`)
17+
"""
18+
super().__init__(tag='q-carousel-slide')
19+
self.carousel = cast(ValueElement, context.slot.parent)
20+
name = name or f'slide_{len(self.carousel.default_slot.children)}'
21+
self._props['name'] = name
22+
if self.carousel.value is None:
23+
self.carousel.value = name

nicegui/elements/date.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
from typing import Any
2-
31
from ..defaults import DEFAULT_PROP, DEFAULT_PROPS, resolve_defaults
42
from ..events import Handler, ValueChangeEventArguments
53
from .mixins.disableable_element import DisableableElement
64
from .mixins.value_element import ValueElement
75

86

9-
class Date(ValueElement[Any], DisableableElement):
7+
class Date(ValueElement[str | dict[str, str] | list[str] | list[str | dict[str, str]] | None], DisableableElement):
108

119
@resolve_defaults
1210
def __init__(self,

nicegui/elements/step.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from typing import cast
2+
3+
from ..context import context
4+
from ..defaults import DEFAULT_PROP, resolve_defaults
5+
from .mixins.disableable_element import DisableableElement
6+
from .mixins.icon_element import IconElement
7+
from .mixins.value_element import ValueElement
8+
9+
10+
class Step(IconElement, DisableableElement, default_classes='nicegui-step'):
11+
12+
@resolve_defaults
13+
def __init__(self, name: str, title: str | None = None, icon: str | None = DEFAULT_PROP | None) -> None:
14+
"""Step
15+
16+
This element represents `Quasar's QStep <https://quasar.dev/vue-components/stepper#qstep-api>`_ component.
17+
It is a child of a `ui.stepper` element.
18+
19+
:param name: name of the step (will be the value of the `ui.stepper` element)
20+
:param title: title of the step (default: `None`, meaning the same as `name`)
21+
:param icon: icon of the step (default: `None`)
22+
"""
23+
super().__init__(tag='q-step', icon=icon)
24+
self._props['name'] = name
25+
self._props['title'] = title if title is not None else name
26+
self.stepper = cast(ValueElement, context.slot.parent)
27+
if self.stepper.value is None:
28+
self.stepper.value = name

nicegui/elements/stepper.py

Lines changed: 5 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
1-
from __future__ import annotations
1+
from typing import Any
22

3-
from typing import Any, cast
4-
5-
from ..context import context
63
from ..defaults import DEFAULT_PROP, DEFAULT_PROPS, resolve_defaults
7-
from ..element import Element
84
from ..events import Handler, ValueChangeEventArguments
9-
from .mixins.disableable_element import DisableableElement
10-
from .mixins.icon_element import IconElement
115
from .mixins.value_element import ValueElement
6+
from .step import Step
127

138

14-
class Stepper(ValueElement[str | None], default_classes='nicegui-stepper'):
9+
class Stepper(ValueElement[str | Step | None], default_classes='nicegui-stepper'):
1510

1611
@resolve_defaults
1712
def __init__(self, *,
1813
value: str | Step | None = DEFAULT_PROPS['model-value'] | None,
19-
on_value_change: Handler[ValueChangeEventArguments[str | None]] | None = None,
14+
on_value_change: Handler[ValueChangeEventArguments[str | Step | None]] | None = None,
2015
keep_alive: bool = DEFAULT_PROP | True,
2116
) -> None:
2217
"""Stepper
@@ -32,17 +27,9 @@ def __init__(self, *,
3227
:param on_value_change: callback to be executed when the selected step changes
3328
:param keep_alive: whether to use Vue's keep-alive component on the content (default: `True`)
3429
"""
35-
_value = cast('str | None', value.props['name'] if isinstance(value, Step) else value)
36-
super().__init__(tag='q-stepper', value=_value, on_value_change=on_value_change)
30+
super().__init__(tag='q-stepper', value=value, on_value_change=on_value_change)
3731
self._props.set_bool('keep-alive', keep_alive)
3832

39-
def set_value(self, value: str | Step | None) -> None:
40-
"""Set the value of this element.
41-
42-
:param value: step name or `Step` element
43-
"""
44-
super().set_value(cast('str | None', value.props['name'] if isinstance(value, Step) else value))
45-
4633
def _value_to_model_value(self, value: Any) -> Any:
4734
return value.props['name'] if isinstance(value, Step) else value
4835

@@ -60,39 +47,3 @@ def next(self) -> None:
6047
def previous(self) -> None:
6148
"""Show the previous step."""
6249
self.run_method('previous')
63-
64-
65-
class Step(IconElement, DisableableElement, default_classes='nicegui-step'):
66-
67-
@resolve_defaults
68-
def __init__(self, name: str, title: str | None = None, icon: str | None = DEFAULT_PROP | None) -> None:
69-
"""Step
70-
71-
This element represents `Quasar's QStep <https://quasar.dev/vue-components/stepper#qstep-api>`_ component.
72-
It is a child of a `ui.stepper` element.
73-
74-
:param name: name of the step (will be the value of the `ui.stepper` element)
75-
:param title: title of the step (default: `None`, meaning the same as `name`)
76-
:param icon: icon of the step (default: `None`)
77-
"""
78-
super().__init__(tag='q-step', icon=icon)
79-
self._props['name'] = name
80-
self._props['title'] = title if title is not None else name
81-
self.stepper = cast(ValueElement, context.slot.parent)
82-
if self.stepper.value is None:
83-
self.stepper.value = name
84-
85-
86-
class StepperNavigation(Element):
87-
88-
def __init__(self, *, wrap: bool = True) -> None:
89-
"""Stepper Navigation
90-
91-
This element represents `Quasar's QStepperNavigation https://quasar.dev/vue-components/stepper#qsteppernavigation-api>`_ component.
92-
93-
:param wrap: whether to wrap the content (default: `True`)
94-
"""
95-
super().__init__('q-stepper-navigation')
96-
97-
if wrap:
98-
self._classes.append('wrap')
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from ..element import Element
2+
3+
4+
class StepperNavigation(Element):
5+
6+
def __init__(self, *, wrap: bool = True) -> None:
7+
"""Stepper Navigation
8+
9+
This element represents `Quasar's QStepperNavigation https://quasar.dev/vue-components/stepper#qsteppernavigation-api>`_ component.
10+
11+
:param wrap: whether to wrap the content (default: `True`)
12+
"""
13+
super().__init__('q-stepper-navigation')
14+
15+
if wrap:
16+
self._classes.append('wrap')

nicegui/elements/tab.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from .. import helpers
2+
from .mixins.disableable_element import DisableableElement
3+
from .mixins.icon_element import IconElement
4+
from .mixins.label_element import LabelElement
5+
from .tabs import Tabs
6+
7+
8+
class Tab(LabelElement, IconElement, DisableableElement):
9+
10+
def __init__(self, name: str, label: str | None = None, icon: str | None = None) -> None:
11+
"""Tab
12+
13+
This element represents `Quasar's QTab <https://quasar.dev/vue-components/tabs#qtab-api>`_ component.
14+
It is a direct or indirect child of a `ui.tabs` element.
15+
16+
:param name: name of the tab (will be the value of the `ui.tabs` element)
17+
:param label: label of the tab (default: `None`, meaning the same as `name`)
18+
:param icon: icon of the tab (default: `None`)
19+
"""
20+
if label is None:
21+
label = name
22+
super().__init__(tag='q-tab', label=label, icon=icon)
23+
self._props['name'] = name
24+
self.tabs = next(
25+
(e for e in self.ancestors() if isinstance(e, Tabs)),
26+
None, # DEPRECATED: raise an error in NiceGUI 4.0 if no ui.tabs ancestor is found
27+
)
28+
if self.tabs is None:
29+
helpers.warn_once('A ui.tab should be a child of a ui.tabs element. '
30+
'This will raise an error in NiceGUI 4.0.')

nicegui/elements/tab_panel.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from .mixins.disableable_element import DisableableElement
2+
from .tab import Tab
3+
4+
5+
class TabPanel(DisableableElement, default_classes='nicegui-tab-panel'):
6+
7+
def __init__(self, name: Tab | str) -> None:
8+
"""Tab Panel
9+
10+
This element represents `Quasar's QTabPanel <https://quasar.dev/vue-components/tab-panels#qtabpanel-api>`_ component.
11+
It is a child of a `TabPanels` element.
12+
13+
:param name: `ui.tab` or the name of a tab element
14+
"""
15+
super().__init__(tag='q-tab-panel')
16+
self._props['name'] = name.props['name'] if isinstance(name, Tab) else name

nicegui/elements/tab_panels.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from typing import Any
2+
3+
from ..defaults import DEFAULT_PROP, resolve_defaults
4+
from ..events import Handler, ValueChangeEventArguments
5+
from .mixins.value_element import ValueElement
6+
from .tab import Tab
7+
from .tab_panel import TabPanel
8+
from .tabs import Tabs
9+
10+
11+
class TabPanels(ValueElement[str | Tab | TabPanel | None]):
12+
13+
@resolve_defaults
14+
def __init__(self,
15+
tabs: Tabs | None = None, *,
16+
value: Tab | TabPanel | str | None = None,
17+
on_change: Handler[ValueChangeEventArguments[str | Tab | TabPanel | None]] | None = None,
18+
animated: bool = DEFAULT_PROP | True,
19+
keep_alive: bool = DEFAULT_PROP | True,
20+
) -> None:
21+
"""Tab Panels
22+
23+
This element represents `Quasar's QTabPanels <https://quasar.dev/vue-components/tab-panels#qtabpanels-api>`_ component.
24+
It contains individual tab panels.
25+
26+
To avoid issues with dynamic elements when switching tabs,
27+
this element uses Vue's `keep-alive <https://vuejs.org/guide/built-ins/keep-alive.html>`_ component.
28+
If client-side performance is an issue, you can disable this feature.
29+
30+
:param tabs: an optional `ui.tabs` element that controls this element
31+
:param value: `ui.tab`, `ui.tab_panel`, or name of the tab panel to be initially visible
32+
:param on_change: callback to be executed when the visible tab panel changes (*since version 3.6.0*: event ``value`` is the tab name)
33+
:param animated: whether the tab panels should be animated (default: `True`)
34+
:param keep_alive: whether to use Vue's keep-alive component on the content (default: `True`)
35+
"""
36+
super().__init__(tag='q-tab-panels', value=value, on_value_change=on_change)
37+
if tabs is not None:
38+
tabs.bind_value(self, 'value')
39+
self._props.set_bool('animated', animated)
40+
self._props.set_bool('keep-alive', keep_alive)
41+
42+
def _value_to_model_value(self, value: Any) -> str | None:
43+
return value.props['name'] if isinstance(value, (Tab, TabPanel)) else value
44+
45+
def _value_to_event_value(self, value: Any) -> str | None:
46+
return self._value_to_model_value(value)

0 commit comments

Comments
 (0)