How to create a custom module form with ajax add,update and delete operations without page refresh in Drupal 8 and Drupal 9

Here we are going to discuss how to make your custom form into an ajax submission form and display submitted data on the same page without page reload. Also, we will create custom pagination for the table, and while changing the page, the table will be populated without page refresh. Also, delete and edit operations are done on the same page without page refresh.

See the final screen and source code in the below link.

Download the source of the completed module here 

There are a lot of challenges while creating a single-page application with Drupal and it’s ajax API.

Drupal AJAX API in Core one of the most powerful API available in Drupal 8 and Drupal 9 versions.

Here in this tutorial, we are going to split these tasks into the below steps

  • Save data and display data in a table without page load using ajax
  • Edit table  data and refresh table without page load using ajax
  • Delete table row and  refresh table without page load using ajax
  • Custom pagination and refresh each page without page load using ajax

Save data and display data in a table without page load using ajax

So here first we going to create a simple ajax form that submits data into the database. Then after submission, we will show a success message in the same form and populate the table list on the same page.

First, we are going to create a custom table for students with the below fields.

CREATE TABLE `students` (

`id` int(11) NOT NULL,

`fname` varchar(500) NOT NULL,

`sname` varchar(500) NOT NULL,

`age` int(11) NOT NULL,

`marks` int(11) NOT NULL

) ENGINE=InnoDB DEFAULT CHARSET=latin1;

 

Schema script can be created in .install file of you module as below.

<?php
use Drupal\Core\Database\Database;
 
/**
 * Implements hook_schema().
 */
function dn_students_schema(){
    $schema['students'] = array(
        'description' => 'The table for storing the students data.',
        'fields' => array(
            'id' => array(
                'description' => 'The primary identifier for student',
                'type' => 'serial',
                'not null' => TRUE,
                'unsigned' => TRUE,
            ),
            'fname' => array(
                'description' => 'Student name',
                'type' => 'varchar',
                'length' => 255,
                'not null' => TRUE,
                'default' => '',
            ),
           'sname' => array(
                'description' => 'Student second name.',
                'type' => 'varchar',
                'length' => 255,
                'not null' => TRUE,
                'default' => '',
            ),
            'age' => array(
                'description' => 'Age of student',
                'type' => 'int',
                'length' => 100,
                'not null' => TRUE,
               
            ),
            'marks' => array(
                'description' => 'Mark of student',
                'type' => 'int',
                'length' => 100,
                'not null' => TRUE,
            ),
        ),
        'primary key' => array('id'),
    );
    return $schema;
}

Next, we are going to create a custom save form in our module.  This form is used for saving data to students table without page reload.

We have 4 fields in the field and see the code.

Create the below file in the custom module path.

\dn_students\src\Form\StudentForm.php

dn_students is the custom module name.

See the build from function below.

public function buildForm(array $form, FormStateInterface $form_state,$record = NULL) {
   
    $form['fname'] = [
      '#type' => 'textfield',
      '#title' => $this->t('First Name'),
      '#maxlength' => 20,
	  '#attributes' => array(
       'class' => ['txt-class'],
       ),
      '#default_value' =>'',
	  '#prefix' => '<div id="div-fname">',
      '#suffix' => '</div><div id="div-fname-message"></div>',
    ];
	 $form['sname'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Second Name'),
      '#maxlength' => 20,
	  '#attributes' => array(
       'class' => ['txt-class'],
       ),
      '#default_value' => '',
    ];
	$form['age'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Age'),
      '#maxlength' => 20,
	  '#attributes' => array(
       'class' => ['txt-class'],
       ),
      '#default_value' =>  '',
    ];
	 $form['marks'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Marks'),
      '#maxlength' => 20,
	  '#attributes' => array(
       'class' => ['txt-class'],
       ),
      '#default_value' => '',
    ];
	
	
    $form['actions']['#type'] = 'actions';
    $form['actions']['Save'] = [
      '#type' => 'submit',
      '#button_type' => 'primary',
	  '#ajax' => ['callback' => '::saveDataAjaxCallback'] ,
      '#value' => $this->t('Save') ,
    ];
	 $form['actions']['clear'] = [
      '#type' => 'submit',
      '#ajax' => ['callback' => '::clearForm','wrapper' => 'form-div',] ,
      '#value' => 'Clear',
     ];
	 $render_array['#attached']['library'][] = 'dn_students/global_styles';
    return $form;

  } 

You can see ajax call back for save and clear buttons.

'#ajax' => ['callback' => '::saveDataAjaxCallback'] ,
 '#ajax' => ['callback' => '::clearForm','wrapper' => 'form-div',]

Since we are using different Ajax API’s use below use statements at the top of your  StudentForm.php

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Database\Database;
use Drupal\Core\Url;
use Drupal\Core\Routing;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\AlertCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Form\FormState;
use Drupal\Core\Link;

in our module ibraries.yml, we need to load some ajax libraries as shown below.

dn_students\dn_students.libraries.yml
global_styles:

  js:
    js/pagination.js: {}  
  css:
     theme:
          css/style.css: {}
 
  dependencies:
    - core/jquery
    - core/drupal.ajax
    - core/drupal
    - core/drupalSettings
    - core/jquery.once

We will be discussing more pagination.js in the pagination section.

Logic for saving and clear fields are written in below Ajax call back functions.

saveDataAjaxCallback function

in this call back function, we are doing below things.

  • Validate input fields
  • Insert data into students table.
  • Update table data with inserted data.

The below code validates whether input field is filled or not. You replicate this code to other fields.

if($fields["fname"] == ''){
		$css = ['border' => '1px solid red'];
		$text_css = ['color' => 'red'];
        $message = ('First Name not valid.');
	
		//$response = new \Drupal\Core\Ajax\AjaxResponse();
		$response->addCommand(new \Drupal\Core\Ajax\CssCommand('#edit-fname', $css));
		$response->addCommand(new \Drupal\Core\Ajax\CssCommand('#div-fname-message', $text_css));
		$response->addCommand(new \Drupal\Core\Ajax\HtmlCommand('#div-fname-message', $message));
		return $response;
	}

Here we highlighting fname field with an error message.

Next in the same function, we are inserting data to data if all validations are successful.

$conn->insert('students')->fields($fields)->execute();
$field = $form_state->getValues();
	
   
	$fields["fname"] = $field['fname'];
	$fields["sname"] = $field['sname'];
	$fields["age"] = $field['age'];
	$fields["marks"] = $field['marks'];
$conn->insert('students')
           ->fields($fields)->execute();

Next, we are going to load table data with newly inserted data.

$render_array = \Drupal::formBuilder()->getForm('Drupal\dn_students\Form\StudentTableForm','All');
		 $response->addCommand(new HtmlCommand('.result_message','' ));
	 $response->addCommand(new \Drupal\Core\Ajax\AppendCommand('.result_message', $render_array));
	 $response->addCommand(new HtmlCommand('.pagination','' ));
	 $response->addCommand(new \Drupal\Core\Ajax\AppendCommand('.pagination', getPager()));
	  $response->addCommand(new \Drupal\Core\Ajax\InvokeCommand('.pagination-link', 'removeClass', array('active')));
	   $response->addCommand(new \Drupal\Core\Ajax\InvokeCommand('.pagination-link:first', 'addClass', array('active')));
	 $response->addCommand(new InvokeCommand('.txt-class', 'val', ['']));

Here we are loading StudentTable form into div tag which has result_message  class. we are discussing more  about StudentTableForm in the next section. This table will be dynamically loaded for each operation.

Also in the above code, we are making the first page an active page for each update.

Create  StudentTableForm.php file in the below path.

\dn_students\src\Form\StudentTableForm.php

See below the source code of the file.

<?php
namespace Drupal\dn_students\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Database\Database;
use Drupal\Core\Url;
use Drupal\Core\Routing;
use Drupal\Core\Link;

/**
 * Provides the list of Students.
 */
class StudentTableForm extends FormBase {
	
	 /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'dn_student_table_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state,$pageNo = NULL) {
    
   
   //$pageNo = 2;
    $header = [
      'id' => $this->t('Id'),
      'fname' => $this->t('First Name'),
	  'sname' => $this->t('Second Name'),
	  'age'=> $this->t('age'),
	  'Marks'=> $this->t('Marks'),	  
	  'opt' =>$this->t('Operations')
    ];

    
	
   if($pageNo != ''){
    $form['table'] = [
      '#type' => 'table',
      '#header' => $header,
      '#rows' => $this->get_students($pageNo),
      '#empty' => $this->t('No users found'),
    ];
   }else{
	    $form['table'] = [
      '#type' => 'table',
      '#header' => $header,
      '#rows' => $this->get_students("All"),
      '#empty' => $this->t('No records found'),
    ];
   }
   //$form['form2'] = $this->get_students("All");
    $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
	//$form['#attached']['library'][] = 'core/drupal.ajax';
	$form['#attached']['library'][] = 'dn_students/global_styles';
	
     $form['#theme'] = 'student_form';
	  $form['#prefix'] = '<div class="result_message">';
	   $form['#suffix'] = '</div>';
	  // $form_state['#no_cache'] = TRUE;
	   $form['#cache'] = [
      'max-age' => 0
    ];
    return $form;

  }

As you can see above $pageNo variable is used for pagination purposes.

get_students function is the implementation of retrieving data from the database.

You can place this function in your .module file as below.

function get_students($opt) {
	$res = array();
	//$opt = 2;
 if($opt == "All"){

  $results = \Drupal::database()->select('students', 'st');
 
  $results->fields('st');
  $results->range(0, 15);
  $results->orderBy('st.id','DESC');
  $res = $results->execute()->fetchAll();
  $ret = [];
 }else{
	 $query = \Drupal::database()->select('students', 'st');
  
  $query->fields('st');
  $query->range($opt*15, 15);
  $query->orderBy('st.id','DESC');
  $res = $query->execute()->fetchAll();
  $ret = [];
 }
    foreach ($res as $row) {

      
	  $edit = Url::fromUserInput('/ajax/dn_students/students/edit/' . $row->id);
	  //array('attributes' => array('onclick' => "return confirm('Are you Sure')"))
	  $delete = Url::fromUserInput('/del/dn_students/students/delete/' . $row->id,array('attributes' => array('onclick' => "return confirm('Are you Sure')")));
      
	  $edit_link = Link::fromTextAndUrl(t('Edit'), $edit);
	  $delete_link = Link::fromTextAndUrl(t('Delete'), $delete);
	  $edit_link = $edit_link->toRenderable();
      $delete_link  = $delete_link->toRenderable();
	  $edit_link['#attributes'] = ['class'=>'use-ajax'];
	  $delete_link['#attributes'] = ['class'=>'use-ajax'];
	 
       
      $mainLink = t('@linkApprove  @linkReject', array('@linkApprove' => $edit_link, '@linkReject' => $delete_link));
      
	  
      $ret[] = [
        'id' => $row->id,
        'fname' => $row->fname,
		'sname' => $row->sname,
		'age' => $row->age,
		'marks' => $row->marks,
        'opt' => render($delete_link),
		'opt1' => render($edit_link),
      ];
    }
    return $ret;
}

Here we are displaying 15 rows and bases on page number query range will be updated.

Here we are creating edit and delete which are mailed to controller functions in .routing.yml file, these functionalities will be discussed in the edit and delete sections.

Now we have created a save and table form.

Next, we have to display these two forms on the same page.

We are going to create a route in routing.yml which will display below forms on a single page.

\dn_students\src\Form\StudentForm.php

\dn_students\src\Form\StudentTableForm.php

Add the below route to your module .routing.yml file.

dn_students.studentmanage:
  path: '/admin/structure/dn_students/students/manageStudents'
  defaults:
    _title: 'Students'
    _controller: '\Drupal\dn_students\Controller\StudentController::manageStudents'

Create the below controller file.

\modules\dn_students\src\Controller\StudentController.php

Add below function in this controller.

/**
   * {@inheritdoc}
   */
  public function manageStudents() {
	$form['form'] = $this->formBuilder()->getForm('Drupal\dn_students\Form\StudentForm');
	$render_array = $this->formBuilder()->getForm('Drupal\dn_students\Form\StudentTableForm','All');
	   $form['form1'] = $render_array;
	    $form['form']['#suffix'] = '<div class="pagination">'.getPager().'</div>';
    return $form;
  } 

We will discuss more getPager() function in the last section of this article.

Now we have a page where we can save data and display data without page refresh.

 Edit table data and refresh table without page load using ajax

Next, we are going to discuss edit functionality.

As you saw in the previous step, while clicking on the edit link, we have provided a mapping routing.yml file.

See the below route.

dn_students.edit_student_ajax:
  path: '/ajax/dn_students/students/edit/{cid}'
  defaults:
    _controller: '\Drupal\dn_students\Controller\StudentController::editStudentAjax'
    _title: 'Edit Student'
  requirements:
    _permission: 'administer Students'

See below  editStudentAjax function in StudentController

public function editStudentAjax($cid) {
    
	  $conn = Database::getConnection();
      $query = $conn->select('students', 'st');
      $query->condition('id', $cid)->fields('st');
      $record = $query->execute()->fetchAssoc();
    
	 $render_array = \Drupal::formBuilder()->getForm('Drupal\dn_students\Form\StudentEditForm',$record);
	 //$render_array['#attached']['library'][] = 'dn_students/global_styles';
	$response = new AjaxResponse();
	 $response->addCommand(new OpenModalDialogCommand('Edit Form', $render_array, ['width' => '800']));
	 
    return $response;
  }

As seen in the above code   OpenModalDialogCommand will create a pop-up in which StudentEditForm will appear as an edit form with select row data loaded.

See below code for  \dn_students\src\Form\StudentEditForm.php

<?php

namespace Drupal\dn_students\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Database\Database;
use Drupal\Core\Url;
use Drupal\Core\Routing;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\AlertCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Form\FormState;

/**
 * Provides the form for edit students.
 */
class StudentEditForm extends FormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'dn_student_form_edit';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state,$record = NULL) {
    $conn = Database::getConnection();
    
    $language = \Drupal::languageManager()->getLanguages();

    if(isset($record['id'])){
		$form['id'] = [
		  '#type' => 'hidden',
		  '#attributes' => array(
             'class' => ['txt-class'],
           ),
		  '#default_value' => (isset($record['id'])) ? $record['id'] : '',
		];
	}
    $form['fname'] = [
      '#type' => 'textfield',
      '#title' => $this->t('First Name'),
      '#required' => TRUE,
      '#maxlength' => 20,
	  '#attributes' => array(
       'class' => ['txt-class'],
       ),
      '#default_value' => (isset($record['fname'])) ? $record['fname'] : '',
    ];
	 $form['sname'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Second Name'),
      '#required' => TRUE,
      '#maxlength' => 20,
	  '#attributes' => array(
       'class' => ['txt-class'],
       ),
      '#default_value' => (isset($record['sname'])) ? $record['sname'] : '',
	  
    ];
	$form['age'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Age'),
      '#required' => TRUE,
      '#maxlength' => 20,
	  '#attributes' => array(
       'class' => ['txt-class'],
       ),
      '#default_value' => (isset($record['age'])) ? $record['age'] : '',
    ];
	 $form['marks'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Marks'),
      '#required' => TRUE,
      '#maxlength' => 20,
	  '#attributes' => array(
       'class' => ['txt-class'],
       ),
      '#default_value' => (isset($record['marks'])) ? $record['marks'] : '',
    ];
	
	
	
    $form['actions']['#type'] = 'actions';
    $form['actions']['Save'] = [
      '#type' => 'submit',
      '#button_type' => 'primary',
	   '#attributes' => [
        'class' => [
          'use-ajax',
        ],
      ],
	  '#ajax' => ['callback' => '::updateStudentData'] ,
      '#value' => (isset($record['fname'])) ? $this->t('Update') : $this->t('Save') ,
    ];
	
	 
	
	 $form['#prefix'] = '<div class="form-div-edit" id="form-div-edit">';
	 $form['#suffix'] = '</div>';
	
	  $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
    return $form;

  }
  
   /**
   * {@inheritdoc}
   */
  public function validateForm(array & $form, FormStateInterface $form_state) {
        //print_r($form_state->getValues());exit;
		
  }
 
  public function updateStudentData(array $form, FormStateInterface $form_state) {
    $response = new AjaxResponse();

    // If there are any form errors, re-display the form.
    if ($form_state->hasAnyErrors()) {
      $response->addCommand(new ReplaceCommand('#form-div-edit', $form));
    }
    else {
		 $conn = Database::getConnection();
	$field = $form_state->getValues();
	$re_url = Url::fromRoute('dn_students.student');
   
	$fields["fname"] = $field['fname'];
	$fields["sname"] = $field['sname'];
	$fields["age"] = $field['age'];
	$fields["marks"] = $field['marks'];
	
      $conn->update('students')
           ->fields($fields)->condition('id', $field['id'])->execute();
      $response->addCommand(new OpenModalDialogCommand("Success!", 'The table has been submitted.', ['width' => 800]));
	  $render_array = \Drupal::formBuilder()->getForm('Drupal\dn_students\Form\StudentTableForm','All');
	
	
	  $response->addCommand(new HtmlCommand('.result_message','' ));
	   $response->addCommand(new \Drupal\Core\Ajax\AppendCommand('.result_message', $render_array));
	   $response->addCommand(new \Drupal\Core\Ajax\InvokeCommand('.pagination-link', 'removeClass', array('active')));
	   $response->addCommand(new \Drupal\Core\Ajax\InvokeCommand('.pagination-link:first', 'addClass', array('active')));
	 
    }

    return $response;
  }
  
  
  /**
   * {@inheritdoc}
   */
 public function submitForm(array & $form, FormStateInterface $form_state) {
	
  }

}
  

As seen in the above Ajax call back of edit form updateStudentData  will update selected row and  reload StudenTable Form

Delete table row and  refresh table without page load using ajax

Add below entry in routing.yml file

dn_students.delete_student_ajax:
  path: '/ajax/dn_students/students/delete/{cid}'
  defaults:
    _controller: '\Drupal\dn_students\Controller\StudentController::deleteStudentAjax'
    _title: 'Delete Student'
  requirements:
    _permission: 'administer Students'

In StudentController  file see below deleteStudentAjax

public function deleteStudentAjax($cid) {
     $res = \Drupal::database()->query("delete from students where id = :id", array(':id' => $cid)); 
	// $render_array = \Drupal::formBuilder()->getForm('Drupal\dn_students\Form\StudentTableForm','All');
	 $render_array = $this->formBuilder->getForm('Drupal\dn_students\Form\StudentTableForm','All');
	$response = new AjaxResponse();
	  //$response->addCommand(new HtmlCommand('.result_message',$render_array ));
	   $response->addCommand(new HtmlCommand('.result_message','' ));
	   $response->addCommand(new \Drupal\Core\Ajax\AppendCommand('.result_message', $render_array));
	   $response->addCommand(new \Drupal\Core\Ajax\InvokeCommand('.pagination-link', 'removeClass', array('active')));
	   $response->addCommand(new \Drupal\Core\Ajax\InvokeCommand('.pagination-link:first', 'addClass', array('active')));
	   
    return $response;

  }

Custom pagination and refresh each page without page load using ajax

Here we are not using the pager available in Drupal. In order to make page traverse as an ajax call, we are creating a custom pager.

In  your .module, create a function as below.

function getPager(){
	
   $query = \Drupal::database()->select('students', 't');
   $query->addExpression('COUNT(*)');
   $count = $query->execute()->fetchField();
 
  
  $count = ceil($count/15);

 $page_link = Url::fromUserInput('/ajax/dn_students/table/page/0');
 $page_link = Link::fromTextAndUrl('<<', $page_link);
 $page_link = $page_link->toRenderable();
 $page_link['#attributes'] = ['class'=>['use-ajax']];
  $out = render($page_link);
  for($i = 0; $i < $count; $i++){
   $page = Url::fromUserInput('/ajax/dn_students/table/page/'.$i);
   $pageLink =  Link::fromTextAndUrl($i, $page); 
   $pageLink = $pageLink->toRenderable();
   $pageLink['#attributes'] = ['class'=>['use-ajax','pagination-link']];
   $out = $out.render($pageLink); 
  }
  $last_page = $count-1;
  $page_link_last = Url::fromUserInput('/ajax/dn_students/table/page/'.$last_page);
  $page_link_last = Link::fromTextAndUrl('>>', $page_link_last);
  $page_link_last = $page_link_last->toRenderable();
  $page_link_last['#attributes'] = ['class'=>['use-ajax']];
   $out = $out.render($page_link_last);
  return $out;
	
}

As you can see in the above code, we are making links with class ‘use-ajax’ so that we can apply ajax response while clicking on hyperlinks.

This function will be called in manageStudents function in StudentController.php

So link /ajax/dn_students/table/page/{pageNo} is mapped into   tablePaginationAjax function as below in routing.yml file.

dn_students.pagination_student_ajax:
  path: '/ajax/dn_students/table/page/{no}'
  defaults:
    _controller: '\Drupal\dn_students\Controller\StudentController::tablePaginationAjax'
    _title: 'Table Pagination Student'
  requirements:
    _permission: 'administer Students'

See below tablePaginationAjax function in StudentController.php

So here we are passing page no into StudentTableForm and which will be passed to get_students  function for getting the results between query range.

public function tablePaginationAjax($no){
	  $response = new AjaxResponse();
	  $render_array = \Drupal::formBuilder()->getForm('Drupal\dn_students\Form\StudentTableForm',$no);
	   $response->addCommand(new HtmlCommand('.result_message','' ));
	    $response->addCommand(new \Drupal\Core\Ajax\AppendCommand('.result_message', $render_array));
		
	 
	 return $response;
	  
  }

We have below css and js files for making the look and feel of paginations div.

\dn_students\js\pagination.js

\dn_students\css\style.css

Load this in your libraries.yml

global_styles:

  js:
    js/pagination.js: {}  
  css:
     theme:
          css/style.css: {}
 
  dependencies:
    - core/jquery
    - core/drupal.ajax
    - core/drupal
    - core/drupalSettings
    - core/jquery.once

 

Also, load this in the table as below in .module file.

function dn_students_element_info_alter(array &$types) {
	
  if (isset($types['table'])) {
    $types['table']['#attached']['library'][] = 'dn_students/dn_students';
  }
}

Download the source of the completed module here 

 



Get Free E-book
Get a free Ebook on Drupal 8 -theme tutorial
I agree to have my personal information transfered to MailChimp ( more information )
if you like this article Buy me a Coffee this will be an ispiration for me to write articles like this.

You may also like...