HTML新玩法:用原生Dialog和Popover手写一个会记住状态的模态框

2026-09-02 0 873

前阵子接了个企业内部工具的小需求,要做一个“用户反馈”弹层,里面有输入框、评分星星、提交按钮。第一反应是找组件库,但转念一想,这个项目是一个纯静态页面,不想引进任何第三方依赖。后来发现,现在HTML已经原生支持模态框了,而且还能用Popover API做辅助气泡。今天就把我实际改造过程写下来,完全基于原生HTML和少量JavaScript。

先说说原生模态框 之 前有多难用

以前你想在页面上搞一个模态框,要么自己用div加遮罩模拟,要么引Bootstrap或者Element。遮罩层要处理层级、点击事件、键盘Esc关闭。还要考虑body滚动穿透。现在<dialog>元素直接把这些行为全部收敛了。Especially最重要的是,它自带语义和快捷键,屏幕阅读器也能识别。

打开新大门的Dialog元素

原生Dialog用起来特别简单,一个<dialog>标签就是模态框容器。它有两个方法:

dialog.show();          // 非模态,显示浮层,不影响页面其他内容
dialog.showModal();     // 模态,显示浮层且带一层遮罩,其他区域不可交互

关闭也有两种方式:

dialog.close();                     // 直接关闭
// 或者用form的method="dialog"提交,会自动关闭并返回form里的数据

这个form close非常有意思,因为它是HTML原生自带的行为,不需要额外写JS。

看个最简单的例子:

<dialog id="demo">
  <p>看,原生的!</p>
  <form method="dialog">
    <button>关闭</button>
  </form>
</dialog>

<button id="openDemo">打开对话框</button>

<script>
  const dialog = document.getElementById('demo');
  document.getElementById('openDemo').onclick = () => dialog.showModal();
</script>

这个框一弹出,后边页面全被遮住了。按Esc也能关闭,不用你监听keyup。这个先记住。

Popover API:轻轻一点出来个小气泡

Popover是另一项拿来即用的API,特别适合做下拉气泡、tips菜单。它的原理就是把一个普通元素变成浮层,然后绑定给某个按钮。

<button popovertarget="tip">悬停我</button>
<div id="tip" popover>这是一个气泡提示</div>

不用写一行JS,点一下按钮就出来,再点一下按钮就消失。这就是声明式UI的魅力。

Popover和Dialog有什么区别?区别在于Popover没有遮罩,并且默认可以用light-dismiss,也就是点击页面上其他区域它会自动关闭。而Dialog除了showModal模式外,也可以用非模态方式显示,但不会自动关闭。我一般这么用:Dialog做正式提交的弹窗,Popover做辅助提示。

实战案例:一个会记住上次输入的用户反馈弹窗

接着说我那实际需求:用户点一个按钮,弹出一个表单,里面有名字、内容、星级。提交后模拟发送数据,然后显示一个“发送成功”的气泡。最骚的是,用户关闭后再次打开,上一次填写的内容还在里面。怎么实现的呢?我用了localStorage保存数据,然后在打开对话框的时候自动回填。整个代码没几行。

第一步:建立dialog结构

<dialog id="feedbackDialog">
  <form id="feedbackForm" method="dialog">
    <h2>用户反馈</h2>
    <label>你的称呼:
      <input type="text" name="username" required>
    </label>
    <label>反馈内容:
      <textarea name="content" required></textarea>
    </label>
    <div>
      <span>总体评价:</span>
      <label><input type="radio" name="level" value="好"> 好</label>
      <label><input type="radio" name="level" value="一般"> 一般</label>
      <label><input type="radio" name="level" value="差"> 差</label>
    </div>
    <menu>
      <button value="cancel" formnovalidate>取消</button>
      <button value="send">发送</button>
    </menu>
  </form>
</dialog>

我用了一个form带method=”dialog”,这样点击按钮就会自动关闭对话框,而且不用处理表单submit事件。关键是这里有两个按钮,一个取消一个发送。取消按钮加了formnovalidate,这样就不会触发必填校验。发送按钮没有加,所以会按照HTML5校验规则强制检查必填。

第二步:用Popover做“发送成功”的提示

<button id="showTip" popovertarget="successTip" style="display:none">打开提示</button>
<div id="successTip" popover>发送成功啦!感谢你的反馈。</div>

为什么我藏了一个button在旁边?因为我想在JS里调用popover的toggle方法,这样就不需要手动模拟点击隐藏按钮。不过实际上用popovertarget绑定一个元素,也可以用popover.toggle()。看后面代码:

第三步:写业务逻辑JS

const dialog = document.getElementById('feedbackDialog');
const form = document.getElementById('feedbackForm');
const successTip = document.getElementById('successTip');

function saveDraft() {
  const data = new FormData(form);
  const draft = {
    username: data.get('username') || '',
    content: data.get('content') || '',
    level: data.get('level') || ''
  };
  localStorage.setItem('feedback_draft', JSON.stringify(draft));
}

function restoreDraft() {
  const stored = localStorage.getItem('feedback_draft');
  if (!stored) return;
  try {
    const draft = JSON.parse(stored);
    if (draft.username) form.elements.username.value = draft.username;
    if (draft.content) form.elements.content.value = draft.content;
    if (draft.level) {
      const radio = form.querySelector(`input[name="level"][value="${draft.level}"]`);
      if (radio) radio.checked = true;
    }
  } catch (e) {}
}

// 打开对话框前恢复草稿
document.getElementById('openBtn').onclick = () => {
  restoreDraft();
  dialog.showModal();
};

// 点击取消按钮时,仍然保存当前填写内容(不校验)
form.addEventListener('click', e => {
  if (e.target.value === 'cancel') {
    saveDraft();
    dialog.close('cancel');
  }
});

// 点击发送,并且通过校验后(submit事件会在close之前触发)
form.addEventListener('submit', e => {
  // 因为form method=dialog,这里submit事件发生后表单会自己close
  saveDraft();
  // 清空草稿,因为提交了
  localStorage.removeItem('feedback_draft');
  // 显示popover提示
  successTip.showPopover();
  setTimeout(() => successTip.hidePopover(), 2000);
});

这里有个细节:当form method=”dialog”且表单验证通过后,浏览器会先执行submit事件,然后自动close。在submit事件里我们saveDraft又removeItem——虽说先存再删只是多此一举,但这个动作保证了不管用户是取消还是提交,都会先把当前内容保存一份。但提交以后就清除了,所以下次打开不会有旧数据。如果用户点取消,我们明确保存草稿,下次打开就回填。妙啊。

第四步:防止点了遮罩直接关掉后丢草稿

dialog.showModal()打开的对话框,点击遮罩层默认不会关闭,这倒是省心。但如果用户按Esc键关闭呢?也必须在cancel或close事件里保存草稿。我处理一下:

dialog.addEventListener('cancel', e => {
  saveDraft();
  // 不会阻止默认,让对话框关闭
});

dialog.addEventListener('close', e => {
  if (dialog.returnValue !== 'send') {
    // 如果不是通过发送按钮关闭的,就说明是取消或者Esc,保留草稿
  }
});

其实上面form按钮点击,dialog的close事件也会触发。但因为我设定了returnValue,只有发送按钮的value=”send”才是发送,取消按钮value=”cancel”。所以在close里判断returnValue就好。但考虑到表单提交事件里我们已经把localStorage删除了,所以逻辑上不会冲突。

加一点Web Components的味道

这个项目里我还顺手把整个弹窗封装成了一个自定义元素,这样以后其他页面直接写<user-feedback>就完了。核心思路就是用<template>承装内部HTML,然后在connectedCallback里attachShadow。因为shadow DOM可以起到样式隔离作用,虽然我这里因为特殊原因没有写样式,但你们在实际项目里不用怕样式冲突。

class UserFeedback extends HTMLElement {
  connectedCallback() {
    const template = document.getElementById('feedback-template');
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.appendChild(template.content.cloneNode(true));
    // 把之前的dialog逻辑搬进shadow根作用域
  }
}
customElements.define('user-feedback', UserFeedback);

这样组装的好处是:业务逻辑全都内聚,页面代码干干净净。用到的地方就一行标签,跟原生HTML元素一样。

踩到的一些奇奇怪怪的小坑

虽然原生API很方便,但还是有几个坑值得说一下:

1. form method=dialog配合required校验。如果你在form里放多个button,不加formnovalidate的那个按钮触发提交时,浏览器会先做校验。但有些Android WebView对required校验支持不太好,建议在关键表单上用简易JS校验兜底。

2. popover与dialog叠加层级。popover元素默认层级很高,但若dialog里也用了popover,某些浏览器会出现在dialog后面。解决办法是给dialog设置更高的z-index(但这里我们不用样式,就别管了)。

3. localStorage在隐私模式下可能会写入失败。比如Safari无痕模式,你调setItem直接抛异常。所以我前面的代码里应该包一层try catch,不然会阻断表单提交逻辑。实战中我确实被这个坑过,改成了try catch。

4. 自定义元素和原生dialog组合时,click事件要小心。Shadow DOM里的节点事件在冒泡到外部时会经过retarget处理,你判断event.target时可能会拿到自定义元素本身。解决方法是在shadow dom内部绑定事件时,先确保element就是期望的目标。

最终效果和后续优化

整个功能完成以后,代码量比预想少了至少一半。同事看到以后很吃惊,说原来现在原生就能做这种事,都不用引包了。我也觉得挺爽的。

如果你也想试试,建议就在一个简单的内部页面上用一下,感受下现代HTML带来的清爽。当然这个方案也是有适用边界的,如果你需要非常复杂的动画或者跨域iframe嵌入,那还是老实用组件库更稳。

以后如果再有人问我页面弹窗怎么做,我直接甩给他这篇。最后,祝你的页面也被原生Dialog解放一次。

HTML新玩法:用原生Dialog和Popover手写一个会记住状态的模态框
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 html HTML新玩法:用原生Dialog和Popover手写一个会记住状态的模态框 https://www.taomawang.com/web/html/2695.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务