Model & schemas¶
How does one represent a form in KForm? Let’s take a look at an example. Imagine that you want to build a form that allows users to purchase bus tickets. A possible model for this form might look like the following:
data class BusTripForm(var email: String, var passengers: Table<Passenger>)
data class Passenger(var name: String, var age: Int?)
As we can see, this form is represented by standard Kotlin classes with mutable properties which,
apart from the Table data type (explained below) are not at all related to KForm.
So, how can KForm reason about these seemingly arbitrary types of data? This is what schemas are for: they offer KForm insight on the structure and types of your data. Schemas define the structure of your form and its validations: they provide the means for KForm to create, manipulate, and validate values of your form.
The following example shows how the schema of our bus ticket purchasing form can be defined (for now without any validations):
val BusTripFormSchema = ClassSchema {
BusTripForm::email { StringSchema() }
BusTripForm::passengers {
TableSchema {
ClassSchema {
Passenger::name { StringSchema() }
Passenger::age { NullableSchema { IntSchema() } }
}
}
}
}
This definition enables KForm to understand that our form is represented by a class BusTripForm
containing two fields: email and passengers, of types String and Table<Passenger>
respectively.
As you might notice, a form’s schema usually has a one-to-one correspondence with its model.
Supported data types¶
KForm natively supports many of Kotlin’s standard data types:
- Classes: via
ClassSchema, as shown above. Boolean,Byte,Char,Short,Int,Long,Float,Double, andString: viaBooleanSchema,ByteSchema,CharSchema,ShortSchema,IntSchema,LongSchema,FloatSchema,DoubleSchema, andStringSchemarespectively.- Nullable data types: via
NullableSchema. E.g., the typeInt?can be represented asNullableSchema { IntSchema() }. -
Enum classes: via
EnumSchema. E.g.: -
Any?: viaAnySchema. Rarely used, but useful when wanting the form to hold some data that KForm doesn’t need to know the type of.
The following collections are currently supported:
Table: viaTableSchema. Tables are KForm’s recommended data structure for representing collections since they provide stable identifiers for each element. E.g., the typeTable<Int>can be represented asTableSchema { IntSchema() }.List: viaListSchema. E.g., the typeList<Int>can be represented asListSchema { IntSchema() }.
For temporal-related data types, KForm supports:
Instant: viaInstantSchema.- kotlinx-datetime’s
LocalDateandLocalDateTime: viaLocalDateSchemaandLocalDateTimeSchemarespectively.
Big numbers are supported via kt-math:
BigIntegerandBigDecimal: viaBigIntegerSchemaandBigDecimalSchema.
Files are supported via KForm’s own File datatype:
File: viaFileSchema. This datatype is used by KForm to represent files to be uploaded in a form.
How to model your form¶
When creating the model of your form, you might wonder which data types to use. Should your field be
typed as String or String?, Int or Int?? What data type should be used for collections?
Here are some guidelines:
-
The preferred data types often depend on what controls will be used to display/edit the form in your UI. For example, it is ofter better to represent a text field (e.g.
<input type="text">) as aStringrather thanString?, otherwise there would be two possible representations for an “empty” text box:nulland"".If, however, there’s a limited set of possible values for the field, and the form control becomes a
<select>element or similar, then it might make more sense to useString?or a nullable enum. -
Free-form numeric fields (e.g.
<input type="number">) should typically be nullable, otherwise there won’t be a reasonable value for representing empty fields. -
The previous rules allude to the following, which is worth mentioning: validation requirements don’t always dictate the data type that should be used. I.e., just because a field is mandatory, it doesn’t mean that it should be typed as non-nullable. It is more important to consider whether your data types are able to represent all possible states of your UI form controls.
Instead, it might make sense to denote certain characteristics of your form values as validations rather than constraining the data type itself. For more information on validations, see Validations.
-
For collections over complex data, prefer
Table<T>overList<T>, e.g. when modeling a table where rows can be added and removed, as shown in the previousBusTripFormexample. Tables provide stable identifiers for each element, which means that paths to elements in the table remain stable even if the table is modified. - Use
List<T>for small collections of primitive types, e.g.List<String>(e.g. when modeling a checkbox group or a<select multiple>element). UsingTable<T>for this kind of collection is often also okay, butList<T>is typically enough.
Refactoring schemas¶
Schemas can be split amongst multiple variables and/or files. For example, the previous bus ticket form schema could be split as follows (with each part possibly placed in separate files):
val PassengerSchema = ClassSchema {
Passenger::name { StringSchema() }
Passenger::age { NullableSchema { IntSchema() } }
}
val BusTripFormSchema = ClassSchema {
BusTripForm::email { StringSchema() }
BusTripForm::passengers { TableSchema { PassengerSchema } }
}
Composing schemas¶
Suppose there’s a need for modelling multiple forms which share some common fields. For example, consider the following forms for registering pets, which differ per type of pet and share some common fields via an interface:
interface PetRegistrationForm {
var name: String
}
data class DogRegistrationForm(override var name: String = "", var breed: String = "") :
PetRegistrationForm
data class CatRegistrationForm(
override var name: String = "",
var livesIndoors: Boolean = false,
) : PetRegistrationForm
To share common schema definitions between forms, we can create an auxiliary function for defining the schema of the common interface fields, which is then called in the schema definition of each actual form:
fun ClassSchemaBuilder<PetRegistrationForm>.petRegistrationFormSchema() {
PetRegistrationForm::name { StringSchema(Required()) }
}
val DogRegistrationFormSchema = ClassSchema {
petRegistrationFormSchema()
DogRegistrationForm::breed { StringSchema(Required()) }
}
val CatRegistrationFormSchema = ClassSchema {
petRegistrationFormSchema()
CatRegistrationForm::livesIndoors { BooleanSchema() }
}