Skip to content

Commit 04554f0

Browse files
committed
Tabs control finished
1 parent f000b2b commit 04554f0

File tree

7 files changed

+301
-75
lines changed

7 files changed

+301
-75
lines changed

client/lib/controls/tabs.dart

Lines changed: 94 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import 'package:flet_view/controls/error.dart';
21
import 'package:flet_view/utils/icons.dart';
32
import 'package:flutter/material.dart';
43
import 'package:flutter_redux/flutter_redux.dart';
54

5+
import '../actions.dart';
66
import '../models/app_state.dart';
77
import '../models/control.dart';
8-
import '../models/control_view_model.dart';
8+
import '../models/controls_view_model.dart';
9+
import '../protocol/update_control_props_payload.dart';
910
import '../web_socket_client.dart';
1011
import 'create_control.dart';
1112

@@ -32,12 +33,19 @@ class _TabsControlState extends State<TabsControl>
3233
List<String> _tabsIndex = [];
3334
String? _value;
3435
TabController? _tabController;
36+
dynamic _dispatch;
3537

3638
@override
3739
void initState() {
3840
super.initState();
39-
_tabsIndex = widget.children.map((c) => c.attrString("key", "")!).toList();
40-
_tabController = TabController(length: _tabsIndex.length, vsync: this);
41+
_tabsIndex = widget.children
42+
.map((c) => c.attrString("key") ?? c.attrString("text", "")!)
43+
.toList();
44+
_tabController = TabController(
45+
length: _tabsIndex.length,
46+
animationDuration: Duration(
47+
milliseconds: widget.control.attrInt("animationDuration", 50)!),
48+
vsync: this);
4149
_tabController!.addListener(_tabChanged);
4250
}
4351

@@ -48,6 +56,12 @@ class _TabsControlState extends State<TabsControl>
4856
var value = _tabsIndex[_tabController!.index];
4957
if (_value != value) {
5058
debugPrint("Selected tab: $value");
59+
List<Map<String, String>> props = [
60+
{"i": widget.control.id, "value": value}
61+
];
62+
_dispatch(
63+
UpdateControlPropsAction(UpdateControlPropsPayload(props: props)));
64+
ws.updateControlProps(props: props);
5165
ws.pageEventFromWeb(
5266
eventTarget: widget.control.id,
5367
eventName: "change",
@@ -60,13 +74,18 @@ class _TabsControlState extends State<TabsControl>
6074
Widget build(BuildContext context) {
6175
debugPrint("TabsControl build: ${widget.control.id}");
6276

63-
var tabsIndex =
64-
widget.children.map((c) => c.attrString("key", "")!).toList();
77+
var tabsIndex = widget.children
78+
.map((c) => c.attrString("key") ?? c.attrString("text", "")!)
79+
.toList();
6580
if (tabsIndex.length != _tabsIndex.length ||
6681
!tabsIndex.every((item) => _tabsIndex.contains(item))) {
6782
_tabsIndex =
6883
widget.children.map((c) => c.attrString("key", "")!).toList();
69-
_tabController = TabController(length: _tabsIndex.length, vsync: this);
84+
_tabController = TabController(
85+
length: _tabsIndex.length,
86+
animationDuration: Duration(
87+
milliseconds: widget.control.attrInt("animationDuration", 50)!),
88+
vsync: this);
7089
_tabController!.addListener(_tabChanged);
7190
}
7291

@@ -82,72 +101,75 @@ class _TabsControlState extends State<TabsControl>
82101
}
83102
}
84103

85-
var tabs = Column(
86-
crossAxisAlignment: CrossAxisAlignment.start,
87-
children: [
88-
TabBar(
89-
controller: _tabController,
90-
isScrollable: true,
91-
indicatorColor: Theme.of(context).colorScheme.primary,
92-
labelColor: Theme.of(context).colorScheme.primary,
93-
unselectedLabelColor: Theme.of(context).colorScheme.onSurface,
94-
tabs: widget.children
95-
.map((c) => StoreConnector<AppState, ControlViewModel>(
96-
distinct: true,
97-
converter: (store) {
98-
return ControlViewModel.fromStore(store, c.id);
99-
},
100-
builder: (context, tabView) {
101-
var text = tabView.control.attrString("text");
102-
var icon = getMaterialIcon(
103-
tabView.control.attrString("icon", "")!);
104-
var tabContentCtrls = tabView.children
105-
.where((c) => c.name == "tab_content");
106-
107-
Widget tabChild;
108-
List<Widget> widgets = [];
109-
if (tabContentCtrls.isNotEmpty) {
110-
tabChild = createControl(
111-
widget.control, tabContentCtrls.first.id, disabled);
112-
} else {
113-
if (icon != null) {
114-
widgets.add(Icon(icon));
115-
if (text != null) {
116-
widgets.add(const SizedBox(width: 8));
117-
}
118-
}
119-
if (text != null) {
120-
widgets.add(Text(text));
104+
var tabs = StoreConnector<AppState, ControlsViewModel>(
105+
distinct: true,
106+
converter: (store) => ControlsViewModel.fromStore(
107+
store, widget.children.map((c) => c.id)),
108+
builder: (content, viewModel) {
109+
_dispatch = viewModel.dispatch;
110+
111+
// check if all tabs have no content
112+
bool emptyTabs = !viewModel.controlViews
113+
.any((t) => t.children.any((c) => c.name == "content"));
114+
115+
var tabBar = TabBar(
116+
controller: _tabController,
117+
isScrollable: true,
118+
indicatorColor: Theme.of(context).colorScheme.primary,
119+
labelColor: Theme.of(context).colorScheme.primary,
120+
unselectedLabelColor: Theme.of(context).colorScheme.onSurface,
121+
tabs: viewModel.controlViews.map((tabView) {
122+
var text = tabView.control.attrString("text");
123+
var icon =
124+
getMaterialIcon(tabView.control.attrString("icon", "")!);
125+
var tabContentCtrls =
126+
tabView.children.where((c) => c.name == "tab_content");
127+
128+
Widget tabChild;
129+
List<Widget> widgets = [];
130+
if (tabContentCtrls.isNotEmpty) {
131+
tabChild = createControl(
132+
widget.control, tabContentCtrls.first.id, disabled);
133+
} else {
134+
if (icon != null) {
135+
widgets.add(Icon(icon));
136+
if (text != null) {
137+
widgets.add(const SizedBox(width: 8));
138+
}
139+
}
140+
if (text != null) {
141+
widgets.add(Text(text));
142+
}
143+
tabChild = Row(
144+
children: widgets,
145+
mainAxisAlignment: MainAxisAlignment.center);
146+
}
147+
return Tab(child: tabChild);
148+
}).toList());
149+
150+
if (emptyTabs) {
151+
return tabBar;
152+
}
153+
154+
return Column(
155+
crossAxisAlignment: CrossAxisAlignment.start,
156+
children: [
157+
tabBar,
158+
Expanded(
159+
child: TabBarView(
160+
controller: _tabController,
161+
children: viewModel.controlViews.map((tabView) {
162+
var contentCtrls =
163+
tabView.children.where((c) => c.name == "content");
164+
if (contentCtrls.isEmpty) {
165+
return const SizedBox.shrink();
121166
}
122-
tabChild = Row(
123-
children: widgets,
124-
mainAxisAlignment: MainAxisAlignment.center);
125-
}
126-
return Tab(child: tabChild);
127-
}))
128-
.toList()),
129-
Expanded(
130-
child: TabBarView(
131-
controller: _tabController,
132-
children: widget.children
133-
.map((c) => StoreConnector<AppState, ControlViewModel>(
134-
distinct: true,
135-
converter: (store) {
136-
return ControlViewModel.fromStore(store, c.id);
137-
},
138-
builder: (context, tabView) {
139-
var contentCtrls = tabView.children
140-
.where((c) => c.name == "content");
141-
if (contentCtrls.isEmpty) {
142-
return const ErrorControl(
143-
"Tab should have a content.");
144-
}
145-
return createControl(
146-
widget.control, contentCtrls.first.id, disabled);
147-
}))
148-
.toList()))
149-
],
150-
);
167+
return createControl(
168+
widget.control, contentCtrls.first.id, disabled);
169+
}).toList()))
170+
],
171+
);
172+
});
151173

152174
return constrainedControl(tabs, widget.parent, widget.control);
153175
}

client/lib/models/control_view_model.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ class ControlViewModel extends Equatable {
1919
}
2020

2121
@override
22-
List<Object?> get props => [control];
22+
List<Object?> get props => [control, children];
2323
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:redux/redux.dart';
3+
4+
import 'app_state.dart';
5+
import 'control_view_model.dart';
6+
7+
class ControlsViewModel extends Equatable {
8+
final List<ControlViewModel> controlViews;
9+
final dynamic dispatch;
10+
11+
const ControlsViewModel({required this.controlViews, required this.dispatch});
12+
13+
static ControlsViewModel fromStore(
14+
Store<AppState> store, Iterable<String> ids) {
15+
return ControlsViewModel(
16+
controlViews:
17+
ids.map((id) => ControlViewModel.fromStore(store, id)).toList(),
18+
dispatch: store.dispatch);
19+
}
20+
21+
@override
22+
List<Object?> get props => [controlViews];
23+
}

docs/roadmap.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,7 @@ Properties:
897897

898898
- tabs
899899
- value
900+
- animationDuration - in milliseconds
900901

901902
Events:
902903

sdk/python/flet/tabs.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ def __init__(
1414
content: Control = None,
1515
tab_content: Control = None,
1616
ref: Ref = None,
17-
key=None,
18-
icon=None,
17+
key: str = None,
18+
icon: str = None,
1919
):
2020
Control.__init__(self, ref=ref)
2121
assert key or text, "key or text must be specified"
@@ -101,6 +101,7 @@ def __init__(
101101
# Tabs-specific
102102
tabs: List[Tab] = None,
103103
value: str = None,
104+
animation_duration: int = None,
104105
on_change=None,
105106
):
106107

@@ -118,6 +119,7 @@ def __init__(
118119

119120
self.tabs = tabs
120121
self.value = value
122+
self.animation_duration = animation_duration
121123
self.on_change = on_change
122124

123125
def _get_control_name(self):
@@ -164,3 +166,13 @@ def value(self, value: Optional[str]):
164166
value in keys for keys in [(tab.key, tab.text) for tab in self.tabs]
165167
), f"'{value}' is not a key for any tab"
166168
self._set_attr("value", value or "")
169+
170+
# animation_duration
171+
@property
172+
def animation_duration(self):
173+
return self._get_attr("animationDuration")
174+
175+
@animation_duration.setter
176+
@beartype
177+
def animation_duration(self, value: Optional[int]):
178+
self._set_attr("animationDuration", value)

sdk/python/playground/tabs-test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def main(page: Page):
1313

1414
t = Tabs(
1515
value="tab2",
16+
animation_duration=300,
1617
tabs=[
1718
Tab(
1819
key="tab1",

0 commit comments

Comments
 (0)