Form
bt-form
is a component meant to abstract the repetitive parts you have to deal with when building a form. That includes:
- the creation of the form structure,
- the loading of local / remote initial data,
- the local / remote persisting of the modified data,
- the state management.
More than that, bt-form
is designed to limit to the maximum the amount of JS code you have to write.
For simple forms, you will mostly write HTML code.
NOTE
This component builds on the knowledge acquired across a large part of the documentation. You have to understand the following concepts before you read this document:
- Forms: the part discussed here is just a
Vue
component that uses the underlying framework-agnostic form logic, - Validation: how the form's values are validated,
- Models: you're not forced to use models, but we'll see examples using them,
- Transformers: same as models, it's better if you know how they work.
And of course VueTypescript that is used across the whole UI documentation.
If you're good to go, let's dive in.
Basic usage
You can see bt-form
like a replacement of the <form>
element. In its basic form, it looks like this:
Event if the code is short, many things happen in the background:
- First, a
FormGroup
is created bybt-form
. It will hold the values of the form, - The use of
v-model
creates a binding between the object result and the value attribute of theFormGroup
. Updating one will update the other, - Because the
bt-form-text
components are located inside thebt-form
, they will implicitly use it to resolve their control, - Because strings are assigned to the control prop, a
FormControl
is created automagically for each of them in the background.
To write the same form without bt-form
, you would have to do:
import { FormFactory } from "@banquette/form";
@Component()
export default class Demo {
@Expose() public form = FormFactory.Create({
login: '',
password: ''
});
}
<bt-form-text :form="form" control="login">Username</bt-form-text>
<bt-form-text :form="form" control="password" type="password">Password</bt-form-text>
<!-- Or alternatively -->
<bt-form-text :control="form.get('login')">Username</bt-form-text>
<bt-form-text :control="form.get('password')" type="password">Password</bt-form-text>
Here, among with the increased verbosity, we have to explicitly define username
and password
in the controller, otherwise we would get an error in the view.
So if the form structure can change, bt-form
can greatly simplify the code.
Deep form
A form can contain any number of controls, at any depth, while still using the same syntax as we saw above. Here is an example:
Here a FormGroup
has been created implicitly for address
because of the /
.
Using a model
If your data is structured enough, you can use a Model instead of generating the controls on the fly like we did previously.
For that, all you have to do is to give a value to the model
prop:
<bt-form model="User"></bt-form>
TIP
model
can be a string or a constructor. To give it a string, be sure to define the corresponding alias:
@Alias('User') // That's what the string corresponds to
class User { }
The form will then create an instance of the model automatically, if not provided by v-model
.
It will then create a form component for each attribute that has a @Form()
decorator:
@Alias('User')
class User {
public id!: number; // Will not be part of the form.
@Form()
public username!: string;
@Form()
public email!: string;
}
<bt-form model="User">
<bt-form-text control="username"></bt-form-text>
<bt-form-text control="email"></bt-form-text>
</bt-form>
WARNING
Using a model will lock the structure of the form. This means that if you try to access a control that doesn't exist, an error will be thrown.
A form cannot mutate the structure of a model, only its values.
The way models are defined in relation with forms is explained in detail in the following section. But here is a working example:
Load data
Here we'll see how we can prefill the form with existing data.
NOTE
No validation is performed on the default data, so if you set an invalid value in the source object, no error will show. The validation will run depending on the configuration of the form. By default, when a field is modified.
The validation will always run when the form is submitted.
Local
To prefill the form with local data, you have to use v-model
:
@Component()
export default class Demo {
@Expose() public result = {login: 'admin', password: 'test'};
}
<bt-form v-model="result">
<bt-form-text control="login">Username</bt-form-text>
<bt-form-text control="password" type="password">Password</bt-form-text>
</bt-form>
Here the login form will contain admin
and the password test
.
TIP
Because the binding is bidirectional, any change made on result
will update the form, and any change on the form
will update result
.
If you don't want this behavior, you can set the default values via :modelValue
:
<bt-form :modelValue="result">
<bt-form-text control="login">Username</bt-form-text>
<bt-form-text control="password" type="password">Password</bt-form-text>
</bt-form>
The form will get the same default values but the result
object will be kept unchanged.
You can also use an existing model to prefill the form:
WARNING
You may be tempted to do this:
public constructor(@Form() public username: string,
@Form() public email: string) {
}
instead of:
@Form() public username: string;
@Form() public username: string;
public constructor(username: string, email: string) {
this.username = username;
this.email = email;
}
But DON'T! It will work in development but may break in a production build.
That's because the transpiler actually transform the first sample into the second when building, and by doing so the constuctor parameters
username
and email
become local variables, causing their name to be changed to something shorter.
The form's logic relies on the name of the property to match the FormControl
with the Vue
components, so if their name is changed,
the value you put in the template doesn't correspond anymore, and the fields will stay disabled.
Always define your decorators on class properties, or ensure that your transpiler doesn't mangle the names of the variables on which a decorator is set.
Remote
The form can do Ajax requests for you both for loading the original values and for persisting. Here is an example for the loading:
TIP
You can also use an endpoint. Here is an example with a model:
@Alias('User')
@Endpoint('load', '/api/user/{id}', HttpMethod.GET, {id: true})
class User {
@Api() @Form()
public firstName!: string;
@Api() @Form()
public lastName!: string;
}
<bt-form model="User" load-endpoint="load" :load-url-params="{id: 12}">
<bt-form-text control="firstName">First name</bt-form-text>
<bt-form-text control="lastName">Last name</bt-form-text>
</bt-form>
If you're not familiar with how remotes work, please refer to this section.
If you don't know what an endpoint is, please refer to this section.
Persist
Just like for the loading, you can utilize the result of the form either locally, or sent it to a remote location.
Local
To get the output, you can listen to the @persist-success
event, that is triggered when the form is submitted and valid:
<bt-form v-model="user" @persist-success="onPersist">
<bt-form-text control="firstName">First name</bt-form-text>
<bt-form-text control="lastName">Last name</bt-form-text>
</bt-form>
TIP
If you use :modelValue
, you have to wait for the form to send you the output values, because the original object is not mutated.
If you use v-model
the object is already modified, in real time. But even though the object is up-to-date, you have to ensure it is valid.
So it's always a good practice to use the event.
Remote
To send the form values to a server, you can use the props prefixed by persist
, they work just like all other components:
TIP
You can also use an endpoint. Here is an example with a model:
@Alias('User')
@Endpoint('persist', '/user/profile', HttpMethod.PUT)
class User {
@Api() @Form()
public firstName!: string;
@Api() @Form()
public lastName!: string;
}
<bt-form model="User" persist-endpoint="persist">
<bt-form-text control="firstName">First name</bt-form-text>
<bt-form-text control="lastName">Last name</bt-form-text>
</bt-form>
If you're not familiar with how remotes work, please refer to this section.
If you don't know what an endpoint is, please refer to this section.
Validation
By adding an @Assert()
decorator, you can add a validator to the corresponding form component:
import { Email } from '@banquette/validation';
@Alias('User')
class User {
@Assert(Email())
@Form()
public email!: string;
}
There is nothing else to add in the view. It's already managed by the form component itself.
Here is a working example:
The validation is a subject on its own, detailed here.
Duplicate fields
If you have a complex form with duplicate fields, you can leverage the power of the models to make it quite easily:
This is a bit more involved than what we've seen until now, but nothing crazy. We only used concepts seen before. Using models is easier than manipulating the form manually when your form become more complex.
NOTE
Please note the use of :key
in the v-for
loop. That's very important to ensure the components are deleted where you need them to be, and not always at the end of the list. See here for more detail.
Events
Name | Description |
---|---|
change | Emitted each time any value change in the form. Usage
|
v-model | Bidirectional prop holding the form's values. Usage
|
before-load | Emitted right before the form loads its original data. Can return a You can use this event to modify the load configuration before it is used. Usage
|
load-success | Emitted after everything is loaded and ready to use. Usage
|
load-error | Emitted if, for any reason, the loading fails to complete. Usage
|
before-bind-model | Emitted right before the model and the form are sent to the binder, that will keep them in sync. You can use this event to modify the model. Usage
|
after-bind-model | Emitted after the model and the form have been successfully linked together. Usage
|
before-validate | Emitted for any control in the form, right before its validators are executed. Usage
|
after-validate | Emitted each time a validation round comes to an end, no matter if errors are found or not. Usage
|
before-persist | Emitted just before sending the persistence HTTP request and the Usage
|
persist-success | Emitted after the persistence HTTP request has been performed successfully, or immediately after TIP This is the event you should use to get the result of the form locally. Usage
|
persist-error | Emitted if an error occurs in the persistence process. Usage
|
v-model:disabled | Bidirectional prop holding the form's disable state. Usage
|
Props
Option | Description |
---|---|
Bidirectional binding for the value of the form. | |
Bidirectional binding for the disabled state of the form. | |
Alias of the type of model you want to edit with the form. | |
Raw url to call to get the default values of the form. | |
Name of the Endpoint to use to load the default values of the form. If a model type is defined, the endpoint will first try to resolve for this specific model. | |
Url parameters to use when building the load request. | |
Custom headers to add to the load request. | |
Raw url to call when persisting the result of the form. | |
HTTP method to use when persisting the result of the form. | |
Name of the Endpoint to use when persisting the result of the form. If a model type is defined, the endpoint will first try to resolve for this specific model. | |
Url parameters to use when building the persist request. | |
Custom headers to add to the persist request. | |
Define how the payload should be encoded when sending the request to a server. Available values are:
| |
If true , the form will be submitted if the user press "Enter" with a control in focus. | |
The group of validation to apply to the root of the form. |
Slots
Name | Description |
---|---|
default |
The default slot is meant to render the form content. This is also the fallback slot for other slots if they have to content defined.
Props:
|
loading | This slot is shown when there form is initializing or loading its initial data.
If no content is defined, the Props: none |
persisting | This slot is shown when there form is sending its result to a remote location.
If no content is defined, the Props: none |
load-error | Shown if an error occurs in the loading phase.
If no content is defined, the Props:
|
persist-error | Shown if an error occurs in the persistence phase.
If no content is defined, the Props:
|
persist-success | Shown after a persist has been performed successfully.
If no content is defined, the Props:
|
validation | An optional slot you can use if you want to dissociate your fields from the validation markup if you use the validation components. This is totally optional as validators can be put anywhere in the form, this is just a tool to organize the HTML markup. Props: none |