Skip to content

Updates #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jun 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/pr-run-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Validate tests for pull request

on:
pull_request:
types: [ opened, synchronize, reopened ]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Run tests
run: pnpm run test
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ export default defineConfig({
],

socialLinks: [
{ icon: 'github', link: 'https://github.com/kevinkosterr/vue3-form-generator' }
{ icon: 'github', link: 'https://github.com/kevinkosterr/vue3-form-generator' },
{ icon: 'npm', link: 'https://www.npmjs.com/package/@kevinkosterr/vue3-form-generator' }
]
},
vite: {
Expand Down
90 changes: 44 additions & 46 deletions src/FormGenerator.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,45 @@
<template>
<form
v-if="props.schema !== undefined" :id="props.id ?? ''"
class="vue-form-generator"
:enctype="enctype"
@submit.prevent="onSubmit"
@reset.prevent="onReset"
>
<fieldset v-if="props.schema.fields">
<template v-for="field in props.schema.fields" :key="field">
<FormGroup
:ref="el => (el && '$el' in el) ? fieldElements.push((el as FormGroupInstance)) : null"
:form-options="formOptions"
:field="field"
:model="props.model"
@value-updated="updateGeneratorModel"
@validated="onFieldValidated"
/>
</template>
<template v-for="group in props.schema.groups" :key="group">
<fieldset>
<legend v-if="group.legend">
{{ group.legend }}
</legend>
<template v-for="field in group.fields" :key="field">
<FormGroup
:ref="el => (el && '$el' in el) ? fieldElements.push((el as FormGroupInstance)) : null"
:form-options="formOptions"
:field="field"
:model="props.model"
@value-updated="updateGeneratorModel"
@validated="onFieldValidated"
/>
</template>
</fieldset>
</template>
</fieldset>
</form>
</template>

<script setup lang="ts">
import type { FieldValidation, FormGeneratorProps, FormOptions } from '@/resources/types/generic'
import type { Field } from '@/resources/types/field/fields'
import type { ComponentPublicInstance, ComputedRef, Ref } from 'vue'
import { computed, ref } from 'vue'
import { resetObjectProperties, toUniqueArray } from '@/helpers'
Expand All @@ -11,7 +50,8 @@ const emits = defineEmits([ 'submit', 'field-validated' ])
const props = withDefaults(defineProps<FormGeneratorProps>(), {
enctype: 'application/x-www-form-urlencoded',
id: '',
idPrefix: ''
idPrefix: '', // Kept for compatibility reasons.
options: () => ({})
})

type FormGroupInstance = ComponentPublicInstance<InstanceType<typeof FormGroup>>
Expand Down Expand Up @@ -52,9 +92,7 @@ const hasErrors = computed(() => {
return Boolean(Object.values(formErrors.value).map(e => Boolean(e.length)).filter(e => e).length)
})

/**
* Handle the submit event from the form element.
*/
/** Handle the submit event from the form element. */
const onSubmit = () => {
if (!hasErrors.value) emits('submit')
}
Expand All @@ -66,44 +104,4 @@ const onReset = () => {
}

defineExpose({ hasErrors, formErrors })
</script>

<template>
<form
v-if="props.schema !== undefined" :id="props.id ?? ''"
class="vue-form-generator"
:enctype="enctype"
@submit.prevent="onSubmit"
@reset.prevent="onReset"
>
<fieldset v-if="props.schema.fields">
<template v-for="field in props.schema.fields" :key="field">
<FormGroup
:ref="el => (el && '$el' in el) ? fieldElements.push((el as FormGroupInstance)) : null"
:form-options="formOptions"
:field="field"
:model="props.model"
@value-updated="updateGeneratorModel"
@validated="onFieldValidated"
/>
</template>
<template v-for="group in props.schema.groups" :key="group">
<fieldset>
<legend v-if="group.legend">
{{ group.legend }}
</legend>
<template v-for="field in group.fields" :key="field">
<FormGroup
:ref="el => (el && '$el' in el) ? fieldElements.push((el as FormGroupInstance)) : null"
:form-options="formOptions"
:field="field"
:model="props.model"
@value-updated="updateGeneratorModel"
@validated="onFieldValidated"
/>
</template>
</fieldset>
</template>
</fieldset>
</form>
</template>
</script>
103 changes: 51 additions & 52 deletions src/FormGroup.vue
Original file line number Diff line number Diff line change
@@ -1,53 +1,5 @@
<script setup>
import { computed, useTemplateRef } from 'vue'
import { getFieldComponentName } from '@/helpers'

const fieldComponent = useTemplateRef('fieldComponent')

const props = defineProps({
formOptions: {
type: Object,
default: () => ({})
},
model: {
type: Object,
required: true
},
field: {
type: Object,
required: true
},
errors: {
type: Array,
default: () => []
}
})

const emit = defineEmits([ 'value-updated', 'validated' ])

function onInput (value) {
emit('value-updated', { model: props.field.model, value })
}

function onValidated (isValid, fieldErrors, field) {
emit('validated', { isValid, fieldErrors, field })
}

/** Computed */
const fieldId = computed(() => {
return `${props.formOptions.idPrefix ? props.formOptions.idPrefix + '_' : ''}${props.field.name}`
})

const shouldHaveLabel = computed(() => {
if (fieldComponent.value?.noLabel || props.field.noLabel === true) {
return false
}
return props.field.label
})
</script>

<template>
<div class="form-group">
<div class="form-group" :style="fieldStyle">
<label v-if="shouldHaveLabel" :for="fieldId">
<span> {{ props.field.label }}</span>
</label>
Expand All @@ -65,14 +17,61 @@ const shouldHaveLabel = computed(() => {
/>
</div>

<div v-if="fieldComponent && fieldComponent.hint" class="hints">
<div v-if="fieldComponent && fieldHasHint" class="hints">
<span class="hint">{{ fieldComponent.hint }}</span>
</div>

<div v-if="fieldComponent && fieldComponent.errors && fieldComponent.errors.length" class="errors help-block">
<div v-if="fieldComponent && fieldHasErrors" class="errors help-block">
<template v-for="error in fieldComponent.errors" :key="error">
<span class="error">{{ error }}</span> <br>
</template>
</div>
</div>
</template>
</template>

<script setup lang="ts">
import type { ComputedRef, ShallowRef } from 'vue'
import type { FieldComponent, FormGroupProps } from '@/resources/types/generic'
import type { Field } from '@/resources/types/field/fields'
import { computed, useTemplateRef } from 'vue'
import { getFieldComponentName } from '@/helpers'

const fieldComponent = useTemplateRef('fieldComponent') as Readonly<ShallowRef<FieldComponent | undefined>>

const props = withDefaults(defineProps<FormGroupProps>(), {
formOptions: () => ({}),
errors: () => []
})
const emit = defineEmits([ 'value-updated', 'validated' ])

function onInput (value: any) {
emit('value-updated', { model: props.field.model, value })
}

function onValidated (isValid: boolean, fieldErrors: string[], field: Field) {
emit('validated', { isValid, fieldErrors, field })
}

/** Computed */
const fieldId: ComputedRef<string> = computed(() => {
return `${props.formOptions.idPrefix ? props.formOptions.idPrefix + '_' : ''}${props.field.name}`
})

const fieldStyle: ComputedRef<Record<string, string | undefined>> = computed(() => ({
display: fieldComponent.value && fieldComponent.value.isVisible ? undefined : 'none'
}))

const fieldHasErrors: ComputedRef<boolean> = computed(() => {
return Boolean(fieldComponent.value && fieldComponent.value.errors && fieldComponent.value.errors.length)
})
const fieldHasHint: ComputedRef<boolean> = computed(() => {
return Boolean(fieldComponent.value && fieldComponent.value.hint)
})

const shouldHaveLabel: ComputedRef<boolean> = computed(() => {
if (fieldComponent.value?.noLabel || props.field.noLabel === true) {
return false
}
return Boolean(props.field.label)
})
</script>
2 changes: 1 addition & 1 deletion src/directives/onClickOutside.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Directive, DirectiveBinding } from 'vue'

const onClickOutside: Directive<HTMLElement, never> = {
const onClickOutside: Directive<HTMLElement, (event: Event) => void> = {
beforeMount(el: HTMLElement, binding: DirectiveBinding): void {
el.clickOutsideEvent = (event: Event) => {
if (!(el === event.target || el.contains(<Node>event.target))) {
Expand Down
11 changes: 9 additions & 2 deletions src/fields/core/FieldButton.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
<template>
<button type="button" :class="field.buttonClasses" @click.prevent="onClick">
<button
type="button"
:disabled="isDisabled"
:class="field.buttonClasses"
@click.prevent="onClick"
>
{{ field.buttonText }}
</button>
</template>

<script setup lang="ts">
import { toRefs } from 'vue'
import { useFieldAttributes } from '@/composables'
import type { ButtonField, FieldPropRefs, FieldProps } from '@/resources/types/field/fields'

const props = defineProps<FieldProps<ButtonField>>()

const { model, field }: FieldPropRefs<ButtonField> = toRefs(props)
const { isVisible, hint, isDisabled } = useFieldAttributes(model.value, field.value)

const onClick = () => {
return field.value.onClick !== undefined ? field.value.onClick(model.value, field.value) : undefined
}

defineExpose({ noLabel: true })
defineExpose({ noLabel: true, isVisible, hint })
</script>
6 changes: 3 additions & 3 deletions src/fields/core/FieldCheckbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const props = defineProps<FieldProps<CheckboxField>>()
const { field, model }: FieldPropRefs<CheckboxField> = toRefs(props)

const { currentModelValue } = useFormModel(model.value, field.value)
const { isRequired, isDisabled, hint } = useFieldAttributes(model.value, field.value)
const { isRequired, isDisabled, isVisible, hint } = useFieldAttributes(model.value, field.value)
const { errors, validate } = useFieldValidate(
model.value,
field.value,
Expand All @@ -50,5 +50,5 @@ const onFieldValueChanged = (event: Event) => {
emits('onInput', target.checked)
}

defineExpose({ hint, noLabel: true, errors })
</script>
defineExpose({ hint, noLabel: true, errors, isVisible })
</script>
6 changes: 3 additions & 3 deletions src/fields/core/FieldChecklist.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const emits = defineEmits(useFieldEmits())
const props = defineProps<FieldProps<ChecklistField>>()

const { field, model }: FieldPropRefs<ChecklistField> = toRefs(props)
const { hint } = useFieldAttributes(model.value, field.value)
const { hint, isVisible } = useFieldAttributes(model.value, field.value)
const { currentModelValue }: { currentModelValue: Ref<any[]> } = useFormModel(model.value, field.value)
const { validate, errors } = useFieldValidate(model.value, field.value)

Expand Down Expand Up @@ -63,5 +63,5 @@ const onFieldValueChanged = (event: Event) => {
})
}

defineExpose({ hint, errors })
</script>
defineExpose({ hint, errors, isVisible })
</script>
6 changes: 3 additions & 3 deletions src/fields/core/FieldMask.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const maskOptions: ComputedRef<MaskInputOptions> = computed(() => {
})

const { currentModelValue } = useFormModel(model.value, field.value)
const { isRequired, isDisabled, hint } = useFieldAttributes(model.value, field.value)
const { isRequired, isDisabled, isVisible, hint } = useFieldAttributes(model.value, field.value)
const { errors, validate } = useFieldValidate(
model.value,
field.value,
Expand Down Expand Up @@ -76,5 +76,5 @@ onBeforeMount(() => {
}
})

defineExpose({ unmaskedValue, hint, errors })
</script>
defineExpose({ unmaskedValue, hint, errors, isVisible })
</script>
6 changes: 3 additions & 3 deletions src/fields/core/FieldNumber.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const emits = defineEmits(useFieldEmits())

const { field, model }: FieldPropRefs<NumberField> = toRefs(props)

const { isDisabled, isRequired, hint } = useFieldAttributes(model.value, field.value)
const { isDisabled, isRequired, isVisible, hint } = useFieldAttributes(model.value, field.value)
const { currentModelValue } = useFormModel(model.value, field.value)
const { errors, validate } = useFieldValidate(
model.value,
Expand Down Expand Up @@ -60,5 +60,5 @@ const onFieldValueChanged = (event: Event) => {
emits('onInput', parseFloat(target.value))
}

defineExpose({ hint, errors })
</script>
defineExpose({ hint, errors, isVisible })
</script>
Loading