Skip to content

Custom Fields ​

When the built-in field types don't meet your needs, you can create custom field components. Custom fields allow you to build completely custom UI controls for editing block properties.

Overview ​

Custom fields are Vue components that you provide to handle specific editing needs. They integrate seamlessly with the editor's validation system and data flow.

When to Use Custom Fields ​

Consider custom fields when you need:

  • Rich text editing - WYSIWYG editors like TinyMCE, Quill, or TipTap
  • Advanced image/media uploads - Custom upload workflows, multiple files, or specialized processing
  • Complex data structures - JSON editors, code editors, or nested forms
  • Third-party integrations - Date pickers, color palettes, or specialized controls
  • Custom UI patterns - Any interface that doesn't fit standard input types

Built-in Image Field

For image uploads, the built-in image field type is suitable for development and testing. For production, a custom image field component is recommended. See the Fields Guide for details.

Creating a Custom Field Component ​

A custom field component is a standard Vue component that receives specific props and emits updates via v-model.

Component Props ​

Your custom component will receive:

PropTypeDescription
modelValueanyThe current field value (for v-model)
editablebooleanWhether the field is editable
labelstringField label (from field config)
descriptionstringField description (from field config)
requiredbooleanWhether the field is required
...anyAll other properties from EditorFieldConfig

Component Requirements ​

  1. Accept v-model - Use defineModel() or modelValue prop with update:modelValue emit
  2. Respect editable - Disable editing when editable is false
  3. Handle validation - Validation is handled automatically by the editor, but you can display custom error states

Basic Example ​

vue
<!-- src/components/ImageUploadField.vue -->
<template>
   <div class="image-upload-field">
      <div v-if="imageUrl" class="image-preview">
         <img :src="imageUrl" alt="Preview" />
         <button v-if="editable" @click="clearImage">Remove</button>
      </div>
      <div v-else>
         <input v-if="editable" type="file" accept="image/*" @change="handleFileSelect" />
         <p v-else class="text-gray-500">No image selected</p>
      </div>
   </div>
</template>

<script setup lang="ts">
import { computed } from "vue";

const props = defineProps<{
   modelValue?: string | null;
   editable: boolean;
   label?: string;
   description?: string;
}>();

const emit = defineEmits<{
   "update:modelValue": [value: string | null];
}>();

const imageUrl = computed(() => props.modelValue);

function handleFileSelect(event: Event) {
   const file = (event.target as HTMLInputElement).files?.[0];
   if (file) {
      // In a real app, you'd upload the file and get a URL
      const reader = new FileReader();
      reader.onload = (e) => {
         emit("update:modelValue", e.target?.result as string);
      };
      reader.readAsDataURL(file);
   }
}

function clearImage() {
   emit("update:modelValue", null);
}
</script>

Using defineModel() (Vue 3.4+) ​

vue
<!-- src/components/RichTextField.vue -->
<template>
   <div class="rich-text-field">
      <div v-if="editable">
         <!-- Your rich text editor component -->
         <QuillEditor v-model="content" />
      </div>
      <div v-else v-html="content"></div>
   </div>
</template>

<script setup lang="ts">
import { QuillEditor } from "@vueup/vue-quill";

defineProps<{
   editable: boolean;
   label?: string;
   description?: string;
}>();

// Use defineModel for simpler v-model handling
const content = defineModel<string>({ default: "" });
</script>

Registering Custom Fields ​

Once you've created your custom component, register it in your fields.ts file:

typescript
// src/page-builder/blocks/hero/fields.ts
import { createField } from "vue-wswg-editor";
import ImageUploadField from "@/components/ImageUploadField.vue";
import RichTextField from "@/components/RichTextField.vue";

export default {
   heading: createField.text({
      label: "Heading",
      required: true,
   }),

   image: createField.custom({
      label: "Hero Image",
      description: "Upload a hero image",
      component: ImageUploadField,
      required: true,
   }),

   content: createField.custom({
      label: "Content",
      component: RichTextField,
      default: "",
   }),
};

Advanced Examples ​

JSON Editor Field ​

vue
<!-- src/components/JsonEditorField.vue -->
<template>
   <div class="json-editor-field">
      <textarea v-if="editable" v-model="jsonString" @blur="updateValue" class="font-mono text-sm" rows="10"></textarea>
      <pre v-else class="text-sm">{{ formattedJson }}</pre>
   </div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";

const props = defineProps<{
   modelValue?: any;
   editable: boolean;
}>();

const emit = defineEmits<{
   "update:modelValue": [value: any];
}>();

const jsonString = ref(JSON.stringify(props.modelValue || {}, null, 2));

const formattedJson = computed(() => {
   try {
      return JSON.stringify(props.modelValue, null, 2);
   } catch {
      return "Invalid JSON";
   }
});

function updateValue() {
   try {
      const parsed = JSON.parse(jsonString.value);
      emit("update:modelValue", parsed);
   } catch (error) {
      // Handle invalid JSON - validation will catch this
      console.error("Invalid JSON:", error);
   }
}
</script>

Date Range Picker Field ​

vue
<!-- src/components/DateRangeField.vue -->
<template>
   <div class="date-range-field">
      <div v-if="editable" class="flex gap-2">
         <input v-model="startDate" type="date" @change="updateValue" />
         <span>to</span>
         <input v-model="endDate" type="date" @change="updateValue" />
      </div>
      <div v-else>
         {{ formattedRange }}
      </div>
   </div>
</template>

<script setup lang="ts">
import { computed, ref, watch } from "vue";

const props = defineProps<{
   modelValue?: { start: string; end: string } | null;
   editable: boolean;
}>();

const emit = defineEmits<{
   "update:modelValue": [value: { start: string; end: string } | null];
}>();

const startDate = ref(props.modelValue?.start || "");
const endDate = ref(props.modelValue?.end || "");

watch(
   () => props.modelValue,
   (newValue) => {
      startDate.value = newValue?.start || "";
      endDate.value = newValue?.end || "";
   }
);

const formattedRange = computed(() => {
   if (!props.modelValue) return "No date range selected";
   return `${props.modelValue.start} to ${props.modelValue.end}`;
});

function updateValue() {
   if (startDate.value && endDate.value) {
      emit("update:modelValue", {
         start: startDate.value,
         end: endDate.value,
      });
   } else {
      emit("update:modelValue", null);
   }
}
</script>

Validation with Custom Fields ​

Custom fields automatically integrate with the editor's validation system. You can add validators to custom fields just like built-in fields:

typescript
// src/page-builder/blocks/hero/fields.ts
import { createField } from "vue-wswg-editor";
import ImageUploadField from "@/components/ImageUploadField.vue";
import * as yup from "yup";

export default {
   image: createField.custom({
      label: "Hero Image",
      component: ImageUploadField,
      required: true,
      validator: async (value) => {
         if (!value) {
            return "An image is required";
         }
         // Check if URL is valid
         try {
            new URL(value);
            return true;
         } catch {
            return "Invalid image URL";
         }
      },
   }),

   // Using yup for complex validation
   content: createField.custom({
      label: "Content",
      component: RichTextField,
      validator: async (value) => {
         const schema = yup.string().min(10, "Content must be at least 10 characters");
         try {
            await schema.validate(value);
            return true;
         } catch (error) {
            return (error as yup.ValidationError).message;
         }
      },
   }),
};

Accessing Field Config in Custom Components ​

All properties from EditorFieldConfig are passed to your component via v-bind, so you can access any custom properties:

vue
<!-- src/components/CustomField.vue -->
<template>
   <div>
      <label>{{ label }}</label>
      <p v-if="description" class="text-sm text-gray-500">{{ description }}</p>
      <!-- Your custom UI -->
   </div>
</template>

<script setup lang="ts">
defineProps<{
   modelValue: any;
   editable: boolean;
   label?: string;
   description?: string;
   // Access any custom properties you add to fieldConfig
   customProperty?: string;
}>();
</script>

Best Practices ​

1. Handle Loading States ​

vue
<template>
   <div>
      <div v-if="uploading">Uploading...</div>
      <div v-else>
         <!-- Your field UI -->
      </div>
   </div>
</template>

2. Provide Default Values ​

typescript
createField.custom({
   component: MyField,
   default: null, // or appropriate default
});

3. Handle Null/Undefined Values ​

vue
<script setup lang="ts">
const value = defineModel<any>({ default: null });

// Always handle null/undefined
const displayValue = computed(() => value.value ?? "");
</script>

4. Respect the editable Prop ​

Always check editable before allowing user interaction:

vue
<template>
   <button v-if="editable" @click="doSomething">Action</button>
   <span v-else>{{ displayValue }}</span>
</template>

5. Use Scoped Styles ​

Prevent style conflicts by using scoped styles:

vue
<style scoped>
.my-custom-field {
   /* Styles won't leak to other components */
}
</style>

Common Use Cases ​

Image Upload with Preview ​

typescript
createField.custom({
   label: "Image",
   component: ImageUploadField,
   required: true,
});

Rich Text Editor ​

typescript
createField.custom({
   label: "Content",
   component: RichTextField,
   default: "",
});

Code/JSON Editor ​

typescript
createField.custom({
   label: "Configuration",
   component: JsonEditorField,
   default: {},
});

Date/Time Pickers ​

typescript
createField.custom({
   label: "Publish Date",
   component: DateTimePickerField,
   required: true,
});

See Also ​

Released under the MIT License.