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:
yz-yu 2022-07-20 11:59:04 +08:00 committed by GitHub
commit 56b54421f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 202 additions and 7 deletions

View File

@ -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();
});
});

View File

@ -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 }}',
},
},
],
},
],
},
};

View File

@ -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}

View 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>
);
}
);

View File

@ -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],
};

View File

@ -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(() => {