Skip to content

Commit 84ada72

Browse files
committed
#1949 add different tab modes in PropertyView
1 parent 262a60b commit 84ada72

File tree

1 file changed

+92
-75
lines changed

1 file changed

+92
-75
lines changed

client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx

Lines changed: 92 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { NameGenerator } from "comps/utils";
1515
import { ScrollBar, Section, sectionNames } from "lowcoder-design";
1616
import { HintPlaceHolder } from "lowcoder-design";
1717
import _ from "lodash";
18-
import React, { useCallback, useContext, useEffect } from "react";
18+
import React, {useContext, useEffect,useState } from "react";
1919
import styled, { css } from "styled-components";
2020
import { IContainer } from "../containerBase/iContainer";
2121
import { SimpleContainerComp } from "../containerBase/simpleContainerComp";
@@ -34,7 +34,7 @@ import { EditorContext } from "comps/editorState";
3434
import { checkIsMobile } from "util/commonUtils";
3535
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
3636
import { BoolControl } from "comps/controls/boolControl";
37-
import { PositionControl } from "comps/controls/dropdownControl";
37+
import { PositionControl,dropdownControl } from "comps/controls/dropdownControl";
3838
import { SliderControl } from "@lowcoder-ee/comps/controls/sliderControl";
3939
import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils";
4040

@@ -46,6 +46,15 @@ const EVENT_OPTIONS = [
4646
},
4747
] as const;
4848

49+
const TAB_BEHAVIOR_OPTIONS = [
50+
{ label: "Lazy Loading", value: "lazy" },
51+
{ label: "Remember State", value: "remember" },
52+
{ label: "Destroy Inactive", value: "destroy" },
53+
{ label: "Keep Alive (render all)", value: "keep-alive" },
54+
] as const;
55+
56+
const TabBehaviorControl = dropdownControl(TAB_BEHAVIOR_OPTIONS, "lazy");
57+
4958
const childrenMap = {
5059
tabs: TabsOptionControl,
5160
selectedTabKey: stringExposingStateControl("key", "Tab1"),
@@ -61,7 +70,7 @@ const childrenMap = {
6170
onEvent: eventHandlerControl(EVENT_OPTIONS),
6271
disabled: BoolCodeControl,
6372
showHeader: withDefault(BoolControl, true),
64-
destroyInactiveTab: withDefault(BoolControl, false),
73+
tabBehavior: withDefault(TabBehaviorControl, "lazy"),
6574
style: styleControl(TabContainerStyle , 'style'),
6675
headerStyle: styleControl(ContainerHeaderStyle , 'headerStyle'),
6776
bodyStyle: styleControl(TabBodyStyle , 'bodyStyle'),
@@ -72,7 +81,7 @@ const childrenMap = {
7281

7382
type ViewProps = RecordConstructorToView<typeof childrenMap>;
7483
type TabbedContainerProps = ViewProps & { dispatch: DispatchType };
75-
84+
7685
const getStyle = (
7786
style: TabContainerStyleType,
7887
headerStyle: ContainerHeaderStyleType,
@@ -138,11 +147,11 @@ const getStyle = (
138147
`;
139148
};
140149

141-
const StyledTabs = styled(Tabs)<{
150+
const StyledTabs = styled(Tabs)<{
142151
$style: TabContainerStyleType;
143152
$headerStyle: ContainerHeaderStyleType;
144153
$bodyStyle: TabBodyStyleType;
145-
$isMobile?: boolean;
154+
$isMobile?: boolean;
146155
$showHeader?: boolean;
147156
$animationStyle:AnimationStyleType
148157
}>`
@@ -157,13 +166,12 @@ const StyledTabs = styled(Tabs)<{
157166
158167
.ant-tabs-content {
159168
height: 100%;
160-
// margin-top: -16px;
169+
161170
}
162171
163172
.ant-tabs-nav {
164173
display: ${(props) => (props.$showHeader ? "block" : "none")};
165174
padding: 0 ${(props) => (props.$isMobile ? 16 : 24)}px;
166-
// background: white;
167175
margin: 0px;
168176
}
169177
@@ -197,27 +205,20 @@ const TabbedContainer = (props: TabbedContainerProps) => {
197205
headerStyle,
198206
bodyStyle,
199207
horizontalGridCells,
200-
destroyInactiveTab,
208+
tabBehavior,
201209
} = props;
202210

203211
const visibleTabs = tabs.filter((tab) => !tab.hidden);
204212
const selectedTab = visibleTabs.find((tab) => tab.key === props.selectedTabKey.value);
205-
const activeKey = selectedTab
206-
? selectedTab.key
207-
: visibleTabs.length > 0
208-
? visibleTabs[0].key
209-
: undefined;
210-
211-
const onTabClick = useCallback(
212-
(key: string, event: React.KeyboardEvent<Element> | React.MouseEvent<Element, MouseEvent>) => {
213-
// log.debug("onTabClick. event: ", event);
214-
const target = event.target;
215-
(target as any).parentNode.click
216-
? (target as any).parentNode.click()
217-
: (target as any).parentNode.parentNode.click();
218-
},
219-
[]
220-
);
213+
const activeKey = selectedTab? selectedTab.key: visibleTabs.length > 0 ? visibleTabs[0].key : undefined;
214+
215+
// Placeholder-based lazy loading — only for "lazy" mode
216+
const [loadedTabs, setLoadedTabs] = useState<Set<string>>(new Set());
217+
useEffect(() => {
218+
if (tabBehavior === "lazy" && activeKey) {
219+
setLoadedTabs((prev: Set<string>) => new Set([...prev, activeKey]));
220+
}
221+
}, [tabBehavior, activeKey]);
221222

222223
const editorState = useContext(EditorContext);
223224
const maxWidth = editorState.getAppSettings().maxWidth;
@@ -230,23 +231,38 @@ const TabbedContainer = (props: TabbedContainerProps) => {
230231
const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id);
231232
const containerProps = containers[id].children;
232233
const hasIcon = tab.icon.props.value;
234+
233235
const label = (
234236
<>
235-
{tab.iconPosition === "left" && hasIcon && (
236-
<span style={{ marginRight: "4px" }}>{tab.icon}</span>
237-
)}
237+
{tab.iconPosition === "left" && hasIcon && <span style={{ marginRight: 4 }}>{tab.icon}</span>}
238238
{tab.label}
239-
{tab.iconPosition === "right" && hasIcon && (
240-
<span style={{ marginLeft: "4px" }}>{tab.icon}</span>
241-
)}
239+
{tab.iconPosition === "right" && hasIcon && <span style={{ marginLeft: 4 }}>{tab.icon}</span>}
242240
</>
243241
);
244-
return {
245-
label,
246-
key: tab.key,
247-
forceRender: !destroyInactiveTab,
248-
destroyInactiveTab: destroyInactiveTab,
249-
children: (
242+
243+
// Item-level forceRender mapping
244+
const forceRender: boolean = tabBehavior === "keep-alive";
245+
246+
// Render content (placeholder only for "lazy" & not yet opened)
247+
const renderTabContent = () => {
248+
if (tabBehavior === "lazy" && !loadedTabs.has(tab.key)) {
249+
return (
250+
<div
251+
style={{
252+
display: "flex",
253+
justifyContent: "center",
254+
alignItems: "center",
255+
height: "200px",
256+
color: "#999",
257+
fontSize: "14px",
258+
}}
259+
>
260+
Click to load tab content
261+
</div>
262+
);
263+
}
264+
265+
return (
250266
<BackgroundColorContext.Provider value={bodyStyle.background}>
251267
<ScrollBar style={{ height: props.autoHeight ? "auto" : "100%", margin: "0px", padding: "0px" }} hideScrollbar={!props.showVerticalScrollbar} overflow={props.autoHeight ? 'hidden':'scroll'}>
252268
<ContainerInTab
@@ -260,41 +276,49 @@ const TabbedContainer = (props: TabbedContainerProps) => {
260276
/>
261277
</ScrollBar>
262278
</BackgroundColorContext.Provider>
263-
)
264-
}
265-
})
279+
);
280+
};
281+
282+
return {
283+
label,
284+
key: tab.key,
285+
forceRender, // true only for keep-alive
286+
children: renderTabContent(),
287+
};
288+
});
266289

267290
return (
268291
<div style={{padding: props.style.margin, height: props.autoHeight ? "auto" : "100%"}}>
269-
<BackgroundColorContext.Provider value={headerStyle.headerBackground}>
270-
<StyledTabs
271-
$animationStyle={props.animationStyle}
272-
tabPosition={props.placement}
273-
activeKey={activeKey}
274-
$style={style}
275-
$headerStyle={headerStyle}
276-
$bodyStyle={bodyStyle}
277-
$showHeader={showHeader}
278-
onChange={(key) => {
279-
if (key !== props.selectedTabKey.value) {
280-
props.selectedTabKey.onChange(key);
281-
props.onEvent("change");
282-
}
283-
}}
284-
// onTabClick={onTabClick}
285-
animated
286-
$isMobile={isMobile}
287-
items={tabItems}
288-
tabBarGutter={props.tabsGutter}
289-
centered={props.tabsCentered}
290-
>
291-
</StyledTabs>
292-
</BackgroundColorContext.Provider>
293-
</div>
292+
<BackgroundColorContext.Provider value={headerStyle.headerBackground}>
293+
<StyledTabs
294+
destroyOnHidden={tabBehavior === "destroy"}
295+
$animationStyle={props.animationStyle}
296+
tabPosition={props.placement}
297+
activeKey={activeKey}
298+
$style={style}
299+
$headerStyle={headerStyle}
300+
$bodyStyle={bodyStyle}
301+
$showHeader={showHeader}
302+
onChange={(key) => {
303+
if (key !== props.selectedTabKey.value) {
304+
props.selectedTabKey.onChange(key);
305+
props.onEvent("change");
306+
if (tabBehavior === "lazy") {
307+
setLoadedTabs((prev: Set<string>) => new Set([...prev, key]));
308+
}
309+
}
310+
}}
311+
animated
312+
$isMobile={isMobile}
313+
items={tabItems}
314+
tabBarGutter={props.tabsGutter}
315+
centered={props.tabsCentered}
316+
/>
317+
</BackgroundColorContext.Provider>
318+
</div>
294319
);
295320
};
296321

297-
298322
export const TabbedContainerBaseComp = (function () {
299323
return new UICompBuilder(childrenMap, (props, dispatch) => {
300324
return (
@@ -313,14 +337,14 @@ export const TabbedContainerBaseComp = (function () {
313337
})}
314338
{children.selectedTabKey.propertyView({ label: trans("prop.defaultValue") })}
315339
</Section>
316-
340+
317341
{["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && (
318342
<Section name={sectionNames.interaction}>
319343
{children.onEvent.getPropertyView()}
320344
{disabledPropertyView(children)}
321345
{hiddenPropertyView(children)}
322346
{children.showHeader.propertyView({ label: trans("tabbedContainer.showTabs") })}
323-
{children.destroyInactiveTab.propertyView({ label: trans("tabbedContainer.destroyInactiveTab") })}
347+
{children.tabBehavior.propertyView({ label: "Tab Behavior" })}
324348
</Section>
325349
)}
326350

@@ -371,21 +395,18 @@ class TabbedContainerImplComp extends TabbedContainerBaseComp implements IContai
371395
const actions: CompAction[] = [];
372396
Object.keys(containers).forEach((id) => {
373397
if (!ids.has(id)) {
374-
// log.debug("syncContainers delete. ids=", ids, " id=", id);
375398
actions.push(wrapChildAction("containers", wrapChildAction(id, deleteCompAction())));
376399
}
377400
});
378401
// new
379402
ids.forEach((id) => {
380403
if (!containers.hasOwnProperty(id)) {
381-
// log.debug("syncContainers new containers: ", containers, " id: ", id);
382404
actions.push(
383405
wrapChildAction("containers", addMapChildAction(id, { layout: {}, items: {} }))
384406
);
385407
}
386408
});
387409

388-
// log.debug("syncContainers. actions: ", actions);
389410
let instance = this;
390411
actions.forEach((action) => {
391412
instance = instance.reduce(action);
@@ -414,13 +435,11 @@ class TabbedContainerImplComp extends TabbedContainerBaseComp implements IContai
414435
return this;
415436
}
416437
}
417-
// log.debug("before super reduce. action: ", action);
418438
let newInstance = super.reduce(action);
419439
if (action.type === CompActionTypes.UPDATE_NODES_V2) {
420440
// Need eval to get the value in StringControl
421441
newInstance = newInstance.syncContainers();
422442
}
423-
// log.debug("reduce. instance: ", this, " newInstance: ", newInstance);
424443
return newInstance;
425444
}
426445

@@ -464,8 +483,6 @@ class TabbedContainerImplComp extends TabbedContainerBaseComp implements IContai
464483
override autoHeight(): boolean {
465484
return this.children.autoHeight.getView();
466485
}
467-
468-
469486
}
470487

471488
export const TabbedContainerComp = withExposingConfigs(TabbedContainerImplComp, [

0 commit comments

Comments
 (0)