add ability to edit and move form elements

This commit is contained in:
2024-12-31 00:47:25 -07:00
parent 3ccab8ebed
commit 6b4a773bbe
3 changed files with 286 additions and 25 deletions

View File

@ -1,10 +1,13 @@
<template> <template>
<form @submit.prevent="handleSubmit" class=""> <form @submit.prevent="handleSubmit" class="">
<h3 v-if="!!formTitle" class="text-xl font-semibold mb-4">{{formTitle}}</h3> <h3 v-if="!!formTitle" class="text-xl font-semibold mb-4">{{formTitle}}</h3>
<div v-for="input in formInputs" :key="input.id" class="flex flex-col"> <div v-for="(input, index) in formInputs" :key="input.id" class="flex flex-col">
<!-- if showLabel is undefined or set to true then show the label--> <!-- if showLabel is undefined or set to true then show the label-->
<label v-if="input.showLabel === undefined || input.showLabel" :for="input.id" class="mb-2 font-semibold text-gray-700">{{ input.label }}</label> <label v-if="input.showLabel === undefined || input.showLabel" :for="input.id" class="mb-2 font-semibold text-gray-700">{{ input.label }}</label>
<!-- Description -->
<p v-if="input.type === 'description'" class="text-gray-600 mb-2">{{ input.defaultValue }}</p>
<!-- Text, Email, Password, Number --> <!-- Text, Email, Password, Number -->
<input <input
v-if="['text', 'email', 'password', 'number', 'url', 'tel', 'search', 'color', 'date', 'datetime-local', 'month', 'time', 'week'].includes(input.type)" v-if="['text', 'email', 'password', 'number', 'url', 'tel', 'search', 'color', 'date', 'datetime-local', 'month', 'time', 'week'].includes(input.type)"
@ -94,6 +97,13 @@
</ul> </ul>
</div> </div>
<div class="flex flex-row w-auto" v-if="isEditing">
<button class="px-2 bg-gray-200 text-black font-semibold rounded-md shadow-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-300 mt-2 mr-2 " @click.prevent="() => {handleEditRequest(input.id)}">edit</button>
<button class="px-2 bg-gray-200 text-black font-semibold rounded-md shadow-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-300 mt-2" @click.prevent="() => {handleDeleteRequest(input.id)}">delete</button>
<button v-if="index !== 0" class="px-2 bg-gray-200 text-black font-semibold rounded-md shadow-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-300 mt-2 mr-2 ml-auto" @click.prevent="() => {handleMoveUp(input.id)}">move up</button>
<button v-if="index !== formInputs.length -1" :class="index === 0 ? 'ml-auto': ''" class="px-2 bg-gray-200 text-black font-semibold rounded-md shadow-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-300 mt-2" @click.prevent="() => {handleMoveDown(input.id)}">move down</button>
</div>
</div> </div>
<button type="submit" class="px-6 py-3 bg-blue-600 text-white font-semibold rounded-md shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"> <button type="submit" class="px-6 py-3 bg-blue-600 text-white font-semibold rounded-md shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
{{ !!submitLabel ? submitLabel : 'Submit' }} {{ !!submitLabel ? submitLabel : 'Submit' }}
@ -102,7 +112,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref } from 'vue'; import {reactive, ref, watchEffect} from 'vue';
export interface FormInput { export interface FormInput {
id: string; id: string;
@ -117,21 +127,23 @@ export interface FormInput {
options?: { label: string, value: string }[]; // For select, radio, and checkbox types options?: { label: string, value: string }[]; // For select, radio, and checkbox types
} }
const props = defineProps<{ formInputs: FormInput[], submitLabel?: string, formTitle?: string }>(); const props = defineProps<{ formInputs: FormInput[], submitLabel?: string, formTitle?: string, isEditing?: boolean }>();
const emit = defineEmits(['send']) const emit = defineEmits(['send', 'edit', 'delete', 'move-up', 'move-down']);
const formData = reactive<Record<string, any>>({}); const formData = reactive<Record<string, any>>({});
// Initialize formData with default values watchEffect(() => {
props.formInputs.forEach(input => { // Initialize formData with default values
if (input.type === 'checkbox' || input.type === 'multi-input') { props.formInputs.forEach(input => {
// if input.defaultValue is an array, use it, if (input.type === 'checkbox' || input.type === 'multi-input') {
// otherwise create an empty array and // if input.defaultValue is an array, use it,
// if input.defaultValue is a value that matches an option value, insert it into the array // otherwise create an empty array and
formData[input.id] = Array.isArray(input.defaultValue) ? input.defaultValue : (input.defaultValue ? [input.defaultValue] : []); // if input.defaultValue is a value that matches an option value, insert it into the array
} else { formData[input.id] = Array.isArray(input.defaultValue) ? input.defaultValue : (input.defaultValue ? [input.defaultValue] : []);
formData[input.id] = input.defaultValue || ''; } else {
} formData[input.id] = input.defaultValue || '';
}
});
}); });
const multiInputValue = ref<string>(''); const multiInputValue = ref<string>('');
@ -141,6 +153,22 @@ const handleSubmit = () => {
emit('send', formData); emit('send', formData);
}; };
const handleEditRequest = (inputID: string) => {
emit('edit', props.formInputs.find(input => input.id === inputID));
};
const handleDeleteRequest = (inputID: string) => {
emit('delete', inputID);
};
const handleMoveUp = (inputID: string) => {
emit('move-up', inputID);
};
const handleMoveDown = (inputID: string) => {
emit('move-down', inputID);
};
const addMultiInputValue = (id: string) => { const addMultiInputValue = (id: string) => {
if (multiInputValue.value.trim()) { if (multiInputValue.value.trim()) {
formData[id].push(multiInputValue.value.trim()); formData[id].push(multiInputValue.value.trim());

View File

@ -28,7 +28,15 @@
:formInputs="generatedFormInputs" :formInputs="generatedFormInputs"
:submitLabel="submitLabel" :submitLabel="submitLabel"
:formTitle="formTitle" :formTitle="formTitle"
@edit="handleEditRequest"
@delete="handleDeleteRequest"
@move-up="handleMoveUp"
@move-down="handleMoveDown"
:is-editing="showEditOption"
/> />
<button
@click.prevent="showEditOption = !showEditOption"
class="px-2 py-1 bg-gray-200 text-black font-semibold rounded-md shadow-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-300 mt-4">preview: {{showEditOption ? 'off' : 'on'}}</button>
</div> </div>
</div> </div>
</template> </template>
@ -62,6 +70,7 @@ const formBuilderPropertiesInputs = ref<FormInput[]>([
const formTitle = ref(''); const formTitle = ref('');
const formDescription = ref(''); const formDescription = ref('');
const submitLabel = ref(''); const submitLabel = ref('');
const showEditOption = ref(true);
const formBuilderFieldInputs = ref<FormInput[]>([ const formBuilderFieldInputs = ref<FormInput[]>([
{ {
@ -87,7 +96,8 @@ const formBuilderFieldInputs = ref<FormInput[]>([
{ label: 'Datetime-local', value: 'datetime-local' }, { label: 'Datetime-local', value: 'datetime-local' },
{ label: 'Month', value: 'month' }, { label: 'Month', value: 'month' },
{ label: 'Time', value: 'time' }, { label: 'Time', value: 'time' },
{ label: 'Week', value: 'week' } { label: 'Week', value: 'week' },
{ label: 'Description', value: 'description' }
] ]
}, },
{ {
@ -151,6 +161,14 @@ const formBuilderFieldInputs = ref<FormInput[]>([
} }
]); ]);
const defaultFormInput: FormInput = {
id: '',
label: '',
type: '',
required: false,
showLabel: true
};
const generatedFormInputs = ref<FormInput[]>([]); const generatedFormInputs = ref<FormInput[]>([]);
const handleFormPropertiesSubmit = (formData: Record<string, any>) => { const handleFormPropertiesSubmit = (formData: Record<string, any>) => {
@ -159,6 +177,78 @@ const handleFormPropertiesSubmit = (formData: Record<string, any>) => {
submitLabel.value = formData.submitLabel; submitLabel.value = formData.submitLabel;
}; };
const handleEditRequest = (inputToEdit: FormInput) => {
// set formBuilderFieldInputs to the input values
console.log(inputToEdit);
formBuilderFieldInputs.value.forEach((input: FormInput, i) => {
// fieldtype
if (input.id === 'fieldType') {
input.defaultValue = inputToEdit.type;
}
// fieldlabel
if (input.id === 'fieldLabel') {
input.defaultValue = inputToEdit.label;
}
// fieldid
if (input.id === 'fieldID') {
input.defaultValue = inputToEdit.id;
}
// placeholder
if (input.id === 'placeholder') {
input.defaultValue = inputToEdit.placeholder;
}
// required
if (input.id === 'required') {
input.defaultValue = inputToEdit.required ? ['true'] : [];
}
// showlabel
if (input.id === 'showLabel') {
input.defaultValue = inputToEdit.showLabel ? ['true'] : [];
}
// defaultvalue
if (input.id === 'defaultValue') {
input.defaultValue = inputToEdit.defaultValue;
}
// min
if (input.id === 'min') {
input.defaultValue = inputToEdit.min;
}
// max
if (input.id === 'max') {
input.defaultValue = inputToEdit.max;
}
// options
if (input.id === 'options') {
input.defaultValue = inputToEdit.options ? inputToEdit.options.map(opt => opt.value) : [];
}
});
};
const handleDeleteRequest = (inputID: string) => {
const index = generatedFormInputs.value.findIndex(input => input.id === inputID);
if (index > -1) {
generatedFormInputs.value.splice(index, 1);
}
};
const handleMoveUp = (inputID: string) => {
const index = generatedFormInputs.value.findIndex(input => input.id === inputID);
if (index > 0) {
const temp = generatedFormInputs.value[index];
generatedFormInputs.value[index] = generatedFormInputs.value[index - 1];
generatedFormInputs.value[index - 1] = temp;
}
};
const handleMoveDown = (inputID: string) => {
const index = generatedFormInputs.value.findIndex(input => input.id === inputID);
if (index < generatedFormInputs.value.length - 1) {
const temp = generatedFormInputs.value[index];
generatedFormInputs.value[index] = generatedFormInputs.value[index + 1];
generatedFormInputs.value[index + 1] = temp;
}
};
const handleFormBuilderSubmit = (formData: Record<string, any>) => { const handleFormBuilderSubmit = (formData: Record<string, any>) => {
const newField: FormInput = { const newField: FormInput = {
id: formData.fieldID, id: formData.fieldID,
@ -173,20 +263,62 @@ const handleFormBuilderSubmit = (formData: Record<string, any>) => {
options: formData.options ? formData.options.map((opt: string) => ({ label: opt, value: opt })) : undefined options: formData.options ? formData.options.map((opt: string) => ({ label: opt, value: opt })) : undefined
}; };
generatedFormInputs.value.push(newField); // if field id already exists, replace the previous field otherwise add the new field
const existingFieldIndex = generatedFormInputs.value.findIndex(field => field.id === newField.id);
if (existingFieldIndex > -1) {
generatedFormInputs.value.splice(existingFieldIndex, 1, newField);
} else {
generatedFormInputs.value.push(newField);
}
// Reset form builder fields // Reset form builder fields
formBuilderFieldInputs.value.forEach(input => { resetBuilderFields();
if (input.type === 'checkbox' || input.type === 'multi-input') { };
formData[input.id] = [];
if (input.id === 'showLabel') { // showLabel should default to true function resetBuilderFields() {
formData[input.id] = ['true']; formBuilderFieldInputs.value.forEach((input: FormInput, i) => {
} // fieldtype
} else { if (input.id === 'fieldType') {
formData[input.id] = ''; input.defaultValue = defaultFormInput.type;
}
// fieldlabel
if (input.id === 'fieldLabel') {
input.defaultValue = defaultFormInput.label;
}
// fieldid
if (input.id === 'fieldID') {
input.defaultValue = defaultFormInput.id;
}
// placeholder
if (input.id === 'placeholder') {
input.defaultValue = defaultFormInput.placeholder;
}
// required
if (input.id === 'required') {
input.defaultValue = defaultFormInput.required ? ['true'] : [];
}
// showlabel
if (input.id === 'showLabel') {
input.defaultValue = defaultFormInput.showLabel ? ['true'] : [];
}
// defaultvalue
if (input.id === 'defaultValue') {
input.defaultValue = defaultFormInput.defaultValue;
}
// min
if (input.id === 'min') {
input.defaultValue = defaultFormInput.min;
}
// max
if (input.id === 'max') {
input.defaultValue = defaultFormInput.max;
}
// options
if (input.id === 'options') {
input.defaultValue = defaultFormInput.options ? defaultFormInput.options.map(opt => opt.value) : [];
} }
}); });
}; }
</script> </script>
<style scoped> <style scoped>

View File

@ -562,6 +562,11 @@ video {
position: relative; position: relative;
} }
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.mb-2 { .mb-2 {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@ -578,6 +583,26 @@ video {
margin-top: 1.5rem; margin-top: 1.5rem;
} }
.mt-4 {
margin-top: 1rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.ml-auto {
margin-left: auto;
}
.mr-1 {
margin-right: 0.25rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.flex { .flex {
display: flex; display: flex;
} }
@ -594,6 +619,15 @@ video {
width: 1rem; width: 1rem;
} }
.w-max {
width: -moz-max-content;
width: max-content;
}
.w-auto {
width: auto;
}
.max-w-md { .max-w-md {
max-width: 28rem; max-width: 28rem;
} }
@ -614,6 +648,10 @@ video {
align-items: flex-start; align-items: flex-start;
} }
.items-end {
align-items: flex-end;
}
.items-center { .items-center {
align-items: center; align-items: center;
} }
@ -622,6 +660,14 @@ video {
justify-content: center; justify-content: center;
} }
.justify-between {
justify-content: space-between;
}
.justify-items-end {
justify-items: end;
}
.space-x-2 > :not([hidden]) ~ :not([hidden]) { .space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0; --tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse)); margin-right: calc(0.5rem * var(--tw-space-x-reverse));
@ -661,11 +707,21 @@ video {
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
} }
.bg-gray-400 {
--tw-bg-opacity: 1;
background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1));
}
.bg-white { .bg-white {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
} }
.bg-gray-200 {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.p-3 { .p-3 {
padding: 0.75rem; padding: 0.75rem;
} }
@ -678,6 +734,11 @@ video {
padding: 1.5rem; padding: 1.5rem;
} }
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.px-3 { .px-3 {
padding-left: 0.75rem; padding-left: 0.75rem;
padding-right: 0.75rem; padding-right: 0.75rem;
@ -698,6 +759,11 @@ video {
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
} }
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.text-xl { .text-xl {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.75rem; line-height: 1.75rem;
@ -711,11 +777,21 @@ video {
font-weight: 600; font-weight: 600;
} }
.text-black {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity, 1));
}
.text-blue-600 { .text-blue-600 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity, 1)); color: rgb(37 99 235 / var(--tw-text-opacity, 1));
} }
.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
}
.text-gray-700 { .text-gray-700 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity, 1)); color: rgb(55 65 81 / var(--tw-text-opacity, 1));
@ -743,11 +819,26 @@ video {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
} }
.ring-blue-500 {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
}
.ring-gray-100 {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(243 244 246 / var(--tw-ring-opacity, 1));
}
.hover\:bg-blue-700:hover { .hover\:bg-blue-700:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1)); background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
} }
.hover\:bg-gray-300:hover {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
}
.hover\:underline:hover { .hover\:underline:hover {
text-decoration-line: underline; text-decoration-line: underline;
} }
@ -766,4 +857,14 @@ video {
.focus\:ring-blue-500:focus { .focus\:ring-blue-500:focus {
--tw-ring-opacity: 1; --tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1)); --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
}
.focus\:ring-gray-100:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(243 244 246 / var(--tw-ring-opacity, 1));
}
.focus\:ring-gray-300:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity, 1));
} }