mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-21 03:15:49 +08:00
Merge pull request #47 from webzard-io/refactor/trait
Refactor trait new implementation
This commit is contained in:
commit
c2670c9a50
@ -44,8 +44,11 @@
|
||||
componentId: 'test_btn',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters:
|
||||
'{{ test_btn.count > 0 ? 0 : test_btn.count + 1 }}',
|
||||
parameters: {
|
||||
key: 'count',
|
||||
value:
|
||||
'{{ test_btn.count > 0 ? 0 : test_btn.count + 1 }}',
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: false,
|
||||
|
@ -43,7 +43,10 @@
|
||||
componentId: 'btn',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters: '{{ btn.count + 1 }}',
|
||||
parameters: {
|
||||
key: 'count',
|
||||
value: '{{ btn.count + 1 }}',
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: false,
|
||||
|
@ -53,8 +53,11 @@
|
||||
componentId: 'del_btn',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters:
|
||||
'{{ del_btn.count > 0 ? 0 : del_btn.count + 1 }}',
|
||||
parameters: {
|
||||
key: 'count',
|
||||
value:
|
||||
'{{ del_btn.count > 0 ? 0 : del_btn.count + 1 }}',
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: false,
|
||||
@ -64,7 +67,10 @@
|
||||
componentId: 'del_btn',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters: '0',
|
||||
parameters: {
|
||||
key: 'count',
|
||||
value: '0',
|
||||
},
|
||||
},
|
||||
wait: {
|
||||
type: 'delay',
|
||||
|
@ -45,7 +45,10 @@
|
||||
componentId: 'fetch_btn',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters: `{{ fetch_btn.token ? "":"01f0f6265bmsh0efd88b5c7dfa93p136d2ajsn8be6074b61b2" }}`,
|
||||
parameters: {
|
||||
key: 'count',
|
||||
value: `{{ fetch_btn.token ? "":"01f0f6265bmsh0efd88b5c7dfa93p136d2ajsn8be6074b61b2" }}`,
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: false,
|
||||
|
123
packages/runtime/example/input-components/inputValidation.html
Normal file
123
packages/runtime/example/input-components/inputValidation.html
Normal file
@ -0,0 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>meta-ui runtime example: input validation component</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module">
|
||||
import renderApp from '../../src/main.tsx';
|
||||
|
||||
renderApp({
|
||||
version: 'example/v1',
|
||||
metadata: {
|
||||
name: 'inputValidation',
|
||||
description: 'input validation example',
|
||||
},
|
||||
spec: {
|
||||
components: [
|
||||
{
|
||||
id: 'emailInput',
|
||||
type: 'chakra_ui/v1/input',
|
||||
properties: {
|
||||
size: 'lg',
|
||||
left: {
|
||||
type: 'addon',
|
||||
children: '邮箱',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/validation',
|
||||
properties: {
|
||||
value: '{{ emailInput.value || "" }}',
|
||||
maxLength: 20,
|
||||
minLength: 10,
|
||||
rule: 'email',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'emailValidationText',
|
||||
type: 'core/v1/text',
|
||||
properties: {
|
||||
value: {
|
||||
raw: '{{ emailInput.validationResult.errorMsg }}',
|
||||
format: 'plain',
|
||||
},
|
||||
},
|
||||
traits: [],
|
||||
},
|
||||
{
|
||||
id: 'phoneInput',
|
||||
type: 'chakra_ui/v1/input',
|
||||
properties: {
|
||||
size: 'lg',
|
||||
left: {
|
||||
type: 'addon',
|
||||
children: '手机',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/validation',
|
||||
properties: {
|
||||
value: '{{ phoneInput.value || "" }}',
|
||||
maxLength: 100,
|
||||
minLength: 0,
|
||||
rule: 'phoneNumber',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'phoneValidationText',
|
||||
type: 'core/v1/text',
|
||||
properties: {
|
||||
value: {
|
||||
raw: '{{ phoneInput.validationResult.errorMsg }}',
|
||||
format: 'plain',
|
||||
},
|
||||
},
|
||||
traits: [],
|
||||
},
|
||||
{
|
||||
id: 'submitButton',
|
||||
type: 'plain/v1/button',
|
||||
properties: {
|
||||
text: {
|
||||
raw: '提交',
|
||||
format: 'plain',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/event',
|
||||
properties: {
|
||||
events: [
|
||||
{
|
||||
event: 'click',
|
||||
componentId: '$utils',
|
||||
method: {
|
||||
name: 'alert',
|
||||
parameters:
|
||||
'{{ `邮箱:${ emailInput.value } 手机号:${ phoneInput.value }` }}',
|
||||
},
|
||||
wait: {},
|
||||
disabled:
|
||||
'{{ !emailInput.validationResult.isValid || !phoneInput.validationResult.isValid }}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -142,7 +142,10 @@
|
||||
componentId: 'router',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters: '{{router.index + 1}}',
|
||||
parameters: {
|
||||
key: 'index',
|
||||
value: '{{router.index + 1}}',
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: false,
|
||||
@ -182,7 +185,10 @@
|
||||
componentId: 'router',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters: '2',
|
||||
parameters: {
|
||||
key: 'index',
|
||||
value: '2',
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: false,
|
||||
|
@ -139,7 +139,10 @@
|
||||
componentId: 'router',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters: '{{router.index + 1}}',
|
||||
parameters: {
|
||||
key: 'index',
|
||||
value: '{{router.index + 1}}',
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: false,
|
||||
@ -179,7 +182,10 @@
|
||||
componentId: 'router',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters: '2',
|
||||
parameters: {
|
||||
key: 'index',
|
||||
value: '2',
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: false,
|
||||
|
@ -7,11 +7,13 @@ import React, {
|
||||
} from 'react';
|
||||
import {
|
||||
Application,
|
||||
ComponentTrait,
|
||||
createApplication,
|
||||
RuntimeApplication,
|
||||
Trait,
|
||||
} from '@meta-ui/core';
|
||||
import { merge } from 'lodash';
|
||||
import { registry } from './registry';
|
||||
import { registry, TraitResult } from './registry';
|
||||
import { stateStore, deepEval } from './store';
|
||||
import { apiService } from './api-service';
|
||||
import { ContainerPropertySchema } from './traits/core/slot';
|
||||
@ -19,11 +21,18 @@ import { Static } from '@sinclair/typebox';
|
||||
import { watch } from '@vue-reactivity/watch';
|
||||
import _ from 'lodash';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { globalHandlerMap } from './handler';
|
||||
|
||||
type ArrayElement<ArrayType extends readonly unknown[]> =
|
||||
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
|
||||
|
||||
type ApplicationComponent = RuntimeApplication['spec']['components'][0];
|
||||
type ApplicationTrait = ArrayElement<ApplicationComponent['traits']>;
|
||||
|
||||
const ImplWrapper = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
component: RuntimeApplication['spec']['components'][0];
|
||||
component: ApplicationComponent;
|
||||
slotsMap: SlotsMap | undefined;
|
||||
targetSlot: { id: string; slot: string } | null;
|
||||
app: RuntimeApplication;
|
||||
@ -40,7 +49,11 @@ const ImplWrapper = React.forwardRef<
|
||||
c.parsedType.name
|
||||
).impl;
|
||||
|
||||
const handlerMap = useRef<Record<string, (parameters?: any) => void>>({});
|
||||
if (!globalHandlerMap.has(c.id)) {
|
||||
globalHandlerMap.set(c.id, {});
|
||||
}
|
||||
|
||||
let handlerMap = globalHandlerMap.get(c.id)!;
|
||||
useEffect(() => {
|
||||
const handler = (s: {
|
||||
componentId: string;
|
||||
@ -50,11 +63,11 @@ const ImplWrapper = React.forwardRef<
|
||||
if (s.componentId !== c.id) {
|
||||
return;
|
||||
}
|
||||
if (!handlerMap.current[s.name]) {
|
||||
if (!handlerMap[s.name]) {
|
||||
// maybe log?
|
||||
return;
|
||||
}
|
||||
handlerMap.current[s.name](s.parameters);
|
||||
handlerMap[s.name](s.parameters);
|
||||
};
|
||||
apiService.on('uiMethod', handler);
|
||||
return () => {
|
||||
@ -68,91 +81,81 @@ const ImplWrapper = React.forwardRef<
|
||||
},
|
||||
[c.id]
|
||||
);
|
||||
const subscribeMethods = useCallback((map: any) => {
|
||||
handlerMap = { ...handlerMap, ...map };
|
||||
globalHandlerMap.set(c.id, handlerMap);
|
||||
}, []);
|
||||
|
||||
const subscribeMethods = useCallback(
|
||||
(map: any) => {
|
||||
handlerMap.current = merge(handlerMap.current, map);
|
||||
},
|
||||
[handlerMap.current]
|
||||
);
|
||||
// result returned from traits
|
||||
const [traitResults, setTraitResults] = useState<TraitResult[]>([]);
|
||||
|
||||
// traits
|
||||
const [traitPropertiesMap, setTraitPropertiesMap] = useState<
|
||||
Map<typeof c['traits'][0], object>
|
||||
>(
|
||||
c.traits.reduce((prev, cur) => {
|
||||
prev.set(cur, deepEval(cur.properties).result);
|
||||
return prev;
|
||||
}, new Map())
|
||||
);
|
||||
// eval traits' properties then excecute traits
|
||||
useEffect(() => {
|
||||
function excecuteTrait(
|
||||
trait: ApplicationTrait,
|
||||
traitProperty: ApplicationTrait['properties']
|
||||
) {
|
||||
const tImpl = registry.getTrait(
|
||||
trait.parsedType.version,
|
||||
trait.parsedType.name
|
||||
).impl;
|
||||
const traitResult = tImpl({
|
||||
...traitProperty,
|
||||
componentId: c.id,
|
||||
mergeState,
|
||||
subscribeMethods,
|
||||
});
|
||||
setTraitResults(results => results.concat(traitResult));
|
||||
}
|
||||
|
||||
const stops: ReturnType<typeof watch>[] = [];
|
||||
for (const t of c.traits) {
|
||||
const { stop, result } = deepEval(t.properties, ({ result }) => {
|
||||
setTraitPropertiesMap(
|
||||
new Map(traitPropertiesMap.set(t, { ...result }))
|
||||
);
|
||||
excecuteTrait(t, { ...result });
|
||||
});
|
||||
setTraitPropertiesMap(new Map(traitPropertiesMap.set(t, { ...result })));
|
||||
excecuteTrait(t, { ...result });
|
||||
stops.push(stop);
|
||||
}
|
||||
return () => stops.forEach(s => s());
|
||||
}, []);
|
||||
}, [c.traits]);
|
||||
|
||||
const traitsProps = {};
|
||||
const wrappers: React.FC[] = [];
|
||||
for (const t of c.traits) {
|
||||
const tImpl = registry.getTrait(
|
||||
t.parsedType.version,
|
||||
t.parsedType.name
|
||||
).impl;
|
||||
const { props: tProps, component: Wrapper } = tImpl({
|
||||
...traitPropertiesMap.get(t),
|
||||
mergeState,
|
||||
subscribeMethods,
|
||||
});
|
||||
merge(traitsProps, tProps);
|
||||
if (Wrapper) {
|
||||
wrappers.push(Wrapper);
|
||||
}
|
||||
}
|
||||
// reduce traitResults
|
||||
const propsFromTraits: TraitResult = useMemo(() => {
|
||||
return Array.from(traitResults.values()).reduce(
|
||||
(prevProps, result: TraitResult) => {
|
||||
return { ...prevProps, ...result.props };
|
||||
},
|
||||
{ props: null }
|
||||
);
|
||||
}, [traitResults]);
|
||||
|
||||
const [mergedProps, setMergedProps] = useState(
|
||||
deepEval({
|
||||
...c.properties,
|
||||
...traitsProps,
|
||||
}).result
|
||||
// component properties
|
||||
const [evaledComponentProperties, setEvaledComponentProperties] = useState(
|
||||
merge(deepEval(c.properties).result, propsFromTraits)
|
||||
);
|
||||
useEffect(() => {
|
||||
const rawProps: Record<string, unknown> = {
|
||||
...c.properties,
|
||||
...traitsProps,
|
||||
};
|
||||
|
||||
const { stop, result } = deepEval(rawProps, ({ result }) => {
|
||||
setMergedProps({ ...result });
|
||||
// eval component properties
|
||||
useEffect(() => {
|
||||
const { stop, result } = deepEval(c.properties, ({ result }) => {
|
||||
setEvaledComponentProperties({ ...result });
|
||||
});
|
||||
|
||||
setMergedProps({ ...result });
|
||||
setEvaledComponentProperties(result);
|
||||
return stop;
|
||||
}, []);
|
||||
|
||||
const mergedProps = { ...evaledComponentProperties, ...propsFromTraits };
|
||||
|
||||
let C = (
|
||||
<Impl
|
||||
key={c.id}
|
||||
{...mergedProps}
|
||||
{...props}
|
||||
mergeState={mergeState}
|
||||
subscribeMethods={subscribeMethods}
|
||||
slotsMap={slotsMap}
|
||||
/>
|
||||
);
|
||||
|
||||
while (wrappers.length) {
|
||||
const W = wrappers.pop()!;
|
||||
C = <W>{C}</W>;
|
||||
}
|
||||
|
||||
if (targetSlot) {
|
||||
const targetC = app.spec.components.find(c => c.id === targetSlot.id);
|
||||
if (targetC?.parsedType.name === 'grid_layout') {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
@ -14,13 +14,14 @@ export const TextPropertySchema = Type.Object({
|
||||
|
||||
export type TextProps = {
|
||||
value: Static<typeof TextPropertySchema>;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
const Text: React.FC<TextProps> = ({ value }) => {
|
||||
const Text: React.FC<TextProps> = ({ value, style }) => {
|
||||
if (value.format === 'md') {
|
||||
return <ReactMarkdown>{value.raw}</ReactMarkdown>;
|
||||
}
|
||||
return <>{value.raw}</>;
|
||||
return <span style={style}>{value.raw}</span>;
|
||||
};
|
||||
|
||||
export default Text;
|
||||
|
@ -67,6 +67,7 @@ const Input: ComponentImplementation<{
|
||||
left,
|
||||
right,
|
||||
mergeState,
|
||||
data,
|
||||
}) => {
|
||||
const [value, setValue] = React.useState(''); // TODO: pin input
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
@ -74,7 +75,8 @@ const Input: ComponentImplementation<{
|
||||
|
||||
useEffect(() => {
|
||||
mergeState({ value });
|
||||
}, [value]);
|
||||
mergeState({ ...data });
|
||||
}, [value, data]);
|
||||
|
||||
return (
|
||||
<InputGroup size={size}>
|
||||
|
@ -4,12 +4,16 @@ import { Type } from '@sinclair/typebox';
|
||||
import { ComponentImplementation } from '../../registry';
|
||||
import _Text, { TextProps, TextPropertySchema } from '../_internal/Text';
|
||||
|
||||
const Text: ComponentImplementation<TextProps> = ({ value, mergeState }) => {
|
||||
const Text: ComponentImplementation<TextProps> = ({
|
||||
value,
|
||||
mergeState,
|
||||
style,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
mergeState({ value: value.raw });
|
||||
}, [value.raw]);
|
||||
|
||||
return <_Text value={value} />;
|
||||
return <_Text value={value} style={style} />;
|
||||
};
|
||||
|
||||
const StateSchema = Type.Object({
|
||||
|
3
packages/runtime/src/handler.ts
Normal file
3
packages/runtime/src/handler.ts
Normal file
@ -0,0 +1,3 @@
|
||||
type HandlerMap = Record<string, (parameters?: any) => void>;
|
||||
|
||||
export const globalHandlerMap = new Map<string, HandlerMap>();
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { RuntimeComponent, RuntimeTrait } from '@meta-ui/core';
|
||||
import { SlotsMap } from './App';
|
||||
// components
|
||||
@ -29,6 +29,7 @@ import CoreEvent from './traits/core/event';
|
||||
import CoreSlot from './traits/core/slot';
|
||||
import CoreHidden from './traits/core/hidden';
|
||||
import CoreFetch from './traits/core/fetch';
|
||||
import CoreValidation from './traits/core/validation';
|
||||
|
||||
type ImplementedRuntimeComponent = RuntimeComponent & {
|
||||
impl: ComponentImplementation;
|
||||
@ -50,18 +51,25 @@ export type ComponentImplementation<T = any> = React.FC<
|
||||
mergeState: MergeState;
|
||||
subscribeMethods: SubscribeMethods;
|
||||
slotsMap: SlotsMap | undefined;
|
||||
style?: CSSProperties;
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
>;
|
||||
|
||||
export type TraitResult = {
|
||||
props: {
|
||||
data?: unknown;
|
||||
style?: CSSProperties;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type TraitImplementation<T = any> = (
|
||||
props: T & {
|
||||
componentId: string;
|
||||
mergeState: MergeState;
|
||||
subscribeMethods: SubscribeMethods;
|
||||
}
|
||||
) => {
|
||||
props: any;
|
||||
component?: React.FC;
|
||||
};
|
||||
) => TraitResult;
|
||||
|
||||
class Registry {
|
||||
components: Map<string, Map<string, ImplementedRuntimeComponent>> = new Map();
|
||||
@ -134,3 +142,4 @@ registry.registerTrait(CoreEvent);
|
||||
registry.registerTrait(CoreSlot);
|
||||
registry.registerTrait(CoreHidden);
|
||||
registry.registerTrait(CoreFetch);
|
||||
registry.registerTrait(CoreValidation);
|
||||
|
@ -55,11 +55,15 @@ function maskedEval(raw: string) {
|
||||
if (!dynamic) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
return new Function(`with(this) { return ${expression} }`).call({
|
||||
...stateStore,
|
||||
...builtIn,
|
||||
});
|
||||
try {
|
||||
const result = new Function(`with(this) { return ${expression} }`).call({
|
||||
...stateStore,
|
||||
...builtIn,
|
||||
});
|
||||
return result;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const mapValuesDeep = (
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { createTrait } from '@meta-ui/core';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { debounce, throttle, delay } from 'lodash';
|
||||
@ -8,64 +7,55 @@ import { apiService } from '../../api-service';
|
||||
const useEventTrait: TraitImplementation<{
|
||||
events: Static<typeof EventsPropertySchema>;
|
||||
}> = ({ events }) => {
|
||||
const handlerMap = useRef<Record<string, Array<(parameters?: any) => void>>>(
|
||||
{}
|
||||
);
|
||||
const eventHandler = useCallback((s: { name: string; parameters?: any }) => {
|
||||
if (!handlerMap.current[s.name]) {
|
||||
const handlerMap: Record<string, Array<(parameters?: any) => void>> = {};
|
||||
const eventHandler = (s: { name: string; parameters?: any }) => {
|
||||
if (!handlerMap[s.name]) {
|
||||
// maybe log?
|
||||
return;
|
||||
}
|
||||
handlerMap.current[s.name].forEach(fn => fn(s.parameters));
|
||||
}, []);
|
||||
handlerMap[s.name].forEach(fn => fn(s.parameters));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// tear down previous handlers
|
||||
handlerMap.current = {};
|
||||
// setup current handlers
|
||||
for (const event of events) {
|
||||
const handler = () => {
|
||||
let disabled = false;
|
||||
if (typeof event.disabled === 'boolean') {
|
||||
disabled = event.disabled;
|
||||
}
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
apiService.send('uiMethod', {
|
||||
componentId: event.componentId,
|
||||
name: event.method.name,
|
||||
parameters: event.method.parameters,
|
||||
});
|
||||
};
|
||||
if (!handlerMap.current[event.event]) {
|
||||
handlerMap.current[event.event] = [];
|
||||
// setup current handlers
|
||||
for (const event of events) {
|
||||
const handler = () => {
|
||||
let disabled = false;
|
||||
if (typeof event.disabled === 'boolean') {
|
||||
disabled = event.disabled;
|
||||
}
|
||||
handlerMap.current[event.event].push(
|
||||
event.wait.type === 'debounce'
|
||||
? debounce(handler, event.wait.time)
|
||||
: event.wait.type === 'throttle'
|
||||
? throttle(handler, event.wait.time)
|
||||
: event.wait.type === 'delay'
|
||||
? () => delay(handler, event.wait.time)
|
||||
: handler
|
||||
);
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
console.log(event);
|
||||
apiService.send('uiMethod', {
|
||||
componentId: event.componentId,
|
||||
name: event.method.name,
|
||||
parameters: event.method.parameters,
|
||||
});
|
||||
};
|
||||
if (!handlerMap[event.event]) {
|
||||
handlerMap[event.event] = [];
|
||||
}
|
||||
}, [events]);
|
||||
handlerMap[event.event].push(
|
||||
event.wait.type === 'debounce'
|
||||
? debounce(handler, event.wait.time)
|
||||
: event.wait.type === 'throttle'
|
||||
? throttle(handler, event.wait.time)
|
||||
: event.wait.type === 'delay'
|
||||
? () => delay(handler, event.wait.time)
|
||||
: handler
|
||||
);
|
||||
}
|
||||
|
||||
const hub = useMemo(() => {
|
||||
return {
|
||||
return {
|
||||
props: {
|
||||
// HARDCODE
|
||||
onClick() {
|
||||
eventHandler({
|
||||
name: 'click',
|
||||
});
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
props: hub,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { createTrait } from '@meta-ui/core';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { TraitImplementation } from '../../registry';
|
||||
|
||||
let hasFetched = false;
|
||||
|
||||
const useFetchTrait: TraitImplementation<FetchPropertySchema> = ({
|
||||
name,
|
||||
url,
|
||||
@ -13,11 +14,9 @@ const useFetchTrait: TraitImplementation<FetchPropertySchema> = ({
|
||||
mergeState,
|
||||
subscribeMethods,
|
||||
}) => {
|
||||
const lazy = useMemo(() => {
|
||||
return _lazy === undefined ? method.toLowerCase() !== 'get' : _lazy;
|
||||
}, [method, _lazy]);
|
||||
|
||||
const fetchData = useCallback(() => {
|
||||
const lazy = undefined ? method.toLowerCase() !== 'get' : _lazy;
|
||||
const fetchData = () => {
|
||||
hasFetched = true;
|
||||
// before fetching, initial data
|
||||
mergeState({
|
||||
[name]: {
|
||||
@ -77,26 +76,12 @@ const useFetchTrait: TraitImplementation<FetchPropertySchema> = ({
|
||||
});
|
||||
}
|
||||
);
|
||||
}, [url, method, _headers, body, lazy]);
|
||||
|
||||
// intialize data
|
||||
useEffect(() => {
|
||||
mergeState({
|
||||
[name]: {
|
||||
loading: false,
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
// non lazy query, listen to the change and query;
|
||||
useEffect(() => {
|
||||
if (lazy || !url) {
|
||||
return;
|
||||
}
|
||||
if (!lazy && url && !hasFetched) {
|
||||
fetchData();
|
||||
}, [url, method, _headers, body, lazy]);
|
||||
}
|
||||
|
||||
// only subscribe non lazy fetch trait
|
||||
if (lazy) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { createTrait } from '@meta-ui/core';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { TraitImplementation } from '../../registry';
|
||||
@ -7,17 +7,15 @@ type HiddenProps = {
|
||||
hidden: Static<typeof HiddenPropertySchema>;
|
||||
};
|
||||
|
||||
const Hidden: React.FC<HiddenProps> = ({ hidden, children }) => {
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const useHiddenTrait: TraitImplementation<HiddenProps> = ({ hidden }) => {
|
||||
const style: CSSProperties = {};
|
||||
if (hidden) {
|
||||
style.display = 'none';
|
||||
}
|
||||
return {
|
||||
props: null,
|
||||
component: props => <Hidden {...props} hidden={hidden} />,
|
||||
props: {
|
||||
style,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,24 +1,32 @@
|
||||
import { useEffect } from 'react';
|
||||
import { createTrait } from '@meta-ui/core';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { TraitImplementation } from '../../registry';
|
||||
|
||||
const HasInitializedMap = new Map<string, boolean>();
|
||||
|
||||
type KeyValue = { key: string; value: unknown };
|
||||
|
||||
const useStateTrait: TraitImplementation<{
|
||||
key: Static<typeof KeyPropertySchema>;
|
||||
initialValue: Static<typeof InitialValuePropertySchema>;
|
||||
}> = ({ key, initialValue, mergeState, subscribeMethods }) => {
|
||||
useEffect(() => {
|
||||
}> = ({ key, initialValue, componentId, mergeState, subscribeMethods }) => {
|
||||
const hashId = `#${componentId}@${key}`;
|
||||
let hasInitialized = HasInitializedMap.get(hashId);
|
||||
|
||||
if (!hasInitialized) {
|
||||
mergeState({ [key]: initialValue });
|
||||
|
||||
subscribeMethods({
|
||||
setValue(value) {
|
||||
const methods = {
|
||||
setValue({ key, value }: KeyValue) {
|
||||
mergeState({ [key]: value });
|
||||
},
|
||||
reset() {
|
||||
resetValue({ key }: KeyValue) {
|
||||
mergeState({ [key]: initialValue });
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
subscribeMethods(methods);
|
||||
HasInitializedMap.set(hashId, true);
|
||||
}
|
||||
|
||||
return {
|
||||
props: null,
|
||||
@ -50,7 +58,10 @@ export default {
|
||||
methods: [
|
||||
{
|
||||
name: 'setValue',
|
||||
parameters: Type.Any(),
|
||||
parameters: Type.Object({
|
||||
key: Type.String(),
|
||||
value: Type.Any(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'reset',
|
||||
|
127
packages/runtime/src/traits/core/validation.tsx
Normal file
127
packages/runtime/src/traits/core/validation.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import React from 'react';
|
||||
import { createTrait } from '@meta-ui/core';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { TraitImplementation } from '../../registry';
|
||||
import { min } from 'lodash';
|
||||
|
||||
type ValidationResult = { isValid: boolean; errorMsg: string };
|
||||
type ValidationRule = (text: string) => { isValid: boolean; errorMsg: string };
|
||||
|
||||
const rules = new Map<string, ValidationRule>();
|
||||
|
||||
export function addValidationRule(name: string, rule: ValidationRule) {
|
||||
rules.set(name, rule);
|
||||
}
|
||||
(window as any).rules = rules;
|
||||
|
||||
addValidationRule('email', text => {
|
||||
if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(text)) {
|
||||
return {
|
||||
isValid: true,
|
||||
errorMsg: '',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMsg: '请输入正确的 email',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
addValidationRule('phoneNumber', text => {
|
||||
if (/^1[3456789]\d{9}$/.test(text)) {
|
||||
return {
|
||||
isValid: true,
|
||||
errorMsg: '',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMsg: '请输入正确的手机号码',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
type ValidationProps = {
|
||||
value: string;
|
||||
minLength: number;
|
||||
maxLength: number;
|
||||
rule: string;
|
||||
};
|
||||
|
||||
const useValidationTrait: TraitImplementation<ValidationProps> = props => {
|
||||
const { value, minLength, maxLength, rule, mergeState } = props;
|
||||
let result: ValidationResult = {
|
||||
isValid: true,
|
||||
errorMsg: '',
|
||||
};
|
||||
|
||||
if (value.length > maxLength) {
|
||||
result = {
|
||||
isValid: false,
|
||||
errorMsg: `最长不能超过${maxLength}个字符`,
|
||||
};
|
||||
} else if (value.length < minLength) {
|
||||
result = {
|
||||
isValid: false,
|
||||
errorMsg: `不能少于${minLength}个字符`,
|
||||
};
|
||||
}
|
||||
|
||||
const rulesArr = rule.split(',');
|
||||
for (const ruleName of rulesArr) {
|
||||
const validateFunc = rules.get(ruleName);
|
||||
if (validateFunc) {
|
||||
result = validateFunc(value);
|
||||
if (!result.isValid) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
data: {
|
||||
validationResult: result,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ValidationValuePropertySchema = Type.String();
|
||||
const ValidationRulePropertySchema = Type.String();
|
||||
const ValidationMinLengthPropertySchema = Type.Integer();
|
||||
const ValidationMaxLengthPropertySchema = Type.Integer();
|
||||
|
||||
export default {
|
||||
...createTrait({
|
||||
version: 'core/v1',
|
||||
metadata: {
|
||||
name: 'validation',
|
||||
description: 'validation trait',
|
||||
},
|
||||
spec: {
|
||||
properties: [
|
||||
{
|
||||
name: 'value',
|
||||
...ValidationValuePropertySchema,
|
||||
},
|
||||
{
|
||||
name: 'rule',
|
||||
...ValidationRulePropertySchema,
|
||||
},
|
||||
{
|
||||
name: 'minLength',
|
||||
...ValidationMinLengthPropertySchema,
|
||||
},
|
||||
{
|
||||
name: 'maxLength',
|
||||
...ValidationMaxLengthPropertySchema,
|
||||
},
|
||||
],
|
||||
state: {},
|
||||
methods: [],
|
||||
},
|
||||
}),
|
||||
impl: useValidationTrait,
|
||||
};
|
@ -13,7 +13,9 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
"jsx": "react",
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user