Skip to content

Instantly share code, notes, and snippets.

@razbakov
Last active June 4, 2024 19:10
Show Gist options
  • Save razbakov/efd5ca7ef7fafaadcf332bfa1be3f542 to your computer and use it in GitHub Desktop.
Save razbakov/efd5ca7ef7fafaadcf332bfa1be3f542 to your computer and use it in GitHub Desktop.
Form Builder: json array of fields
<template>
<TForm
v-model="profile"
:fields="profileFields"
class="border-t mt-4 pt-4 space-y-4"
@save="saveProfile"
/>
</template>
<script setup>
const profileFields = [
{
name: 'username',
label: t('profile.username.label'),
register: true,
poster: true,
required: true,
component: 'TInputUsername',
before: t('profile.username.before'),
async validate(value, item) {
if (!value) {
return true
}
const profileRef = (
await db
.collection('profiles')
.where('username', '==', value)
.get()
).docs[0]
if (profileRef?.exists && profileRef?.id !== item?.id) {
return false
}
return true
},
validationError: 'This username is already taken',
},
{
name: 'photo',
label: t('profile.photo'),
poster: true,
component: 'TInputPhoto',
},
{
name: 'name',
label: t('profile.name.label'),
placeholder: t('profile.name.placeholder'),
before: t('profile.name.before'),
},
{
name: 'gender',
label: t('profile.pronounce.label'),
component: 'TInputButtons',
hideSearchBox: true,
poster: true,
register: true,
required: true,
options: genderList,
},
{
name: 'type',
label: t('profile.type'),
component: 'TRichSelect',
hideSearchBox: true,
poster: true,
options: typeList,
},
{
when: (item) => item.type === 'Venue',
name: 'address',
label: 'Address',
labelPosition: 'top',
component: 'TInputAddress',
simple: true,
},
{
when: (item) => item.type === 'Venue',
name: 'rooms',
label: 'Areas/Rooms',
component: 'TInputTextarea',
description: 'One area per line',
},
{
when: (item) => item.type === 'Venue',
name: 'map',
component: 'TInputPhoto',
label: 'Venue map',
width: 1280,
height: 720,
},
{
name: 'styles',
label: t('profile.styles.label'),
labelPosition: 'top',
poster: true,
component: 'TInputStylesSelect2',
tips: t('profile.styles.tips'),
},
{
name: 'bio',
label: t('profile.bio.label'),
poster: true,
component: 'TInputTextarea',
labelPosition: 'top',
before: t('profile.bio.before'),
max: 140,
},
{
name: 'story',
label: t('profile.story.label'),
component: 'TInputTextarea',
labelPosition: 'top',
placeholder: t('profile.story.placeholder'),
before: t('profile.story.before'),
},
{
name: 'learning',
label: t('profile.learning.label'),
component: 'TInputTextarea',
labelPosition: 'top',
description: t('profile.learning.description'),
},
{
name: 'jobs',
label: t('profile.jobs.label'),
component: 'TInputTextarea',
labelPosition: 'top',
description: t('profile.jobs.description'),
},
{
name: 'current',
label: t('profile.current.label'),
register: false,
poster: true,
component: 'TInputPlace',
before: t('profile.current.before'),
},
{
name: 'place',
label: t('profile.living.label'),
register: true,
poster: true,
component: 'TInputPlace',
before: t('profile.living.before'),
},
{
name: 'hometown',
label: t('profile.hometown.label'),
component: 'TInputPlace',
before: t('profile.hometown.before'),
},
{
name: 'locales',
label: t('profile.locales'),
component: 'TInputLanguages',
},
{
name: 'team',
component: 'TInputArray',
children: {
component: 'TInputProfile',
},
label: t('profile.teammembers.label'),
labelPosition: 'top',
},
]
</script>
<template>
<TInput
v-model="internalValue"
trim="[^a-z0-9._\-]+"
maxlength="30"
v-bind="$attrs"
/>
</template>
<script>
export default {
name: 'TInputUsername',
inheritAttrs: false,
props: {
value: {
type: String,
default: '',
},
target: {
type: String,
default: '',
},
item: {
type: Object,
default: () => ({}),
},
lowerCase: {
type: Boolean,
default: false,
},
},
computed: {
internalValue: {
get() {
return this.value
},
set(val) {
let newVal = val
if (this.lowerCase) {
newVal = (newVal || '').toLowerCase()
}
this.$emit('input', newVal)
},
},
},
}
</script>
<template>
<div>
<div v-for="field in visibleFields" :key="field.name" :class="fieldWrapper">
<TField
:value="value ? value[field.name] : ''"
:error="errors[field.name]"
:item="value"
v-bind="field"
:label="getLabel(field)"
@input="(val) => onFieldChange(field, val)"
/>
</div>
<slot name="bottom" />
<div
class="flex justify-end space-x-2 bg-white py-4 border-t z-10 items-center bottom-0 sticky"
>
<slot name="buttons" />
<TButton
v-if="!hideSubmit"
:allow-guests="allowGuests"
type="primary"
:label="submitLabel || $t('form.save')"
@click="save"
/>
</div>
</div>
</template>
<template>
<div v-if="hidden"></div>
<div v-else :class="wrapperClasses" class="w-full grid">
<div v-if="!hideLabel" :class="labelClasses">
<label :for="elementId">
<div v-if="tips" class="float-right">
<TButton
icon="help"
type="icon"
class="text-blue-500"
@click="showTips = true"
/>
</div>
<div class="text-gray-700 font-bold">{{ label }}</div>
</label>
</div>
<div :class="inputWrapperClasses">
<slot name="top" />
<TPreview
v-if="before"
:content="before"
no-typo
class="text-gray-500 text-sm mb-2"
/>
<slot>
<component
:is="component"
:id="elementId"
v-bind="$attrs"
:item="item"
:value.sync="computedValue"
:class="{ 'border-red-500': error }"
v-on="$attrs.listeners"
@input="(val) => $emit('input', set(val))"
/>
</slot>
<div v-if="error" class="field-error text-red-500 text-sm mt-2">
{{ error }}
</div>
<TPreview
v-if="description"
:content="description"
no-typo
class="text-gray-500 text-sm mt-2"
/>
<slot name="bottom" />
</div>
<TPopup v-if="showTips" :title="label" @close="showTips = false">
<TPreview :content="tips" class="my-4 max-w-sm" />
</TPopup>
</div>
</template>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment