Skip to main content

Form Fields

Introduction

These components take input: text boxes, a text area, password fields, a search box, checkboxes, radio buttons, a toggle switch and a tag box. The text fields draw the framework's field markup (dcui-form-group, dcui-fld-label, dcui-form-control), so a form built from them looks like every other DoFramework screen. Keep the values in the page's data() and bind them with v-model.

ComponentUse it for
DcInputFieldA one-line text box, with an optional icon or text such as a currency
DcTextFieldA multi-line text area
DcPasswordFieldA password box with a show/hide button
DcPasswordGeneratorA text box with a Generate button that fills in a random password
DcSearchFieldA search box that reports what the user typed, after a short pause
DcFormGroupSpacing and a label around fields that don't have their own
DcCheckboxOne checkbox, alone or as one of a set
DcCheckboxGroupCheckboxes side by side
DcRadioGroupOne choice from a short list
DcToggleSwitchAn on/off setting
DcTagInputA list of free-text tags, added with Enter

For dropdowns, date pickers and other pickers, see Selects And Pickers.

Every example imports from the kit's registry. The path below is from a component in apps/myapp/components/:

import { DcInputField } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

Register each imported component in the components option before you use its tag. See Vue DC UI KIT for the setup.

Most of these fields take a class prop for the input itself. They read it once, when the field is created, so a class that changes later (for example an error class added after validation) never reaches the input. Show validation messages as text instead, as the example at the end of this page does.


DcInputField​

A one-line text box with a label. It can show an icon, or a short text such as a currency, on either side of the box.

import { DcInputField } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

export default {
components: { DcInputField },
data() {
return { name: "" };
},
template: `
<dc-input-field id="myapp-customer-name" label="Name" v-model="name" placeholder="Full name">
<template #label>Name</template>
</dc-input-field>
`,
};

The visible label comes from the label slot. The label prop is required, but it isn't shown. Without the slot there is no <label> element.

Any attribute that isn't a prop goes on the <input>: disabled, maxlength, autocomplete, inputmode, and listeners such as @blur or @keyup.enter. That includes the update:modelValue listener, which is how v-model reaches the input. Both v-model and :model-value with @update:model-value update the parent on every keystroke.

With an icon​

Set icon to Font Awesome classes, or put text in the icon slot. The icon sits on the left unless iconPosition is "right". iconFilled gives it a grey background and a divider, which suits a button. Clicking the icon emits icon-click.

import { DcInputField } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

export default {
components: { DcInputField },
data() {
return { email: "", price: "", coupon: "" };
},
methods: {
apply_coupon() {
// Check the code with the server.
},
},
template: `
<dc-input-field id="myapp-email" label="Email" v-model="email" icon="fa-regular fa-envelope"
inputmode="email" autocomplete="email">
<template #label>Email</template>
</dc-input-field>

<dc-input-field id="myapp-price" label="Price" v-model="price" inputmode="decimal">
<template #label>Price</template>
<template #icon>LKR</template>
</dc-input-field>

<dc-input-field id="myapp-coupon" label="Coupon" v-model="coupon" icon="fa-regular fa-arrow-right"
icon-position="right" :icon-filled="true" @icon-click="apply_coupon">
<template #label>Coupon code</template>
</dc-input-field>
`,
};

Use the custom slot for anything under the box, such as help text or a validation message.

Props

NameTypeDefaultDescription
idStringrequiredThe input's id. The label points at it.
labelStringrequiredRequired, but not shown. Put the visible label in the label slot.
modelValueString""The text. Use v-model.
placeholderStringnoneThe placeholder.
readonlyBooleanfalseMakes the input read-only.
typeString"text"Has no effect. See the note above.
classString""Classes for the <input>. Read once, when the field is created.
containerClassesString""Classes for the wrapper div (dcui-form-group).
iconString""Font Awesome classes for an icon beside the box, such as fa-regular fa-envelope.
iconPositionString"left""left" or "right".
iconFilledBooleanfalseGives the icon a grey background and a divider.

Events

NamePayloadDescription
update:modelValueStringThe text changed. Handled by v-model.
icon-clickthe click eventThe icon was clicked.

Slots

NameDescription
labelThe label text.
iconText or markup in place of icon, such as a currency.
customContent under the box.

The admin panel has an older copy of this component. It has no icon, iconPosition or iconFilled props, no icon slot, and doesn't emit icon-click.


DcTextField​

A multi-line text area with a label, for notes, descriptions and addresses.

import { DcTextField } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

export default {
components: { DcTextField },
data() {
return { notes: "" };
},
template: `
<dc-text-field id="myapp-order-notes" label="Notes" v-model="notes" placeholder="Delivery instructions"
:auto-resize="true" maxlength="500">
<template #label>Notes</template>
</dc-text-field>
`,
};

The box is at least 110px and at most 200px tall. Users can drag it taller, up to that limit, unless resize is false. With autoResize, it grows as the user types, up to the 200px limit, and then scrolls. It sizes itself only on typing, not on text you load into it.

Any attribute that isn't a prop goes on the <textarea>, for example rows, maxlength or disabled. The <label> is always drawn, even without the label slot.

Props

NameTypeDefaultDescription
idStringrequiredThe text area's id. The label points at it.
labelStringrequiredRequired, but not shown. Put the visible label in the label slot.
modelValueString""The text. Use v-model.
placeholderStringnoneThe placeholder.
readonlyBooleanfalseMakes the text area read-only.
resizeBooleantrueLets the user drag the box taller.
autoResizeBooleanfalseGrows the box as the user types.
typeString"text"Not used.
classString""Classes for the <textarea>. Read once, when the field is created.
containerClassesString""Classes for the wrapper div (dcui-form-group).

Events

NamePayloadDescription
update:modelValueStringThe text changed. Handled by v-model.

Slots

NameDescription
labelThe label text.

The admin panel has an older copy of this component, without autoResize.


DcPasswordField​

A password box with an eye button that shows and hides what was typed.

import { DcPasswordField } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

export default {
components: { DcPasswordField },
data() {
return { password: "" };
},
template: `
<dc-password-field id="myapp-password" v-model="password" autocomplete="new-password">
<template #label>Password</template>
<template #custom>
<span class="dcui-text-light">At least 8 characters.</span>
</template>
</dc-password-field>
`,
};

It has no label or readonly prop. Any attribute that isn't a prop goes on the <input>, so autocomplete, readonly and disabled work as attributes. Set autocomplete="new-password" on a field that sets a password, so the browser offers to save it rather than filling in an old one.

Props

NameTypeDefaultDescription
idStringrequiredThe input's id. The label points at it.
modelValueString""The password. Use v-model.
placeholderStringnoneThe placeholder.
classString""Classes for the <input>. Read once, when the field is created.
containerClassesString""Classes for the wrapper div (dcui-form-group).

Events

NamePayloadDescription
update:modelValueStringThe password changed. Handled by v-model.

Slots

NameDescription
labelThe label text.
customContent under the box.

The admin panel's copy has no custom slot.


DcPasswordGenerator​

A text box with a Generate button. Pressing it fills the box with a random password of length characters, made of upper-case letters, lower-case letters and digits. The user can also type a password.

import { DcPasswordGenerator } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

export default {
components: { DcPasswordGenerator },
data() {
return { password: "" };
},
template: `
<dc-password-generator id="myapp-new-password" v-model="password" :length="12">
<template #label>Password</template>
</dc-password-generator>
`,
};

Bind length (:length="12") so it arrives as a number.

Until this is fixed, use a DcInputField with a Generate button in its icon slot, and create the password in your own method so it goes straight into v-model:

import { DcInputField } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

export default {
components: { DcInputField },
data() {
return { password: "" };
},
methods: {
generate_password() {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const values = crypto.getRandomValues(new Uint32Array(12));
this.password = Array.from(values, (n) => chars[n % chars.length]).join("");
},
},
template: `
<dc-input-field id="myapp-new-password" label="Password" v-model="password"
icon-position="right" :icon-filled="true" @icon-click="generate_password" autocomplete="off">
<template #label>Password</template>
<template #icon>Generate</template>
</dc-input-field>
`,
};

Props

NameTypeDefaultDescription
idStringrequiredThe input's id. The label points at it.
modelValueString""The password. Use v-model. See the note above.
placeholderStringnoneThe placeholder.
lengthNumber10How many characters Generate produces.
classString""Accepted, but not applied to anything.

Events

NamePayloadDescription
update:modelValueStringThe user edited the text. Handled by v-model. Not raised by Generate.

Slots

NameDescription
labelThe label text.

DcSearchField​

A search box with a magnifying-glass icon. It doesn't use v-model. Instead it emits search with the text once the user stops typing for half a second. When the box has text, the icon turns into an x that clears it.

import { DcSearchField } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

export default {
components: { DcSearchField },
data() {
return { query: "" };
},
methods: {
on_search(text) {
this.query = text.trim();
// Load the first page of results for this.query.
},
},
template: `
<dc-search-field id="myapp-order-search" placeholder="Search orders" icon_align="left" @search="on_search" />
`,
};

How it behaves:

  • Typing emits search 500ms after the last key press. The text isn't trimmed, so trim it yourself.
  • Emptying the box, by deleting the text or pressing the x, emits search with "" straight away. Treat "" as "show everything".
  • The field keeps its own text. To clear it from code, for example when a filter is reset, call clearSearch() through a ref. That also emits search with "".
  • It has no label and no bottom margin. Put it in a toolbar, or in a DcFormGroup with a label.
  • Attributes that aren't props, such as aria-label, disabled or autofocus, are dropped.

Props

NameTypeDefaultDescription
idStringrequiredThe input's id.
placeholderString"Search..."The placeholder.
icon_alignString"right"Where the icon sits: "left" or "right".
classString""Classes for the <input>. Read once, when the field is created.
containerClassesString""Classes for the wrapper div.

Events

NamePayloadDescription
searchStringThe search text, or "" when the box was emptied.

Methods

NameDescription
clearSearch()Empties the box and emits search with "".
getSearchValue()Returns the current text, trimmed.

DcFormGroup​

A wrapper that gives its content the same bottom margin as a field (dcui-form-group). The text fields above already have one. Use DcFormGroup around things that don't, such as a set of checkboxes, a radio group or a toggle, with a dcui-fld-label label on top.

import { DcFormGroup, DcRadioGroup } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

export default {
components: { DcFormGroup, DcRadioGroup },
data() {
return { delivery: "standard" };
},
template: `
<dc-form-group>
<label class="dcui-fld-label">Delivery</label>
<dc-radio-group v-model="delivery" :options="[
{ label: 'Standard', value: 'standard' },
{ label: 'Express', value: 'express' },
]" :inline="true" />
</dc-form-group>
`,
};

Props

NameTypeDefaultDescription
classString""Extra classes for the wrapper. Read once, when it's created.

Events

None.

Slots

NameDescription
defaultThe group's content.

DcCheckbox​

One checkbox with its label. Bind v-model to a Boolean for a single yes/no box. To collect several boxes into one list, bind them all to the same array and give each a value: the array holds the value of every ticked box.

import { DcCheckbox } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

export default {
components: { DcCheckbox },
data() {
return { newsletter: false };
},
template: `
<dc-checkbox id="myapp-newsletter" v-model="newsletter">Send me the monthly newsletter</dc-checkbox>
`,
};

modelValue is declared as a String, but the box works with a Boolean or an array, as described above. The production build of Vue that the framework loads doesn't warn about the mismatch.

Every checkbox needs its own id, because the visible box is drawn by the label, which finds the input through that id. Attributes that aren't props, such as disabled or title, go on the input and on the wrapper. A disabled box is drawn greyed out.

Props

NameTypeDefaultDescription
idStringrequiredThe input's id. Must be unique on the screen.
modelValueString""The state. Use v-model with a Boolean, or with an array together with value.
valueString""The value added to an array v-model when the box is ticked.
no_labelBooleanfalseHas no visible effect. See the note above.
classString""Classes for the hidden input. No visible effect.

Events

NamePayloadDescription
update:modelValueBoolean, or the updated arrayThe box was ticked or unticked. Handled by v-model.

Slots

NameDescription
defaultThe label text.

DcCheckboxGroup​

Lays checkboxes side by side, 40px apart, instead of one per line. It has no props and no v-model: bind each DcCheckbox to the same array.

import { DcFormGroup, DcCheckboxGroup, DcCheckbox } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

export default {
components: { DcFormGroup, DcCheckboxGroup, DcCheckbox },
data() {
return { days: ["mon", "wed"] };
},
template: `
<dc-form-group>
<label class="dcui-fld-label">Delivery days</label>
<dc-checkbox-group>
<dc-checkbox id="myapp-day-mon" v-model="days" value="mon">Monday</dc-checkbox>
<dc-checkbox id="myapp-day-wed" v-model="days" value="wed">Wednesday</dc-checkbox>
<dc-checkbox id="myapp-day-fri" v-model="days" value="fri">Friday</dc-checkbox>
</dc-checkbox-group>
</dc-form-group>
`,
};

Here days holds the values of the ticked boxes, such as ["mon", "fri"]. Without the group, the same checkboxes stack one per line.

Props

None.

Events

None.

Slots

NameDescription
defaultThe checkboxes.

DcRadioGroup​

A set of radio buttons for picking one option. v-model holds the chosen option's value.

import { DcRadioGroup } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

export default {
components: { DcRadioGroup },
data() {
return {
payment: "card",
payment_options: [
{ label: "Card", value: "card" },
{ label: "Bank transfer", value: "bank" },
{ label: "Cash on delivery", value: "cod", disabled: true },
],
};
},
template: `
<dc-radio-group v-model="payment" :options="payment_options" />
`,
};

Each option is an object { label, value, disabled }, where disabled is optional. An option can also be a plain value, such as "Small", which is then both its label and its value.

The buttons stack one per line; inline puts them side by side. Each group gets its own name, so two groups on one screen don't interfere. An option is shown as chosen only when its value is strictly equal to v-model, so 1 and "1" don't match. Keep the types the same.

Props

NameTypeDefaultDescription
modelValueString, Number, BooleannullThe chosen option's value. Use v-model.
optionsArrayrequiredThe options: { label, value, disabled } objects, or plain values.
inlineBooleanfalsePuts the buttons side by side.
disabledBooleanfalseDisables every option.
nameString""The radio inputs' name. Empty gives the group a unique name.

Events

NamePayloadDescription
update:modelValuethe option's valueAn option was chosen. Handled by v-model.
changethe option's valueAn option was chosen.

The admin-panel registry doesn't export DcRadioGroup.


DcToggleSwitch​

An on/off slider for a setting that takes effect on its own, such as a notification preference. v-model is a Boolean.

import { DcToggleSwitch } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

export default {
components: { DcToggleSwitch },
data() {
return { notify: true };
},
template: `
<div class="dcui-flex-item dcui-align-center">
<dc-toggle-switch id="myapp-notify-shipped" v-model="notify" />
<label for="myapp-notify-shipped" class="dcui-m-l-10">Email me when an order ships</label>
</div>
`,
};

The switch has no text of its own. Put a <label> beside it with for set to the switch's id, as above, so clicking the text flips the switch too. Without an id, the switch makes one up, and you can't point a label at it. mini draws a smaller switch.

Props

NameTypeDefaultDescription
modelValueBooleanfalseWhether the switch is on. Use v-model.
miniBooleanfalseA smaller switch.
disabledBooleanfalseStops the switch from changing.
idString""The checkbox's id. Empty gives it a unique one.

Events

NamePayloadDescription
update:modelValueBooleanThe switch was flipped. Handled by v-model.
changeBooleanThe switch was flipped. Use it to save the setting straight away.

The admin-panel registry doesn't export DcToggleSwitch.


DcTagInput​

A box for a list of short free-text tags. The user types a tag and presses Enter to add it. Each tag shows with a button that removes it. v-model is an array of strings.

import { DcTagInput } from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";

export default {
components: { DcTagInput },
data() {
return { tags: ["gift", "fragile"] };
},
template: `
<dc-tag-input v-model="tags" placeholder="Add a tag and press Enter" />
`,
};

Tags are trimmed, and empty ones are ignored. The same tag can be added twice.

Props

NameTypeDefaultDescription
modelValueArray[]The tags. Use v-model. Read only when the component is created.
placeholderString""The input's placeholder.

Events

NamePayloadDescription
update:modelValueArray of stringsA tag was added or removed. Handled by v-model.

A complete form​

This screen creates a user account. It uses two text fields side by side, a password field, a radio group for the role, a toggle and a checkbox, and validates on save. Messages go in each field's custom slot, and the server's reply decides whether the form closes. myapp_services stands for your app's services.js (see Frontend Runtime (XP)). DcButton is described in Buttons And Links.

import {
DcInputField, DcPasswordField, DcTextField, DcFormGroup,
DcRadioGroup, DcToggleSwitch, DcCheckbox, DcButton,
} from "../../xp_system/components/dc_ui_kit_componenets/dc_ui_kit_registry.js";
import { myapp_services } from "../services.js";

export default {
components: { DcInputField, DcPasswordField, DcTextField, DcFormGroup, DcRadioGroup, DcToggleSwitch, DcCheckbox, DcButton },
data() {
return {
form: {
name: "",
email: "",
password: "",
role: "staff",
send_welcome: true,
notes: "",
confirmed: false,
},
roles: [
{ label: "Staff", value: "staff" },
{ label: "Manager", value: "manager" },
],
errors: {},
saving: false,
save_error: "",
};
},
methods: {
validate() {
const errors = {};
if (!this.form.name.trim()) errors.name = "Enter the user's name.";
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(this.form.email)) errors.email = "Enter an email address, like sam@example.com.";
if (this.form.password.length < 8) errors.password = "Use at least 8 characters.";
if (!this.form.confirmed) errors.confirmed = "Confirm that this person works for your company.";
this.errors = errors;
return Object.keys(errors).length === 0;
},
async save() {
if (!this.validate()) return;
this.saving = true;
this.save_error = "";
try {
const data = new FormData();
data.append("name", this.form.name.trim());
data.append("email", this.form.email.trim());
data.append("password", this.form.password);
data.append("role", this.form.role);
data.append("send_welcome", this.form.send_welcome ? "1" : "0");
data.append("notes", this.form.notes);
const res = await myapp_services.create_user(data);
if (res.response.success) {
this.$emit("saved", res.data.user);
} else {
this.save_error = res.response.statusMsg;
}
} catch (e) {
this.save_error = "Couldn't create the user. Try again.";
} finally {
this.saving = false;
}
},
},
template: `
<form @submit.prevent="save">
<div class="dcui-row">
<div class="dcui-col-md-6">
<dc-input-field id="myapp-user-name" label="Name" v-model="form.name" autocomplete="name">
<template #label>Name <span class="dcui-f-l-required">*</span></template>
<template #custom>
<span v-if="errors.name" class="dcui-text-danger">{{ errors.name }}</span>
</template>
</dc-input-field>
</div>
<div class="dcui-col-md-6">
<dc-input-field id="myapp-user-email" label="Email" v-model="form.email"
icon="fa-regular fa-envelope" inputmode="email" autocomplete="email">
<template #label>Email <span class="dcui-f-l-required">*</span></template>
<template #custom>
<span v-if="errors.email" class="dcui-text-danger">{{ errors.email }}</span>
</template>
</dc-input-field>
</div>
</div>

<dc-password-field id="myapp-user-password" v-model="form.password" autocomplete="new-password">
<template #label>Password <span class="dcui-f-l-required">*</span></template>
<template #custom>
<span :class="errors.password ? 'dcui-text-danger' : 'dcui-text-light'">
{{ errors.password || "At least 8 characters." }}
</span>
</template>
</dc-password-field>

<dc-form-group>
<label class="dcui-fld-label">Role</label>
<dc-radio-group v-model="form.role" :options="roles" :inline="true" />
</dc-form-group>

<dc-form-group>
<div class="dcui-flex-item dcui-align-center">
<dc-toggle-switch id="myapp-user-welcome" v-model="form.send_welcome" />
<label for="myapp-user-welcome" class="dcui-m-l-10">Email the user a welcome message</label>
</div>
</dc-form-group>

<dc-text-field id="myapp-user-notes" label="Notes" v-model="form.notes" :auto-resize="true">
<template #label>Notes</template>
</dc-text-field>

<dc-form-group>
<dc-checkbox id="myapp-user-confirmed" v-model="form.confirmed">This person works for our company</dc-checkbox>
<div v-if="errors.confirmed" class="dcui-text-danger dcui-m-t-5">{{ errors.confirmed }}</div>
</dc-form-group>

<div class="dcui-flex-item dcui-justify-space-between dcui-align-center">
<span class="dcui-text-danger">{{ save_error }}</span>
<dc-button type="submit" :disabled="saving">Create user</dc-button>
</div>
</form>
`,
};

How the parts work:

  • The <form> lets Enter in any text box save, through @submit.prevent. DcButton is type="button" by default, so set type="submit" on the save button. Keep DcTagInput out of a form like this one.
  • Validation messages go in the custom slot of DcInputField and DcPasswordField, or under the field inside a DcFormGroup. Say what to enter, not only that the value is wrong.
  • Required fields carry a dcui-f-l-required asterisk in their label.
  • Saving. The button is disabled while the request runs, and a server error appears beside it. The form keeps everything the user typed, so they can fix it and try again.
  • The toggle here is part of the form, so it's saved with the rest. For a setting that should save on its own, handle the switch's change event instead.