mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-21 03:15:49 +08:00
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:
commit
569f708a8d
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
43
packages/runtime/__tests__/testLib/TestList.tsx
Normal file
43
packages/runtime/__tests__/testLib/TestList.tsx
Normal 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>
|
||||
);
|
||||
});
|
@ -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],
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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(() => {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user