5. Validations

We have already learned which widgets exist and how we can use them to create a graphical interface. Now we are going to improve an elementary aspect such as validating the input data, a requirement we cannot overlook when we are creating a UI or form. It only applies if the user enters free text, such as editable-field or text.

Validations demo

To validate you must work with 2 properties: :valid-regexp and :error.

(widget-create 'editable-field
           :size 5
           :help-echo "Type a number"
           :valid-regexp "^[0-9]+$" ;; New
           :error "Error: Only numbers are allowed" ;; New
           :notify #'input-show-error ;; New
           :format "Type only numbers: %v")

The :valid-regexp property lets us define a regular expression to validate the value of the field. For example, if we want to accept only numbers we can use the regular expression ^[0-9]+$, which means that only numbers from 0 to 9 are accepted and that the field cannot be empty.

The error message is defined with the :error property. But where will it be shown? Just like in an HTML form, it is you who must define the place. In the example I have decided to display it in the minibuffer with the message function. It is up to you to decide other places, such as a tooltip or an overlay, or even a widget of type item that shows the message right below the field.

There is another important detail left, when should the error message be shown? As you type? When leaving the field? (losing focus) When pressing a button? Whatever your decision, you must face the :notify property. It lets us define a function that will run whenever the field changes (similar to an onchange in JavaScript). There are no more events to capture. This forces us to build functions with complex logic to know when to show the error message. In my case I have decided to take the quick path, validating as the user types. For that I have created the input-show-error function that will run every time the user types in the field.

(defun input-show-error (widget &rest ignore)
  "Show an error message if the input value is not valid."
  ;; Check whether the change is not valid and is not empty
  (when (and
     (widget-apply widget :validate)
     (not (string= (widget-value widget) "")))
    ;; Show the error message in the minibuffer
    (let (
      (message-error
       (propertize
        (format "Error: %s"
            ;; Color the error message in red
            (widget-get widget :error)) 'face '(:foreground "red"))))
      (message message-error))))

Let's see step by step what it does.

Before showing the message, we must know whether the input value is valid. You can use the following code for that:

(widget-apply widget :validate)

In case the value is not valid, the widget-apply function will return t.

If we combine it with a condition that the field cannot be empty, we get the following:

(when (and
     (widget-apply widget :validate)
     (not (string= (widget-value widget) "")))
     ;; Show the error message
     )

To finish, I have decided to show the error message in red in the minibuffer.

(let (
      (message-error
       (propertize
        (format "Error: %s"
            (widget-get widget :error)) 'face '(:foreground "red"))))
      (message message-error))))

The function is reusable; you can attach it to all the fields you need, as I have done with both fields.

Complete example

Below you can see a simple calculator that adds 2 numbers. I have added a button to calculate the sum and display the result in a read-only field. In case the user enters a non-numeric value, an error message will be shown in the minibuffer.

I will explain the structure in the next lesson; for now focus on the validations:

;; -*- coding: utf-8 -*-
;; Imports
(require 'widget)

(eval-when-compile
  (require 'wid-edit))

;; Variables
(defvar input-field-1)
(defvar input-field-2)
(defvar input-result)

;; Functions

(defun input-show-error (widget &rest ignore)
  "Show error message if the input is invalid."
  ;; Validate the input and ignore if is empty
  (when (and
     (widget-apply widget :validate)
     (not (string= (widget-value widget) "")))
    ;; Show error message
    (let (
      (message-error
       (propertize
        (format "Error: %s"
            (widget-get widget :error)) 'face '(:foreground "red"))))
      (message message-error))))

(defun sum-inputs (widget &rest ignore)
  "Sum the two numberns in the input fields."
  (let ((num1 (widget-value input-field-1))
        (num2 (widget-value input-field-2))
        result)
    (setq result (+ (string-to-number num1) (string-to-number num2)))
    (widget-value-set input-result (format "%s" result))))

(defun main-layout ()
  "Make widgets for the main layout."
  (interactive)
  ;; Create the buffer
  (switch-to-buffer "*Sum calculator*")
  ;; Clear the buffer
  (kill-all-local-variables)
  (let ((inhibit-read-only t))
    (erase-buffer))
  (remove-overlays)
  ;; Create the widgets
  (widget-insert "Sum Calculator\n\n")
  (setq input-field-1 (widget-create 'editable-field
                     :size 5
                     :tag "Number 1"
                     :help-echo "Type a number"
                     :valid-regexp "^[0-9]+$"
                     :error "Invalid number"
                     :notify #'input-show-error
                     :format "%v"))
  ;; Add hook focus-out input-field-1
  (widget-insert " + ")
  (setq input-field-2 (widget-create 'editable-field
                     :size 5
                     :tag "Number 2"
                     :help-echo "Type a number"
                     :valid-regexp "^[0-9]+$"
                     :error "Invalid number"
                     :notify #'input-show-error
                     :format "%v"))
  (widget-insert " = ")
  (setq input-result (widget-create 'item
                    :size 5
                    :tag "Result"
                    :format "%v"
                    :value "0"))
  (widget-insert "\n\n")
  (widget-create 'push-button
         :notify #'sum-inputs
         :tag "Calculate"
         :help-echo "Calculate the sum of the two numbers"
         :highlight t
         "Calculate")
  (widget-insert "\n\n")


  ;; Display the buffer
  (use-local-map widget-keymap)
  (widget-setup)
  ;; Go to the first input field
  (widget-forward 1))

;; Initialization
(main-layout)

I have given focus to the first input field so the user does not have to do it manually. For that I run the widget-forward function.

(widget-forward 1)

You must do it after creating all the widgets.

(use-local-map widget-keymap)
(widget-setup)
(widget-forward 1)

In addition I have used a field of type item so the result is read-only.

(setq input-result (widget-create 'item
                    :size 5
                    :tag "Result"
                    :format "%v"
                    :value "0"))

You can also notice some tweaks in the layout to make it more readable and pretty.

Activity 1

Improve the calculator so it accepts decimal numbers.

Activity 2

Create an identification form.

  • Email: Validate that the entered text is an email.
  • Password: It must contain at least 8 characters.
  • Button: When the button is pressed, show a welcome message in the minibuffer if a valid email and password have been entered.

This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.

Desafíos de programación atemporales y multiparadigmáticos

Desafíos de programación atemporales y multiparadigmáticos

Te encuentras ante un librillo de actividades, divididas en 2 niveles de dificultad. Te enfrentarás a los casos más comunes que te puedes encontrar en pruebas técnicas o aprender conceptos elementales de programación.

Buy the book

Will you buy me a coffee?

This is how I keep writing without ads or paywalls.

Comments

There are no comments yet.