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 by bt-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 the FormGroup. Updating one will update the other,
    • Because the bt-form-text components are located inside the bt-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 hereopen in new window for more detail.

                  Events

                  Name Description
                  change

                  Emitted each time any value change in the form.

                  Usage
                  import { ValueChangedFormEvent } from "@banquette/form";
                  
                  function onChange(event: ValueChangedFormEvent): void {
                      console.log('Form changed:', event.newValue);
                  }
                  
                  <bt-form @change="onChange"></bt-form>
                  
                  v-model

                  Bidirectional prop holding the form's values.

                  Usage
                  const values: any = {};
                  
                  function onChange(values: any): void {}
                  
                  <!-- Bidirectional -->
                  <bt-form v-model="values"></bt-form>
                  
                  <!-- Or, upward only.-->
                  <bt-form @update:modelValue="onChange"></bt-form>
                  
                  <!-- Or, downward only -->
                  <bt-form :modelValue="values"></bt-form>
                  
                  before-load

                  Emitted right before the form loads its original data. Can return a Promise so the form will wait to continue.

                  You can use this event to modify the load configuration before it is used.

                  Usage
                  import { ValueChangedFormEvent } from "@banquette/form";
                  import { BeforeLoadEventArg } from "@banquette/ui";
                  
                  function onBeforeLoad(event: BeforeLoadEventArg): void {
                      event.vm.loadRemote.updateConfiguration({
                          url: '/load-url-override'
                      });
                  }
                  
                  <bt-form @before-load="onBeforeLoad"></bt-form>
                  
                  load-success

                  Emitted after everything is loaded and ready to use.

                  Usage
                  import { ValueChangedFormEvent } from "@banquette/form";
                  
                  function onLoadSuccess(): void {
                      // Here you can do any operation that requires 
                      // the form and model to be fully initialized.
                  }
                  
                  <bt-form @load-success="onLoadSuccess"></bt-form>
                  
                  load-error

                  Emitted if, for any reason, the loading fails to complete.

                  Usage
                  import { ValueChangedFormEvent } from "@banquette/form";
                  import { ActionErrorEventArg } from "@banquette/ui";
                  
                  function onLoadError(event: ActionErrorEventArg): void {
                      console.error(event.error);
                  }
                  
                  <bt-form @load-error="onLoadError"></bt-form>
                  
                  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
                  import { ValueChangedFormEvent } from "@banquette/form";
                  import { BeforeBindModelEventArg } from "@banquette/ui";
                  
                  function onBeforeBindModel(event: BeforeBindModelEventArg): void {
                      console.log('Model instance:', event.model);
                  }
                  
                  <bt-form @before-bind-model="onBeforeBindModel"></bt-form>
                  
                  after-bind-model

                  Emitted after the model and the form have been successfully linked together.

                  Usage
                  import { ValueChangedFormEvent } from "@banquette/form";
                  import { AfterBindModelEventArg } from "@banquette/ui";
                  
                  function onAfterBindModel(event: AfterBindModelEventArg): void {
                      console.log('Model instance:', event.model);
                      console.log('Model binder:', event.binder);
                  }
                  
                  <bt-form @after-bind-model="onBeforeBindModel"></bt-form>
                  
                  before-validate

                  Emitted for any control in the form, right before its validators are executed.

                  Usage
                  import { ValueChangedFormEvent } from "@banquette/form";
                  import { BeforeValidateEventArg } from "@banquette/ui";
                  
                  function onBeforeValidate(event: BeforeValidateEventArg): void {
                      console.log('Before validate:', event.source.path);
                  
                      // To disable this round of validation:
                      event.preventDefault();
                  }
                  
                  <bt-form @before-validate="onBeforeValidate"></bt-form>
                  
                  after-validate

                  Emitted each time a validation round comes to an end, no matter if errors are found or not.

                  Usage
                  import { ValueChangedFormEvent } from "@banquette/form";
                  import { AfterValidateEventArg } from "@banquette/ui";
                  
                  function onAfterValidate(event: AfterValidateEventArg): void {
                      console.log(
                          'After validate:',
                          event.source.path,
                          event.result.valid,
                          event.result.violations
                      );
                  }
                  
                  <bt-form @after-validate="onAfterValidate"></bt-form>
                  
                  before-persist

                  Emitted just before sending the persistence HTTP request and the persist-success event is emitted. You can use this event to modify the payload before it is sent, or to prevent the action from continuing.

                  Usage
                  import { ValueChangedFormEvent } from "@banquette/form";
                  import { BeforePersistEventArg } from "@banquette/ui";
                  
                  function onBeforePersist(event: BeforePersistEventArg): void {
                      console.log('Before persist:', event.payload);
                  
                      // To stop the persist from continuing:
                      event.preventDefault();
                  }
                  
                  <bt-form @before-persist="onBeforePersist"></bt-form>
                  
                  persist-success

                  Emitted after the persistence HTTP request has been performed successfully, or immediately after before-persist if no remote has been defined.

                  TIP

                  This is the event you should use to get the result of the form locally.

                  Usage
                  import { ValueChangedFormEvent } from "@banquette/form";
                  import { AfterPersistEventArg } from "@banquette/ui";
                  
                  function onPersistSuccess(event: AfterPersistEventArg): void {
                      console.log('Persist success: ', event.payload);
                  }
                  
                  <bt-form @persist-success="onPersistSuccess"></bt-form>
                  
                  persist-error

                  Emitted if an error occurs in the persistence process.

                  Usage
                  import { ValueChangedFormEvent } from "@banquette/form";
                  import { ActionErrorEventArg } from "@banquette/ui";
                  
                  function onPersistError(event: ActionErrorEventArg): void {
                      console.error(event.error);
                  }
                  
                  <bt-form @persist-error="onPersistError"></bt-form>
                  
                  v-model:disabled

                  Bidirectional prop holding the form's disable state.

                  Usage
                  const disabled: boolean = false;
                  
                  function onDisabledStateChange(disabled: boolean): void {
                      console.log('Disabled state:', disabled);
                  }
                  
                  <!-- Bidirectional -->
                  <bt-form v-model:disabled="disabled"></bt-form>
                  
                  <!-- Or, upward only.-->
                  <bt-form @update:disabled="onDisabledStateChange"></bt-form>
                  
                  <!-- Or, downward only -->
                  <bt-form :disabled="disabled"></bt-form>
                  

                  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:
                  • form-data: encoded using a FormData object.
                  • json: encoded as a JSON string.
                  • raw: the payload is untouched, you will have to encode it yourself using an event hook.
                  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:

                  • v: form's viewData object. It holds all the data that are exposed to the view,
                  • model: (shortcut to v.model), holds the model associated with the form,
                  • control: a function you can call to get a control from the form. If it doesn't exist, it will be created (if no model is defined),
                  • submit: a function you can call to submit the form,
                  • errors: errors map of the bt-form component itself. For form errors, use v.getControlErrors.
                  loading

                  This slot is shown when there form is initializing or loading its initial data. If no content is defined, the default slot is shown instead.


                  Props: none

                  persisting

                  This slot is shown when there form is sending its result to a remote location. If no content is defined, the default slot is kept visible instead.


                  Props: none

                  load-error

                  Shown if an error occurs in the loading phase. If no content is defined, the default slot is kept visible instead.


                  Props:

                  • v: form's viewData object,
                  • error: the error message.
                  persist-error

                  Shown if an error occurs in the persistence phase. If no content is defined, the default slot is kept visible instead.


                  Props:

                  • v: form's viewData object,
                  • error: the error message.
                  persist-success

                  Shown after a persist has been performed successfully. If no content is defined, the default slot is kept visible instead.


                  Props:

                  • v: form's viewData object,
                  • error: the error message.
                  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