This article describes how AngularJS achieves an infinitely-level linkage menu. For more information, see the multi-level linkage menu, which is a common front-end component, for example, province-city linkage, university-college-professional linkage, etc. Although the scenario is common, it is not as simple as you think to make a universal unlimited hierarchical linkage menu through careful analysis. For example, do we need to consider whether sub-menu loading is synchronous or asynchronous? Does the backfilling of initial values occur at the frontend or backend? Is there a strict definition of the return format of the backend API for asynchronous loading? Is synchronous and asynchronous coexistence easy? Can I flexibly support various dependencies? Is there a null value option in the menu ?...... A series of problems need to be carefully handled.
I searched around with these requirements, not surprisingly, and did not find a suitable plug-in or command in the AngularJS ecosystem. So I tried to implement one myself.
The implementation of this article is based on AngularJS, but the idea is common. You can also read it if you are familiar with other framework class libraries.
First, I reorganized the requirements. Since AngularJS rendering occurs on the front end, it was not very suitable to get the options of menus at all levels at the backend based on the existing values and perform rendering at the template layer, I personally do not like this implementation method, as many people do: many times, even if the backend completes the first pull of the option and the backfilling of the initial value, however, because the loading of sub-menus depends on APIs, the front-end also needs to listen to onchange events and perform ajax interaction. In other words, a simple second-level linkage menu actually needs to tear the logic between the front and back-end, this method is not worthy of praise.
For synchronous and asynchronous loading methods, although the entire process is asynchronous most of the time, you can also pull all data from an api for the linkage menu with few options, processed and cached for sub-menu rendering. Therefore, synchronous and asynchronous rendering methods should be supported.
As for the api return format, if a new project is in progress, or the backend programmer can quickly respond to demand changes, or the front-end students are full stacks, this issue may not be so important. However, in many cases, our interactive APIs have been used by other parts of the project for compatibility and stability considerations, adjusting the json format is not an easy decision. Therefore, in this article, the acquisition of sub-menu option data will be decoupled from directive, which is processed by the specific business logic.
How can we support flexible dependencies? In addition to the most common linear dependency, tree dependency, inverted pyramid dependency, and even complex mesh dependency should be supported. Due to the existence of these business scenarios, hard coding of dependencies into logic is more complicated. After consideration, components are communicated through events.
The requirements are as follows:
* Supports Initial Value backfilling at the front end.
* Synchronous and asynchronous acquisition of subset menu options
* Supports flexible dependencies between single food Rooms (such as linear dependencies, tree dependencies, inverted pyramid dependencies, and mesh dependencies)
* Option [value = ""]
* The acquisition logic of the subset menu is decoupled from the component itself.
* Event-driven, menus at all levels are logically independent from each other
The multi-level linkage menu is highly invasive to the original behavior of select tags in AngularJS. To facilitate subsequent programming and reduce potential conflicts, this article will use{Item. text }}Rather than ngOptions.
1. First, let's think about the first question: how to backfill the initial values on the front end
The most obvious feature of a multi-level linkage menu is that after the previous menu is changed, the next menu will be re-rendered (synchronously or asynchronously. In the process of backfilling values, we need to backfill them step by step. This process cannot be completed instantly during page loading (or route loading or component loading. Especially in AngularJS, the option rendering process should occur before ngModel rendering. Otherwise, even if the option has a corresponding value, the matching option cannot be found.
The solution is to first save the initial value of the model and assign it to a null value (you can call $ setViewValue) in the link stage of the command ), after the rendering is complete, it is asynchronously assigned back to the original value.
2. How to decouple the specific logic obtained by sub-options and support synchronous and asynchronous Methods
You can use the "=" Class Attribute in scope to expose an external function to the link Method of directive. After this method is executed each time, determine whether it is a promise instance (or whether there is a then method) and determine synchronous or asynchronous rendering based on the judgment results. With this decoupling, you can easily determine the rendering method in the input external functions. To make the callback function not so ugly, we can also encapsulate synchronous return as an object with the then method. As follows:
// Scope. source is the external function 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) {// unified processing of synchronously or asynchronously returned data}
3. How to Implement event-based communication in a single food room
In general, it is implemented through the subscriber mode and dependency needs to be declared on directive. To support complex dependencies, a subset menu should support multiple dependencies at the same time. In this way, we can listen to any menu that is dependent on it in the following way:
Scope. $ on ('selectupdate', function (e, data) {// data. name is the changed menu, and dependents is the dependent array declared by the current menu if ($. inArray (data. name, dependents) >=0) {onParentChange () ;}}); // to facilitate the call of the source function to change the value, you can traverse the dependent menus and save the current value var values ={}; if (dependents) {$. each (dependents, function (index, dependent) {values [dependent] = selects [dependent]. getValue ();});}
4. Deal with two kinds of expiration Problems
It is easy to think of the asynchronous expiration problem: imagine that the first-level menu changes, triggering the pull of the second-level menu content, but the network speed is slow, this process takes 3 seconds. After 1 second, the user changes the level 1 menu again, and then triggers the pull of level 2 menu content again. At this time, the network speed is fast. After 1 second, the data is returned and the Level 2 menu is re-rendered; however, one second later, the result of the first request is returned, and the second-level menu is rendered again. However, the first-level menu has changed since then and the content has expired. This rendering is incorrect. We can use closures for data expiration verification.
It is not easy to think of synchronization expiration (in fact, it is also asynchronous, but without io interaction, it is a timeout function with a buffer time of 0), that is, because of the existence of the event queue, if you are not careful about it, it may expire, and there will be comments in the code.
5. Details about support for null values
The support for null values is a simple problem,{Empty }}But in actual encoding, it is found that in the link of directive, because the link process of this option is not started, the option tag is actually removed, leaving only the comments placeholder. AngularJS considers that the select statement does not include a null value, so an error is returned. The solution is to discard ng-if and use ng-show. The relationship between the two is extremely subtle and interesting. If you are interested, you can study it on your own ~
The above are the main problems encountered during the encoding process. Please contact us ~
Directive ('multilevelselect', ['$ parse',' $ timeout', function ($ parse, $ timeout) {// use the closure, saves all the multi-level linkage menus in the parent scope to facilitate the value of var selects ={}; return {restrict: 'CA', scope: {// specifies the parent Tag name: '@ name' when the dependency declaration is used. // The dependency array is separated by commas (,):' @ dependents ', // a function that provides the specific option value is called when the parent level is changed. synchronous/asynchronous return results are allowed. // whether synchronous or asynchronous, the data should be [{text: 'text', value: 'value'},] structure source: '= source', // whether control options are supported. If yes, what is the label of null values? empty: '@ empty', // used for parse parsing to obtain the model value (instead of the viewValue value) modelName:' @ ngModel '}, template: ''// use ng-show instead of ng-if. The reason is as mentioned above +'{Empty }}'// Use the simple ng-repeat +'{Item. text }}', 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); // encapsulate the getValue function of the current menu and put it in the selects object in the closure to conveniently call selects [scope. name] = {getValue: function () {return $ parse (scope. modelName) (parentScope) ;}}; // Save the initial value because var initValue = selects [scope. name]. getValue (); var inited =! InitValue; model. $ setViewValue (''); // The callback function onParentChange () {var values ={} called when the parent tag changes {}; // obtain the current value of all dependent menus if (dependents) {$. each (dependents, function (index, dependent) {values [dependent] = selects [dependent]. getValue () ;}) ;}// use the closure to determine the asynchronous expiration caused by io (function (thenValues) {// call the source function and obtain the new option data var returned = scope. source? Scope. source (values): false; // wrap the synchronization result as an object with the then method using multi-layer closures! Returned | (returned = returned. then? Returned: {then: (function (data) {return function (callback) {callback. call (window, data) ;};} (returned )}). then (function (items) {// prevent expiration caused by Asynchronization for (var name in thenValues) {if (thenValues [name]! = Selects [name]. getValue () {return ;}} scope. items = items; $ timeout (function () {// prevents expiration if (scope. items! = Items) return; // if there is a null value, select null. Otherwise, select the first option if (scope. empty) {model. $ setViewValue ('');} else {model. $ setViewValue (scope. items [0]. value) ;}// determine whether the condition for restoring the initial value is mature var initValueIncluded =! Inited & (function () {for (var I = 0; I <scope. items. length; I ++) {if (scope. items [I]. value === initValue) {return true ;}} return false ;}) (); // restore the initial value if (initValueIncluded) {inited = true; model. $ setViewValue (initValue);} model. $ render () ;}) ;}( values) ;}// whether dependency exists. If no dependency exists, onParentChange is directly triggered to restore the initial value! Dependents? OnParentChange (): scope. $ on ('selectupdate', function (e, data) {if ($. inArray (data. name, dependents) >=0) {onParentChange () ;}}); // listens to the current value and broadcasts parentScope when it changes. $ watch (scope. modelName, function (newValue, oldValue) {if (newValue | ''! = OldValue | '') {scope. $ root. $ broadcast ('selectupdate', {// broadcast the name attribute of the changed menu to facilitate identification of name: scope based on its menu. name}) ;}}}};}}]);