如果想直接看如何实现的,可以直接阅读最后一部分。
设计思路
- 样式根据设计稿来,不使用表单的提交(便于做校验和拦截,和错误提示的一致性)
- 利用js校验相关键点
- 用户名、密码、确认密码 三项为必填,提交的时候需要检验
- 确认密码 和 密码的输入在提交前需要校验是否一致
- 用户如果输入了后面几项,需要校验输入的值是否合法
- 校验的时机可以在用户输入完成后(防抖)
- js中需要保存每项数据校验不合格时的错误提示
- 用户点击提交的时候,校验必填选项是否已经填入;填入的信息是否都合法
效果展示
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.error
和div.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';
}
}
校验密码
【我觉得在校验数据,密码校验应该是逻辑最复杂的】
校验密码主要有两块:
- 密码和确认密码都不能为空
- 当用户输入的密码发生变化时,需要校验确认密码是否和密码保持一致
// 校验密码
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>