Home Reference Source

js/controllers/Form/FormController.js

import ViewController from '~/controllers/ViewController';
import ForeignInteractor from '~/interactors/ForeignInteractor';
import FormControllerDelegate from '~/delegate/FormControllerDelegate';

import Request from '~/models/Request/Request';

export const FORM_VALIDATION_FAILED = 'form-validation--state-error';

/**
 * Performs really basic form validation
 */
export default class FormController extends ViewController {
    /**
     * Sets up an empty form validation for a form. This will run when the form
     * is submitted, or manually using the `.submit()` function. Setup a
     * delegate too
     *
     * @param {HTMLFormElement} form Form element
     */
    constructor(form) {
        super();

        form.controller = this;

        this._form = form;
        this._registerForm(form);

        /**
         * @type {FormControllerDelegate}
         */
        this.delegate = new FormControllerDelegate();

        this._method = this._form.method;
        this._action = this._form.action;

        this._constraints = [];
        this._displays = [];
    }

    _registerForm(form) {
        form.addEventListener("submit", (event) => {
            if (this.submit() === false) {
                event.preventDefault();
            }
        }, false);
    }

    /**
     * Returns a foreign synchronization node
     * @param {string} text Name of the syncronization node
     * @param {number} [time=50] Minimum time before resyncronization
     */
    foreignSynchronize(text, time = 50) {
        let interactor = new ForeignInteractor(`/post/preview`);
        let interactionTargets = this._form.getElementsByClassName('foreign-synchronize');

        for (let i = 0; i < interactionTargets.length; i++) {
            let target = interactionTargets[i];
            let name = target.name;

            interactor.sendKey(name, "");
            interactionTargets[i].addEventListener('input', () => {
                interactor.queueKey(name, time, target.value);
            });
        }

        return <a href={interactor.link} target="_blank">{text}</a>;
    }

    /**
     * Adds a form constraint
     * @param {FormConstraint} constraint A constraint to add to the current
     *                                    form.
     */
    addConstraint(constraint) {
        this._constraints.push(constraint);
    }

    /**
     * Adds a list of constraints.
     * @param {FormConstraint[]} constraints A list of form constraints to add
     */
    addConstraints(constraints) {
        for (let i = 0; i < constraints.length; i++) {
            this.addConstraint(constraints[i]);
        }
    }

    /**
     * Validates the form.
     * @return {ValidationError[]} List of errors
     */
    validate() {
        let formErrors = [];
        for (let i = 0; i < this._constraints.length; i++) {
            let constraint = this._constraints[i];
            let errors = constraint.validate();
            for (let j = 0; j < errors.length; j++) {
                formErrors.push(errors[j]);
            }
        }
        return formErrors;
    }

    /**
     * Hides error displays
     */
    clearDisplays() {
        this._displays.forEach(el => {
            el.parentNode.classList.remove(FORM_VALIDATION_FAILED);
            el.parentNode.removeChild(el)
        });
        this._displays = [];
    }

    /**
     * Displays the errors.
     * @param {ValidationError[]} errors List of validation errors.
     */
    display(errors) {
        let sortedErrors = new Map();

        this.clearDisplays();
        for (let i = 0; i < errors.length; i++) {
            let {error, node} = errors[i];

            if (sortedErrors.has(node)) {
                sortedErrors.get(node).push(error);
            } else {
                sortedErrors.set(node, [error]);
            }
        }

        let displays = [];
        for (let [node, errors] of sortedErrors) {
            let target = node;
            let parent = node.parentNode;

            parent.classList.add(FORM_VALIDATION_FAILED);

            parent.insertBefore(
                <ul class="form-validation__error_list">
                    { errors.map(error => <li class="form-validation__error">{ error }</li>) }
                </ul>,
                node.nextSibling
            );
        }

        this._displays = displays;
    }

    /**
     * Submits a form.
     * @return {boolean} `true` if succesful, false otherwise.
     */
    submit() {
        let errors = this.validate();
        if (errors.length === 0) {
            // No errors!
            setTimeout(() => {
                this.delegate.formDidSubmit(this);
            }, 0);

            let override = this.delegate.formWillSubmit(this);
            if (typeof override === 'undefined') {
                return true;
            } else {
                return override;
            }
        } else {
            setTimeout(() => {
                this.delegate.formDidError(this, errors);
            }, 0);
            return false;
        }
    }

    /**
     * Creates or sets a form field with a `name` as an identifying key.
     *
     * @param {string} value - Value of the new field.
     * @param {string} name - name identifying field
     */
    setFieldWithName(value, name) {
        let input = this._form.elements[name];

        if (typeof input === 'undefined') {
            this._form.appendChild(<input type="hidden" name={name} value={value}/>);
        } else {
            input.value = value;
        }
    }

    /**
     * Returns form data.
     * @type {FormData}
     */
    get formData() { return new FormData(this._form); }

    /**
     * Returns HTTP method
     * @type {string}
     */
    get method() { return this._method; }

    /**
     * Returns action target.
     * @type {string}
     */
    get path() { return this._action; }

    /**
     * Prepares a AJAX request with form endpoints
     * @type {Request}
     */
     get request() {
        return new Request({
            path: this.path,
            method: this.method,
            data: this.formData
        });
     }
}