mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-21 03:15:49 +08:00
Merge pull request #537 from smartxworks/slot-fallback
fix(runtime): only teardown state when the component is not hidden before this check
This commit is contained in:
commit
56b54421f4
@ -1,5 +1,5 @@
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, screen, waitFor, act } from '@testing-library/react';
|
||||
import produce from 'immer';
|
||||
import { initSunmaoUI } from '../../src';
|
||||
import { TestLib } from '../testLib';
|
||||
@ -10,6 +10,7 @@ import {
|
||||
HiddenTraitSchema,
|
||||
MergeStateSchema,
|
||||
AsyncMergeStateSchema,
|
||||
TabsWithSlotsSchema,
|
||||
} from './mockSchema';
|
||||
|
||||
// A pure single sunmao component will render twice when it mount.
|
||||
@ -132,3 +133,46 @@ describe('when component merge state asynchronously', () => {
|
||||
clearTesterMap();
|
||||
});
|
||||
});
|
||||
|
||||
describe('slot trait if condition', () => {
|
||||
it('only teardown component state when it is not hidden before the check', () => {
|
||||
const { App, stateManager, apiService } = initSunmaoUI({ libs: [TestLib] });
|
||||
stateManager.noConsoleError = true;
|
||||
const { unmount } = render(<App options={TabsWithSlotsSchema} />);
|
||||
expect(screen.getByTestId('tabs')).toHaveTextContent(`Tab OneTab Two`);
|
||||
|
||||
act(() => {
|
||||
apiService.send('uiMethod', {
|
||||
componentId: 'input',
|
||||
name: 'setValue',
|
||||
parameters: 'new-value',
|
||||
});
|
||||
});
|
||||
expect(stateManager.store).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"input": Object {
|
||||
"value": "new-value",
|
||||
},
|
||||
"tabs": Object {
|
||||
"selectedTabIndex": 0,
|
||||
},
|
||||
}
|
||||
`);
|
||||
act(() => {
|
||||
screen.getByTestId('tabs-tab-1').click();
|
||||
});
|
||||
expect(stateManager.store).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"input": Object {
|
||||
"value": "new-value",
|
||||
},
|
||||
"tabs": Object {
|
||||
"selectedTabIndex": 1,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
unmount();
|
||||
clearTesterMap();
|
||||
});
|
||||
});
|
||||
|
@ -173,3 +173,47 @@ export const AsyncMergeStateSchema: Application = {
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const TabsWithSlotsSchema: Application = {
|
||||
kind: 'Application',
|
||||
version: 'example/v1',
|
||||
metadata: {
|
||||
name: 'nested_components',
|
||||
description: 'nested components example',
|
||||
},
|
||||
spec: {
|
||||
components: [
|
||||
{
|
||||
id: 'tabs',
|
||||
type: 'test/v1/tabs',
|
||||
properties: {
|
||||
tabNames: ['Tab One', 'Tab Two'],
|
||||
initialSelectedTabIndex: 0,
|
||||
},
|
||||
traits: [],
|
||||
},
|
||||
{
|
||||
id: 'input',
|
||||
type: 'test/v1/input',
|
||||
properties: {
|
||||
text: {
|
||||
raw: 'only in tab {{ $slot.tabIndex + 1 }}',
|
||||
format: 'plain',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: {
|
||||
container: {
|
||||
id: 'tabs',
|
||||
slot: 'content',
|
||||
},
|
||||
ifCondition: '{{ $slot.tabIndex === 0 }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -23,16 +23,25 @@ export default implementRuntimeComponent({
|
||||
state: Type.Object({
|
||||
value: Type.String(),
|
||||
}),
|
||||
methods: {},
|
||||
methods: {
|
||||
setValue: Type.String(),
|
||||
},
|
||||
slots: {},
|
||||
styleSlots: [],
|
||||
events: [],
|
||||
},
|
||||
})(({ component, defaultValue, mergeState, elementRef }) => {
|
||||
})(({ component, defaultValue, mergeState, elementRef, subscribeMethods }) => {
|
||||
const [value, setValue] = useState(defaultValue || '');
|
||||
useEffect(() => {
|
||||
mergeState({ value });
|
||||
}, [mergeState, value]);
|
||||
|
||||
useEffect(() => {
|
||||
subscribeMethods({
|
||||
setValue,
|
||||
});
|
||||
}, [subscribeMethods]);
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={elementRef}
|
||||
|
80
packages/runtime/__tests__/testLib/Tabs.tsx
Normal file
80
packages/runtime/__tests__/testLib/Tabs.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { implementRuntimeComponent } from '../../src/utils/buildKit';
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default implementRuntimeComponent({
|
||||
version: 'test/v1',
|
||||
metadata: {
|
||||
name: 'tabs',
|
||||
displayName: 'Tabs',
|
||||
description: 'for test',
|
||||
isDraggable: false,
|
||||
isResizable: false,
|
||||
exampleProperties: {},
|
||||
exampleSize: [1, 1],
|
||||
annotations: {
|
||||
category: 'Advance',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
properties: Type.Object({
|
||||
tabNames: Type.Array(Type.String(), {
|
||||
title: 'Tab Names',
|
||||
}),
|
||||
initialSelectedTabIndex: Type.Number({
|
||||
title: 'Default Selected Tab Index',
|
||||
}),
|
||||
}),
|
||||
state: Type.Object({
|
||||
selectedTabIndex: Type.Number(),
|
||||
}),
|
||||
methods: {},
|
||||
slots: {
|
||||
content: {
|
||||
slotProps: Type.Object({
|
||||
tabIndex: Type.Number(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
styleSlots: [],
|
||||
events: [],
|
||||
},
|
||||
})(
|
||||
({
|
||||
component,
|
||||
tabNames,
|
||||
mergeState,
|
||||
elementRef,
|
||||
initialSelectedTabIndex,
|
||||
slotsElements,
|
||||
}) => {
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(
|
||||
initialSelectedTabIndex ?? 0
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
mergeState({ selectedTabIndex });
|
||||
}, [mergeState, selectedTabIndex]);
|
||||
|
||||
return (
|
||||
<div ref={elementRef} data-testid={component.id}>
|
||||
<div className="tabs">
|
||||
{tabNames.map((n, idx) => (
|
||||
<div
|
||||
key={n}
|
||||
data-testid={`${component.id}-tab-${idx}`}
|
||||
onClick={() => setSelectedTabIndex(idx)}
|
||||
>
|
||||
{n}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="panels">
|
||||
{tabNames.map((n, idx) => (
|
||||
<div key={n}>{slotsElements?.content?.({ tabIndex: idx })}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
@ -2,10 +2,11 @@
|
||||
import TestButton from './Button';
|
||||
import TestTester from './Tester';
|
||||
import TestInput from './Input';
|
||||
import TestTabs from './Tabs';
|
||||
import TimeoutTrait from './TimeoutTrait';
|
||||
import { SunmaoLib } from '../../src';
|
||||
|
||||
export const TestLib: SunmaoLib = {
|
||||
components: [TestButton, TestTester, TestInput, TimeoutTrait],
|
||||
components: [TestButton, TestTester, TestInput, TestTabs],
|
||||
traits: [TimeoutTrait],
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { RuntimeTraitSchema } from '@sunmao-ui/core';
|
||||
import { ImplWrapperMain } from './ImplWrapperMain';
|
||||
import { useRuntimeFunctions } from './hooks/useRuntimeFunctions';
|
||||
@ -11,6 +11,8 @@ export const UnmountImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperPr
|
||||
const { component: c, services, slotContext } = props;
|
||||
const { stateManager, registry } = services;
|
||||
const { executeTrait } = useRuntimeFunctions(props);
|
||||
const renderCount = useRef(0);
|
||||
renderCount.current++;
|
||||
|
||||
const unmountTraits = useMemo(
|
||||
() =>
|
||||
@ -33,14 +35,29 @@ export const UnmountImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperPr
|
||||
const traitChangeCallback = useCallback(
|
||||
(trait: RuntimeTraitSchema, properties: Record<string, unknown>) => {
|
||||
const result = executeTrait(trait, properties);
|
||||
const prevIsHidden = isHidden;
|
||||
setIsHidden(!!result.unmount);
|
||||
if (result.unmount) {
|
||||
// Every component's state is initialized in initStateAnd Method.
|
||||
// So if if it should not render, we should remove it from store.
|
||||
delete stateManager.store[c.id];
|
||||
|
||||
/**
|
||||
* prevIsHidden: Only clear the store when the component is not
|
||||
* hidden before this check.
|
||||
*
|
||||
* renderCount: Currently we call initStateAndMethod to setup the
|
||||
* state store, and let here to teardown the hidden components'
|
||||
* state. If a component is hidden forever, it still need to teardown
|
||||
* at the first time it rendered.
|
||||
* Not a perfect solution, and we should have better lifecycle
|
||||
* management for the state store.
|
||||
*/
|
||||
if (!prevIsHidden || renderCount.current === 1) {
|
||||
delete stateManager.store[c.id];
|
||||
}
|
||||
}
|
||||
},
|
||||
[c.id, executeTrait, stateManager]
|
||||
[c.id, executeTrait, stateManager, isHidden]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
Loading…
Reference in New Issue
Block a user