@@ -15,7 +15,7 @@ import { NameGenerator } from "comps/utils";
1515import { ScrollBar , Section , sectionNames } from "lowcoder-design" ;
1616import { HintPlaceHolder } from "lowcoder-design" ;
1717import _ from "lodash" ;
18- import React , { useCallback , useContext , useEffect } from "react" ;
18+ import React , { useContext , useEffect , useState } from "react" ;
1919import styled , { css } from "styled-components" ;
2020import { IContainer } from "../containerBase/iContainer" ;
2121import { SimpleContainerComp } from "../containerBase/simpleContainerComp" ;
@@ -34,7 +34,7 @@ import { EditorContext } from "comps/editorState";
3434import { checkIsMobile } from "util/commonUtils" ;
3535import { messageInstance } from "lowcoder-design/src/components/GlobalInstances" ;
3636import { BoolControl } from "comps/controls/boolControl" ;
37- import { PositionControl } from "comps/controls/dropdownControl" ;
37+ import { PositionControl , dropdownControl } from "comps/controls/dropdownControl" ;
3838import { SliderControl } from "@lowcoder-ee/comps/controls/sliderControl" ;
3939import { 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+
4958const 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
7382type ViewProps = RecordConstructorToView < typeof childrenMap > ;
7483type TabbedContainerProps = ViewProps & { dispatch : DispatchType } ;
75-
84+
7685const 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-
298322export 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
471488export const TabbedContainerComp = withExposingConfigs ( TabbedContainerImplComp , [
0 commit comments