多级联动菜单是常见的前端组件,比如省份-城市联动、高校-学院-专业联动等等。场景虽然常见,但仔细分析起来要实现一个通用的无限分级联动菜单却不一定像想象的那么简单。比如,我们需要考虑子菜单的加载是同步的还是异步的?对于初始值的回填发生在前端还是后端?如果异步加载,是否对于后端API的返回格式有严格的定义?是否容易实现同步、异步共存?是否可以灵活的支持各类依赖关系?菜单中是否有空值选项?……一系列的问题都需要精心处理。
带着这些需求搜索了一圈,不太出乎意料,并没有能在AngularJS的生态中找到一个很适合的插件或者指令。于是只好尝试自己实现了一个。
本文的实现基于AngularJS,但是思路通用,熟悉其他框架类库的同学也可以阅读。
首先重新梳理了一下需求,由于AngularJS的渲染发生在前端,以前在后端根据已有值获取各级菜单的option并在模板层进行渲染的方案并不是很适合,而且和很多同学一样,我个人并不喜欢这样实现方式:很多时候,即使在后端完成了第一次对option选项的拉取和对初始值的回填,但由于子级菜单的加载依赖于api,前端也需要监听onchange事件并进行ajax交互,换言之,一个简单的二级联动菜单竟然需要把逻辑撕裂在前、后端,这样的方式并不值得推崇。
关于同步、异步的加载方式,虽然大多数时候整个步骤是异步的,但是对于部分选项不多的联动菜单,也可以由一个api拉取所有数据,进行处理、缓存后供子级菜单渲染使用。因此同步、异步的渲染方式都应该支持。
至于api返回格式的问题,如果正在进行的是一个新的项目,或者后端程序员可以快速响应需求变动,或者前端同学本身就是全栈,这个问题可能不那么重要;但是很多时候,我们交互的api已经被项目的其他部分所使用,出于兼容性、稳定性的考虑,调整json的格式并非是一个可以轻松做出的决定;因此在本文中,对于子级菜单option数据的获取将从directive本身解耦出来,由具体业务逻辑处理。
那如何实现对灵活依赖关系的支持呢?除了最常见的线性依赖以外,也应支持树状依赖、倒金字塔依赖甚至复杂的网状依赖。由于这些业务场景的存在,将依赖关系硬编码到逻辑较为复杂。经过权衡,组件间将通过事件进行通信。
需求整理如下:
* 支持在前端完成初始值回填
* 支持子集菜单选项的同步、异步获取
* 支持菜单间灵活的依赖关系(比如线性依赖、树状依赖、倒金字塔依赖、网状依赖)
* 支持菜单空值选项(option[value=""])
* 子集菜单的获取逻辑从组件本身解耦
* 事件驱动,各级菜单在逻辑上相互独立互不影响
由于多级联动菜单对于AngularJS中select标签的原有行为侵入性较大,为了之后编程方便,减少潜在冲突,本文将采用
1. 首先来思考第一个问题,如何在前端进行初始值的回填
多级联动菜单最明显的特点是,上一级菜单更改后,下一级菜单会被(同步或异步地)重新渲染。在回填值的过程中,我们需要逐级回填,无法在页面加载时(或路由加载或组件加载等等)时瞬间完成该过程。尤其在AngularJS中,option的渲染过程应该发生在ngModel的渲染之前,否则即使option中有对应值,也会造成找不到匹配option的情况。
解决方案是在指令的link阶段,首先保存model的初始值,并将其赋为空值(可以调用$setViewValue),并在渲染完成后再异步地对其赋回原值。
2. 如何解耦子选项获取的具体逻辑,并同时支持同步、异步的方式
可以使用scope中的"="类属性,将一个外部函数暴露到directive的link方法中。每次在执行该方法后,判断其是否为promise实例(或是否有then方法),根据判断结果决定同步或异步渲染。通过这样的解耦,使用者就可以在传入的外部函数中轻松地决定渲染方式了。为了使回调函数不那么难看,我们还可以将同步返回也封装为一个带then方法的对象。如下所示:
// scope.source为外部函数 var returned = scope.source ? scope.source(values) : false; !returned || (returned = returned.then ? returned : { then: (function (data) { return function (callback) { callback.call(window, data); }; })(returned) }).then(function (items) { // 对同步或异步返回的数据进行统一处理 }
3. 如何实现菜单间基于事件的通信
大体上还是通过订阅者模式实现,需要在directive上声明依赖;由于需要支持复杂的依赖关系,应该支持一个子集菜单同时有多个依赖。这样在任何一个所依赖的菜单变化时,我们都可以通过如下方式进行监听:
scope.$on('selectUpdate', function (e, data) { // data.name是变化的菜单,dependents是当前菜单所声明的依赖数组 if ($.inArray(data.name, dependents) >= 0) { onParentChange(); } }); // 并且为了方便上文提到的source函数对于变动值的调用,可以对所依赖的菜单进行遍历并保存当前值 var values = {}; if (dependents) { $.each(dependents, function (index, dependent) { values[dependent] = selects[dependent].getValue(); }); }
4. 处理两类过期问题
容易想到的是异步过期的问题:设想第一级菜单发生变化,触发对第二级菜单内容的拉取,但网速较慢,该过程需要3秒。1秒后用户再次改变第一级菜单,再次触发对第二级菜单内容的拉取,此时网速较快,1秒后数据返回,第二级菜单重新渲染;但是1秒后,第一次请求的结果返回,第二级菜单再次被渲染,但事实上第一级菜单此后已经发生过变化,内容已经过期,此次渲染是错误的。我们可以用闭包进行数据过期校验。
不容易想到的是同步过期(其实也是异步,只是未经io交互,都是缓冲时间为0的timeout函数)的问题,即由于事件队列的存在,稍不谨慎就可能出现过期,代码中会有相关注释。
5. 支持空值选项的细节问题
对于空值的支持本来觉得是一个很简单的问题,即可,但实际编码中发现,在directive的link中,由于此option的link过程并未开始,option标签被实际上移除,只剩下相关注释占位。AngularJS认为该select不含有空值选项,于是报错。解决方案是弃用ng-if,使用ng-show。这二者的关系极其微妙有意思,有兴趣的同学可以自己研究~
以上就是编码过程中遇到的主要问题,欢迎交流~
directive('multiLevelSelect', ['$parse', '$timeout', function ($parse, $timeout) { // 利用闭包,保存父级scope中的所有多级联动菜单,便于取值 var selects = {}; return { restrict: 'CA', scope: { // 用于依赖声明时指定父级标签 name: '@name', // 依赖数组,逗号分割 dependents: '@dependents', // 提供具体option值的函数,在父级change时被调用,允许同步/异步的返回结果 // 无论同步还是异步,数据应该是[{text: 'text', value: 'value'},]的结构 source: '=source', // 是否支持控制选项,如果是,空值的标签是什么 empty: '@empty', // 用于parse解析获取model值(而非viewValue值) modelName: '@ngModel' }, template: '' // 使用ng-show而非ng-if,原因上文已经提到 + '<option ng-show="empty" value="">{{empty}}</option>' // 使用朴素的ng-repeat + '<option ng-repeat="item in items" value="{{item.value}}">{{item.text}}</option>', require: 'ngModel', link: function (scope, elem, attr, model) { var dependents = scope.dependents ? scope.dependents.split(',') : false; var parentScope = scope.$parent; scope.name = scope.name || 'multi-select-' + Math.floor(Math.random() * 900000 + 100000); // 将当前菜单的getValue函数封装起来,放在闭包中的selects对象中方便调用 selects[scope.name] = { getValue: function () { return $parse(scope.modelName)(parentScope); } }; // 保存初始值,原因上文已经提到 var initValue = selects[scope.name].getValue(); var inited = !initValue; model.$setViewValue(''); // 父级标签变化时被调用的回调函数 function onParentChange() { var values = {}; // 获取所有依赖的菜单的当前值 if (dependents) { $.each(dependents, function (index, dependent) { values[dependent] = selects[dependent].getValue(); }); } // 利用闭包判断io造成的异步过期 (function (thenValues) { // 调用source函数,取新的option数据 var returned = scope.source ? scope.source(values) : false; // 利用多层闭包,将同步结果包装为有then方法的对象 !returned || (returned = returned.then ? returned : { then: (function (data) { return function (callback) { callback.call(window, data); }; })(returned) }).then(function (items) { // 防止由异步造成的过期 for (var name in thenValues) { if (thenValues[name] !== selects[name].getValue()) { return; } } scope.items = items; $timeout(function () { // 防止由同步(严格的说也是异步,注意事件队列)造成的过期 if (scope.items !== items) return; // 如果有空值,选择空值,否则选择第一个选项 if (scope.empty) { model.$setViewValue(''); } else { model.$setViewValue(scope.items[0].value); } // 判断恢复初始值的条件是否成熟 var initValueIncluded = !inited && (function () { for (var i = 0; i < scope.items.length; i++) { if (scope.items[i].value === initValue) { return true; } } return false; })(); // 恢复初始值 if (initValueIncluded) { inited = true; model.$setViewValue(initValue); } model.$render(); }); }); })(values); } // 是否有依赖,如果没有,直接触发onParentChange以还原初始值 !dependents ? onParentChange() : scope.$on('selectUpdate', function (e, data) { if ($.inArray(data.name, dependents) >= 0) { onParentChange(); } }); // 对当前值进行监听,发生变化时对其进行广播 parentScope.$watch(scope.modelName, function (newValue, oldValue) { if (newValue || '' !== oldValue || '') { scope.$root.$broadcast('selectUpdate', { // 将变动的菜单的name属性广播出去,便于依赖于它的菜单进行识别 name: scope.name }); } }); } }; }]);

JavaScript字符串替换方法详解及常见问题解答 本文将探讨两种在JavaScript中替换字符串字符的方法:在JavaScript代码内部替换和在网页HTML内部替换。 在JavaScript代码内部替换字符串 最直接的方法是使用replace()方法: str = str.replace("find","replace"); 该方法仅替换第一个匹配项。要替换所有匹配项,需使用正则表达式并添加全局标志g: str = str.replace(/fi

因此,在这里,您准备好了解所有称为Ajax的东西。但是,到底是什么? AJAX一词是指用于创建动态,交互式Web内容的一系列宽松的技术。 Ajax一词,最初由Jesse J创造

10款趣味横生的jQuery游戏插件,让您的网站更具吸引力,提升用户粘性!虽然Flash仍然是开发休闲网页游戏的最佳软件,但jQuery也能创造出令人惊喜的效果,虽然无法与纯动作Flash游戏媲美,但在某些情况下,您也能在浏览器中获得意想不到的乐趣。 jQuery井字棋游戏 游戏编程的“Hello world”,现在有了jQuery版本。 源码 jQuery疯狂填词游戏 这是一个填空游戏,由于不知道单词的上下文,可能会产生一些古怪的结果。 源码 jQuery扫雷游戏

本教程演示了如何使用jQuery创建迷人的视差背景效果。 我们将构建一个带有分层图像的标题横幅,从而创造出令人惊叹的视觉深度。 更新的插件可与JQuery 1.6.4及更高版本一起使用。 下载

本文讨论了在浏览器中优化JavaScript性能的策略,重点是减少执行时间并最大程度地减少对页面负载速度的影响。

Matter.js是一个用JavaScript编写的2D刚体物理引擎。此库可以帮助您轻松地在浏览器中模拟2D物理。它提供了许多功能,例如创建刚体并为其分配质量、面积或密度等物理属性的能力。您还可以模拟不同类型的碰撞和力,例如重力摩擦力。 Matter.js支持所有主流浏览器。此外,它也适用于移动设备,因为它可以检测触摸并具有响应能力。所有这些功能都使其值得您投入时间学习如何使用该引擎,因为这样您就可以轻松创建基于物理的2D游戏或模拟。在本教程中,我将介绍此库的基础知识,包括其安装和用法,并提供一

本文演示了如何使用jQuery和ajax自动每5秒自动刷新DIV的内容。 该示例从RSS提要中获取并显示了最新的博客文章以及最后的刷新时间戳。 加载图像是选择


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

适用于 Eclipse 的 SAP NetWeaver 服务器适配器
将Eclipse与SAP NetWeaver应用服务器集成。

Dreamweaver Mac版
视觉化网页开发工具

SecLists
SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。

SublimeText3 Linux新版
SublimeText3 Linux最新版

EditPlus 中文破解版
体积小,语法高亮,不支持代码提示功能