Feedback

Validation

#Defining Validation Rules

Let's create a simple newsletter subscription form as in our previous chapter at app\Forms\NewsletterForm.php, but this time also add validation rules to email field:

<?php

namespace App\Forms;

class NewsletterForm extends \Laraform
{
  public $model = \App\Subscriber::class;

  public function schema() {
    return [
      'email' => [
        'type' => 'text',
        'placeholder' => 'Email address',
        'rules' => 'required|email'
      ]
    ];
  };

  public function buttons() {
    return [[
      'label' => 'Submit'
    ]];
  };
}

Just like Laravel validation rules they can be separated using | pipes or can be defined as an array:

public function schema() {
  return [
    'type' => 'text',
    'placeholder' => 'Email address',
    'rules' => ['required', 'email']
  ];
}

Now render the form and try to submit with empty value. You should receive an error message under the field saying The Email address field is required. Next try submitting with a non-email value. Then fill in correctly and see that it submits the data.

What you don't see is that Laraform also validates the data on the backend. You don't see it because you can only submit valid data, but if the user manages to bypass the frontend validation it will still be secured.

When you assign a validation rule to an element that will validate the element both on frontend and backend. On the frontend Laraform's own validation rules are used, while on the backend it relies on Laravel's rules.

#Frontend & Backend Rules

Sometimes you need to define different validation rules for frontend and backend. You can do this quite straightforwardly:

public function schema() {
  return [
    'type' => 'text',
    'placeholder' => 'Email address',
    'rules' => [
      'frontend' => ['required', 'email'],
      'backend' => ['required', 'email', 'unique:subscribers,email']
    ]
  ];
}

It's great because now we can check if the user is already added to our subscriber list, but only on the backend. Let's see how we can do that on the frontend.

#Async Rules

You might ask, why can't we use the same unique:subscribers,email rule on the frontend to validate the uniqueness of the email address? The answer is simple: for security reasons we don't want to reveal actual data table and field names on the frontend. Therefore we need to make a workaround to have it secured.

The exists and unique rules have a different setup then the Laravel version and therefore they need to be added as separate frontend and backend validation rules each case.

Let's add the unique to our frontend rule set using subscriber as the first parameter:

public function schema() {
  return [
    'type' => 'text',
    'placeholder' => 'Email address',
    'rules' => [
      'frontend' => ['required', 'email', 'unique:subscriber'],
      'backend' => ['required', 'email', 'unique:subscribers,email']
    ]
  ];
}

The unique rule accepts a list of parameters like unique:param1,param2,... instead of unique:database,email. In this case we're passing the subscriber param to the backend so it will know what type of validation it should perform. Let's go through the next steps to see how we can make it work.

Set Endpoint For Unique Rule

The unique validation rule is async, meaning it sends a request to an endpoint to perform validation on the backend.

First we need to define which endpoint we want to use to perform the validation. Let's add that to our configuration:

import Vue from 'vue'
import Laraform from '@laraform/laraform'

Laraform.config({
  endpoints: {
    validators: {
      unique: '/validate/unique'
    }
  }
})

Vue.use(Laraform)

// ...

Create Route

Create a route that will handle the unique validation endpoint in routes/web.php:

Route::post('/validate/unique', 'ValidationController@unique');

Create Controller

Now let's create a controller that will perform the validation at app\Http\Controllers\ValidationController.php:

<?php

namespace App\Http\Controllers;

use Validator;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class ValidationController extends Controller
{
  public function unique(Request $request) {
    $type = $request->params[0];

    switch ($type) {
      case 'subscriber':
        $validator = Validator::make($request->all(), [
          'value' => 'unique:subscribers,email',
        ]);
        break;
    }

    return response()->json(!$validator->fails());
  }
}

It might look a bit cumbersome to take these many steps to make it work, but this is how we can make the process secure.

Once you have these set up you can easily add different unique checkers too:

switch ($type) {
  case 'subscriber':
    $validator = Validator::make($request->all(), [
      'value' => 'unique:subscribers,email',
    ]);
    break;

  case 'user':
    $validator = Validator::make($request->all(), [
      'value' => 'unique:subscribers,email',
    ]);
    break;

  // ...
}

Now if you reload the form you should see that the email field is initiating a backend call on each character typed and will return false and show the field error if you type an email that already exists in the database.

#Debounce

We might not want to initiate a backend check on each and every character typed, but after just a few seconds if the typing stops. To do that you can use debounce:

public function schema() {
  return [
    'type' => 'text',
    'placeholder' => 'Email address',
    'debounce' => 1000,
    'rules' => [
      'frontend' => ['required', 'email', 'unique:subscriber'],
      'backend' => ['required', 'email', 'unique:subscribers,email']
    ]
  ];
}

If you reaload the form and start typing you'll notice that the unique validator only triggers if the user stops typing for 1 second.

In case you don't want every rule to have a delay you can also assign debounce to specific rules:

public function schema() {
  return [
    'type' => 'text',
    'placeholder' => 'Email address',
    'rules' => [
      'frontend' => ['required', 'email:debounce=1000', 'unique:subscriber,debounce=1000'],
      'backend' => ['required', 'email', 'unique:subscribers,email']
    ]
  ];
}

#Error Messages

Errors messages as stored in Laraform's frontend locale file at @laraform/laraform/locales directory. They will be displayed according to the form's selected locale.

#Custom Messages

The form has a messages method which can return custom error messages for validation rules. This is how we'd overwrite required rule's message for example:

class CustomMessagesForm extends \Laraform
{
  public function messages() {
    return [
      'required' => 'You must fill in this field'
    ];
  }

  // ...
}

The same is true for elements, where messages property can be defined on an element level and also accepts an object in the same format as locale or form would:

class CustomNameMessageForm extends \Laraform
{
  public function schema() {
    return [
      'name' => [
        'type' => 'text',
        'label' => 'Name',
        'rules' => 'required',
        'messages' => [
          'required' => 'We must know your name, please fill in'
        ]
      ]
    ];
  }
}

#Available Validation Rules

Here's the list of available validator rules:

accepted

Examines if the value is true, 'true', 'yes', 'on', '1' or 1.

active_url

Aims to validate if the value is an URL with active A or AAAA type DNS record. To use the default endpoint provided by Laraform backend set the following configuration:

Laraform.config({
  endpoints: {
    validators: {
      active_url: '/laraform/validator/active_url'
    }
  }
})

You can also create your own validator endpoint. In such case return a true if the provided string is a valid A/AAAA type DNS record and false otherwise.

after:date

The value must be after the given date. The date parameter can be either a concrete date like '2018-01-01', a relative date which is either 'today', 'tomorrow' or 'yesterday' or the path of an other field like 'start_date'.

Examples
// being a concrete date
'rules' => 'date|after:2018-01-01'

// being a relative date
'rules' => 'date|after:today'

// being an other field's path
'rules' => 'date|after:accomodation.checkin_date'

Date must be a valid RFC2822 or ISO format according to moment.js.

after_or_equal:date

It's the same as after rule, except that the value must be after or equal to the given date.

alpha

The value can only contain English alphabetic characters.

alpha_dash

The value can only contain numbers, English alphabetic characters, dash - and underscore _.

alpha_num

The value can only contain numbers and English alphabetic characters.

array

The value must be an array.

before:date

It's the same as after rule, except that the value must be before the given date.

before_or_equal:date

It's the same as after rule, except that the value must be before or equal to the given date.

between:min,max

The value must be between the size of min and max. The size is evaluated the same way as described at size rule.

boolean

The value must be some form of a boolean which is either true, false, 1, 0, '1' or '0'.

confirmed

The value must identical to an other field's value which has the same name ending with _confirmation.

Example
'password' => [
  'type' => 'password',
  'label' => 'Password',
  'rules' => 'confirmed'
],
'password_confirmation' => [
  'type' => 'password',
  'label' => 'Password Again'
]

date

The value must be a valid RFC2822 or ISO format according to moment.js.

date_equals:date

The value must equal the given date and must be in a valid RFC2822 or ISO format according to moment.js.

date_format:format

The value must match the given date format according to moment.js.

This rule is not compatible with Laravel's date_format rule because strotime function is using different date/time tokens.

different:field

The value must be different from a different field's value.

digits:value

The value must be numeric and have an exact length of value.

digits_between:min,max

The value must be numeric and have a length between min and max.

dimensions

The value must be an image with dimension constraints. Available constraints:

  • min_width
  • min_height
  • max_witdth
  • max_height
  • height
  • width
  • ratio

The ratio can be a float, like 0.66 or a statement like 2/3 which represents width / height ratio.

Examples
'rules' => 'dimensions:min_width=768'
'rules' => 'dimensions:min_height=1024'
'rules' => 'dimensions:min_width=768,ratio=2/3'

distinct

The value must be a unique member of an array.

Example
'favorite_numbers' => [
  'type' => 'list',
  'label' => 'Favorite numbers:',
  'element' => [
    'type' => 'text',
    'placeholder' => 'Number',
    'rules' => 'distinct'
  ]
]

email

The value must be a valid email format.

exists:param1,param2,...

Aims to check if a given value exists in a database by calling the endpoint retrieved from Configuration endpoints.validators.exists using axios POST request. The value is submitted as value along with the provided params array. If any param is identical to an element's name in the form the value of that element will be sent instead of the name of the param. It expects to have a true or false response, if the response is true the field will be resolved as valid.

This rule is not compatible with Laravel's exists rule because of security reasons but you can use separate rules for backend and frontend.

file

The value must be an instance of File.

filled

The value must be not empty.

gt:field

The value must be greater than the size of the given field. The size is evaluated the same way as described at size rule.

gte:field

The value must be greater than or equal to the size of the given field. The size is evaluated the same way as described at size rule.

image

The value must a file with jpeg, png, bmp, gif or svg extension.

in:foo,bar,...

The value must be one of the provided values.

Example
'role' => [
  'type' => 'select',
  'label' => 'Roles',
  'items' => [
    'admin' => 'Admin',
    'editor' => 'Editor'
  ],
  'rules' => 'in:admin,editor'
]

in_array:anotherfield.*

The value must be present in the anotherfield array.

integer

The value must be an integer according to Number.isInteger() method.

ip

The value must be a valid IP address.

ipv4

The value must be a valid IPv4 address.

ipv6

The value must be a valid IPv6 address.

json

The value must be a valid JSON string.

lt:field

The value must be lower than the size of the given field. The size is evaluated the same way as described at size rule.

lte:field

The value must be lower than or equal to the size of the given field. The size is evaluated the same way as described at size rule.

max:value

The size of the value must be lower than or equal to max. The size is evaluated the same way as described at size rule.

mimetypes:text/plain,...

The value must be an instance of File and have one of the listed mimetype.

mimes:zip,rar,...

The value must be an instance of File and have one of the listed extensions.

min:value

The size of the value must be at least min. The size is evaluated the same way as described at size rule.

not_in:foo,bar,...

The value must not be one of the provided values.

not_regex:pattern

The value must not match the provided regex pattern.

Example
'rules' => 'not_regex:/^.+$/i'

When using pipe | in the pattern you must define the rules as array instead of a string.

nullable

Certain rules should only execute if the input has value otherwise it should be ignored. For example if a user may optionally fill in his birthday, obviously the date format should be validated if the input is filled. This is where nullable rule comes in. If it's present among the validation rules it instructs the validator to only execute validation rules if the field has a value.

'birthday' => [
  'type' => 'date',
  'label' => 'Birthday',
  'rules' => 'nullable|date_format:YYYY-MM-DD'
]

numeric

The value must be numeric.

regex:pattern

The value must match the provided regex pattern.

Example
'rules' => 'regex:/^.+$/i'

When using pipe | in the pattern you must define the rules as array instead of a string.

required

The value must not be empty. The value is considered empty as the following:

  • the value is null
  • the value is undefined
  • if the value is a string it's ''
  • if the value is an array it contains no items
  • if the value is a File object it's name is empty

same:field

The value must be the same as the give field's value.

Example
'password' => [
  'type' => 'password',
  'label' => 'Password',
  'rules' => 'same:password_again'
],
'password_again' => [
  'type' => 'password',
  'label' => 'Password Again'
]

size:value

The size of the value must be exactly as the given value. Size is calculated as the following:

  • if the value is string then it's the length of the string
  • if the value is numeric then it's the actual numeric value
  • if the value is array then it's the length of the array
  • if the value is File then it's the size of the file.

string

The value must be a string.

timezone

The value must be a valid timezone, eg. 'America/Los_Angeles'.

unique:param1,param2,...

Aims to check if a given value exists in a database by calling the endpoint retrieved from Configuration endpoints.validators.unique using axios POST request. The value is submitted as value along with the provided params array. If any param is identical to an element's name in the form the value of that element will be sent instead of the name of the param. It expects to have a true or false response, if the response is true the field will be resolved as valid.

This rule is not compatible with Laravel's unique rule because of security reasons but you can use separate rules for backend and frontend.

url

The value must be a valid URL format.

uuid

The value must be a valid UUID format.

#Multilingual Rules

Validation rules can be specified for different languages when using multilingual elements:

<?php

namespace App\Forms;

class TranslatableForm extends \Laraform
{
  public $multilingual = true;

  public $languages = [
    'en' => [
      'code' => 'en',
      'label' => 'English'
    ],
    'de' => [
      'code' => 'de',
      'label' => 'German'
    ]
  ];

  public function schema() {
    return [
      'title' => [
        'type' => 't-text',
        'label' => 'Title',
        'rules' => [
          'en' => 'required',
        ]
      ],
    ];
  }
}

In this case the title element will be required in English, while can be left empty in German.

#Conditional Rules

You may add conditional rules to elements which should only trigger under certain circumstances. Let's see an example, where we'd require phone number from a user only if delivery option is selected:

'delivery' => [
  'type' => 'select',
  'label' => 'Do you want delivery?',
  'items' => [
    'yes' => 'Yes',
    'no' => 'No'
  ]
],
'phone' => [
  'type' => 'text',
  'label' => 'Phone number',
  'rules' => [
    [
      'required' => ['delivery', 'yes']
    ]
  ]
]

As you can see the rule is in an array, where the key is the rule itself, while the value is another array. This second array should contain minimum 2 values, the first is the data path of the other element and the second is the expected value.

The data path equals to the element's path except for Group element, because that doesn't nest data structure. Eg. if you have a name input in group element called profile it's path will be profile.name but it's data path will be name.

Multiple Accepted Values

It's also possible to accept multiple values for the condition by defining an array of values as the second parameter:

'delivery' => [
  'type' => 'select',
  'label' => 'How do you want receive your package?',
  'items' => [
    1 => 'Pick at the store',
    2 => 'Delivery with UPS',
    3 => 'Delivery with FedEx'
  ]
],
'phone' => [
  'type' => 'text',
  'label' => 'Phone number',
  'rules' => [
    [
      'required' => ['delivery', [2,3]]
    ]
  ]
]

The values of the array can be either numbers or strings.

Custom Operator

You might want to use different operators to examine the condition value, which can be done by simply defining the operator as the second array element, and the comparison value third:

'deposit' => [
  'type' => 'text',
  'label' => 'Deposit amount'
],
'id_number' => [
  'type' => 'text',
  'label' => 'ID Card number',
  'rules' => [
    [
      'required' => ['deposit', '>', 500]
    ]
  ]
]

In this example, ID Card number will only be required if the Deposit amount is higher than 500.

Here's the list of available operators:

  • !=
  • =
  • >
  • >=
  • <
  • <=

#Custom Conditions

You can also create custom conditions for rules. In this case conditions must be defined separately for backend and frontend. To demonstrate this, let's implement our previous example with custom condition.

 Create CustomConditionForm.php

First create a new backend form at app/Forms/CustomConditionForm.php:

<?php

namespace App\Forms;

class CustomConditionForm extends \Laraform
{
  public $component = 'custom-condition-form';

  public function schema() {
    return [
      'deposit' => [
        'type' => 'text',
        'label' => 'Deposit amount'
      ],
      'id_number' => [
        'type' => 'text',
        'label' => 'ID card number',
      ]
    ];
  }

  public function buttons() {
    return [[
      'label' => 'Submit'
    ]];
  }
}

Create CustomConditionForm.vue

Now create a frontend form at resources/js/components/forms/CustomConditionForm.vue:

<script>
  export default {
    mixins: [Laraform],
    data() {
      return {
        schema: {
          id_number: {
            rules: [
              {
                required: function(Validator){
                  return this.el$('deposit').value > 500
                }
              }
            ]
          }
        },
        buttons: [{
          disabled() {
            return this.form$.invalid
          }
        }]
      }
    }
  }
</script>

What we are doing here is extending the form's id_number on the frontend by adding rules and we define custom condition for required rule, which should only apply when the deposit amount is larger than 500. In this example we're also extending the button to become disabled if the form is invalid.

Add CustomConditionForm.vue to app.js

Next, let's add the frontend form to our app.js (or main JS file):

import Vue from 'vue'
import Laraform from '@laraform/laraform'
import CustomConditionForm from './components/forms/CustomConditionForm'

Vue.use(Laraform)

const app = new Vue({
  el: '#app',
  components: {
    CustomConditionForm
  }
})

After that, assign this form to the view using app('App\Forms\CustomConditionForm') and render it with ->render() method as we've learned in the Rendering chapter.

You should see Deposit and ID card number fields and if you enter higher amount than 500 you'll notice that ID card number will become required if you try to submit the form.

But what happens if you mistakenly enter 1000 instead of 100 to deposit, submit the form and receive an error under ID card number? Even after you change the amount to 100 the ID card number's error won't go away. Let's see what we can do against it.

Watching For Condition Change

To watch for value changes of an other element we can use the Validator's .watch() method, which only sets our watcher once, instead of Vue's this.$watch() which would execute each time the condition is evaluated.

Here's how we can watch the other field's value and revalidate our field upon the change of deposit:

rules: [
  {
    'required': function(Validator){
      // By using the Validator's .watch() method we
      // make sure that our watcher is only registered
      // once instead of registering it each time the
      // condition is checked. The data we are watching
      // is relative to the root Laraform component
      // so in this case we are watching its data.deposit
      // which is exactly what we need here.
      Validator.watch('data.deposit', () => {

        // By using the Validator's validate() method
        // we can simply revalidate the rule.
        Validator.validate()
      })

      return this.el$('deposit').value > 500
    }
  }
]

Now if you decrease Deposit amount below 500 when ID Card number is already displaying an error, you can see that it disappears as the element gets revalidated.

We're doing great so far but what happens after the user submits the data? We need to care of backend validation of course.

Add Custom Conditional Rule To Backend

Let's extend our backend form's schema with the conditional required rule:

public function schema() {
  return [
    'deposit' => [
      'type' => 'text',
      'label' => 'Deposit amount'
    ],
    'id_number' => [
      'type' => 'text',
      'label' => 'ID card number',
      'rules' => [
        'backend' => [
          [
            'required' => function($data) {
              return $data->deposit > 500;
            }
          ]
        ]
      ]
    ]
  ];
}

That's it, the backend will required ID card number on the backend too only if the deposit amount is larger than 500.