Skip to content
Snippets Groups Projects
select-field.ts 9.53 KiB
Newer Older
import { Field } from "../field";
import { SelectEntry } from "./select-entry";
import { arraysAreEqual } from "../../../util/util";
import { FormulaireNode } from "../formulaire-node";
import { ServiceFactory } from "app/services/service-factory";
import { FormulaireDefinition } from "../../definition/form-definition";
import { enumValueFromString, Nub } from "jalhyd";

export abstract class SelectField extends Field {

    /**
     * select options
     */
    protected _entries: SelectEntry[];

    /**
     * currently selected option
     */
    protected _selectedEntry: SelectEntry | SelectEntry[];

    /** if true, user can select multiple values */
    protected _multiple = false;

    /** string to build the select entries IDs from */
    protected _entriesBaseId: string;

    /** name of the Nub property associated to this field, if any */
    protected _associatedProperty: string;

    /** default value (from configuration) for this field */
    protected _configDefaultValue: string;

    /**
     * message to display below the select field when no option is selected,
     * if this message is defined and not empty
     */
    protected _messageWhenEmpty: string;

    /**
     * map id <-> select entry
     */
    protected _entryMap: { [key: string]: SelectEntry } = {};

    constructor(parent: FormulaireNode) {
        super(parent);
        this.clearEntries();
    }

    /**
     * associated nub
     */
    protected get nub(): Nub {
        const parent = this.parentForm;
        if (parent === undefined) {
            return undefined;
        }
        return (this.parentForm as FormulaireDefinition).currentNub;
    }

    public getValue(): SelectEntry | SelectEntry[] {
        return this._selectedEntry;
    }

    /**
     * Updates _selectedEntry; notifies observers only if
     * value.id has changed
     */
    public setValue(v: SelectEntry | SelectEntry[]) {
        // if multiple is true, value must be an array
        if (this._multiple && !Array.isArray(v)) {
            throw new Error("select value is not an array");
        }

        const previousSelectedEntry = this._selectedEntry;
        this._selectedEntry = v;

        // if old and new values are not both undefined
        if (!(previousSelectedEntry === undefined && v === undefined)) {
            // if value changed
            const valueChanged = (
                (previousSelectedEntry === undefined && v !== undefined)
                || (
                    previousSelectedEntry !== undefined && v !== undefined &&
                    !Array.isArray(previousSelectedEntry)
                    && !Array.isArray(v)
                    && previousSelectedEntry.id !== v.id
                )
                || (
                    Array.isArray(previousSelectedEntry)
                    && Array.isArray(v)
                    && !arraysAreEqual(previousSelectedEntry, v, "id", true)
                )
            );
            if (valueChanged) {
                this.notifyValueChanged();
            }
        }
    }

    public notifyValueChanged() {
        this.notifyObservers({
            "action": "select",
            "value": this._selectedEntry
        }, this);
    }

    /**
     * Sets value from given entry id(s); if it was not found, sets the
     * first available entry as selectedValue
     */
    protected setValueFromId(id: string | string[]) {
        let found = false;
        const entries = [];
        if (Array.isArray(id)) {
            for (const e of this._entries) {
                if (id.includes(e.id)) {
                    entries.push(e);
                    found = true;
                }
            }
            this.setValue(entries);
        } else {
            for (const e of this._entries) {
                if (e.id === id) {
                    found = true;
                    this.setValue(e);
                }
            }
        }
        if (!found) {
            this.findAndSetDefaultValue();
        }
    }

    /**
     * Try to find a default value to select.
     * Priority order :
     *   - passed parameter
     *   - nub value for associated property
     *   - default value from configuration file
     *   - first select entry
    protected findAndSetDefaultValue(value?: string) {
        // default to first available entry if any
        if (this._entries.length > 0) {
            if (value !== undefined) {
                val = this.getEntryFromValue(enumValueFromString(this._associatedProperty, value));
                if (val === undefined) {
                    throw Error("invalid select default value " + value + " for " + this._associatedProperty + " property");
                }
            } else {
                if (this.nub !== undefined) {
                    val = this.getEntryFromValue(this.nub.getPropValue(this._associatedProperty));
                    // nub may not have "this._associatedProperty" as a property
                }
                if (val === undefined && this._configDefaultValue !== undefined) {
                    val = this.getEntryFromValue(enumValueFromString(this._associatedProperty, this._configDefaultValue));
                    if (val === undefined) {
                        throw Error("invalid select default value " + this._configDefaultValue + " for " + this._associatedProperty + " property");
                    }
                if (val === undefined) {
                    val = this._entries[0];
            }
        } else {
            // notify observers that no value is selected anymore
            this.notifyValueChanged();
        }
    }

    public parseConfig(field: {}, data?: {}) {
        this._confId = field["id"];
        this._entriesBaseId = this._confId + "_";
        this._helpLink = field["help"];
        //this.afterParseConfig(); // done in FieldSet.parse_select()
    }

    /**
     * Once config is parsed, init original value from model
     * (needs config for this._entriesBaseId to be set).
     * Triggered at the end of parseConfig()
     */
        this.populate();
        this.initSelectedValue();
    }

    /**
     * fill select with options
     */
    protected abstract populate();

    /**
     * initialise select (loads UI with the value held by the model)
     */
    protected abstract initSelectedValue();

    /**
     * associated nub property
     */
    public get associatedProperty(): string {
        return this._associatedProperty;
    }

    /**
     * @returns true if select field is associated to a nub property
     */
    public get hasAssociatedNubProperty(): boolean {
        return this._associatedProperty !== undefined;
    }

    /**
     * default value from configuration
     */
    public get configDefaultValue(): string {
        return this._configDefaultValue;
    }

    private clearEntries() {
        this._entries = [];
    }

    public get entries() {
        return this._entries;
    }

    protected addEntry(e: SelectEntry) {
        this._entries.push(e);
    }

    protected createOrGetEntry(id: string, val: any, lbl?: string): SelectEntry {
        let res: SelectEntry = this._entryMap[id];
        if (res === undefined) {
            res = new SelectEntry(id, val, lbl);
            this._entryMap[id] = res;
        }
        return res;
    }

    public getEntryFromValue(val: any): SelectEntry {
        for (const se of this._entries) {
            if (se.value === val) {
                return se;
            }
        }
    }

    /**
    * Reloads available entries, trying to keep the current selected
    * value; does not notify observers if value did not change
    */
    public updateEntries() {
        // store previous selected entry
        const pse = this._selectedEntry;
        // empty
        this.clearEntries();
        // populate
        this.populate();
        this.updateLocalisation();
        // if no entry is available anymore, unset value
        if (this._entries.length === 0) {
            if (this._multiple) {
                this.setValue([]);
            } else {
                this.setValue(undefined);
            }
        } else {
            // keep previously selected entry(ies) if possible
            if (pse) {
                if (!Array.isArray(pse) && pse.id) {
                    this.setValueFromId(pse.id);
                } else if (Array.isArray(pse) && pse.length > 0) {
                    this.setValueFromId(pse.map((e) => e.id));
                } else {
                    this.findAndSetDefaultValue();
                }
            } else {
                this.findAndSetDefaultValue();
            }
        }
    }

    public get entriesBaseId(): string {
        return this._entriesBaseId;
    }

    public get messageWhenEmpty(): string {
        let msg: string;
        if (this._selectedEntry === undefined && this._messageWhenEmpty) {
            msg = ServiceFactory.i18nService.localizeText(this._messageWhenEmpty);
        }
        return msg;
    }

    public get multiple(): boolean {
        return this._multiple;
    }

    public updateLocalisation() {
        super.updateLocalisation();
        for (const e of this._entries) {
            const aId = e.id.split("_");
            const trad = ServiceFactory.formulaireService.localizeText(
                `${aId[1].toUpperCase()}_${aId[2]}`,
                this.parentForm.currentNub.calcType
            );
            e.label = trad;
        }
    }
}