tiddy 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,417 @@
1
+ <template>
2
+ <div
3
+ ref="formItemRef"
4
+ :class="formItemClasses"
5
+ :role="isGroup ? 'group' : undefined"
6
+ :aria-labelledby="isGroup ? labelId : undefined"
7
+ >
8
+ <FormLabelWrap
9
+ :is-auto-width="labelStyle.width === 'auto'"
10
+ :update-all="formContext?.labelWidth === 'auto'"
11
+ >
12
+ <component
13
+ :is="labelFor ? 'label' : 'div'"
14
+ v-if="hasLabel"
15
+ :id="labelId"
16
+ :for="labelFor"
17
+ :class="ns.e('label')"
18
+ :style="labelStyle"
19
+ >
20
+ <slot name="label" :label="currentLabel">
21
+ {{ currentLabel }}
22
+ </slot>
23
+ </component>
24
+ </FormLabelWrap>
25
+
26
+ <div :class="ns.e('content')" :style="contentStyle">
27
+ <slot />
28
+ <TransitionGroup :name="`${ns.namespace.value}-zoom-in-top`">
29
+ <slot v-if="shouldShowError" name="error" :error="validateMessage">
30
+ <div :class="validateClasses">
31
+ {{ validateMessage }}
32
+ </div>
33
+ </slot>
34
+ </TransitionGroup>
35
+ </div>
36
+ </div>
37
+ </template>
38
+
39
+ <script lang="ts" setup>
40
+ import {
41
+ computed,
42
+ inject,
43
+ nextTick,
44
+ onBeforeUnmount,
45
+ onMounted,
46
+ provide,
47
+ reactive,
48
+ ref,
49
+ toRefs,
50
+ useAttrs,
51
+ useSlots,
52
+ watch,
53
+ type CSSProperties,
54
+ } from 'vue';
55
+ import AsyncValidator, { type RuleItem, type Rules } from 'async-validator';
56
+ import {
57
+ formContextKey,
58
+ formItemContextKey,
59
+ useFormSize,
60
+ useId,
61
+ useNamespace,
62
+ formItemProps,
63
+ type FormItemContext,
64
+ type FormItemProps as ElFormItemProps,
65
+ type FormItemRule,
66
+ type FormItemValidateState,
67
+ type FormValidateFailure,
68
+ } from 'element-plus';
69
+ import { refDebounced as debounceRef } from '@vueuse/core';
70
+ import { addUnit } from '../utils';
71
+ import { clone, ensureArray, getDeepValue, isBoolean, isFunction, isString, setDeepValue } from 'yatter';
72
+ import FormLabelWrap from './form-label-wrap';
73
+
74
+ export interface FormItemProps extends Partial<ElFormItemProps> {
75
+ messageLabel?: string;
76
+ hideRequiredAsterisk?: boolean;
77
+ }
78
+
79
+ const props = defineProps({
80
+ ...formItemProps,
81
+ messageLabel: String,
82
+ hideRequiredAsterisk: {
83
+ type: Boolean,
84
+ default: undefined,
85
+ },
86
+ });
87
+ const attrs = useAttrs();
88
+ const slots = useSlots();
89
+
90
+ const formContext = inject(formContextKey, undefined);
91
+ const parentFormItemContext = inject(formItemContextKey, undefined);
92
+
93
+ const _size = useFormSize(undefined, { formItem: false });
94
+ const ns = useNamespace((attrs['name-space'] as string) || 'form-item');
95
+
96
+ const labelId = useId().value;
97
+ const inputIds = ref<string[]>([]);
98
+
99
+ const validateState = ref<FormItemValidateState>('');
100
+ const validateStateDebounced = debounceRef(validateState, 100);
101
+ const validateMessage = ref('');
102
+ const formItemRef = ref<HTMLDivElement>();
103
+ // special inline value.
104
+ let initialValue: any = undefined;
105
+ let isResettingField = false;
106
+
107
+ const labelPosition = computed(() => props.labelPosition || formContext?.labelPosition);
108
+
109
+ const labelStyle = computed<CSSProperties>(() => {
110
+ if (labelPosition.value === 'top') {
111
+ return {};
112
+ }
113
+
114
+ const labelWidth = addUnit(props.labelWidth || formContext?.labelWidth || '');
115
+ if (labelWidth) return { width: labelWidth };
116
+ return {};
117
+ });
118
+
119
+ const contentStyle = computed<CSSProperties>(() => {
120
+ if (labelPosition.value === 'top' || formContext?.inline) {
121
+ return {};
122
+ }
123
+ if (!props.label && !props.labelWidth && isNested) {
124
+ return {};
125
+ }
126
+ const labelWidth = addUnit(props.labelWidth || formContext?.labelWidth || '');
127
+ if (!props.label && !slots.label) {
128
+ return { marginLeft: labelWidth };
129
+ }
130
+ return {};
131
+ });
132
+
133
+ const formItemClasses = computed(() => [
134
+ ns.b(),
135
+ ns.m(_size.value),
136
+ ns.is('error', validateState.value === 'error'),
137
+ ns.is('validating', validateState.value === 'validating'),
138
+ ns.is('success', validateState.value === 'success'),
139
+ ns.is('required', isRequired.value || props.required),
140
+ ns.is('no-asterisk', props.hideRequiredAsterisk ?? formContext?.hideRequiredAsterisk),
141
+ formContext?.requireAsteriskPosition === 'right' ? 'asterisk-right' : 'asterisk-left',
142
+ {
143
+ [ns.m('feedback')]: formContext?.statusIcon,
144
+ [ns.m(`label-${labelPosition.value}`)]: labelPosition.value,
145
+ },
146
+ ]);
147
+
148
+ const _inlineMessage = computed(() =>
149
+ isBoolean(props.inlineMessage) ? props.inlineMessage : formContext?.inlineMessage || false,
150
+ );
151
+
152
+ const validateClasses = computed(() => [ns.e('error'), { [ns.em('error', 'inline')]: _inlineMessage.value }]);
153
+
154
+ const propString = computed(() => {
155
+ if (!props.prop) return '';
156
+ return isString(props.prop) ? props.prop : props.prop.join('.');
157
+ });
158
+
159
+ const hasLabel = computed<boolean>(() => {
160
+ return !!(props.label || slots.label);
161
+ });
162
+
163
+ const labelFor = computed<string | undefined>(() => {
164
+ return props.for || (inputIds.value.length === 1 ? inputIds.value[0] : undefined);
165
+ });
166
+
167
+ const isGroup = computed<boolean>(() => {
168
+ return !labelFor.value && hasLabel.value;
169
+ });
170
+
171
+ const isNested = !!parentFormItemContext;
172
+
173
+ const fieldValue = computed(() => {
174
+ const model = formContext?.model;
175
+ if (!model || !props.prop) {
176
+ return;
177
+ }
178
+ return getDeepValue(model, propString.value);
179
+ });
180
+
181
+ const normalizedRules = computed(() => {
182
+ const { required } = props;
183
+
184
+ const rules: FormItemRule[] = [];
185
+
186
+ if (props.rules) {
187
+ rules.push(...ensureArray(props.rules));
188
+ }
189
+
190
+ const formRules = formContext?.rules;
191
+ if (formRules && props.prop) {
192
+ const _rules = getDeepValue<OneOrMore<FormItemRule>>(formRules, propString.value);
193
+ if (_rules) {
194
+ rules.push(...ensureArray(_rules));
195
+ }
196
+ }
197
+ if (required !== undefined) {
198
+ const requiredRules = rules
199
+ .map((rule, i) => [rule, i] as const)
200
+ .filter(([rule]) => Object.keys(rule).includes('required'));
201
+
202
+ if (requiredRules.length > 0) {
203
+ for (const [rule, i] of requiredRules) {
204
+ if (rule.required === required) continue;
205
+ rules[i] = { ...rule, required };
206
+ }
207
+ } else {
208
+ rules.push({ required });
209
+ }
210
+ }
211
+ return rules;
212
+ });
213
+
214
+ const validateEnabled = computed(() => normalizedRules.value.length > 0);
215
+
216
+ const getFilteredRule = (trigger: string) => {
217
+ const rules = normalizedRules.value;
218
+ return (
219
+ rules
220
+ .filter(rule => {
221
+ if (!rule.trigger || !trigger) return true;
222
+ if (Array.isArray(rule.trigger)) {
223
+ return rule.trigger.includes(trigger);
224
+ }
225
+ return rule.trigger === trigger;
226
+ })
227
+ // exclude trigger
228
+
229
+ .map(({ trigger, ...rule }): RuleItem => rule)
230
+ );
231
+ };
232
+
233
+ const isRequired = computed(() => normalizedRules.value.some(rule => rule.required));
234
+
235
+ const shouldShowError = computed(
236
+ () => validateStateDebounced.value === 'error' && props.showMessage && (formContext?.showMessage ?? true),
237
+ );
238
+
239
+ const currentLabel = computed(() => `${props.label || ''}${formContext?.labelSuffix || ''}`);
240
+
241
+ const setValidationState = (state: FormItemValidateState) => {
242
+ validateState.value = state;
243
+ };
244
+
245
+ const onValidationFailed = (error: FormValidateFailure) => {
246
+ const { errors, fields } = error;
247
+ if (!errors || !fields) {
248
+ console.error(error);
249
+ }
250
+ const messageLabel = props.messageLabel || currentLabel.value || propString.value;
251
+ setValidationState('error');
252
+ const message = errors ? (errors?.[0]?.message ?? `${props.prop} is required`) : '';
253
+ validateMessage.value = message.replace(propString.value, messageLabel);
254
+ formContext?.emit('validate', props.prop!, false, validateMessage.value);
255
+ };
256
+
257
+ const onValidationSucceeded = () => {
258
+ setValidationState('success');
259
+ formContext?.emit('validate', props.prop!, true, '');
260
+ };
261
+
262
+ const doValidate = async (rules: RuleItem[]): Promise<true> => {
263
+ const descriptor: Rules = {
264
+ [propString.value]: rules,
265
+ };
266
+ const model = { [propString.value]: fieldValue.value };
267
+
268
+ const validator = new AsyncValidator(descriptor);
269
+ return validator
270
+ .validate(model, { firstFields: true })
271
+ .then(() => {
272
+ onValidationSucceeded();
273
+ return true as const;
274
+ })
275
+ .catch((err: FormValidateFailure) => {
276
+ onValidationFailed(err as FormValidateFailure);
277
+ return Promise.reject(err);
278
+ });
279
+ };
280
+
281
+ const validate: FormItemContext['validate'] = async (trigger, callback) => {
282
+ // skip validation if its resetting
283
+ if (isResettingField || !props.prop) {
284
+ return false;
285
+ }
286
+
287
+ const hasCallback = isFunction(callback);
288
+ if (!validateEnabled.value) {
289
+ callback?.(false);
290
+ return false;
291
+ }
292
+
293
+ const rules = getFilteredRule(trigger);
294
+ if (rules.length === 0) {
295
+ callback?.(true);
296
+ return true;
297
+ }
298
+
299
+ setValidationState('validating');
300
+
301
+ return doValidate(rules)
302
+ .then(() => {
303
+ callback?.(true);
304
+ return true as const;
305
+ })
306
+ .catch((err: FormValidateFailure) => {
307
+ const { fields } = err;
308
+ callback?.(false, fields);
309
+ return hasCallback ? false : Promise.reject(fields);
310
+ });
311
+ };
312
+
313
+ const clearValidate: FormItemContext['clearValidate'] = () => {
314
+ setValidationState('');
315
+ validateMessage.value = '';
316
+ isResettingField = false;
317
+ };
318
+
319
+ const resetField: FormItemContext['resetField'] = async () => {
320
+ const model = formContext?.model;
321
+ if (!model || !props.prop) return;
322
+
323
+ // const computedValue = getDeepValue<any>(model, propString.value);
324
+
325
+ // prevent validation from being triggered
326
+ isResettingField = true;
327
+
328
+ // computedValue.value = clone(initialValue);
329
+ setDeepValue(model, propString.value, clone(initialValue));
330
+
331
+ await nextTick();
332
+ clearValidate();
333
+
334
+ isResettingField = false;
335
+ };
336
+
337
+ const addInputId: FormItemContext['addInputId'] = (id: string) => {
338
+ if (!inputIds.value.includes(id)) {
339
+ inputIds.value.push(id);
340
+ }
341
+ };
342
+
343
+ const removeInputId: FormItemContext['removeInputId'] = (id: string) => {
344
+ inputIds.value = inputIds.value.filter(listId => listId !== id);
345
+ };
346
+
347
+ watch(
348
+ () => props.error,
349
+ val => {
350
+ validateMessage.value = val || '';
351
+ setValidationState(val ? 'error' : '');
352
+ },
353
+ { immediate: true },
354
+ );
355
+
356
+ watch(
357
+ () => props.validateStatus,
358
+ val => setValidationState(val || ''),
359
+ );
360
+
361
+ const context: FormItemContext = reactive({
362
+ ...toRefs(props),
363
+ $el: formItemRef,
364
+ size: _size,
365
+ validateState,
366
+ labelId,
367
+ inputIds,
368
+ isGroup,
369
+ hasLabel,
370
+ fieldValue,
371
+ addInputId,
372
+ removeInputId,
373
+ resetField,
374
+ clearValidate,
375
+ validate,
376
+ });
377
+
378
+ provide(formItemContextKey, context);
379
+
380
+ onMounted(() => {
381
+ if (props.prop) {
382
+ formContext?.addField(context);
383
+ initialValue = clone(fieldValue.value);
384
+ }
385
+ });
386
+
387
+ onBeforeUnmount(() => {
388
+ formContext?.removeField(context);
389
+ });
390
+
391
+ defineExpose({
392
+ /**
393
+ * @description Form item size.
394
+ */
395
+ size: _size,
396
+ /**
397
+ * @description Validation message.
398
+ */
399
+ validateMessage,
400
+ /**
401
+ * @description Validation state.
402
+ */
403
+ validateState,
404
+ /**
405
+ * @description Validate form item.
406
+ */
407
+ validate,
408
+ /**
409
+ * @description Remove validation status of the field.
410
+ */
411
+ clearValidate,
412
+ /**
413
+ * @description Reset current field and remove validation result.
414
+ */
415
+ resetField,
416
+ });
417
+ </script>
@@ -0,0 +1,105 @@
1
+ import { formContextKey, formItemContextKey, useNamespace } from 'element-plus';
2
+ import {
3
+ Fragment,
4
+ computed,
5
+ defineComponent,
6
+ inject,
7
+ nextTick,
8
+ onBeforeUnmount,
9
+ onMounted,
10
+ onUpdated,
11
+ ref,
12
+ watch,
13
+ } from 'vue';
14
+ import { useResizeObserver } from '@vueuse/core';
15
+ import type { CSSProperties } from 'vue';
16
+
17
+ const COMPONENT_NAME = 'ElLabelWrap';
18
+ export default defineComponent({
19
+ name: COMPONENT_NAME,
20
+ props: {
21
+ isAutoWidth: Boolean,
22
+ updateAll: Boolean,
23
+ },
24
+
25
+ setup(props, { slots }) {
26
+ const formContext = inject(formContextKey, undefined);
27
+ const formItemContext = inject(formItemContextKey);
28
+ if (!formItemContext) {
29
+ const error = new Error(`[${COMPONENT_NAME}] usage: <el-form-item><label-wrap /></el-form-item>`);
30
+ error.name = 'ElementPlusError';
31
+ throw error;
32
+ }
33
+ const ns = useNamespace('form');
34
+
35
+ const el = ref<HTMLElement>();
36
+ const computedWidth = ref(0);
37
+
38
+ const getLabelWidth = () => {
39
+ if (el.value?.firstElementChild) {
40
+ const width = window.getComputedStyle(el.value.firstElementChild).width;
41
+ return Math.ceil(Number.parseFloat(width));
42
+ }
43
+ return 0;
44
+ };
45
+
46
+ const updateLabelWidth = (action: 'update' | 'remove' = 'update') => {
47
+ nextTick(() => {
48
+ if (slots.default && props.isAutoWidth) {
49
+ if (action === 'update') {
50
+ computedWidth.value = getLabelWidth();
51
+ } else if (action === 'remove') {
52
+ formContext?.deregisterLabelWidth(computedWidth.value);
53
+ }
54
+ }
55
+ });
56
+ };
57
+ const updateLabelWidthFn = () => updateLabelWidth('update');
58
+
59
+ onMounted(() => {
60
+ updateLabelWidthFn();
61
+ });
62
+ onBeforeUnmount(() => {
63
+ updateLabelWidth('remove');
64
+ });
65
+ onUpdated(() => updateLabelWidthFn());
66
+
67
+ watch(computedWidth, (val, oldVal) => {
68
+ if (props.updateAll) {
69
+ formContext?.registerLabelWidth(val, oldVal);
70
+ }
71
+ });
72
+
73
+ useResizeObserver(
74
+ computed(() => (el.value?.firstElementChild ?? null) as HTMLElement | null),
75
+ updateLabelWidthFn,
76
+ );
77
+
78
+ return () => {
79
+ if (!slots) return null;
80
+
81
+ const { isAutoWidth } = props;
82
+ if (isAutoWidth) {
83
+ const autoLabelWidth = formContext?.autoLabelWidth;
84
+ const hasLabel = formItemContext?.hasLabel;
85
+ const style: CSSProperties = {};
86
+ if (hasLabel && autoLabelWidth && autoLabelWidth !== 'auto') {
87
+ const marginWidth = Math.max(0, Number.parseInt(autoLabelWidth, 10) - computedWidth.value);
88
+ const labelPosition = formItemContext.labelPosition || formContext.labelPosition;
89
+
90
+ const marginPosition = labelPosition === 'left' ? 'marginRight' : 'marginLeft';
91
+
92
+ if (marginWidth) {
93
+ style[marginPosition] = `${marginWidth}px`;
94
+ }
95
+ }
96
+ return (
97
+ <div ref={el} class={[ns.be('item', 'label-wrap')]} style={style}>
98
+ {slots.default?.()}
99
+ </div>
100
+ );
101
+ }
102
+ return <Fragment ref={el}>{slots.default?.()}</Fragment>;
103
+ };
104
+ },
105
+ });
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <ElForm ref="form" v-bind="subProps" @submit.prevent="submit">
3
+ <slot name="prefix"></slot>
4
+ <slot>
5
+ <FormField v-for="field in fields" :key="getKey(field)" v-bind="field" />
6
+ </slot>
7
+ <slot name="suffix"></slot>
8
+ </ElForm>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import {
13
+ ElForm,
14
+ type FormProps as ElFormProps,
15
+ type FormEmits,
16
+ type FormInstance,
17
+ type FormItemProps,
18
+ formProps,
19
+ } from 'element-plus';
20
+ import type { FieldProps } from './interface';
21
+ import { computed, provide, useAttrs, useSlots, useTemplateRef } from 'vue';
22
+ import { cut } from 'yatter';
23
+ import { getKey, getSlotsFactory } from '../utils';
24
+ import { formCtxKey } from './util';
25
+ import FormField from './form-field.vue';
26
+
27
+ export interface FormProps extends Partial<ElFormProps> {
28
+ fields?: FieldProps[];
29
+ item?: FormItemProps;
30
+ }
31
+
32
+ defineOptions({
33
+ name: 'SeForm',
34
+ });
35
+
36
+ type SeFormEmit = FormEmits & {
37
+ submit: [data: any];
38
+ };
39
+
40
+ const props = defineProps({
41
+ ...formProps,
42
+ fields: {
43
+ type: Array<FieldProps>,
44
+ default: () => [],
45
+ },
46
+ item: {
47
+ type: Object,
48
+ default: () => ({}),
49
+ },
50
+ });
51
+ const emit = defineEmits<SeFormEmit>();
52
+ const slots = useSlots();
53
+
54
+ const subProps = computed(() => cut(props, ['fields']));
55
+
56
+ const formRef = useTemplateRef<FormInstance>('form');
57
+
58
+ function reValidateErrorFields() {
59
+ formRef.value?.fields.forEach(field => {
60
+ if (field.validateStatus === 'error') {
61
+ field.validate('');
62
+ }
63
+ });
64
+ }
65
+
66
+ const expose: any = {
67
+ reValidateErrorFields,
68
+ model: computed(() => props.model),
69
+ };
70
+
71
+ defineExpose(
72
+ new Proxy(expose, {
73
+ get(target, key) {
74
+ // @ts-expect-error 忽略表单类型
75
+ return target[key] || formRef.value?.[key];
76
+ },
77
+ has(target, key) {
78
+ return Object.hasOwn(target, key) || Reflect.has(formRef.value!, key);
79
+ },
80
+ }),
81
+ );
82
+
83
+ async function submit() {
84
+ try {
85
+ await formRef.value?.validate();
86
+ emit('submit', props.model);
87
+ } catch {
88
+ // nothing to do
89
+ }
90
+ }
91
+
92
+ provide(formCtxKey, {
93
+ model: props.model,
94
+ itemOption: props.item as FormItemProps,
95
+ getParentSlots: getSlotsFactory(slots),
96
+ });
97
+ </script>
98
+
99
+ <style lang="scss" scoped></style>
@@ -0,0 +1 @@
1
+ export * from './form.vue';
@@ -0,0 +1,61 @@
1
+ import type { FormItemProps, FormItemRule } from 'element-plus';
2
+ import type { AllowedComponentProps, Component, ComputedRef, CSSProperties, Raw } from 'vue';
3
+
4
+ type FormItemLabelProps = Partial<Pick<FormItemProps, 'label' | 'labelPosition' | 'labelWidth'>>;
5
+ type IndexPropFunc<P, T> = P extends 'object' ? T : T | ((index: number) => T);
6
+
7
+ export interface CommonFieldProps<T = 'object'> extends AllowedComponentProps {
8
+ prop?: OrFunction<string>;
9
+ fullProp?: string;
10
+ slots?: OneOrMore<string>;
11
+ rules?: OneOrMore<FormItemRule>;
12
+ hide?: boolean | ComputedRef;
13
+ hideLabel?: IndexProp<T, boolean>;
14
+ label?: IndexProp<T, string>;
15
+ labelWidth?: IndexProp<T, string | number>;
16
+ labelPosition?: IndexProp<T, 'left' | 'right' | 'auto'>;
17
+ }
18
+
19
+ export interface WidgetFieldProps<T = 'object'> extends CommonFieldProps<T> {
20
+ type?: 'widget';
21
+ component: string | Component | Raw<Component>;
22
+ item?: FormItemProps;
23
+ widget?: Record<string, any>;
24
+ on?: Record<string, AnyFunction>;
25
+ prependSlots?: OneOrMore<string>;
26
+ appendSlots?: OneOrMore<string>;
27
+ default?: any;
28
+ }
29
+
30
+ export interface CommonArrayFieldProps {
31
+ type: 'array';
32
+ rawValue?: () => any;
33
+ emptyAction?: string;
34
+ rowAction?: string;
35
+ lineStyle?: CSSProperties;
36
+ mandatory?: boolean;
37
+ }
38
+
39
+ export interface WidgetArrayFieldProps extends CommonArrayFieldProps, Partial<WidgetFieldProps<'array'>> {}
40
+ export interface ObjectArrayFieldProps extends CommonArrayFieldProps, ObjectFieldProps<'array'> {}
41
+
42
+ export type ArrayFieldProps = WidgetArrayFieldProps | ObjectArrayFieldProps;
43
+
44
+ export interface ObjectFieldProps<P = 'object'> extends CommonFieldProps<P> {
45
+ type?: 'object';
46
+ fields: FieldProps[];
47
+ }
48
+
49
+ export interface LayoutFieldProps extends CommonFieldProps {
50
+ type: 'layout';
51
+ fields?: FieldProps[];
52
+ slots?: SlotDef[];
53
+ }
54
+
55
+ export type FieldProps = ArrayFieldProps | ObjectFieldProps | LayoutFieldProps | WidgetFieldProps;
56
+
57
+ export interface FormContext {
58
+ model: any;
59
+ itemOption?: FormItemProps;
60
+ getParentSlots: GetSlotsFunction;
61
+ }