原生html + js实现表单

1,731 阅读5分钟

如果想直接看如何实现的,可以直接阅读最后一部分。

设计思路

  1. 样式根据设计稿来,不使用表单的提交(便于做校验和拦截,和错误提示的一致性)
  2. 利用js校验相关键点
  • 用户名、密码、确认密码 三项为必填,提交的时候需要检验
  • 确认密码 和 密码的输入在提交前需要校验是否一致
  • 用户如果输入了后面几项,需要校验输入的值是否合法
  1. 校验的时机可以在用户输入完成后(防抖)
  2. js中需要保存每项数据校验不合格时的错误提示
  3. 用户点击提交的时候,校验必填选项是否已经填入;填入的信息是否都合法

效果展示 image.png

html + css

我们拿到一个需求,进行设计分析完成后,肯定是来写html和css。
布局我这儿还是采用比较经典的一行一行的布局,没啥特殊的。
在html中我们需要label展示输入框的主题,input提供输入功能,div.error展示错误信息。
最后肯定还需要一个提交按钮。
因为 用户名、密码、确认密码 是必填内容,所以加上required属性做区分。
根据不同值,给input加上不同的type类型

<div class="wrap">
    <h2 class="title">修改用户信息</h2>
    <div class="item must">
        <label for="nickname">用户名:</label>
        <input id="nickname" placeholder="请输入用户名" required type="text">
        <div class="error"></div>
    </div>
    <div class="item must">
        <label for="passport">密码:</label>
        <input id="passport" placeholder="请输入密码" required type="password">
        <div class="error"></div>
    </div>
    <div class="item must">
        <label for="sure-passport">确认密码:</label>
        <input id="sure-passport" placeholder="请再次确认密码" required type="password">
        <div class="error"></div>
    </div>
    <div class="item">
        <label for="name">姓名:</label>
        <input id="name" placeholder="请输入姓名"  type="text">
    </div>
    <div class="item">
        <label for="qq">QQ:</label>
        <input id="qq" placeholder="请输入QQ" type="number">
        <div class="error qq-error"></div>
    </div>
    <div class="item">
        <label for="wechat">微信:</label>
        <input id="wechat" placeholder="请输入微信"  type="text">
    </div>
    <div class="item">
        <label for="phone">手机:</label>
        <input id="phone" placeholder="请输入手机"  type="number">
        <div class="error"></div>
    </div>
    <div class="item submit">
        <input class="btn" id="submit" placeholder="请输入手机" type="submit">
        <div class="info" id="submit-info"></div>
    </div>
</div>

css部分
这部分感觉没啥可以说的,比较简单。(如果这一部分都不会写,感觉可以恶补一下咯)。
简单说下我写的代码,从最终需要效果来说,

  • 左侧是的label是右对齐的,所以我在这儿给它设定了定宽,并用css变量--label-width保存了这个值,用变量的原因主要是为了便于修改。
  • 变量--left-width是在--label-width的基础上加了4px, 因为input是inline-block,左侧会有4px的空隙
  • 变量--left-width用于了div.errordiv.submit的位置的确定
  • 为了避免div.error的变化导致页面的抖动,所以用了绝对定位
<style>
    :root {
        --input-width: 300px;
        --input-height: 39px;
        --label-width: 160px;

        /* 160 + 4 */
        --left-width: 164px; 
    }
    .wrap {
        display: inline-block;
        display: flex;
        flex-direction: column;
        padding: 20px 20px 20px 40px;
        min-width: 480px;
    }
    .title {
        padding-left: 30px;
    }
    .item {
        position: relative;
        margin-bottom: 30px;
    }
    .item label {
        display: inline-block;
        width: var(--label-width);
        text-align: right;
    }
    .item.must label::before{
        content: '*';
        display: inline-block;
        color: red;
        margin-right: 3px;
    }
    .item input {
        box-sizing: border-box;
        height: var(--input-height);
        width: var(--input-width);
        padding-left: 10px;
        outline: none;
        border: 1px solid #e3e3e3;
        border-radius: 4px;
    }
    
    /* 隐藏input[type='number']右侧的数字控件 */
    .item input::-webkit-outer-spin-button,
    .item input::-webkit-inner-spin-button {
            -webkit-appearance: none !important;
    }
    .item .error {
        position: absolute;
        left: var(--left-width);
        bottom: -24px;
        color: red;
        font-size: 14px;
    }
    .item.submit {
        padding-left: var(--left-width);
    }
    .submit .btn {
        background-color: #1891ff;
        border: 1px solid #1891ff;
        color: #fff;

    }
    .submit .info {
        margin-top: 10px;
        color: #6cc35b;
        font-size: 14px;
    }


</style>

JS部分

最关键的两个部分是

  • input输入的监听,校验数据是否合法
  • 提交按钮,校验数据合法性,并触发合法请求。

debounce

因为我们需要在用户输入完成后,校验输入的值,所以我们需要一个防抖函数

function debounce(delay, fn) {
    let timer = null;
    return function (...arg) {
        if(timer) {
            clearTimeout(timer);
            timer = null;
        }
        timer = setTimeout(() => {
            fn.call(this, ...arg);
            clearTimeout(timer);
            timer = null;
        }, delay);
    }
}

校验用户名

用户名只需要校验值不为空

function checkNickname() {
    let str = this.value;
    const errDom = this.nextElementSibling;
    if (str) {
        errDom.innerText = '';
    } else {
        errDom.innerText = '用户名不能为空';
    }
}

校验手机号

function checkPhone() {
    let str = this.value;
    var reg = /^(0|86|17951)?(13[0-9]|15[012356789]|18[0-9]|14[57]|17[678])[0-9]{8}$/;
    const errDom = this.nextElementSibling;
    if (reg.test(str) || !str) {
        errDom.innerText = '';
    } else {
        errDom.innerText = '请输入正确格式的手机号码';
    }
}

校验qq号

function checkQq() {
    let str = this.value;
    var reg = /^[1-9][0-9]{4,9}$/gim;
    const errDom = this.nextElementSibling;
    if (reg.test(str) || !str) {
        errDom.innerText = '';
    } else {
        errDom.innerText = '请输入正确格式的qq';
    }
}

校验密码

【我觉得在校验数据,密码校验应该是逻辑最复杂的】

校验密码主要有两块:

  1. 密码和确认密码都不能为空
  2. 当用户输入的密码发生变化时,需要校验确认密码是否和密码保持一致
// 校验密码
function checkPassport(){
    const str = this.value;
    const errDom = this.nextElementSibling;
    if (str) {
        errDom.innerText = '';
    }
    else {
        errDom.innerText = '密码不能为空';
    }
    
    // 密码发生变化时,如果确认秘密有值,需要校验两者是否一致
    const surePassport = document.getElementById('sure-passport');
    if (surePassport.value) {
        checkSurePassport.call(surePassport);
    }
}

// 校验确认密码
function checkSurePassport(){
    const str = this.value;
    const errDom = this.nextElementSibling;
    const passport = document.getElementById('passport').value;
    if(str) {
        // 如果密码有值,校验两者是否一致
        if(passport && str !== passport) {
            errDom.innerText = '密码请保持一致';
        }
        else {
            errDom.innerText = '';
        }
    }
    else {
        errDom.innerText = '密码不能为空';
    }

}

eventListerner

因为需要校验的input比较多,如果去逐个添加监听input事件,阅读性可读性都会比较差,所以我们先封装一个 eventListener。
因为我们不需要用户每次触发input都执行函数处理,所以我们加了一个防抖,在用户输入完200ms后触发函数处理。
我们在校验函数中使用了this,所以需要用call给校验函数绑定this,此时this是指向触发input事件的input dom。

const eventListener = debounce(200, dealValue);
function dealValue(isSubmit = false){
    switch(this.id) {
        case 'phone':
            checkPhone.call(this);
            break;
        case 'qq':
            checkQq.call(this);
            break;
        case 'passport':
            checkPassport.call(this);
            break;
        case 'sure-passport':
            checkSurePassport.call(this);
            break;
        case 'nickname':
            checkNickname.call(this);
            break;
        default:
            break;
    }
}

监听input事件【关键】

我们在这一步的时候,就可以直接获取所有input,然后都加上事件监听

let inputDoms = document.querySelectorAll('input');
inputDoms.forEach(inputDom => {
    inputDom.addEventListener('input', eventListener);
})

清空input的值

在我们提交完成且成功后,需要晴空用户上次的输入内容,当然这个实际业务不一定是这样的,可以根据实际情况来

function clearInputsValue() {
    const inputs = [...document.querySelectorAll('input')];
    inputs.forEach((input) => {
        input.value = ''
    });
}

请求后端的方法

我们拿到数据后,肯定是需要提交到后端的,我们可以封装一个发送请求的方法,方法里面需要处理 修改成功和修改失败的情况

function modifyAction(data) {     
    let xhr = new XMLHttpRequest();
    let url = '这儿是请求的地址'
    xhr.open(url);
    xhr.onreadystatechange = function(data) {
        if (xhr.readyState === 4 && xhr.status === 200) {
            let res = xhr.responseText;
            if (res.success) {
                clearInputsValue();
                const infoDom = document.getElementById('submit-info');
                infoDom.innerText = '信息修改成功';

                // 恢复提交功能
                const submit = document.getElementById('submit');
                submit.addEventListener('click', submitClick);

                setTimeout(() => {
                    infoDom.innerText = '';
                }, 2000)
            }
            else {
                // 没有修改成功的处理逻辑
            }
        }
    }
    xhr.send(data);
};

提交事件

便于我们控制事件的监听(移除和添加),所以封装成一个方法

function submitClick() {
    // 检查所有必填项
    const requiredDoms = [...document.querySelectorAll('input[required]')];
    for(let i = 0, l = requiredDoms.length; i < l; i++) {
        const input = requiredDoms[i];
        if (!input.value) {
            // 聚焦用户没有输入的输入框
            input.focus();
            dealValue.call(input);
            return;
        }
    }

    // 检查是否有错误
    const errDoms = [...document.querySelectorAll('.error')];
    for(let i = 0, l = errDoms.length; i < l; i++) {
        const error = errDoms[i];
        if (error.innerText) {
            // 聚焦有错误信息的输入框
            error.previousElementSibling.focus();
            return;
        }
    }
    // 防止用户重复提提交相同内容
    submit.removeEventListener('click', submitClick);

    // 获取用户所有输入的信息
    let data = {};
    const inputs = [...document.querySelectorAll('input')];
    inputs.reduce((pre, input) => {
        pre[input.id] = input.value || '';
        return pre;
    }, data);
    modifyAction(data);
}

监听用户提交【关键】

 const submit = document.getElementById('submit');
 submit.addEventListener('click', submitClick);

可优化的点

在校验 用户名、qq号、手机号的时候,函数整体结构和内容都是非常相似的,所以我们是可以把这三个函数封装成一个函数的。
【昨天晚上写这个到凌晨一点,干不动了,哈哈哈哈,后面再优化吧。不过优化也不难,大家自己动手试试吧】。

完成代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        :root {
            --input-width: 300px;
            --input-height: 39px;
            --label-width: 160px;

            /* 160 + 4 */
            --left-width: 164px; 
        }
        .wrap {
            display: inline-block;
            display: flex;
            flex-direction: column;
            padding: 20px 20px 20px 40px;
            min-width: 480px;
        }
        .title {
            padding-left: 30px;
        }
        .item {
            position: relative;
            margin-bottom: 30px;
        }
        .item label {
            display: inline-block;
            width: var(--label-width);
            text-align: right;
        }
        .item.must label::before{
            content: '*';
            display: inline-block;
            color: red;
            margin-right: 3px;
        }
        .item input {
            box-sizing: border-box;
            height: var(--input-height);
            width: var(--input-width);
            padding-left: 10px;
            outline: none;
            border: 1px solid #e3e3e3;
            border-radius: 4px;
        }
        .item input::-webkit-outer-spin-button,
        .item input::-webkit-inner-spin-button {
                -webkit-appearance: none !important;
        }
        .item .error {
            position: absolute;
            left: var(--left-width);
            bottom: -24px;
            color: red;
            font-size: 14px;
        }
        .item.submit {
            padding-left: var(--left-width);
        }
        .submit .btn {
            background-color: #1891ff;
            border: 1px solid #1891ff;
            color: #fff;
            
        }
        .submit .info {
            margin-top: 10px;
            color: #6cc35b;
            font-size: 14px;
        }


    </style>
</head>
<body>
    
    <div class="wrap">
        <h2 class="title">修改用户信息</h2>
        <div class="item must">
            <label for="nickname">用户名:</label>
            <input id="nickname" placeholder="请输入用户名" required type="text">

            <div class="error"></div>
        </div>
        <div class="item must">
            <label for="passport">密码:</label>
            <input id="passport" placeholder="请输入密码" required type="password">
            <div class="error"></div>
        </div>
        <div class="item must">
            <label for="sure-passport">确认密码:</label>
            <input id="sure-passport" placeholder="请再次确认密码" required type="password">
            <div class="error"></div>
        </div>
        <div class="item">
            <label for="name">姓名:</label>
            <input id="name" placeholder="请输入姓名"  type="text">
            <!-- <div class="error"></div> -->
        </div>
        <div class="item">
            <label for="qq">QQ:</label>
            <input id="qq" placeholder="请输入QQ" type="number">
            <div class="error qq-error"></div>
        </div>
        <div class="item">
            <label for="wechat">微信:</label>
            <input id="wechat" placeholder="请输入微信"  type="text">
            <!-- <div class="error"></div> -->
        </div>
        <div class="item">
            <label for="phone">手机:</label>
            <input id="phone" placeholder="请输入手机"  type="number">
            <div class="error"></div>
        </div>
        <div class="item submit">
            <input class="btn" id="submit" placeholder="请输入手机" type="submit">
            <div class="info" id="submit-info"></div>
        </div>
    </div>

    <script>
        /*
        实际功能设计思路:(用原生html+js)实现
        1. 样式根据设计稿来,不使用表单的提交(便于做校验和拦截)
        2. 利用js校验相关点
            - 用户名、密码、确认密码 三项为必填 (因为这个只是测试代码,我们可以只用用 input的required属性)
            - 确认密码 和 密码的输入在提交前需要校验一一致
            - 用户如果输入了后面几项,需要校验输入的值是否合法
        3. 校验的时机可以在用户输入完成后(防抖)
        4. js中需要保存每项数据校验不合格是的报错信
        */
        const eventListener = debounce(200, dealValue);
        function dealValue(isSubmit = false){
            switch(this.id) {
                case 'phone':
                    checkPhone.call(this);
                    break;
                case 'qq':
                    checkQq.call(this);
                    break;
                case 'passport':
                    checkPassport.call(this);
                    break;
                case 'sure-passport':
                    checkSurePassport.call(this);
                    break;
                case 'nickname':
                    checkNickname.call(this);
                    break;
                default:
                    break;
            }
        }
        let inputDoms = document.querySelectorAll('input');
        inputDoms.forEach(inputDom => {
            inputDom.addEventListener('input', eventListener);
        });
        function debounce(delay, fn) {
            let timer = null;
            return function (...arg) {
                if(timer) {
                    clearTimeout(timer);
                    timer = null;
                }
                timer = setTimeout(() => {
                    
                    fn.call(this, ...arg);
                    clearTimeout(timer);
                    timer = null;
                }, delay);
            }
        }

        function checkPhone() {
            let str = this.value;
            var reg = /^(0|86|17951)?(13[0-9]|15[012356789]|18[0-9]|14[57]|17[678])[0-9]{8}$/;
            const errDom = this.nextElementSibling;
            if (reg.test(str) || !str) {
                errDom.innerText = '';
            } else {
                errDom.innerText = '请输入正确格式的手机号码';
            }
        }
        function checkQq() {
            let str = this.value;
            var reg = /^[1-9][0-9]{4,9}$/gim;
            const errDom = this.nextElementSibling;
            if (reg.test(str) || !str) {
                errDom.innerText = '';
            } else {
                errDom.innerText = '请输入正确格式的qq';
            }
        }
        function checkPassport(){
            const str = this.value;
            const errDom = this.nextElementSibling;
            if (str) {
                errDom.innerText = '';
            }
            else {
                errDom.innerText = '密码不能为空';
            }
            const surePassport = document.getElementById('sure-passport');
            if (surePassport.value) {
                checkSurePassport.call(surePassport);
            }
        }
        function checkSurePassport(){
            const str = this.value;
            const errDom = this.nextElementSibling;
            const passport = document.getElementById('passport').value;
            if(str) {
                if(passport && str !== passport) {
                    errDom.innerText = '密码请保持一致';
                }
                else {
                    errDom.innerText = '';
                }
            }
            else {
                errDom.innerText = '密码不能为空';
            }
            
        }
        function checkNickname() {
            let str = this.value;
            const errDom = this.nextElementSibling;
            if (str) {
                errDom.innerText = '';
            } else {
                errDom.innerText = '用户名不能为空';
            }
        }

        const submit = document.getElementById('submit');
        submit.addEventListener('click', submitClick);
        function submitClick() {

            // 检查所有必填向
            const requiredDoms = [...document.querySelectorAll('input[required]')];
            for(let i = 0, l = requiredDoms.length; i < l; i++) {
                const input = requiredDoms[i];
                if (!input.value) {
                    input.focus();
                    dealValue.call(input);
                    return;
                }
            }

            // 检查是否有错误
            const errDoms = [...document.querySelectorAll('.error')];
            for(let i = 0, l = errDoms.length; i < l; i++) {
                const error = errDoms[i];
                if (error.innerText) {
                    error.previousElementSibling.focus();
                    return;
                }
            }
            // 防止用户重复提提交
            submit.removeEventListener('click', submitClick);

            // 获取用户所有输入的信息
            let data = {};
            const inputs = [...document.querySelectorAll('input')];
            inputs.reduce((pre, input) => {
                pre[input.id] = input.value || '';
                return pre;
            }, data);
            modifyAction(data);
        }
        function modifyAction(data) {
            
            let xhr = new XMLHttpRequest();
            let url = '这儿是请求的地址'
            xhr.open(url);
            xhr.onreadystatechange = function(data) {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    let res = xhr.responseText;
                    if (res.success) {
                        clearInputsValue();
                        const infoDom = document.getElementById('submit-info');
                        infoDom.innerText = '信息修改成功';

                        // 恢复提交功能
                        const submit = document.getElementById('submit');
                        submit.addEventListener('click', submitClick);

                        setTimeout(() => {
                            infoDom.innerText = '';
                        }, 2000)
                    }
                    else {
                        // 没有修改成功的处理逻辑
                    }
                }
            }
            xhr.send(data);

        };
        function clearInputsValue() {
            const inputs = [...document.querySelectorAll('input')];
            inputs.forEach((input) => {
                input.value = ''
            });
        }
    </script>
</body>
</html>