refactor(editor): migrate FormInput to Composition API (#4406)

♻️ Refactor N8nFormInput to use composition API and make labels accesible
This commit is contained in:
OlegIvaniv
2022-10-24 09:39:22 +02:00
committed by GitHub
parent a40deef518
commit 8a4b9722c5
5 changed files with 170 additions and 199 deletions

View File

@@ -19,7 +19,9 @@ const Template = (args, { argTypes }) => ({
components: { components: {
N8nFormInput, N8nFormInput,
}, },
template: '<n8n-form-input v-bind="$props" v-model="val" @input="onInput" @change="onChange" @focus="onFocus" />', template: `
<n8n-form-input v-bind="$props" v-model="val" @input="onInput" @change="onChange" @focus="onFocus" />
`,
methods, methods,
data() { data() {
return { return {

View File

@@ -4,11 +4,11 @@
v-bind="$props" v-bind="$props"
@input="onInput" @input="onInput"
@focus="onFocus" @focus="onFocus"
ref="input" ref="inputRef"
></n8n-checkbox> />
<n8n-input-label v-else :label="label" :tooltipText="tooltipText" :required="required && showRequiredAsterisk"> <n8n-input-label v-else :inputName="name" :label="label" :tooltipText="tooltipText" :required="required && showRequiredAsterisk">
<div :class="showErrors ? $style.errorInput : ''" @keydown.stop @keydown.enter="onEnter"> <div :class="showErrors ? $style.errorInput : ''" @keydown.stop @keydown.enter="onEnter">
<slot v-if="hasDefaultSlot"></slot> <slot v-if="hasDefaultSlot" />
<n8n-select <n8n-select
v-else-if="type === 'select' || type === 'multi-select'" v-else-if="type === 'select' || type === 'multi-select'"
:value="value" :value="value"
@@ -17,7 +17,8 @@
@change="onInput" @change="onInput"
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
ref="input" :name="name"
ref="inputRef"
> >
<n8n-option <n8n-option
v-for="option in (options || [])" v-for="option in (options || [])"
@@ -28,6 +29,7 @@
</n8n-select> </n8n-select>
<n8n-input <n8n-input
v-else v-else
:name="name"
:type="type" :type="type"
:placeholder="placeholder" :placeholder="placeholder"
:value="value" :value="value"
@@ -36,11 +38,11 @@
@input="onInput" @input="onInput"
@blur="onBlur" @blur="onBlur"
@focus="onFocus" @focus="onFocus"
ref="input" ref="inputRef"
/> />
</div> </div>
<div :class="$style.errors" v-if="showErrors"> <div :class="$style.errors" v-if="showErrors">
<span>{{ validationError }}</span> <span v-text="validationError" />
<n8n-link <n8n-link
v-if="documentationUrl && documentationText" v-if="documentationUrl && documentationText"
:to="documentationUrl" :to="documentationUrl"
@@ -52,13 +54,14 @@
</n8n-link> </n8n-link>
</div> </div>
<div :class="$style.infoText" v-else-if="infoText"> <div :class="$style.infoText" v-else-if="infoText">
<span size="small">{{ infoText }}</span> <span size="small" v-text="infoText" />
</div> </div>
</n8n-input-label> </n8n-input-label>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import Vue from 'vue'; import { computed, reactive, onMounted, ref, watch, useSlots } from 'vue';
import N8nInput from '../N8nInput'; import N8nInput from '../N8nInput';
import N8nSelect from '../N8nSelect'; import N8nSelect from '../N8nSelect';
import N8nOption from '../N8nOption'; import N8nOption from '../N8nOption';
@@ -68,185 +71,137 @@ import N8nCheckbox from '../N8nCheckbox';
import { getValidationError, VALIDATORS } from './validators'; import { getValidationError, VALIDATORS } from './validators';
import { Rule, RuleGroup, IValidator } from "../../types"; import { Rule, RuleGroup, IValidator } from "../../types";
import Locale from '../../mixins/locale'; import { t } from '../../locale';
import mixins from 'vue-typed-mixins';
export default mixins(Locale).extend({ export interface Props {
name: 'n8n-form-input', value: any;
components: { label: string;
N8nInput, infoText?: string;
N8nInputLabel, required?: boolean;
N8nOption, showRequiredAsterisk?: boolean;
N8nSelect, type?: string;
N8nCheckbox, placeholder?: string;
}, tooltipText?: string;
data() { showValidationWarnings?: boolean;
return { validateOnBlur?: boolean;
hasBlurred: false, documentationUrl?: string;
isTyping: false, documentationText?: string;
}; validationRules?: Array<Rule | RuleGroup>;
}, validators?: {[key: string]: IValidator | RuleGroup};
props: { maxlength?: number;
value: { options?: Array<{value: string | number, label: string}>;
}, autocomplete?: string;
label: { name?: string;
type: String, focusInitially?: boolean;
}, labelSize?: 'small' | 'medium';
infoText: { }
type: String,
},
required: {
type: Boolean,
},
showRequiredAsterisk: {
type: Boolean,
default: true,
},
type: {
type: String,
default: 'text',
},
placeholder: {
type: String,
},
tooltipText: {
type: String,
},
showValidationWarnings: {
type: Boolean,
},
validateOnBlur: {
type: Boolean,
default: true,
},
documentationUrl: {
type: String,
},
documentationText: {
type: String,
default: 'Open docs',
},
validationRules: {
type: Array,
},
validators: {
type: Object,
},
maxlength: {
type: Number,
},
options: {
type: Array,
},
autocomplete: {
type: String,
},
focusInitially: {
type: Boolean,
},
labelSize: {
type: String,
default: 'medium',
validator: (value: string): boolean =>
['small', 'medium'].includes(value),
},
},
mounted() {
this.$emit('validate', !this.validationError);
if (this.focusInitially && this.$refs.input) { const props = withDefaults(defineProps<Props>(), {
(this.$refs.input as HTMLTextAreaElement).focus(); documentationText: 'Open docs',
} labelSize: 'medium',
}, type: 'text',
computed: { showRequiredAsterisk: true,
validationError(): string | null { validateOnBlur: true,
const error = this.getValidationError();
if (error) {
return this.t(error.messageKey, error.options);
}
return null;
},
hasDefaultSlot(): boolean {
return !!this.$slots.default;
},
showErrors(): boolean {
return (
!!this.validationError &&
((this.validateOnBlur && this.hasBlurred && !this.isTyping) || this.showValidationWarnings)
);
},
},
methods: {
getValidationError(): ReturnType<IValidator['validate']> {
const rules = (this.validationRules || []) as Array<Rule | RuleGroup>;
const validators = {
...VALIDATORS,
...(this.validators || {}),
} as { [key: string]: IValidator | RuleGroup };
if (this.required) {
const error = getValidationError(this.value, validators, validators.REQUIRED as IValidator);
if (error) {
return error;
}
}
for (let i = 0; i < rules.length; i++) {
if (rules[i].hasOwnProperty('name')) {
const rule = rules[i] as Rule;
if (validators[rule.name]) {
const error = getValidationError(
this.value,
validators,
validators[rule.name] as IValidator,
rule.config,
);
if (error) {
return error;
}
}
}
if (rules[i].hasOwnProperty('rules')) {
const rule = rules[i] as RuleGroup;
const error = getValidationError(
this.value,
validators,
rule,
);
if (error) {
return error;
}
}
}
return null;
},
onBlur() {
this.hasBlurred = true;
this.isTyping = false;
this.$emit('blur');
},
onInput(value: any) {
this.isTyping = true;
this.$emit('input', value);
},
onFocus() {
this.$emit('focus');
},
onEnter(event: Event) {
event.stopPropagation();
event.preventDefault();
this.$emit('enter');
},
},
watch: {
validationError(newValue: string | null, oldValue: string | null) {
this.$emit('validate', !newValue);
},
},
}); });
const emit = defineEmits<{
(event: 'validate', shouldValidate: boolean): void,
(event: 'input', value: any): void,
(event: 'focus'): void,
(event: 'blur'): void,
(event: 'enter'): void,
}>();
const state = reactive({
hasBlurred: false,
isTyping: false,
});
const slots = useSlots();
const inputRef = ref<HTMLTextAreaElement | null>(null);
function getInputValidationError(): ReturnType<IValidator['validate']> {
const rules = props.validationRules || [];
const validators = {
...VALIDATORS,
...(props.validators || {}),
} as { [key: string]: IValidator | RuleGroup };
if (props.required) {
const error = getValidationError(props.value, validators, validators.REQUIRED as IValidator);
if (error) return error;
}
for (let i = 0; i < rules.length; i++) {
if (rules[i].hasOwnProperty('name')) {
const rule = rules[i] as Rule;
if (validators[rule.name]) {
const error = getValidationError(
props.value,
validators,
validators[rule.name] as IValidator,
rule.config,
);
if (error) return error;
}
}
if (rules[i].hasOwnProperty('rules')) {
const rule = rules[i] as RuleGroup;
const error = getValidationError(
props.value,
validators,
rule,
);
if (error) return error;
}
}
return null;
}
function onBlur() {
state.hasBlurred = true;
state.isTyping = false;
emit('blur');
}
function onInput(value: any) {
state.isTyping = true;
emit('input', value);
}
function onFocus() {
emit('focus');
}
function onEnter(event: Event) {
event.stopPropagation();
event.preventDefault();
emit('enter');
}
const validationError = computed<string | null>(() => {
const error = getInputValidationError();
return error ? t(error.messageKey, error.options) : null;
});
const hasDefaultSlot = computed(() => !!slots.default);
const showErrors = computed(() => (
!!validationError.value &&
((props.validateOnBlur && state.hasBlurred && !state.isTyping) || props.showValidationWarnings)
));
onMounted(() => {
if (props.focusInitially && inputRef.value) inputRef.value.focus();
});
watch(() => validationError.value, (error) => emit('validate', !error));
defineExpose({ inputRef });
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@@ -14,6 +14,7 @@
<n8n-form-input <n8n-form-input
v-else v-else
v-bind="input.properties" v-bind="input.properties"
:name="input.name"
:value="values[input.name]" :value="values[input.name]"
:showValidationWarnings="showValidationWarnings" :showValidationWarnings="showValidationWarnings"
@input="(value) => onInput(input.name, value)" @input="(value) => onInput(input.name, value)"

View File

@@ -6,6 +6,7 @@
:autoComplete="autocomplete" :autoComplete="autocomplete"
ref="innerInput" ref="innerInput"
v-on="$listeners" v-on="$listeners"
:name="name"
> >
<template #prepend> <template #prepend>
<slot name="prepend" /> <slot name="prepend" />
@@ -66,6 +67,9 @@ export default Vue.extend({
title: { title: {
type: String, type: String,
}, },
name: {
type: String,
},
autocomplete: { autocomplete: {
type: String, type: String,
default: 'off', default: 'off',

View File

@@ -1,12 +1,16 @@
<template> <template>
<div :class="$style.container"> <div :class="$style.container">
<div v-if="label || $slots.options" :class="{ <label
'n8n-input-label': true, v-if="label || $slots.options"
[this.$style.heading]: !!label, :for="inputName"
[this.$style.underline]: underline, :class="{
[this.$style[this.size]]: true, [$style.inputLabel]: true,
[$style.heading]: !!label,
[$style.underline]: underline,
[$style[size]]: true,
[$style.overflow]: !!$slots.options, [$style.overflow]: !!$slots.options,
}"> }"
>
<div :class="$style.title" v-if="label"> <div :class="$style.title" v-if="label">
<n8n-text :bold="bold" :size="size" :compact="!underline && !$slots.options" :color="color"> <n8n-text :bold="bold" :size="size" :compact="!underline && !$slots.options" :color="color">
{{ label }} {{ label }}
@@ -16,15 +20,15 @@
<span :class="[$style.infoIcon, showTooltip ? $style.visible: $style.hidden]" v-if="tooltipText && label"> <span :class="[$style.infoIcon, showTooltip ? $style.visible: $style.hidden]" v-if="tooltipText && label">
<n8n-tooltip placement="top" :popper-class="$style.tooltipPopper"> <n8n-tooltip placement="top" :popper-class="$style.tooltipPopper">
<n8n-icon icon="question-circle" size="small" /> <n8n-icon icon="question-circle" size="small" />
<div slot="content" v-html="addTargetBlank(tooltipText)"></div> <div slot="content" v-html="addTargetBlank(tooltipText)" />
</n8n-tooltip> </n8n-tooltip>
</span> </span>
<div v-if="$slots.options && label" :class="{[$style.overlay]: true, [$style.visible]: showOptions}"><div></div></div> <div v-if="$slots.options && label" :class="{[$style.overlay]: true, [$style.visible]: showOptions}" />
<div v-if="$slots.options" :class="{[$style.options]: true, [$style.visible]: showOptions}"> <div v-if="$slots.options" :class="{[$style.options]: true, [$style.visible]: showOptions}">
<slot name="options"></slot> <slot name="options"/>
</div> </div>
</div> </label>
<slot></slot> <slot />
</div> </div>
</template> </template>
@@ -54,6 +58,9 @@ export default Vue.extend({
tooltipText: { tooltipText: {
type: String, type: String,
}, },
inputName: {
type: String,
},
required: { required: {
type: Boolean, type: Boolean,
}, },
@@ -88,7 +95,9 @@ export default Vue.extend({
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.inputLabel {
display: block;
}
.container:hover,.inputLabel:hover { .container:hover,.inputLabel:hover {
.infoIcon { .infoIcon {
opacity: 1; opacity: 1;