All,

Here is my solution to implementing inline errors in your custom form for server-side validations. In summary, I am going to use a hidden form field to store error messages that were generated within the validateForm method and then use JQuery to parse and output the results. The sample code are snippets of actual code of a module I created. It has been heavily edited for demonstrative purposes.

Below is the sample form code. While I am using text boxes, the solution does work on other form elements. You may need to adjust the jQuery script depending on your theme.


    public function buildForm (array $form, FormStateInterface $form_state) { 

        // Embed my jQuery script 
        $form['#attached']['library'] = array(
            'my_module/my_module_inline_errors',
        );

        // Field Test 1
        $form['field_test_1'] = array(
            '#type' => 'textfield',
            '#title' => t('Field 1'),
        );

        // Field Test 2
        $form['field_test_2'] = array(
            '#type' => 'textfield',
            '#title' => t('Field 2'),
        );

        // Field Test 3
        $form['field_test_3'] = array(
            '#type' => 'textfield',
            '#title' => t('Field 3'),
        );

        // Submit
        $form['submit'] = array(
            '#type' => 'submit',
            '#value' => t('Submit'),
        );

      return $form ;
    }
    
    public function validateForm (array &$form, FormStateInterface $form_state) {
        // INIT
        $values = $form_state->getValues();

        // Validate
        if ( $values['field_test_1']  == '' ) {
            $form_state->setErrorByName('field_test_1', t('Error 1'));
        }

        if ( $values['field_test_2']  == '' ) {
            $form_state->setErrorByName('field_test_2', t('Error 2'));
        }

        if ( $values['field_test_3']  == '' ) {
            $form_state->setErrorByName('field_test_3', t('Error 3'));
        }

        // If validation errors, save them to the hidden form field in JSON format
        if ( $errors = $form_state->getErrors() ) {
            $form['my_module_error_msgs']['#value'] = json_encode($errors);
        }
        
        return;
    }

As you can see, the code is very straightforward. The buildForm is business as usual in that we are defining 4 form elements (3 text boxes and 1 submit button). Note that we are also attaching our jQuery script which will be discussed later on

Within the validateForm method I am checking to see if any errors were previously detected. If so then we retrieve the associative array of errors from $form_state, transform it into a JSON string and save it in the hidden form field. Note that the array keys will be the field names and the values will be the error messages.

In the next section of code, we are going to automatically alter the form using hook_form_alter.

/**
  * Implments hook_form_alter
  **/
function my_module_form_alter (&$form, FormStateInterface &$form_state) {
    // Create hidden field to store error messages
    $form['my_module_error_msgs']['#type'] = 'hidden';
    
    // Automatically add ID attribute for each form element
    foreach ($form as $key => $frm) {
	if ( stristr($key, 'field_' ) ) {
	     $form[$key]['#id'] = $key; 
	}
    }
}

So here I am taking advantage of hook_form_alter to automate a few things that help simplify the buildForm code. This also allows me to create other custom forms with very minimal code additions.

Note that I like to standardize on a naming convention for my form field names, hence they all begin with the prefix "field_". The purpose of the foreach loop is to overwrite the Drupal default ID attribute with the element name and again minimizes the code within buildForm. As you will see in the next example, using this naming convention has its advantages.

// my_module_inline_errors.js
jQuery(document).ready(function($){

    var form_errors = $('input[name="my_module_error_msgs"]').val();
    if (form_errors) {
        form_errors = JSON.parse(form_errors);
        var keys = Object.keys(form_errors);
        var count = Object.keys(form_errors).length
        for (i = 0; i < count; i++) {
            msg = form_errors[keys[i]];
            $('#' + keys[i]).after('<div class="my-module-inline-error">' + msg + '</div>' );
        }
    }
});

And so here is where the magic happens. When this JQuery script is loaded, it will check to see if any error messages have been deposited in the hidden field. If found the JSON string is converted to a JSON object. By parsing through the entire array, I can identify the invalid field and append the error message at the end of the input field. The CSS class can be customized to control proper positioning and highlighting.

My JQuery selector locates each field by ID which as you will recall, I purposely set this value in hook_form_alter. This method of searching in jQuery is much more efficient than searching via the name attribute (i.e $('input[name="field_name"]')). Regardless, you can choose whatever method works for you.

So the overall advantages for me is simplicity. Previously I tried to store my errors messages in a session variable but this will not work within the life cycle of the FormAPI. The idea was within buildForm to fetch the error messages from the session variable and output them within buildForm. The problem appeared to be with the form cache and my messages would only appear if I refresh the page. I have previously posted this question seeking help on this forum. I am fully aware there is an ongoing project to develop Inline Errors for Drupal 8 as a module and I have tried to contact the project members but to no avail.

Anyway I am very happy with this and would love to hear your comments and feedback,

Cheers

Comments

wengerk’s picture

Hello,

I just wrote a full article about my implementation of inline validations for Drupal 8 !

https://antistatique.net/en/we/blog/2017/02/16/drupal-8-inline-messages-...

It's fare simpler because I use the Form API whitout using Javascript or externals scripts !

Check it out and leave a comment if you found a better solution.

dotmundo’s picture

Awesome. I'll give it a look later and let you know.

Amit Dwivedi’s picture

Hi,

Thanks for your great Tutorial.
But I am not able to get the inline validations against the example form in tutorial.
It seems something changed in latest release. Can you please help.

Thank you very much again.

Regards
Amit

wengerk’s picture

I can help you but I need some code, could you post your code on a gist ?

Amit Dwivedi’s picture

Hi Wengerk,

Thanks for your response.
I am just using the code provided by you in above mentioned link. (After success will start with requirement (form))

The problem I am facing here is on submit I am getting the Flash/Growl messages with Inline messages.
Although I have used $form['#attributes']['novalidate'] = 'novalidate';, but it doesn't show any effect.
I can hide the Flash/Growl messages with CSS but looking for a better approach.

Also if you can guide me how to achieve Live validation Approach, it will be great.

Thanks again.

Regards
Amit Dwivedi

wengerk’s picture

Yeah, the growl messages are generated because I use the standard way to set error using setErrorByName.
The $form['#attributes']['novalidate'] = 'novalidate'; is only need to avoid HTML5 validations.

To remove all the grow messages you must add some code after the loop // Add error to fields using Symfony Accessor.
Eg. $form_state->clearErrors();.

So in my example you will get something like:

// If validation errors, add inline errors
if ($errors = $form_state->getErrors()) {
    // Add error to fields using Symfony Accessor
    $accessor = PropertyAccess::createPropertyAccessor();
    foreach ($errors as $field => $error) {
        if ($accessor->getValue($form, $field)) {
            $accessor->setValue($form, $field.'[#prefix]', '<div class="form-group error">');
            $accessor->setValue($form, $field.'[#suffix]', '<div class="input-error-desc">' .$error. '</div></div>');
        }
    }
    $form_state->clearErrors();
}

But be carefull @amit-dwivedi, for accessiblities it's a very good approach to have both inline and growl messages :D.

Rob230’s picture

The trouble with FormState::clearErrors() is that it stops the validation and the form goes to the submit step. The whole point of using setErrorByName() is that it returns the form with an error on it.

I've been looking for a way around this for a while now, but not having much luck.

It's a real shame that the static method FormState::setAnyErrors() is protected. This seems to be the only way to stop the form from submitting. It gets called in setErrorByname().

I can't intercept the messages at the drupal_set_message() level due to the way Drupal stores them in the session. I tried to intercept them in my own validation callback, but of course it's firing before the messages are set. I don't see anywhere that I can intercept them. preprocess_status_messages() is not useful for me. I don't want to remove all messages, or remove them by matching to a long string with variables in it.

It's also annoying that the preprocess for status_messages calls drupal_get_messages(), instead of being passed the messages in $variables. This seems like an unDrupal way of doing things. If I want to render some status messages I have to do it myself without using the existing status_messages template, because that template has a destructive preprocess and cannot be passed the messages you want to render.

wengerk’s picture

Yeah I know, I already search a long time.... for me, my trait is one of the cleanest & drupal way to do the job.

I hope the core team will add some method to improve form validations/messages.

g_miric’s picture

You could use "Inline form errors" core module for this.

chintukarthi’s picture

I enabled the inline form errors module. But still the error message is not getting displayed when the form is submitted. The form just stays as it. Some help here please.

harishpatel86’s picture

Hello @wengerk,
   I implement it with " class="use-ajax" data-dialog-type="modal" in hook form alter on submit but i am not better luck. Please review my code .

 if ($form_id == 'node_abcd_form') {

  $form['#prefix'] ='<span class="file-valid-message"></span>';
  $form['#cache']['max-age'] = 0;

  $term_id = \Drupal::request()->query->get('termid'); 
  $form['field_reference']['widget'][0]['target_id']['#default_value'] =  \Drupal\taxonomy\Entity\Term::load($term_id);

    $form['actions']['submit']['#ajax']=[
        'callback' => 'validateDocAjax',
        'event' => 'click',
        ];
       
    }
  

  *
 * Ajax callback to validate the Document field in toolkit content type.
 */

 function validateDocAjax(array &$form, FormStateInterface $form_state) {

    /*$response = new \Drupal\Core\Ajax\AjaxResponse();
    $message = 'Please fill the form with title and document';
    if (!$form_state->hasAnyErrors()) {
     $response->addCommand(new \Drupal\Core\Ajax\CloseDialogCommand());  
        $destination = \Drupal::destination()->get();
      $response->addCommand(new RedirectCommand($destination));
     return $response; 
    
     } else 
     {
      $response->addCommand(new HtmlCommand('.file-valid-message', $message));
      return $response; 
     }*/

      // Assert the firstname is valid
    if (!$form_state->getValue('title') || empty($form_state->getValue('title'))) {
        $form_state->setErrorByName('title', 'Votre prénom est obligatoire.');
    }
   

}

//  Its show an ajax error message and also not show inline message 

AjaxError: 
An AJAX HTTP error occurred.
HTTP Result Code: 200
Debugging information follows.
Path: /node/add/abcd?destination=/{{ same page }}&_wrapper_format=drupal_modal&ajax_form=1
StatusText: OK
ResponseText: 

wengerk’s picture

Hello,

I think you should ask your question on Stackoverflow, you would have a lot better chance from people to answer your questions there ^^.

Stackoverflow Drupal 8 Tagged -> https://stackoverflow.com/questions/tagged/drupal-8
Drupal Stack -> https://drupal.stackexchange.com

Also, just to give you a quick answer - I would suggest you use the Inline Form Error Core module which helps you way more than this old-fashioned way.

chintukarthi’s picture

I am new to drupal. I face the same issue. 

I made changes but it's not working. It shows some internal error occurred in the website. Can anyone help me out?

wengerk’s picture

Sorry didn't see your previous message ..... 

Could you be more precise where the error occurs ? It's a custom form or in the Drupal admin UI ?

Hello !

Since Drupal 8.4.x Drupal has a stable Core module Inline Form Errors which perform exactly what you need. Did you try it ?

https://www.drupal.org/docs/8/core/modules/inline-form-errors/inline-for...

chintukarthi’s picture

Actually i am creating a page which has a form and it contains details of a specific product. If the user tries to input the product name twice, it should print the error stating that the product name already exists. But for me it is not showing error and also it doesn't move to save items page as it has errors. 

chintukarthi’s picture

Can you explain how to catch the errors without using a common name like field_

I am having fields like name, experience, ...

How can you validate using these fields.