Skip to content

#28 FieldObject #32

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 6 commits into from
May 19, 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
1 change: 1 addition & 0 deletions apps/docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export default defineConfig({
{ text: 'FieldColor', link: '/guide/fields/FieldColor' },
{ text: 'FieldMask', link: '/guide/fields/FieldMask' },
{ text: 'FieldNumber', link: '/guide/fields/FieldNumber' },
{ text: 'FieldObject', link: '/guide/fields/FieldObject' },
{ text: 'FieldPassword', link: '/guide/fields/FieldPassword' },
{ text: 'FieldRadio', link: '/guide/fields/FieldRadio' },
{ text: 'FieldReset', link: '/guide/fields/FieldReset' },
Expand Down
63 changes: 63 additions & 0 deletions apps/docs/components/examples/fields/FieldObjectExample.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<template>
Person: <code>{{ form.model.person }}</code>
<vue-form-generator :schema="form.schema" :model="form.model" />
</template>

<script setup>
import { onBeforeMount, ref } from 'vue'

const props = defineProps({
addValidators: Boolean
})

const form = ref({
model: {
person: {
name: '',
surname: '',
age: null
}
},
schema: {
fields: [
{
type: 'object',
model: 'person',
schema: {
fields: [
{
type: 'input',
inputType: 'text',
model: 'name',
label: 'Name'
},
{
type: 'input',
inputType: 'text',
model: 'surname',
label: 'Surname'
},
{
type: 'input',
inputType: 'number',
model: 'age',
label: 'Age'
}
]
}
}
]
}
})

onBeforeMount(() => {
if (props.addValidators) {
const fields = form.value.schema.fields[0].schema.fields

const minLengthThree = (value) => value && value.length >= 3
fields[0].validator = minLengthThree
fields[1].validator = minLengthThree
fields[2].validator = (value) => value && value >= 18
}
})
</script>
112 changes: 112 additions & 0 deletions apps/docs/guide/fields/FieldObject.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# FieldObject
`FieldObject` is a field that has its own `schema`, meaning the field itself
renders other fields. These fields will return their values to the object inside
the model that is assigned to the `FieldObject` component.

### type `object`

<script setup>
import FieldObjectExample from '/components/examples/fields/FieldObjectExample.vue'
</script>

## Basic example
::: details Code
```js
const form = ref({
model: {
person: {
name: '',
surname: '',
age: null
}
},
schema: {
fields: [
{
type: 'object',
model: 'person',
schema: {
fields: [
{
type: 'input',
inputType: 'text',
model: 'name',
label: 'Name'
},
{
type: 'input',
inputType: 'text',
model: 'surname',
label: 'Surname'
},
{
type: 'input',
inputType: 'number',
model: 'age',
label: 'Age'
}
]
}
}
]
}
})
```
:::
<FieldObjectExample/>

## With validators
::: details Code
```js
function minLengthThree (value) {
return value && value.length >= 3
}

function overEighteen (value) {
return value && value >= 18
}

// ......
fields: [
{
type: 'object',
model: 'person',
schema: {
fields: [
{
type: 'input',
inputType: 'text',
model: 'name',
label: 'Name',
validator: minLengthThree
},
{
type: 'input',
inputType: 'text',
model: 'surname',
label: 'Surname',
validator: minLengthThree
},
{
type: 'input',
inputType: 'number',
model: 'age',
label: 'Age',
validator: overEighteen
}
]
}
}
]
```
:::
::: info
In this example, `name` and `surname` must have a length of three letters or more, `age` must be at least 18.
:::
<FieldObjectExample add-validators/>


## Properties
| Property | Default | Type | Description |
|----------|---------|----------|---------------------------------------------|
| schema | `{}` | `Object` | A form schema, as seen in `FormGenerator.vue` |
25 changes: 24 additions & 1 deletion apps/docs/guide/form-generator/events.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
These are events emitted by the `vue-form-generator` component.

## `field-validated`
Emitted when a field inside the form has been validated.

### Event arguments
- `validations` - an object with the field's errors and the field schema
- `fieldErrors` - an array of error messages that have been thrown during validations;
- `field` - the field as defined in the schema

An example from the [`FieldObject`](/guide/fields/FieldObject) component:
```vue [FieldObject.vue]
<script setup>
const onFieldValidated = (validation) => {
const key = `${field.value.model}.${validation.field.model}`
emits(
'validated',
validation.fieldErrors.length === 0,
validation.fieldErrors,
{ ...field.value, model: key }
)
}
</script>
```

## `submit`
Emitted when all fields have been validated and no errors occurred during said validations.
Emitted when all fields have been validated and no errors occurred during said validations.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"main": "./dist/vue3-form-generator.js",
"scripts": {
"dev": "vite",
"dev:sass": "sass --watch src/scss/themes:playground/css/",
"dev:sass": "sass --watch src/scss/themes:apps/playground/css/",
"test": "vitest",
"build": "vite build && sass src/scss/themes/:dist/themes/",
"preview": "vite preview",
Expand Down
5 changes: 3 additions & 2 deletions src/FormGenerator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { computed, ref } from 'vue'
import { resetObjectProperties, toUniqueArray } from '@/helpers'
import FormGroup from './FormGroup.vue'

const emits = defineEmits([ 'submit' ])
const emits = defineEmits([ 'submit', 'field-validated' ])

const props = defineProps({
id: {
Expand Down Expand Up @@ -55,6 +55,7 @@ const updateGeneratorModel = ({ model, value }) => {
* @param field field schema object that has been validated.
*/
const onFieldValidated = ({ fieldErrors, field }) => {
emits('field-validated', { fieldErrors, field })
if (!fieldErrors.length) {
if (!(field.model in formErrors.value)) return
else {
Expand Down Expand Up @@ -82,7 +83,7 @@ const onReset = () => {
props.model = resetObjectProperties(props.model)
}

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

<template>
Expand Down
43 changes: 43 additions & 0 deletions src/fields/core/FieldObject.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<template>
<div class="field-object">
<FormGenerator
ref="formGenerator"
:schema="field.schema"
:model="currentModelValue"
:options="props.formOptions"
@field-validated="onFieldValidated"
/>
</div>
</template>

<script setup>
import FormGenerator from '@/FormGenerator.vue'
import { useFieldProps, useFieldEmits, useFormModel } from '@/composables'
import { toRefs, useTemplateRef, computed } from 'vue'

const emits = defineEmits(useFieldEmits())
const props = defineProps(useFieldProps())

const formGenerator = useTemplateRef('formGenerator')
const hasErrors = computed(() => formGenerator.value?.hasErrors ?? false)

const { field, model } = toRefs(props)

const { currentModelValue } = useFormModel(model.value, field.value)

/**
* Emits the validated event
* @param validation
*/
const onFieldValidated = (validation) => {
const key = `${field.value.model}.${validation.field.model}`
emits(
'validated',
validation.fieldErrors.length === 0,
validation.fieldErrors,
{ ...field.value, model: key }
)
}

defineExpose({ hasErrors })
</script>
3 changes: 2 additions & 1 deletion src/fields/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import FieldTextarea from '@/fields/core/FieldTextarea.vue'
import FieldMask from '@/fields/core/FieldMask.vue'
import FieldChecklist from '@/fields/core/FieldChecklist.vue'
import FieldCheckbox from '@/fields/core/FieldCheckbox.vue'
import FieldObject from '@/fields/core/FieldObject.vue'

import FieldSubmit from '@/fields/core/FieldSubmit.vue'
import FieldReset from '@/fields/core/FieldReset.vue'
Expand All @@ -22,7 +23,7 @@ import FieldButton from '@/fields/core/FieldButton.vue'
const fieldComponents = {
FieldColor, FieldText, FieldCheckBox, FieldPassword, FieldSelect, FieldSelectNative, FieldRadio,
FieldNumber, FieldSubmit, FieldReset, FieldButton, FieldSwitch, FieldTextarea, FieldMask, FieldChecklist,
FieldCheckbox
FieldCheckbox, FieldObject
} as const

type FieldComponentNames = keyof typeof fieldComponents
Expand Down
4 changes: 2 additions & 2 deletions tests/_resources/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ export function generateSchemaSingleField (
/**
* Generate props for a single field component
* @param {Object} formSchema - entire form schema object
* @returns {{field: *, model, id: string, formGenerator: {}}}
* @returns {{field: *, model, id: string, formOptions: {}}}
*/
export function generatePropsSingleField (formSchema) {
return {
id: formSchema.name + '_test_id',
formGenerator: {},
formOptions: {},
field: { ...formSchema.schema.fields[0] },
model: { ...formSchema.model }
}
Expand Down
55 changes: 55 additions & 0 deletions tests/components/FormGenerator.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { expect, it, describe, beforeAll } from 'vitest'
import { config, mount } from '@vue/test-utils'

import FormGenerator from '@/FormGenerator.vue'
import FieldText from '@/fields/core/FieldText.vue'
import FieldTextarea from '@/fields/core/FieldTextarea.vue'
import FieldSubmit from '@/fields/core/FieldSubmit.vue'
import { generateSchemaSingleField } from '@test/_resources/utils.js'

beforeAll(() => {
config.global.components = { FieldText, FieldTextarea, FieldSubmit }
})

const textSchema = generateSchemaSingleField(
'text',
'textModel',
'input',
'text',
'Text input label',
'',
{
required: true
}
)

const textAreaSchema = generateSchemaSingleField(
'textArea',
'textAreaModel',
'textarea',
null,
'Text area label',
'',
{}
)

const schema = {
schema: { fields: [ ...textSchema.schema.fields, ...textAreaSchema.schema.fields ] },
model: { ...textSchema.model, ...textAreaSchema.model }
}

describe('FormGenerator', () => {

it('Shouldn\'t render without a schema', async () => {
const wrapper = mount(FormGenerator)
expect(wrapper.find('form').exists()).toBeFalsy()
})

it('Should render with a schema', async () => {
const wrapper = mount(FormGenerator, { props: { model: schema.model, schema: schema.schema } })
expect(wrapper.find('form').exists()).toBeTruthy()
expect(wrapper.findComponent(FieldText).exists()).toBeTruthy()
expect(wrapper.findComponent(FieldTextarea).exists()).toBeTruthy()
})

})
Loading