Input

A generator for an input element that binds the value of the input to an object. This is not an element, but a generator that adds listeners to an input element. If one day, Safari adds support for extending built in elements, this can be converted.

Example

Syntax

Input.create('number', {})
// <input ...>
Input.new('number', {})
// Input()

Options

types description arguments
record Object

Object to bind to

attribute String

Attribute of the Object to bind to

dump Function

transform textContent to value on change

value
load Function

transform value for input's content

value
SOURCE CODE
import KompElement from './element.js';
import ContentArea from './content-area.js';
import { except, dig, bury } from '../support.js';
import { createElement, HTML_ATTRIBUTES } from 'dolla';

class Input {
    
    static assignableAttributes = {
        record: null,
        attribute: null,
        dump: (v, record) => v,
        load: (v, record) => v
    }
    
    static create(type, options={}) {
        return this.new(type, options).input
    }
    
    static new (type, options={}) {
        const klass = {
            button: ButtonInput,
            checkbox: BinaryInput,
            radio: BinaryInput,
            select: SelectInput,
            date: DateInput,
            textarea: TextareaInput,
            'datetime-local': DateTimeInput,
            contentarea: ContentAreaInput,
        }[type]
        if (klass) {
            return new klass(Object.assign({
                type: type
            }, options))
        } else {
            return new this(Object.assign({
                type: type
            }, options))
        }
    }
    
    constructor (options={}) {
        Object.keys(this.constructor.assignableAttributes).forEach(k => {
            if (options[k] != undefined) {
                this[k] = options[k]
            }
        })
        if (typeof this.dump != "function") {
            this.dump = v => v
        }
        if (typeof this.load != "function") {
            this.load = v => v
        }
        this.input = this.createInput(except(options, 'load', 'dump'));
        this.input._loading = this._load(null, options.value);
        this.setupInputListener(this.inputChange.bind(this));
        this.setupRecordListener(this.recordChange.bind(this));
    }
    
    get value () {
        this.input.value
    }
    
    set value (v) {
        this.input.value = v
    }
    
    createInput (options={}) {
        return createElement('input', Object.assign({
            type: options.type
        }, options))
    }
    
    setupInputListener (listener) {
        this.input.addEventListener('change', listener.bind(this));
        this.input.addEventListener('blur', listener.bind(this));
    }
    
    setupRecordListener (listener) {
        if (this.record && this.record.addEventListener) {
            this.record.addEventListener('change', listener);
        }
        if (this.record && this.record.addListener) {
            this.record.addListener(listener)
        }
    }
    
    inputChange (e) {
        // for 50ms cancel calls to inputChange, for quick events of change and blur
        if (!this._dumping) {
            if (this.input.closest('[preventChange]')) { return false }
            this._dump();
            this._dumping = new Promise(done => {
                setTimeout(() => {
                    delete this._dumping
                    done()
                }, 50)
            })
        }
    }
    
    recordChange () {
        this.input._loading = this._load()
    }
    
    _load (e, v) {
        const value = this.load(v ? v : this._loadValue(), this.record, {explicitValue: v})
        if (value !== undefined && value !== null) {
            this.input.value = value
        }
    }
    
    _loadValue () {
        if (this.record) {
            return dig(this.record, this.attribute)
        }
    }
    
    _dump (e, v) {
        const value = this.dump(v ? v : this.input.value, this.record)
        return this._dumpValue(value)
    }
    
    _dumpValue(v) {
        let attributes = Array.isArray(this.attribute) ? this.attribute : [this.attribute]
        attributes = attributes.concat([v])
        bury(this.record, ...attributes)
        return v
    }
}

class ContentAreaInput extends Input {
    createInput (options) {
        return new ContentArea(options)
    }
}

class BinaryInput extends Input {
    async _load () {
        const value = this.load(await this._loadValue())
        const inputValue = this.input.value == "on" ? true : this.input.value
        if (this.input.multiple) {
            this.input.checked = Array.isArray(value) ? value.includes(inputValue) : false
        } else {
            this.input.checked = value == inputValue
        }
    }
    _dump () {
        let value
        let inputValue = this.input.value == "on" ? true : this.input.value
        if (this.input.multiple) {
            const currentValues = this._loadValue() || []
            if (this.input.checked) {
                if (currentValues.includes(inputValue)) {
                    value = this.dump(currentValues)
                } else {
                    value = this.dump(currentValues.concat(inputValue))
                }
            } else {
                value = this.dump(currentValues.filter(x => x != inputValue))
            }
        } else if (typeof inputValue == 'boolean') {
            value = this.dump(this.input.checked ? inputValue : !inputValue)
        } else {
            value = this.dump(this.input.checked ? this.input.value : null)
        }
        return this._dumpValue(value)
    }
    
    setupInputListener (listener) {
        this.input.addEventListener('change', listener.bind(this));
    }
}

class DateInput extends Input {
    setupInputListener () {
        this.input.addEventListener('blur', this.inputChange.bind(this));
    }
    
    async _load (e) {
        let value = await this._loadValue()
        if (value instanceof Date) {
            value = [
                value.getUTCFullYear(),
                (value.getMonth() + 1).toString().padStart(2, "0"),
                value.getDate().toString().padStart(2, "0")
            ].join("-")
        }
        return super._load(e, value)
    }
    
    _dump (e) {
        let value = this.input.value
        if (value == "") value = null
        super._dump(e, value)
    }
}

class DateTimeInput extends Input {
    async _load (e) {
        let value = await this._loadValue()
        if (value instanceof Date) {
            value = [
                [
                    value.getUTCFullYear(),
                    (value.getMonth() + 1).toString().padStart(2, "0"),
                    value.getDate().toString().padStart(2, "0")
                ].join("-"),
                'T',
                [
                    value.getHours().toString().padStart(2, "0"),
                    value.getMinutes().toString().padStart(2, "0")
                ].join(":")
            ].join("")
        }
        super._load(e, value)
    }
}

class TextareaInput extends Input {
    createInput (options) {
        return createElement('textarea', options)
    }
}

class ButtonInput extends Input {
    createInput (options) {
        return createElement('button', options)
    }
    setupInputListener () {
        this.input.addEventListener('click', this._dump.bind(this));
    }
    _load () {}
}

class SelectInput extends Input {
    createInput (options={}) {
        const input = createElement('select', options)
        if (options.includeBlank) {
            input.append(createElement('option', Object.assign({
                content: 'Unset',
                value: null
            }, options.includeBlank)))
        }
        if (options.options) {
            options.options.forEach(option => {
                input.append(createElement('option', {
                    content: Array.isArray(option) ? option[1] : option,
                    value: Array.isArray(option) ? option[0] : option
                }))
            })
        }
        
        return input
    }
    async _load (e) {
        if (this.input.multiple) {
            const values = this.load(await this._loadValue());
            this.input.querySelectorAll('option').forEach(option => {
                if (values.includes(option.value)) {
                    option.setAttribute('selected', true)
                } else {
                    option.removeAttribute('selected')
                }
            })
        } else {
            super._load()
        }
    }
    _dump (e) {
        if (this.input.multiple) {
            const values = Array.from(this.input.options).filter(x => x.selected).map(x => x.value)
            this._dumpValue(this.dump(values))
            return values
        } else {
            let value = this.input.value
            if (value == "null") value = null
            value = this.dump(value)
            this._dumpValue(value)
            return value
        }
    }
}