Merge pull request #591 from smartxworks/slot-fallback

fix(runtime): fix #590, use an init set to check first time render
This commit is contained in:
yz-yu 2022-09-15 12:04:26 +08:00 committed by GitHub
commit 569f708a8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 151 additions and 17 deletions

View File

@ -13,6 +13,7 @@ import {
AsyncMergeStateSchema,
TabsWithSlotsSchema,
ParentRerenderSchema,
MultiSlotsSchema,
} from './mockSchema';
// A pure single sunmao component will render twice when it mount.
@ -199,4 +200,25 @@ describe('slot trait if condition', () => {
unmount();
clearTesterMap();
});
it('only teardown component state in the last render', () => {
const { App, stateManager } = initSunmaoUI({ libs: [TestLib] });
stateManager.mute = true;
const { unmount } = render(<App options={MultiSlotsSchema} />);
expect(stateManager.store).toMatchInlineSnapshot(`
Object {
"input1": Object {
"value": "1",
},
"input2": Object {
"value": "2",
},
"testList0": Object {},
}
`);
unmount();
clearTesterMap();
});
});

View File

@ -266,3 +266,51 @@ export const TabsWithSlotsSchema: Application = {
],
},
};
export const MultiSlotsSchema: Application = {
kind: 'Application',
version: 'example/v1',
metadata: { name: 'sunmao application', description: 'sunmao empty application' },
spec: {
components: [
{
id: 'testList0',
type: 'custom/v1/testList',
properties: { number: 2 },
traits: [],
},
{
id: 'input1',
type: 'test/v1/input',
properties: {
defaultValue: '1',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: { id: 'testList0', slot: 'content' },
ifCondition: '{{$slot.index === 0}}',
},
},
],
},
{
id: 'input2',
type: 'test/v1/input',
properties: {
defaultValue: '2',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: { id: 'testList0', slot: 'content' },
ifCondition: '{{$slot.index === 1}}',
},
},
],
},
],
},
};

View File

@ -0,0 +1,43 @@
import { implementRuntimeComponent } from '../../src/utils/buildKit';
import { Type } from '@sinclair/typebox';
import { PRESET_PROPERTY_CATEGORY } from '@sunmao-ui/shared';
export default implementRuntimeComponent({
version: 'custom/v1',
metadata: {
name: 'testList',
displayName: 'TestList',
description: '',
isDraggable: false,
isResizable: false,
exampleProperties: {},
exampleSize: [1, 1],
annotations: {
category: PRESET_PROPERTY_CATEGORY.Basic,
},
},
spec: {
properties: Type.Object({
number: Type.Number(),
}),
state: Type.Object({}),
methods: {},
slots: {
content: {
slotProps: Type.Object({
index: Type.Number(),
}),
},
},
styleSlots: ['content'],
events: [],
},
})(({ number, slotsElements }) => {
// implement your component here
return (
<div>
{new Array(number)
.fill(0)
.map((_, index) => slotsElements?.content?.({ index }, null, `content_${index}`))}
</div>
);
});

View File

@ -3,10 +3,11 @@ import TestButton from './Button';
import TestTester from './Tester';
import TestInput from './Input';
import TestTabs from './Tabs';
import TestList from './TestList';
import TimeoutTrait from './TimeoutTrait';
import { SunmaoLib } from '../../src';
export const TestLib: SunmaoLib = {
components: [TestButton, TestTester, TestInput, TestTabs],
components: [TestButton, TestTester, TestInput, TestTabs, TestList],
traits: [TimeoutTrait],
};

View File

@ -7,7 +7,7 @@ import { useRuntimeFunctions } from './hooks/useRuntimeFunctions';
import { getSlotElements } from './hooks/useSlotChildren';
import { useGlobalHandlerMap } from './hooks/useGlobalHandlerMap';
import { useEleRef } from './hooks/useEleMap';
import { initStateAndMethod } from '../../../utils/initStateAndMethod';
import { initSingleComponentState } from '../../../utils/initStateAndMethod';
import ComponentErrorBoundary from '../ComponentErrorBoundary';
export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
@ -23,7 +23,7 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
// This code is to init dynamic generated components
// because they have not be initialized before
if (!stateManager.store[c.id]) {
initStateAndMethod(registry, stateManager, [c]);
initSingleComponentState(registry, stateManager, c);
}
const { eleRef, onRef, onRecoverFromError } = useEleRef(props);

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { RuntimeTraitSchema } from '@sunmao-ui/core';
import { ImplWrapperMain } from './ImplWrapperMain';
import { useRuntimeFunctions } from './hooks/useRuntimeFunctions';
@ -12,8 +12,6 @@ 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 slotKey = slotContext?.slotKey || '';
@ -39,9 +37,13 @@ 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) {
const isLastRender = slotContext ? slotContext.renderSet.size === 0 : true;
if (result.unmount && isLastRender) {
// Every component's state is initialized in initStateAnd Method.
// So if if it should not render, we should remove it from store.
@ -49,19 +51,17 @@ export const UnmountImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperPr
* 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.
* initSet: when init set has the component's id, it means there
* is an initial state setup by us. If a component is hidden
* forever, it still need to teardown at the first time it rendered.
*/
if (!prevIsHidden || renderCount.current === 1) {
if (!prevIsHidden || stateManager.initSet.has(c.id)) {
delete stateManager.store[c.id];
stateManager.initSet.delete(c.id);
}
}
},
[c.id, executeTrait, stateManager, isHidden]
[c.id, executeTrait, stateManager, isHidden, slotContext]
);
useEffect(() => {

View File

@ -45,6 +45,18 @@ export class StateManager {
store = reactive<Record<string, any>>({});
slotStore = reactive<Record<string, any>>({});
/**
* In `initStateAndMethod`, we setup all components' state with a
* default value.
*
* If some components were unmounted later, the `UnmountImplWrapper`
* will teardown the state.
*
* So we provide this flag set for the `UnmountImplWrapper`, let
* it know whether the component's state is setup by the init process.
*/
initSet = new Set<string>();
dependencies: Record<string, unknown>;
mute = true;

View File

@ -15,17 +15,23 @@ export function initStateAndMethod(
stateManager: StateManagerInterface,
components: RuntimeComponentSchema[]
) {
components.forEach(c => initSingleComponentState(registry, stateManager, c));
components.forEach(c => {
const inited = initSingleComponentState(registry, stateManager, c);
if (inited) {
stateManager.initSet.add(c.id);
}
});
}
export function initSingleComponentState(
registry: RegistryInterface,
stateManager: StateManagerInterface,
c: RuntimeComponentSchema
) {
): boolean {
if (stateManager.store[c.id]) {
return false;
}
let state = {};
c.traits.forEach(t => {
const tSpec = registry.getTrait(t.parsedType.version, t.parsedType.name).spec;
@ -53,4 +59,6 @@ export function initSingleComponentState(
stateManager.store[moduleSchema.id] = moduleInitState;
} catch {}
}
return true;
}