Home > Web Front-end > JS Tutorial > Detailed example of AngularJS implementing infinite linkage menu_AngularJS

Detailed example of AngularJS implementing infinite linkage menu_AngularJS

WBOY
Release: 2016-05-16 15:19:46
Original
1243 people have browsed it

Multi-level linkage menus are common front-end components, such as province-city linkage, university-college-major linkage, etc. Although the scenarios are common, upon careful analysis, implementing a universal infinite hierarchical linkage menu may not be as simple as imagined. For example, we need to consider whether the submenu is loaded synchronously or asynchronously? Does backfilling of initial values ​​occur on the front end or on the back end? If loaded asynchronously, is there a strict definition of the return format of the backend API? Is it easy to achieve synchronous and asynchronous coexistence? Can it flexibly support various dependencies? Is there a null value option in the menu? …A series of issues need to be dealt with carefully.

After searching around with these requirements, not surprisingly, I couldn’t find a very suitable plug-in or instruction in the AngularJS ecosystem. So I had to try to implement one myself.

The implementation of this article is based on AngularJS, but the ideas are general and students who are familiar with other framework libraries can also read it.

First of all, I reorganized the requirements. Since the rendering of AngularJS occurs on the front end, the previous solution of obtaining the options of menus at all levels based on the existing values ​​in the back end and rendering them in the template layer was not very suitable, and like many students, , I personally don’t like this implementation: many times, even if the first pull of the option and the backfill of the initial value are completed on the backend, because the loading of the sub-menu depends on the API, the frontend also needs to listen onchange event and perform ajax interaction. In other words, a simple secondary linkage menu needs to separate the logic between the front and back ends. This method is not worthy of praise.

Regarding the synchronous and asynchronous loading methods, although most of the time the entire step is asynchronous, for some linkage menus with few options, an API can also pull all the data, process it, cache it and provide it to the sub-menu Used for rendering. Therefore, both synchronous and asynchronous rendering methods should be supported.

As for the issue of API return format, if you are working on a new project, or the back-end programmers can quickly respond to demand changes, or the front-end students themselves are full-stack, this issue may not be so important; but many times, The API we interact with has been used by other parts of the project. For the sake of compatibility and stability, adjusting the format of json is not an easy decision; therefore, in this article, the acquisition of sub-menu option data is It will be decoupled from the directive itself and processed by specific business logic.

How to implement support for flexible dependencies? In addition to the most common linear dependencies, tree dependencies, inverted pyramid dependencies and even complex network dependencies should also be supported. Due to the existence of these business scenarios, hardcoding dependencies into the logic is complex. After trade-offs, components communicate through events.

The requirements are summarized as follows:

* Support initial value backfilling on the front end
* Support synchronous and asynchronous acquisition of subset menu options
* Support flexible dependencies between menus (such as linear dependency, tree dependency, inverted pyramid dependency, mesh dependency)
* Support menu empty value option (option[value=""])
* The acquisition logic of the subset menu is decoupled from the component itself
* Event-driven, menus at all levels are logically independent of each other and do not affect each other

Since the multi-level linkage menu is more intrusive to the original behavior of the select tag in AngularJS, in order to facilitate subsequent programming and reduce potential conflicts, this article will use

1. First, let’s think about the first question, how to backfill the initial value on the front end

The most obvious feature of the multi-level linkage menu is that after the upper-level menu is changed, the lower-level menu will be re-rendered (synchronously or asynchronously). In the process of backfilling values, we need to backfill step by step, and this process cannot be completed instantly when the page is loaded (or route loaded, component loaded, etc.). Especially in AngularJS, the rendering process of option should occur before the rendering of ngModel. Otherwise, even if there is a corresponding value in option, the matching option will not be found.
The solution is to first save the initial value of the model in the link phase of the instruction, assign it a null value (you can call $setViewValue), and then asynchronously assign it back to the original value after rendering is completed.

2. How to decouple the specific logic of sub-option acquisition and support both synchronous and asynchronous methods

You can use the "=" class attribute in the scope to expose an external function to the link method of the directive. Each time after executing this method, it is judged whether it is a promise instance (or whether it has a then method), and synchronous or asynchronous rendering is decided based on the judgment result. Through such decoupling, users can easily decide the rendering method in the external function passed in. In order to make the callback function less ugly, we can also encapsulate the synchronous return as an object with a then method. As shown below:

// 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) {
// 对同步或异步返回的数据进行统一处理
}
Copy after login

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();
});
}
Copy after login

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 &#63; 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 &#63; scope.source(values) : false;
// 利用多层闭包,将同步结果包装为有then方法的对象
!returned || (returned = returned.then &#63; 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 &#63; 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
});
}
});
}
};
}]);
Copy after login

source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template