【JavaScript】シンプルな入力フォームを簡易に作るためのJS

本記事ではシンプルな入力フォームが作りたいなあというモチベーションのもと作成しています。
シンプルな入力フォームのモックを作り（HTML, CSS）、そのHTMLの作りをもとにJavaScriptで書き出していくといった流れで作成します。最終的にはHTMLフォーム上で項目名や2カラムで表示させる項目などを指定し、同画面のtextareaにForm HTMLを書き出すというものを作りたい。（現状はForm HTMLをtextやemailなどのinput要素のみを1カラムで表示するといったものまで作成。）

本記事を作成するにあたり、参考にしたのは次の文献です。


https://formbold.com/templates

https://developer.mozilla.org/ja/docs/Web/HTML

前者はFormのレイアウトに、後者はHTML周りなどの全般的な参考文献として活用しています。

以下のような流れで紹介していきます。

1. 作成フォームのモック
2. Form要素
3. JavaScriptの作成
4. まとめ


# 1. フォームのモック
[こちら](https://formbold.com/templates)のページを参考にして、

![html_form_pc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3275023/9c209719-24f9-c918-d160-1c9d296b51fe.png)

こんな感じ物を作りました。要素としては1カラムと2カラムのレイアウトで、text入力のほかにDate入力、Selectオプションの入力、ラジオボタンまたはチェックボックスができるようにしています。

こちらのHTMLとCSSは次の通り。（※CSSについては作成途中で順番が適当なところがあるのでご注意ください。）

```html: form.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTML MAKER</title>
    <link rel="stylesheet" href="style.css">
    <!-- <script src="index.js" defer></script> -->
</head>

<body>
    <div class="wrapper">
        <form action="#">
            <div class="form-title">
                <h2>Form Title</h2>
                <p>
                    This is sample form.
                </p>
            </div>
            <div class="input-flex">
                <div class="input-control">
                    <label for="FirstName" class="form-label required">FirstName</label>
                    <input type="text" id="FirstName" name="FirstName" value="" class="form-input">
                    <div class="error"></div>
                </div>
                <div class="input-control">
                    <label for="LastName" class="form-label required">LastName</label>
                    <input type="text" id="LastName" name="LastName" value="" class="form-input">
                    <div class="error"></div>
                </div>
            </div>
            <div class="input-flex">
                <div class="input-control">
                    <label for="BirthDate" class="form-label required">BirthDate</label>
                    <input type="date" id="BirthDate" name="BirthDate" value="" class="form-input">
                    <div class="error"></div>
                </div>
                <div class="input-control">
                    <label for="From" class="form-label required">From</label>
                    <input type="email" id="From" name="From" value="" class="form-input">
                    <div class="error"></div>
                </div>
            </div>
            <div class="input-control">
                <label for="EmailAddress" class="form-label required">EmailAddress</label>
                <input type="email" id="EmailAddress" name="EmailAddress" value="" class="form-input">
                <div class="error"></div>
            </div>
            <div class="mb-3 input-control">
                <label for="Password" class="form-label required">Password</label>
                <input type="password" id="Password" name="Password" value="" class="form-input">
                <div class="error"></div>
            </div>
            <div class="mb-3 input-control">
                <label for="PasswordConfirm" class="form-label required">PasswordConfirm</label>
                <input type="password" id="PasswordConfirm" name="PasswordConfirm" value="" class="form-input">
                <div class="error"></div>
            </div>
            <div class="mb-3 input-control">
                <label for="fruits" class="form-label">Favorite Fruits</label>
                <select name="fruits" id="fruits" class="form-select">
                    <option name="00">選択してください 。</option>
                    <option name="01">りんご</option>
                    <option name="02">もも</option>
                    <option name="03">バナナ</option>
                    <option name="04">梨</option>
                    <option name="05">ぶどう</option>
                    <option name="99">その他</option>
                </select>
                <div class="error"></div>
            </div>
            <div class="mb-3">
                <label for="" class="form-label required">Item</label>
                <div class="radio-area input-control">
                    <div style="text-align:left;" class="error"></div>
                    <input type="radio" id="Item-1" name="Item" value="Item-1" class="radio-input">
                    <label for="Item-1" class="radio-label">Item-1</label>
                    <input type="radio" id="Item-2" name="Item" value="Item-2" class="radio-input">
                    <label for="Item-2" class="radio-label">Item-2</label>
                    <input type="radio" id="Item-3" name="Item" value="Item-3" class="radio-input">
                    <label for="Item-3" class="radio-label">Item-3</label>
                    <input type="radio" id="Item-4" name="Item" value="Item-4" class="radio-input">
                    <label for="Item-4" class="radio-label">Item-4</label>
                    <input type="radio" id="Item-5" name="Item" value="Item-5" class="radio-input">
                    <label for="Item-5" class="radio-label">Item-5</label>
                    <input type="radio" id="Item-6" name="Item" value="Item-6" class="radio-input">
                    <label for="Item-6" class="radio-label">Item-6</label>
                </div>
            </div>
            <div class="mb-3 input-control">
                <label for="AcceptPolicy" class="checkbox-label">
                    <input type="checkbox" id="AcceptPolicy" name="AcceptPolicy" value=""> &nbsp;AcceptPolicy is <a
                        href="#">here</a>.
                </label>
                <div class="error"></div>
            </div>
            <input type="submit" id="SubmitBtn" name="SubmitBtn" value="Sign Up" class="btn">
        </form>
    </div>
</body>

</html>
```

```css: style.css
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300&family=Poppins:wght@200;500&family=Roboto+Condensed&family=VT323&display=swap');

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html {
    font-size: 10px;
}

body {
    font-family: "Poppins", sans-serif;
}

.mb-3 {
    margin-bottom: 1.5rem;
}

.relative {
    position: relative;
}

label.required:after {
    margin-left: 1rem;
    padding: 0 .5rem;
    border-radius: .5rem;
    font-size: 1.1rem;
    color: white;
    background-color: #ff3860;
    content: "必須";
}

.wrapper {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 4rem;
}

.form-wrapper {
    margin: 0 auto;
    min-width: 400px;
    width: 100%;
    background: white;
    padding: 4rem;
}

.form-title {
    margin-bottom: 2rem;
}

.form-title h2 {
    font-weight: 600;
    font-size: 2.8rem;
    line-height: 3.6rem;
    color: #07074d;
}

.form-title p {
    font-size: 1.6rem;
    line-height: 2.4rem;
    color: #536387;
    margin-top: 1.1rem;
}

.input-flex {
    display: flex;
    gap: 2rem;
    margin-bottom: 1.5rem;
}

.input-flex>div {
    width: 50%;
}

.input-flex>div.divide-4 {
    width: 25%;
}

.input-flex>div.divide-5 {
    width: 20%;
}

.input-flex>div.divide-6 {
    width: 16%;
}

.input-flex>div.divide-7 {
    width: 14%;
}

.form-input {
    text-align: left;
    width: 100%;
    padding: 1.5rem 2rem;
    border-radius: .5rem;
    border: .2rem solid #dde3ec;
    background: #ffffff;
    font-weight: 500;
    font-size: 1.6rem;
    color: #536387;
}

.form-input:focus {
    border-color: #6a64f1;
}

.form-label {
    color: #536387;
    font-size: 1.6rem;
    line-height: 2.4rem;
    display: block;
    margin-bottom: 1rem;
}

.form-label-col {
    vertical-align: bottom;
    text-decoration: underline;
    padding: 0;
    margin: 0 auto;
}

.form-select {
    display: block;
    appearance: none;
    cursor: pointer;
    width: 100%;
    padding: 1.5rem 2rem;
    border-radius: .5rem;
    border: .2rem solid #dde3ec;
    background: #ffffff;
    font-weight: 500;
    font-size: 1.6rem;
    color: #536387;
}

.form-select:focus {
    outline: none;
}

.checkbox-label {
    display: flex;
    cursor: pointer;
    user-select: none;
    font-size: 1.6rem;
    line-height: 2.4rem;
    color: #536387;
}

.checkbox-label a {
    margin-left: .5rem;
    color: #6a64f1;
    font-weight: 500;
}

.btn {
    display: block;
    width: 80%;
    margin: 0 auto;
    font-size: 1.6rem;
    border-radius: 2rem;
    padding: 1.5rem 2.5rem;
    border: none;
    font-weight: 500;
    background-color: #6a64f1;
    color: white;
    cursor: pointer;
    margin-top: 2rem;
}

.btn:hover {
    background: #07074d;
}

.radio-area {
    border: none;
    text-align: center;
}

.radio-input {
    clip: rect(1px, 1px, 1px, 1px);
    position: absolute !important;
}

.radio-input:checked + .radio-label{
    background:#07074d;
    color: #ffffff;
}

.radio-input:focus + .radio-label {
    outline-color: #4D90FE;
    outline-offset: -2px;
    outline-style: auto;
    outline-width: 5px;
}

.radio-label {
    display: inline-block;
    margin: 0;
    margin-right: 1.6rem;
    padding: 1rem 1.5rem;
    font-size: 1.6rem;
    border-radius: 1.5rem;
}

.input-control.success input {
    border-color: #09c372;
}

.input-control.error input {
    border-color: #ff3860;
}

.input-control .error {
    color: #ff3860;
    font-size: 1.2rem;
    height: 1.6rem;
}
```

⇒モックのため、JavaScriptの検証ロジックが組み込まれていないが、自動でHTMLを作成したときに最低限の入力チェックも同時に動くような作りにしたい。（願望）

# 2. Form要素

作成を想定しているForm要素は以下の通り。

* inputタグの一般的なもの（text,number,password,email, etc...）
* selectタグ
* textareaタグ

# 3. JavaScriptの作成

基本的な構想としては、JavaScriptで作成したHTMLが`style.css` を読み込んでいい感じのフォームになるというものなので、JavaScriptで `form.html` のコンポーネントを設定していきます。

手順はシンプルで

1. Formを設定（action, method, id, etc...）
2. Form要素を設定（label, input, id, name, etc...）
3. 手順1, 2の設定値をもとにJavaScriptでHTML作成
4. JavaScriptで手順3のHTMLを書き出す

なのですが、ご紹介できるのは手順3のみです。手順1, 2は変数`config`で代替しており、手順4はコンソール表示のみのため。

⇒コンポーネントの設定画面および出力画面は今後作成予定。


基本的なコンポーネントの構造は次の通り。

```html: 基本コンポーネント
<div class="mb-3 input-control">
    <label for="EmailAddress" class="form-label required">EmailAddress</label>
    <input type="email" id="EmailAddress" name="EmailAddress" value="" class="form-input">
    <div class="error"></div>
</div>
```

`div`タグで`label`, `input(select, textarea)`,`div.error`を包み込んだものを基本コンポーネントとしています。ちなみにsubmitボタンは

```html: submitボタン
<input type="submit" id="SubmitBtn" name="SubmitBtn" value="Sign Up" class="btn">
```

とシンプルなものです。なので基本的なフォームの構造は次の通り：

```html: Form構造
<form action="#" method="post">
    <div class="form-title">
        <h2>Form Title</h2>
        <p>
            This is sample form.
        </p>
    </div>
    <div class="mb-3 input-control">
        <label for="EmailAddress" class="form-label required">EmailAddress</label>
        <input type="email" id="EmailAddress" name="EmailAddress" value="" class="form-input">
        <div class="error"></div>
    </div>
    ....
    <input type="submit" id="SubmitBtn" name="SubmitBtn" value="Sign Up" class="btn">
</form>
```

次に、JavaScriptを紹介。今まで紹介したコンポーネントを定数で定義し、設定値を入れる箇所はプレースホルダー`{{REPLACEMENT}}`で定義します。例えば、基本コンポーネントは

```js: 基本コンポーネント定数
const formElementTemplate = '<div class="{{CLASS}}">{{LABEL}}{{INPUT}}<div class="error"></div></div>'
```

となります。この定数について`formElementTemplate.replace("{{CLASS}}","input-control mb-3")`のような置換を繰り返していくことでHTMLを作成していきます。

さて、設定値ですがあくまでプロトタイプとして次のように定義しています。長いので簡略化したものが次の通り。

```js: Form設定値
const config = {
    language: "日本語",
    Head: {
        encoding: "utf-8",
        metas: [
            { name: "robots", content: "noindex,nofollow" },
            { name: "viewport", content: "width=device-width,initial-scale=1" },
            { name: "description", content: "フォームを簡単に作成するツール" }
        ],
        title : "フォーム",
    },
    Body: {
        Form: {
            id: "form",
            class: "",
            action: "https://google.com/",
            method: "POST",
            novalidate: true,
            title: "Input Form",
            description: "This is sample text."
        },
        FormElements: [
            {
                id: "Username",
                class: "input-control mb-3",
                classInput: "form-input",
                label: "form-label",
                tag: "input",
                type: "text",
                name: "Username",
                value: ""
            },
        ...
        ]
    }
}
```

基本的なHTMLのメタタグの設定などのHEAD部分とFormを設定するBODY部分に大きく分類されています。アクセス方法は例えば、タイトルであれば`config.Head.title`です。
⇒この部分については入力値としてJavaScript側で受け取るようにしたいと考えており、実装次第で構造は若干変更する予定です。（特にフォーム要素の箇所）

実装について必要な情報は以上になりますが、項目が多いので複雑に見えます...。

```js: HTMLを作成するJavaScript（途中工程）
const inputTypes = ["button","checkbox","color","date","datetime-local","email","file","hidden","image","month","number","password","radio","range","reset","search","submit","tel","text","time","url","week"];
const languageTypes = {
    "日本語": "ja",
	"英語": "en",
	"中国語（簡体字）": "zh-CN",
    "フランス語": "fr",
    "イタリア語": "it"
}

const labelTemplate = '<label for="{{ID}}" class="{{CLASS}}">{{NAME}}</label>'
const inputTemplate = '<input id="{{ID}}" class="{{CLASS}}" type="{{TYPE}}" name="{{NAME}}" value="{{VALUE}}">'
const selectTemplate = '<select id="{{ID}}" class="{{CLASS}}" name="{{NAME}}">{{OPTIONS}}</select>'
const selectOptionTemplate = '<option name="{{VALUE}}">{{NAME}}</option>'
const formTemplate = '<form id="{{ID}}" class="{{CLASS}}" action="{{HREF}}" method="{{METHOD}}" {{NO_VALIDATE}}>{{FORM_TITLE}}{{FORM_ELEMENTS}}</form>'
const formTitleTemplate = '<div class="form-title"><h2>{{TITLE}}</h2><p>{{DESCRIPTION}}</p></div>'
const formElementTemplate = '<div class="{{CLASS}}">{{LABEL}}{{INPUT}}<div class="error"></div></div>'
const blockTemplate = '<div class="wrapper">{{CONTENTS}}</div>';
const metaTemplate = '<meta name="{{NAME}}" content="{{CONTENT}}">';
const titleTemplate = '<title>{{TITLE}}</title>';
const linkTemplate = '<link rel="stylesheet" href="{{HREF}}">';
// const styleTemplate = '<style>{{STYLE}}</style>';
// const scriptTemplate = '<script>{{SCRIPT}}</script>';
const htmlTemplate = '<!DOCTYPE html><html lang="{{LANGUAGE}}">{{HEAD}}{{BODY}}</html>'
const headTemplate = '<head><meta charset="{{ENCODING}}">{{METAS}}{{TITLE}}{{STYLES}}{{SCRIPTS}}</head>'
const bodyTemplate = '<body>{{CONTENTS}}{{SCRIPTS}}</body>'

const config = {
    language: "日本語",
    Head: {
        encoding: "utf-8",
        metas: [
            { name: "robots", content: "noindex,nofollow" },
            { name: "viewport", content: "width=device-width,initial-scale=1" },
            { name: "description", content: "フォームを簡単に作成するツール" }
        ],
        title : "フォーム",
        styles: [
            { type: "link", href: "style.css" },
            { type: "style", style: "" }
        ],
        scripts: [
            { type: "link", href: "script.js", loaded: true },
            { type: "script", script: "form.js", loaded: true }
        ]
    },
    Body: {
        Form: {
            id: "form",
            class: "",
            action: "https://google.com/",
            method: "POST",
            novalidate: true,
            title: "Input Form",
            description: "This is sample text."
        },
        FormElements: [
            {
                id: "Username",
                class: "input-control mb-3",
                classInput: "form-input",
                label: "form-label",
                tag: "input",
                type: "text",
                name: "Username",
                value: ""
            },
            {
                id: "EmailAddress",
                class: "input-control mb-3",
                classInput: "form-input",
                label: "form-label",
                tag: "input",
                type: "email",
                name: "EmailAddress",
                value: ""
            },
            {
                id: "Password",
                class: "input-control mb-3",
                classInput: "form-input",
                label: "form-label",
                tag: "input",
                type: "password",
                name: "Password",
                value: ""
            },
            {
                id: "Password2",
                class: "input-control mb-3",
                classInput: "form-input",
                label: "form-label",
                tag: "input",
                type: "password",
                name: "Password2",
                value: ""
            },
            {
                id: "fruits",
                class: "input-control mb-3",
                classInput: "form-select",
                label: "form-label",
                tag: "select",
                options: [
                    { name: "選択してください。",  value: "00" },
                    { name: "りんご",  value: "01" },
                    { name: "もも",  value: "02" },
                    { name: "バナナ",  value: "03" },
                    { name: "梨",  value: "04" },
                    { name: "ぶどう",  value: "05" }
                ],
                name: "fruits"
            },
            {
                id: "SubmitBtn",
                classInput: "btn",
                tag: "input",
                type: "submit",
                name: "SubmitBtn",
                value: "Sign Up"
            }
        ]
    }
}


// Meta作成
let metaElements = '';
config.Head.metas.forEach((e)=> {
    metaElements += metaTemplate
        .replace("{{NAME}}", e.name)
        .replace("{{CONTENT}}", e.content);
})

// Title作成
let titleElement = titleTemplate.replace("{{TITLE}}",config.Head.title)

// Style作成 /* TODO: 今後作成 */
let styles = '';
let linkElements = '';
let styleElements = '';

linkElements = linkTemplate.replace("{{HREF}}", "style.css");

styles += linkElements;
styles += styleElements;

// Script作成 /* TODO: 今後作成 */
let scriptElements = '';

// Head作成
let head =  headTemplate
    .replace("{{ENCODING}}", config.Head.encoding)
    .replace("{{METAS}}", metaElements)
    .replace("{{TITLE}}", titleElement)
    .replace("{{STYLES}}", styles)
    .replace("{{SCRIPTS}}", scriptElements)

// Form作成
let form = "";
let formTitle = formTitleTemplate
    .replace("{{TITLE}}", config.Body.Form.title)
    .replace("{{DESCRIPTION}}", config.Body.Form.description);
let formElements = "";
config.Body.FormElements.forEach((e) => {
    let tmpLabel = "";
    let tmpElement = "";
    if(e.tag === "input") {
        tmpElement = inputTemplate
            .replace("{{TYPE}}",e.type)
            .replace("{{ID}}", e.id)
            .replace("{{CLASS}}", e.classInput)
            .replace("{{NAME}}",e.name)
            .replace("{{VALUE}}",e.value);
    } else if (e.tag === "select") {
        let tmpOptions = [];
        let options = e?.options;
        if(options) {
            options.forEach(option => {
                tmpOptions.push(
                    selectOptionTemplate
                        .replace("{{NAME}}",option.name)
                        .replace("{{VALUE}}",option.value)
                )
            });
            tmpElement = selectTemplate
                .replace("{{NAME}}",e.name)
                .replace("{{ID}}",e.id)
                .replace("{{CLASS}}",e.classInput)
                .replace("{{OPTIONS}}", tmpOptions.join(""));
        }
    }
    if(e.type !== "submit") {
        tmpLabel = labelTemplate
            .replace("{{ID}}", e.id)
            .replace("{{CLASS}}", e.label)
            .replace("{{NAME}}", e.name);
    }
    formElements += e.type !== "submit"
        ? formElementTemplate
            .replace("{{CLASS}}", e.class)
            .replace("{{LABEL}}", tmpLabel)
            .replace("{{INPUT}}", tmpElement)
        : tmpElement;
});

form = formTemplate
    .replace("{{ID}}", config.Body.Form.id)
    .replace("{{CLASS}}", config.Body.Form.class)
    .replace("{{HREF}}", config.Body.Form.action)
    .replace("{{METHOD}}", config.Body.Form.method)
    .replace("{{NO_VALIDATE}}", config.Body.Form.novalidate ? "novalidate" : "")
    .replace("{{FORM_TITLE}}", formTitle)
    .replace("{{FORM_ELEMENTS}}", formElements);


// BodyScript作成 /* TODO: 今後作成 */
let bodyScript = '';

// Body作成 /* TODO: 見出しなど */
let body = '';
let mainElement = '';

mainElement += blockTemplate.replace("{{CONTENTS}}", form);

body = bodyTemplate
    .replace("{{CONTENTS}}", mainElement)
    .replace("{{SCRIPTS}}", bodyScript);

// Html作成
let html = htmlTemplate
    .replace("{{LANGUAGE}}", languageTypes[config.language])
    .replace("{{HEAD}}", head)
    .replace("{{BODY}}", body)

console.log(html);
```

上記コードを実行すると、

```js: 実行結果
<!DOCTYPE html><html lang="ja"><head><meta charset="utf-8"><meta name="robots" content="noindex,nofollow"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="フォームを簡単に作成するツール"><title>フォーム</title><link rel="stylesheet" href="style.css"></head><body><div class="wrapper"><form id="form" class="" action="https://google.com/" method="POST" novalidate><div class="form-title"><h2>Input Form</h2><p>This is sample text.</p></div><div class="input-control mb-3"><label for="Username" class="form-label">Username</label><input id="Username" class="form-input" type="text" name="Username" value=""><div class="error"></div></div><div class="input-control mb-3"><label for="EmailAddress" class="form-label">EmailAddress</label><input id="EmailAddress" class="form-input" type="email" name="EmailAddress" value=""><div class="error"></div></div><div class="input-control mb-3"><label for="Password" class="form-label">Password</label><input id="Password" class="form-input" type="password" name="Password" value=""><div class="error"></div></div><div class="input-control mb-3"><label for="Password2" class="form-label">Password2</label><input id="Password2" class="form-input" type="password" name="Password2" value=""><div class="error"></div></div><div class="input-control mb-3"><label for="fruits" class="form-label">fruits</label><select id="fruits" class="form-select" name="fruits"><option name="00">選択してください。</option><option name="01">りんご</option><option name="02">もも</option><option name="03">バナナ</option><option name="04">梨</option><option name="05">ぶどう</option></select><div class="error"></div></div><input id="SubmitBtn" class="btn" type="submit" name="SubmitBtn" value="Sign Up"></form></div></body></html>
```

このような出力がされるので、これと#1で紹介した`style.css`がリンクされると次のようになります。

![html_form_make.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3275023/4c360bf5-32a3-6b8f-4385-2faa6e58eb71.png)

（2カラムにしていないため要素が細い...oh）

⇒コンポーネントの追加や適宜修正箇所あり。（2023/12/17現在）

# 4. まとめ

本記事はすべて紹介しきるのが難しかったので見切り発車で作成してしまいましたが、やっていること自体は単純だなと感じました。（構造が複雑に見えるだけのはず...?）

次回は`config`を設定するフォームとHTMLを出力するtextareaを作成していきたいと思います。