※プログラミング歴一年未満時点の自分用メモです。

参考:/programming-notes/easyDOMgenerator

やりたいこと

最終的にできる形

// 実行例
const div = createDOM('div', {
    id: 'hoge',
    className: 'test',
    onclick: () => {
        // クリック時イベント
    },
    css: {
        color: 'red',
        'font-size': '14px',
    },
}, document.body, 'prepend')

editDOM(hoge, {
    _addClass: ['fuga'],
    '$.foo': {
        textContent: 'bar',
    },
    All$span: {
        contenteditable: true,
        onfocus: () => {
            // フォーカス時の処理
        },
    },
})

↑は以下の処理と同じ

const div = document.createElement('div');
document.body.prepend(div);
div.id = 'hoge';
div.className = 'test';
div.addEventListener('click', () => {
    // クリック時イベント
})
div.style.color = 'red';
div.style.fontSize = '14px';

hoge.classList.add(...['fuga']);
hoge.querySelector('.foo').textContent = 'bar';
hoge.querySelectorAll('span').forEach(elm => {
    elm.setAttribute('contenteditable', true);
    elm.addEventListener('focus', () => {
        // フォーカス時の処理
    })
})

関数を作る

// createElementを伴う処理
function createDOM(tagname, option = {}, target, position = 'append') {
    const elm = document.createElement(tagname);
    editDOM(elm, option, target, position); // createElement以外の処理は編集用関数を呼び出す
    return elm; // 生成したHTML要素を返す
}

// 既に存在する要素を編集する処理
function editDOM(elm, option = {}, target, position = 'append') {
    if (target) { // 要素の挿入先を指定している時
        switch (position) {
            case 'append': // 初期値はappend
                target.append(elm);
                break;
            case 'prepend':
                target.prepend(elm);
                break;
            case 'after':
                target.after(elm);
                break;
            case 'before':
                target.before(elm);
                break;
            default:
                console.error('editDOM: position is invalid');
        }
    }
    if (typeof option !== 'object') return;
    for (const key in option) {
        if (key.startsWith('on')) { // onで始まるイベントの処理
            elm.addEventListener(key.slice(2), option[key]);
            continue;
        } else if (key.startsWith('$')) { // 特定の子要素についてもまとめて書けるように
            editDOM(elm.querySelector(key.replace('$', '')), option[key])
            continue;
        } else if (key.startsWith('All$')) { // queryに当てはまる全ての子要素について
            elm.querySelectorAll(key.replace('All$', '')).forEach(element => {
                editDOM(element, option[key])
            })
            continue;
        }
        switch (key) {
            case '_addClass': // プロパティはArray
                elm.classList.add(...option[key]);
                break;
            case '_removeClass': // プロパティはArray
                elm.classList.remove(...option[key]);
                break;
            case 'css': // プロパティはObject
                Object.entries(option[key]).forEach(([key, value]) => elm.style.setProperty(key, value));
                break;
            case 'id': // 以下プロパティはString
            case 'className':
            case 'value':
            case 'innerText':
            case 'innerHTML':
            case 'textContent':
            case 'checked':
                elm[key] = option[key];
                break;
            default:
                elm.setAttribute(key, option[key]); // キーがいずれにも当てはまらなければ属性と解釈
        }
    }
}

※普段使っている処理のみを反映しているので他に必要が生じたらその都度書き足す