Creating Question Type

Creating NgSurvey Question Type

NgSurvey question types are regular WordPress plugins which make use of the built-in hooks and actions of NgSurvey to add new question types. Apart from these hooks/actions, NgSurvey defines few constructs to define the structure of your plugin. We will go through all the details below.

Plugin File

Every plugin starts with the standard WordPress plugin header like below.

<?php
/**
 * NgSurvey extension to add Choice Question Type functionality.
 *
 * @link              https://ngideas.com
 * @since             1.0.0
 * @package           NgSurvey
 *
 * @wordpress-plugin
 * Plugin Name:       NgSurvey Question - Multiple Choice
 * Plugin URI:        https://ngideas.com/
 * Description:       This is NgSurvey extension to add Choice Question Type to NgSurvey plugin. 
 * Version:           1.0.0
 * Author:            NgIdeas
 * Author URI:        https://ngideas.com/
 * License:           GPL-2.0+
 * License URI:       http://www.gnu.org/licenses/gpl-2.0.txt
 * Text Domain:       ngsurvey
 * Domain Path:       /languages
 * NgSurvey Type:     Question
 */

We use standard PHP class to define all question type functionality and initiate them when the plugin is loaded. Most of the hooks are executed during plugins_loaded hook. So we define our class constructor to use this action hook.

Let’s assume your question type name is myqntype, so create a new folder wp-content/plugins/ngsurvey-extension-myqntype. Now add a new file ngsurvey-extension-myqntype.php into this folder. Add the header content as shown above in this file.

Now we have successfully created a new plugin. This can be activated via WordPress plugin installer. The plugin does nothing as of now. Lets add some initialization code to get started.

NgSurvey loads all its dependacies and hooks up the routines in the plugins_loaded action hook with priority 20. So we need to add our hooks in the action hook loaded after plugins_loaded hook or in the plugins_loaded hook with priority less than 20. So lets get started with adding some initialization code.

public function ngsurvey_myqntype_init() {
  add_action( 'plugins_loaded', 'ngsurvey_myqntype_add_hooks' );
}

ngsurvey_myqntype_init();

The above code will initialize the plugin by adding the action hook ngsurvey_myqntype_add_hooks on plugins_loaded event. Lets define the function myqntype_add_hooks which we can use to hook all action hooks and filters. We will use a separate file class-myqntype-question.php to define the question type functionality which is explained later in this documentation.

public function ngsurvey_myqntype_add_hooks() {
  $plugin = include __DIR__ . '/includes/class-myqntype-question.php';
  	        
  // Add filter to inject the question type to list of available question types
  add_filter( 'ngsurvey_fetch_question_types', array( $plugin, 'get_type' ) );

  // Add action to save the question form
  add_action( 'ngsurvey_save_question_form', array( $plugin, 'save_form' ) );
	        
  // Add action to handle custom form operation
  add_action( 'ngsurvey_custom_form_action', array( $plugin, 'handle_custom' ), 10, 2 );
	        
  // Add filter to render the response form when showing single survey
  add_filter( 'ngsurvey_response_form', array( $plugin, 'get_display' ) );
	        
  // Add filter to inject the question form for adding new question/edit question
  add_filter( 'ngsurvey_fetch_question_form', array( $plugin, 'get_form' ) );
	        
  // Add filter to inject conditional rules of the question type
  add_filter( 'ngsurvey_conditional_rules', array( $plugin, 'get_rules' ) );
	        
  // Add filter to show response details of a user response
  add_filter( 'ngsurvey_survey_results', array( $plugin, 'get_results' ) );
	        
  // Add filter to get the consolidated report of this question type
  add_filter( 'ngsurvey_consolidated_report', array( $plugin, 'get_reports' ) );
	        
  // Add filter to validate the user response
  add_filter( 'ngsurvey_validate_response', array( $plugin, 'validate' ), 10, 2 );
	        
  // Add filter to return the user response data that should be saved to data
  add_filter( 'ngsurvey_filter_user_responses', array( $plugin, 'filter_response_data' ), 10, 2 );
}

We added all action hooks and filters in the above function. Your plugin may not need all of them but needs few hooks which are needed to render the question type in the form, response and reports. Before we go deep into the details of the above hooks, let’s discuss about an important abstract class which helps us define the default functionality and avoid duplicating code.

Implementation File

Start creating a new file includes/class-myqntype-question.php under your plugin folder. Add a new PHP class to this file which should extend the abstract class NgSurvey_Question provided by NgSurvey. This abstract class defines the contract of all the functions needed to create the question type functionality. It also defines default functionality for most of the functions so you need not implement them again. Let’s get started.

<?php
/**
 * The file that defines the my question type class
 *
 * A class definition that includes attributes and functions used across both the
 * public-facing side of the site and the admin area.
 *
 * @link       https://ngideas.com
 * @since      1.0.0
 *
 * @package    NgSurvey
 * @subpackage NgSurvey/extensions
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

if( ! class_exists( 'NgSurvey_MyQnType_Question', false ) ):

/**
 * The survey choice question type class.
 *
 * This is used to define choice question type class.
 *
 * @package    NgSurvey
 * @author     Nagarjun <support@ngideas.com>
 * @license    https://www.gnu.org/licenses/gpl-3.0.txt GNU/GPLv3
 * @link       https://ngideas.com
 * @since      1.0.0
 */
class NgSurvey_MyQnType_Question extends NgSurvey_Question {

 /**
  * Define the construction of my question type functionality of the plugin.
  *
  * Set the plugin name and the plugin version that can be used throughout the plugin.
  * Load the dependencies, define the locale, and set the hooks for the admin area and
  * the public-facing side of the site.
  *
  * @since    1.0.0
  */
  public function __construct( $config = array() ) {
      // Continue reading this documentation to know how to define constructor code.
  }

  /**
   * The function to filter the response data and return the array of rows to save into database.
   *
   * @since    1.0.0
   * @access   public
   * @var      array $response_data the user response data associated with this question
   * 
   * @return   array $filtered_data the filtered response data
   */
  public function filter_response_data ( $filtered_data, $question, $response_data ) {
      return $filtered_data;
  }

  //.... rest of the code goes here which we will discuss soon
}
endif;

return new NgSurvey_MyQnType_Question();

We defined our implementation class, now lets add the required code to it. The first and important code that we need to add is the constructor of this class which defines the question type information.

/**
 * Define the construction of my question type functionality of the plugin.
 *
 * Set the plugin name and the plugin version that can be used throughout the plugin.
 * Load the dependencies, define the locale, and set the hooks for the admin area and
 * the public-facing side of the site.
 *
 * @since    1.0.0
 */
public function __construct( $config = array() ) {
        
    $config = array_merge( $config, array(
        'name'  => 'myqntype',
        'icon'  => 'dashicons dashicons-forms',
        'group' => 'choice',
        'title' => __( 'My Question Type', NGSURVEY_TEXTDOMAIN ),
        'template'  => new NgSurvey_Template_Loader(array(
            'plugin_directory' => plugin_dir_path( dirname( __FILE__ ) ),
            'filter_prefix' => 'ngsurvey-question-myqntype'
        )),
        'options' => array(
            (object) [
                'title'    => __( 'Show Custom Answer', NGSURVEY_TEXTDOMAIN ),
                'type'     => 'select',
                'name'     => 'show_custom_answer',
                'help'     => __( 'Shows a text box to the user to enter their own answer.', NGSURVEY_TEXTDOMAIN ),
                'options'  => [ 
                    1 => __( 'Show', NGSURVEY_TEXTDOMAIN ), 
                    0 => __( 'Hide', NGSURVEY_TEXTDOMAIN ) 
                ],
                'default'  => 0,
                'filter'   => 'uint',
            ],
            (object) [
                'title'    => __( 'Custom Answer Placeholder', NGSURVEY_TEXTDOMAIN ),
                'type'     => 'text',
                'name'     => 'custom_answer_placeholder',
                'help'     => __( 'Enter the text that is shown as placeholder in the custom answer textbox.', NGSURVEY_TEXTDOMAIN ),
                'options'  => null,
                'default'  => 'Other',
            ],
            (object) [
                'title'    => __( 'Custom Answer Max Length', NGSURVEY_TEXTDOMAIN ),
                'type'     => 'text',
                'name'     => 'custom_answer_maxlength',
                'help'     => __( 'Enter the maximum number of characters allowed in the custom answer textbox.', NGSURVEY_TEXTDOMAIN ),
                'options'  => null,
                'default'  => 256,
                'filter'   => 'uint',
            ],
       ),
   ) );
    
   parent::__construct( $config );
}

As you can see, we have defined a configiration array with all information of the question type and then call the parent class constructor by passing this information. This will initialize the default information required for the question type. Let’s understand each parameter we pass in this config array.

name – this is the name of the plugin. It must be unique name and recommended to use the same name in your plugin file and other places as we discussed earlier in this documentation.

icon – this is the icon that will show in the question types list, question form, rules etc. You can use any of the icon from the dashicons library.

group – when showing list of question types that can be created on questions form, the questions are grouped into 3 categories – choice, text and special. You can choose any one of these groups for your plugin.

title – This is the name of the plugin shown on front-end. You can use any unambiguous title for your plugin.

template – this defines the template object which is used to render your template files. It takes 2 arguments in an array – plugin_directory and filter_prefix. plugin_directory should be the absolute path of your plugin directory, filter_prefix defines prefix for the template filters and action hooks.

options – these are the list of options to customize your question type. You can, for example, use these options during display of the question in the survey.

With that we have defined our question type with the default functionality. We haven’t added any custom functionality yet. We can start customizing our plugin by extending the functions from NgSurvey_Question abstract class. To do that we need to understand what the functions are meant for. Each function is hooked to a filter or action hook as we defined earlier. Let’s understand the hooks now.

Action Hooks & Filters

ngsurvey_fetch_question_typesfilter – This is a mandatory filter which will feed the question type information to the survey engine. We can hook this to get_type function which defines required code to get our question type information from the config array we created in the constructor. If you override get_type function in your class, you need to add your plugin type information to the return array.

$question_types[] = (object) [
  'name'      => $this->name,
  'group'     => $this->group,
  'icon'      => $this->icon,
  'title'     => $this->title,
  'options'   => $this->options
];

arguments – $question_types array
return – modified $question_types array

ngsurvey_save_question_formaction hook – this is mandatory action hook which saves the question form data. This hook is called when you save your question in the form page. You can hook this to the save_form function in our implementation class. The default functionality saves all configuration options of your question along with question title and description. If you would like to add your own data into the database, you can override the function. Make sure to call parent object save_form function to perform above mentioned default save functions.

The response data can be accessed by from the post variable $_POST[‘ngform’]. Make sure you have sanitized the data before adding it to the return object.

Example:

/**
 * The function to save the data submitted through the edit questions form.
 *
 * @since    1.0.0
 * @access   protected
 * @var      array      $question  The question object
 * @var      boolean    $status    True on success, false otherwise
 */
public function save_form ( $question ) {
    if( $question->qtype != $this->name ) {
        return true;
    }
    $status = parent::save_form( $question );

    if( $status !== false ) {
        global $wpdb;
        // do your db saving logic here
    }
    return $status;
}

arguments –

  • $question object which is the question type object. This includes saved data from the database, if any. See ngs_questions table for fields information.

return – $status – boolean true if success, false otherwise

ngsurvey_response_formfilter – Add your HTML content of the survey response for your question in this filter. If you hook the get_display function of the implementation class, the default display functionality will be convered. You can override the get_display function to implement your own logic, although it is not required.

To render the question on the survey, you need to create the template file and add your own HTML content there. So create the file templates/survey/myqntype.php in your plugin folder. Now you can add your HTML content here. The question object is available as $data variable in this file. Following is the var_dump output of the choice question.

object(stdClass)[3203]
  public 'id' => string '1' (length=1)
  public 'title' => string 'Select an option from the below list of options' (length=47)
  public 'description' => string '<p>This is a multiple-choice question.</p>' (length=137)
  public 'qtype' => string 'choice' (length=6)
  public 'params' => 
    object(NgSurvey_Registry)[3215]
      private 'data' => 
        array (size=13)
          'choice_type' => string 'checkbox' (length=8)
          'minimum_selections' => int 2
          'maximum_selections' => int 3
          'show_answers_inline' => int 0
          'show_custom_answer' => int 1
          'custom_answer_placeholder' => string 'Other' (length=5)
          'custom_answer_maxlength' => int 3
          'required' => int 0
          'show_in_report' => int 1
          'question_class' => string 'card mb-3' (length=9)
          'title_class' => string 'card-header' (length=11)
          'description_class' => string 'card-body pb-0' (length=14)
          'body_class' => string 'card-body pt-0' (length=14)
  public 'page_id' => string '26' (length=2)
  public 'validate' => string '0' (length=1)
  public 'hidden' => string '0' (length=1)
  public 'answers' => 
    array (size=5)
      0 => 
        object(stdClass)[3181]
          public 'id' => string '1' (length=1)
          public 'question_id' => string '1' (length=1)
          public 'answer_type' => string 'x' (length=1)
          public 'title' => string 'This is an answer which can be selected' (length=39)
          public 'sort_order' => string '1' (length=1)
          public 'image' => null
      1 => 
        object(stdClass)[3180]
          public 'id' => string '2' (length=1)
          public 'question_id' => string '1' (length=1)
          public 'answer_type' => string 'x' (length=1)
          public 'title' => string 'Only one of the answer can be selected with radio buttons' (length=57)
          public 'sort_order' => string '2' (length=1)
          public 'image' => null
  public 'columns' => 
    array (size=0)
      // List of columns, if any. ngs_survey_answers.answer_type = 'y'
  public 'responses' => 
    array (size=0)
      empty
  public 'question_type' => 
    object(stdClass)[3271]
      public 'name' => string 'choice' (length=6)
      public 'group' => string 'choice' (length=6)
      public 'icon' => string 'dashicons dashicons-yes-alt' (length=27)
      public 'title' => string 'Multiple Choice' (length=15)
      public 'options' => list of question options that you defined in your constructor
  public 'response_form' => string '' (length=0)

Use the $data to build your question form template which will be shown to the users.

Filter arguments – $question, the question object as shown above
return – Assign html content from the template to to $question->response_form variable and return $question.

ngsurvey_fetch_question_form filter – this filter is used to inject the question form for adding new question/edit question. The default implementation is already defined by the implementation class, all you need to add is the template file to define your question form. The get_form function is hooked to this filter so that the default implementation is handled and your template file is rendered.

To render the question form, you need to create the template file and add your own HTML content there. So create the file templates/form/myqntype.php in your plugin folder. Now you can add your HTML content here. The question object is available as $data variable in this file. Following is the var_dump output of the choice question.

object(stdClass)[3492]
  public 'id' => string '1' (length=1)
  public 'survey_id' => string '13' (length=2)
  public 'title' => string 'Select an option from the below list of options' (length=47)
  public 'description' => string '<p>This is a multiple-choice question</p>' (length=137)
  public 'qtype' => string 'choice' (length=6)
  public 'params' => 
    object(NgSurvey_Registry)[3517]
      private 'data' => 
        array (size=13)
          'choice_type' => string 'checkbox' (length=8)
          'minimum_selections' => int 2
          'maximum_selections' => int 3
          'show_answers_inline' => int 0
          'show_custom_answer' => int 1
          'custom_answer_placeholder' => string 'Other' (length=5)
          'custom_answer_maxlength' => int 3
          'required' => int 0
          'show_in_report' => int 1
          'question_class' => string 'card mb-3' (length=9)
          'title_class' => string 'card-header' (length=11)
          'description_class' => string 'card-body pb-0' (length=14)
          'body_class' => string 'card-body pt-0' (length=14)
  public 'page_id' => string '26' (length=2)
  public 'answers' => 
    array (size=5)
      0 => 
        object(stdClass)[3541]
          public 'id' => string '1' (length=1)
          public 'question_id' => string '1' (length=1)
          public 'answer_type' => string 'x' (length=1)
          public 'title' => string 'This is an answer which can be selected' (length=39)
          public 'sort_order' => string '1' (length=1)
          public 'image' => null
      1 => 
        object(stdClass)[3542]
          public 'id' => string '2' (length=1)
          public 'question_id' => string '1' (length=1)
          public 'answer_type' => string 'x' (length=1)
          public 'title' => string 'Only one of the answer can be selected with radio buttons' (length=57)
          public 'sort_order' => string '2' (length=1)
          public 'image' => null
  public 'columns' => 
    array (size=0)
      // List of columns, if any. ngs_survey_answers.answer_type = 'y'
  public 'question_type' => 
    object(stdClass)[3616]
      public 'name' => string 'choice' (length=6)
      public 'group' => string 'choice' (length=6)
      public 'icon' => string 'dashicons dashicons-yes-alt' (length=27)
      public 'title' => string 'Multiple Choice' (length=15)
      public 'options' => 
        array (size=13)
          // List of options as you defined in the implementation class constructor
  public 'form_html' => string '' (length=0)
  public 'rules' => 
    array (size=0)
      // List of conditional rules, see ngs_survey_rules table for the members list

If you want to use your own template file or custom implementation, override the get_form function in your implementation file.

Filter Arguments – $question as shown above
return – Assign template HTML content to $question->form_html and return $question.

ngsurvey_conditional_rulesfilter – Add your own conditional rules logic to the survey engine using this filter. The $question object is passed as an argument to this filter. The list of rule types should be appended to the $question->rules variable. If your question do not support custom conditional rules, you need not implement this.

Example implementation from choice question type to add conditional rules based on the multiple choice answers.

/**
 * Returns the rules templates to support conditional rules of this question.
 *
 * @since    1.0.0
 * @access   protected
 * @var      string    $rules The conditional rule template of this question
 */
public function get_rules ( $question ) {
    if( $question->qtype != $this->name ) {
        return $question;
    }
    
    $options = array();
    foreach ( $question->answers as $answer ) {
        $options[] = (object) array( 'label' => $answer->title, 'value' => $answer->id );
    }
     
    $rule = (object) array(
        'id'            => $question->id,
        'field'         => $this->name,
        'label'         => $question->title,
        'icon'          => $this->icon,
        'type'          => 'integer',
        'input'         => 'select',
        'values'        => $options,
        'multiple'      => 'true',
        'plugin'        => 'select2',
        'plugin_config' => (object) array(
            'width'     => 'auto',
            'theme'     => 'bootstrap4'
        ),
        'operators'     => array( "in", "not_in", "is_empty", "is_not_empty"  ),
    );
    array_push( $question->rules, json_encode( $rule ) );

    return $question;
}

Arguments – $question object
returns – modified $question object after appending rules to $question->rules

ngsurvey_survey_resultsfilter – The results of the response should be populated in this filter. For example, in choice type question, you can show which answer the user has selected. This filter is called when displaying the individual response in the reports or after user completes the survey response. You can hook the get_results function in the implementation file to this filter. The parent class already defines defult functionality of fetching your template HTML content and assign it to $question->results_html variable.

To render the question response, you need to create the template file and add your own HTML content there. So create the file templates/results/myqntype.php in your plugin folder. The question object is available as $data variable in this file. $data->responses contains the user responses. See the ngs_response_details table for the structure of this variable data.

Filter Arguments – $question object
Returns – Append response HTML to $question->results_html variable and return $question.

ngsurvey_consolidated_report filter – This filter is used to show the consolidated report of the question. You can hook the get_reports function in the implementation file to this filter. The parent class already defines defult functionality of fetching your template HTML content and assign it to $question->reports_html and $question->custom_html variables.

templates/reports/myqntype.php – create this file to show the consolidated report, for example a chart showing number of responses recieved for each answer.

templates/reports/custom/myqntype.php – create this file to show custom answers in your own format. If you don’t want to customize the format, you need not create this file.

The question object is available as $data variable in this file. $data->responses contains the user responses.

Filter arguments – $question object
Returns – $question->reports_html & $question->custom_html are appended with template HTML and returns $question object.

ngsurvey_validate_responsefilter – This filter is used to validate the user responses. Default implementation does not validate your responses. If you need to validate the user responses before saving to database, override the validate function in your implementation class.

Arguments – $errors, $question
Returns – modified $errors array

ngsurvey_filter_user_responsesfilter – This is the mandatory function that you need to implement in your class. You need to process the $filter_data parameter value and return the same. This function is the place you need to add your user response details into the ngs_response_details table. Each entry of the $filtered_data array is array of answer_id, column_id, answer_data values.

Example implementation of choice question type:

/**
 * The function to filter the response data and return the array of rows to save into database.
 *
 * @since    1.0.0
 * @access   public
 * @var      array $filtered_data the filtered data returned to caller
 * @var      stdClass $question the question object
 * 
 * @return   array $filtered_data the filtered response data
 */
 public function filter_response_data ( $filtered_data, $question ) {
    if( $question->qtype != $this->name ) {
        return $filtered_data;
    }

    if( !empty( $_POST[ 'ngform' ][ 'answers' ][ $question->id ][ 'response' ] ) ) {
        foreach ( $question->answers as $answer ) {
            foreach ( $_POST[ 'ngform' ][ 'answers' ][ $question->id ][ 'response' ] as $response ) {
                if( $answer->id == $response ) {
                    $filtered_data[] = array( 'answer_id' => (int) $response, 'column_id' => 0, 'answer_data' => null );
                    break;
                }
            }
        }
    }
    
    if( !empty( $_POST[ 'ngform' ][ 'answers' ][ $question->id ]['custom'] ) ) {
        $filtered_data[] = array( 
          'answer_id' => 1, 
          'column_id' => 0, 
          'answer_data' => $_POST[ 'ngform' ][ 'answers' ][ $question->id ]['custom'] 
        );
    }
        
    return $filtered_data;
}

Arguments:

  • $filtered_data – the array of filter data
  • $question – the question object

Returns – $filtered_data appended with the responses that needs to be saved to database

ngsurvey_custom_form_actionaction hook – implement this action hook to handle custom form operation. For example, you would like to upload a file in your question creation form, you can implement this do that. You need to implement your own JavaScript as well.

Arguments

  • $response – the response array which needs to be appended with the data sent to front-end (will be json encoded)
  • $question – the question object

Returns – Updated $response array

JavaScript & CSS file handling

You can add JavaScript files with regular WordPress filters, however they may or may not load after NgSurvey scripts. So you may not be able to use some common utility functions defined by NgSurvey. To overcome this problem, NgSurvey defines actions/filters to add the script files.

ngsurvey_enqueue_admin_scriptsaction hook – implement this action hook to append yourJavaScript files to the admin pages of NgSurvey. If you followed the steps above to implement your implementation file, then you need not add the function to enqueue JavaScripts. Name your javascript file as media/js/ngsurvey-question-myqntype.js and hook your filter to enqueue_admin_scripts function of your implementation class.

add_filter( 'ngsurvey_enqueue_admin_scripts', array( $plugin, 'enqueue_admin_scripts' ), 10, 2 );

The implementation is already in place to enqueue your script file. If you want to override the enqueue function, see below the arguments.

Arguments –

  • $scripts – the list of array objects define the scripts. Each element added to this array is an array with the values –
    • handle – the unique name of the script
    • url – the URL of the script
    • file – the absolute name of the script file
    • version – the version of the script file
  • $hook – custom hook name
  • $type – type of the plugin, default is ‘extension’

Returns – the updated $scripts array

The source code of the above WordPress plugin is available on GitHub. Checkout the source code here.