@@ -15,7 +15,7 @@ import { NameGenerator } from "comps/utils";
15
15
import { ScrollBar , Section , sectionNames } from "lowcoder-design" ;
16
16
import { HintPlaceHolder } from "lowcoder-design" ;
17
17
import _ from "lodash" ;
18
- import React , { useCallback , useContext , useEffect } from "react" ;
18
+ import React , { useContext , useEffect , useState } from "react" ;
19
19
import styled , { css } from "styled-components" ;
20
20
import { IContainer } from "../containerBase/iContainer" ;
21
21
import { SimpleContainerComp } from "../containerBase/simpleContainerComp" ;
@@ -34,7 +34,7 @@ import { EditorContext } from "comps/editorState";
34
34
import { checkIsMobile } from "util/commonUtils" ;
35
35
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances" ;
36
36
import { BoolControl } from "comps/controls/boolControl" ;
37
- import { PositionControl } from "comps/controls/dropdownControl" ;
37
+ import { PositionControl , dropdownControl } from "comps/controls/dropdownControl" ;
38
38
import { SliderControl } from "@lowcoder-ee/comps/controls/sliderControl" ;
39
39
import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils" ;
40
40
@@ -46,6 +46,15 @@ const EVENT_OPTIONS = [
46
46
} ,
47
47
] as const ;
48
48
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
+
49
58
const childrenMap = {
50
59
tabs : TabsOptionControl ,
51
60
selectedTabKey : stringExposingStateControl ( "key" , "Tab1" ) ,
@@ -61,7 +70,7 @@ const childrenMap = {
61
70
onEvent : eventHandlerControl ( EVENT_OPTIONS ) ,
62
71
disabled : BoolCodeControl ,
63
72
showHeader : withDefault ( BoolControl , true ) ,
64
- destroyInactiveTab : withDefault ( BoolControl , false ) ,
73
+ tabBehavior : withDefault ( TabBehaviorControl , "lazy" ) ,
65
74
style : styleControl ( TabContainerStyle , 'style' ) ,
66
75
headerStyle : styleControl ( ContainerHeaderStyle , 'headerStyle' ) ,
67
76
bodyStyle : styleControl ( TabBodyStyle , 'bodyStyle' ) ,
@@ -72,7 +81,7 @@ const childrenMap = {
72
81
73
82
type ViewProps = RecordConstructorToView < typeof childrenMap > ;
74
83
type TabbedContainerProps = ViewProps & { dispatch : DispatchType } ;
75
-
84
+
76
85
const getStyle = (
77
86
style : TabContainerStyleType ,
78
87
headerStyle : ContainerHeaderStyleType ,
@@ -138,11 +147,11 @@ const getStyle = (
138
147
` ;
139
148
} ;
140
149
141
- const StyledTabs = styled ( Tabs ) < {
150
+ const StyledTabs = styled ( Tabs ) < {
142
151
$style : TabContainerStyleType ;
143
152
$headerStyle : ContainerHeaderStyleType ;
144
153
$bodyStyle : TabBodyStyleType ;
145
- $isMobile ?: boolean ;
154
+ $isMobile ?: boolean ;
146
155
$showHeader ?: boolean ;
147
156
$animationStyle :AnimationStyleType
148
157
} > `
@@ -157,13 +166,12 @@ const StyledTabs = styled(Tabs)<{
157
166
158
167
.ant-tabs-content {
159
168
height: 100%;
160
- // margin-top: -16px;
169
+
161
170
}
162
171
163
172
.ant-tabs-nav {
164
173
display: ${ ( props ) => ( props . $showHeader ? "block" : "none" ) } ;
165
174
padding: 0 ${ ( props ) => ( props . $isMobile ? 16 : 24 ) } px;
166
- // background: white;
167
175
margin: 0px;
168
176
}
169
177
@@ -197,27 +205,20 @@ const TabbedContainer = (props: TabbedContainerProps) => {
197
205
headerStyle,
198
206
bodyStyle,
199
207
horizontalGridCells,
200
- destroyInactiveTab ,
208
+ tabBehavior ,
201
209
} = props ;
202
210
203
211
const visibleTabs = tabs . filter ( ( tab ) => ! tab . hidden ) ;
204
212
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 ] ) ;
221
222
222
223
const editorState = useContext ( EditorContext ) ;
223
224
const maxWidth = editorState . getAppSettings ( ) . maxWidth ;
@@ -230,23 +231,38 @@ const TabbedContainer = (props: TabbedContainerProps) => {
230
231
const childDispatch = wrapDispatch ( wrapDispatch ( dispatch , "containers" ) , id ) ;
231
232
const containerProps = containers [ id ] . children ;
232
233
const hasIcon = tab . icon . props . value ;
234
+
233
235
const label = (
234
236
< >
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 > }
238
238
{ 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 > }
242
240
</ >
243
241
) ;
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 (
250
266
< BackgroundColorContext . Provider value = { bodyStyle . background } >
251
267
< ScrollBar style = { { height : props . autoHeight ? "auto" : "100%" , margin : "0px" , padding : "0px" } } hideScrollbar = { ! props . showVerticalScrollbar } overflow = { props . autoHeight ? 'hidden' :'scroll' } >
252
268
< ContainerInTab
@@ -260,41 +276,49 @@ const TabbedContainer = (props: TabbedContainerProps) => {
260
276
/>
261
277
</ ScrollBar >
262
278
</ 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
+ } ) ;
266
289
267
290
return (
268
291
< 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 >
294
319
) ;
295
320
} ;
296
321
297
-
298
322
export const TabbedContainerBaseComp = ( function ( ) {
299
323
return new UICompBuilder ( childrenMap , ( props , dispatch ) => {
300
324
return (
@@ -313,14 +337,14 @@ export const TabbedContainerBaseComp = (function () {
313
337
} ) }
314
338
{ children . selectedTabKey . propertyView ( { label : trans ( "prop.defaultValue" ) } ) }
315
339
</ Section >
316
-
340
+
317
341
{ [ "logic" , "both" ] . includes ( useContext ( EditorContext ) . editorModeStatus ) && (
318
342
< Section name = { sectionNames . interaction } >
319
343
{ children . onEvent . getPropertyView ( ) }
320
344
{ disabledPropertyView ( children ) }
321
345
{ hiddenPropertyView ( children ) }
322
346
{ children . showHeader . propertyView ( { label : trans ( "tabbedContainer.showTabs" ) } ) }
323
- { children . destroyInactiveTab . propertyView ( { label : trans ( "tabbedContainer.destroyInactiveTab" ) } ) }
347
+ { children . tabBehavior . propertyView ( { label : "Tab Behavior" } ) }
324
348
</ Section >
325
349
) }
326
350
@@ -371,21 +395,18 @@ class TabbedContainerImplComp extends TabbedContainerBaseComp implements IContai
371
395
const actions : CompAction [ ] = [ ] ;
372
396
Object . keys ( containers ) . forEach ( ( id ) => {
373
397
if ( ! ids . has ( id ) ) {
374
- // log.debug("syncContainers delete. ids=", ids, " id=", id);
375
398
actions . push ( wrapChildAction ( "containers" , wrapChildAction ( id , deleteCompAction ( ) ) ) ) ;
376
399
}
377
400
} ) ;
378
401
// new
379
402
ids . forEach ( ( id ) => {
380
403
if ( ! containers . hasOwnProperty ( id ) ) {
381
- // log.debug("syncContainers new containers: ", containers, " id: ", id);
382
404
actions . push (
383
405
wrapChildAction ( "containers" , addMapChildAction ( id , { layout : { } , items : { } } ) )
384
406
) ;
385
407
}
386
408
} ) ;
387
409
388
- // log.debug("syncContainers. actions: ", actions);
389
410
let instance = this ;
390
411
actions . forEach ( ( action ) => {
391
412
instance = instance . reduce ( action ) ;
@@ -414,13 +435,11 @@ class TabbedContainerImplComp extends TabbedContainerBaseComp implements IContai
414
435
return this ;
415
436
}
416
437
}
417
- // log.debug("before super reduce. action: ", action);
418
438
let newInstance = super . reduce ( action ) ;
419
439
if ( action . type === CompActionTypes . UPDATE_NODES_V2 ) {
420
440
// Need eval to get the value in StringControl
421
441
newInstance = newInstance . syncContainers ( ) ;
422
442
}
423
- // log.debug("reduce. instance: ", this, " newInstance: ", newInstance);
424
443
return newInstance ;
425
444
}
426
445
@@ -464,8 +483,6 @@ class TabbedContainerImplComp extends TabbedContainerBaseComp implements IContai
464
483
override autoHeight ( ) : boolean {
465
484
return this . children . autoHeight . getView ( ) ;
466
485
}
467
-
468
-
469
486
}
470
487
471
488
export const TabbedContainerComp = withExposingConfigs ( TabbedContainerImplComp , [
0 commit comments