Multi-level Linkage menu is a common front-end component, such as province-city linkage, university-college-professional linkage and so on. Although the scenario is common, it is not necessarily as simple as it seems to be to implement a generic, infinitely graded linkage menu. For example, do we need to consider whether the loading of submenus is synchronous or asynchronous? Does the backfill for the initial value occur at the front or back end? If asynchronous loading, is there a strict definition of the return format for the backend API? Is it easy to synchronize, asynchronous coexistence? Is it flexible to support a variety of dependencies? Is there a null value option in the menu? ...... A whole range of problems needs to be dealt with carefully.
With these needs to search a lap, not too unexpected, and did not find a angularjs in the ecology of a very suitable plug-ins or instructions. So I had to try to achieve one.
The implementation of this article is based on Angularjs, but the idea is universal, and students who are familiar with other framework libraries can also read.
First of all to comb the demand, because the ANGULARJS rendering occurs in the front-end, previously at the back-end according to the value of the menu options and at the template layer to render the scheme is not very appropriate, and, like many students, I personally do not like this way to achieve: many times, Even after the first pull of option options and backfill of initial values are completed on the back end, but since the loading of the child menu depends on the API, the front-end also needs to listen for onchange events and Ajax interaction, in other words, a simple two-level linkage menu actually needs to tear the logic to the front and back end, Such a way is not worthy of praise.
About synchronous, asynchronous loading mode, although most of the time the entire step is asynchronous, but for some of the options are not many linkage menu, can also be an API pull all the data, processing, caching for child menu rendering use. Therefore, synchronous, asynchronous rendering should be supported.
As for the problem of API return format, if a new project is in progress, or a backend programmer can respond quickly to requirements changes, or the front-end students themselves are the whole stack, the problem may be less important, but many times the API we interact with is already used by other parts of the project for compatibility, Stability considerations, adjusting the JSON format is not a decision that can be easily made; Therefore, in this article, the acquisition of option data for the child menu will be decoupled from the directive itself and handled by the specific business logic.
How do you achieve support for flexible dependencies? In addition to the most common linear dependencies, tree dependencies, inverted pyramid dependencies, and even complex mesh dependencies should be supported. Because of the existence of these business scenarios, it is more complex to hard-code dependencies. After a trade-off, the components communicate through events.
Requirements are collated as follows:
* Support completes initial value backfill on front end
* Synchronous, asynchronous access to subset menu options supported
* Support flexible dependencies between menus (e.g. linear dependencies, tree dependencies, inverted pyramid dependencies, mesh dependencies)
* Support menu Null value option (option[value= ""])
* The acquisition logic of the subset menu is decoupled from the component itself
* Event-driven, menu at all levels logically independent of each other
Because the multilevel Linkage menu for ANGULARJS the original behavior of the Select tag intrusive, in order to facilitate programming, reduce potential conflicts, this article will use <option ng-repeat= "item in the items" value= "{{ Item.value}} ">{{item.text}}</optoin> 's simple way, not ngoptions.
1. First, consider the first question, how to backfill the initial value in front
The most obvious feature of the multilevel linkage menu is that the next level of menu changes will be rendered (synchronously or asynchronously). In the process of backfill values, we need to backfill progressively, and we cannot complete the process instantaneously when the page is loaded (or when a route load or component is loaded, and so on). Especially in Angularjs, the option's rendering process should occur before Ngmodel rendering, otherwise the matching option will not be found if there is a corresponding value in option.
The solution is in the link phase of the instruction, first saving the initial value of model, assigning it to a null value (which can call $setviewvalue), and then asynchronously assigning it back to its original value after rendering is complete.
2. How to decouple the specific logic of the child option, and simultaneously support synchronous, asynchronous way
You can use the "=" class property in scope to expose an external function to the directive link method. Each time the method is executed, it is judged whether it is a promise instance (or whether there is a then method), which determines either synchronous or asynchronous rendering based on the result of the decision. With such decoupling, the user can easily determine the rendering method in the incoming external function. To make the callback function less ugly, we can also encapsulate the synchronous return with an object with the then method. As shown below:
Scope.source is an external function
var returned = Scope.source? Scope.source (values): false;
Returned | | (returned = Returned.then? returned: {
then: (function (data) {return
function (callback) {
Callback.call (w Indow, data);
}
(returned)
}). Then (function (items) {
//For unified processing of data returned synchronously or asynchronously
}
3. How to implement event-based communication between menus
In general, it is implemented through subscriber mode, which needs to be declared dependent on the directive, and because of the need to support complex dependencies, a subset menu should be supported with multiple dependencies. In this way, when any one of the dependent menus changes, we can listen to the following:
Scope. $on (' Selectupdate ', function (e, data) {
//Data.name is a changed menu, dependents is the dependent array declared by the current menu (
$.inarray ( Data.name, dependents) >= 0) {
onparentchange ();
}
});
and in order to facilitate the invocation of the source function mentioned above for the variable value, you can iterate over the menu you are relying on and save the current value
var values = {};
if (dependents) {
$.each (dependents, function (index, dependent) {
values[dependent] = selects[dependent]. GetValue ();
});
4. Dealing with two types of expiration issues
Easy to think of is the problem of asynchronous expiration: Imagine the first level of the menu changes, triggering the second level of the menu content pull, but the slow speed, the process takes 3 seconds. After 1 seconds, the user changes the first level menu again, triggering the pull of the content of the second level menu, at this time the speed is faster, 1 seconds after the data returned, the second level menu to render, but 1 seconds later, the first request results returned, the second level menu is again rendered, but in fact, the first level of the menu has been changed since the content has expired, This rendering is wrong. We can use closures for data expiration checks.
It is not easy to think of the synchronization expiration (in fact, is asynchronous, but not IO interaction, are buffer time of 0 timeout function) problem, that because of the existence of event queues, a little cautious may appear expired, the code will have relevant comments.
5. Support for the details of the null value option
Support for null values was thought to be a very simple problem, <option value= "ng-if=" Empty ">{{empty}}</option> can, but in the actual code found in the directive link, Since the link process for this option has not started, the option tag is actually removed, leaving only the relevant comment placeholder. Angularjs that the select does not contain a null value option and then an error. The solution is to discard ng-if and use Ng-show. The relationship between the two is extremely delicate and interesting, interested students can study their own ~
The above is the coding process of the main problems encountered, Welcome to Exchange ~
directive('multiLevelSelect', ['$parse', '$timeout', function ($parse, $timeout) {
//With closures, all multi-level linkage menus in the parent scope are saved for easy value taking
var selects = {};
Return {
restrict: 'CA',
Scope: {
//Specify parent label when used for dependency declaration
name: '@name',
//Dependent array, comma split
dependents: '@dependents',
//The function that provides the specific option value is called when the parent changes, allowing synchronous / asynchronous return of results
//Whether synchronous or asynchronous, the data should be the structure of [{text: 'text', value: 'value'},]
source: '=source',
//Whether the control option is supported, and if so, what is the label of the null value
empty: '@empty',
//Get model value (not viewvalue value) for parse
modelName: '@ngModel'
}
Template: ''
//Use ng show instead of NG if for the reasons mentioned above
+ '<option ng-show="empty" value="">{{empty}}</option>'
//Using simple 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);
//Encapsulate the getValue function of the current menu and put it in the selections object in the closure for easy calling
selects[scope.name] = {
getValue: function () {
return $parse(scope.modelName)(parentScope);
}
}
//Save the initial value for the reason mentioned above
var initValue = selects[scope.name].getValue();
var inited = !initValue;
model.$setViewValue('');
//Callback function called when parent tag changes
function onParentChange() {
var values = {};
//Get the current value of all dependent menus
if (dependents) {
$.each(dependents, function (index, dependent) {
values[dependent] = selects[dependent].getValue();
};
}
//Using closure to judge asynchronous expiration caused by IO
(function (thenValues) {
//Call the source function to get the new option data
var returned = scope.source ? scope.source(values) : false;
//Using multi-layer closure, the synchronization result is wrapped as an object with then method
!returned || (returned = returned.then ? returned : {
then: (function (data) {
return function (callback) {
callback.call(window, data);
}
} (returned)
}).then(function (items) {
//Prevent expiration due to asynchrony
for (var name in thenValues) {
if (thenValues[name] !== selects[name].getValue()) {
Return;
}
}
scope.items = items;
$timeout(function () {
//Prevent expiration caused by synchronization (strictly asynchronous, note event queues)
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);
}
//Judge whether the conditions to restore the initial value are 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 initial value
if (initValueIncluded) {
inited = true;
model.$setViewValue(initValue);
}
model.$render();
};
};
(values);
}
//Whether there is a dependency, if not, trigger onparentchange directly to restore the initial value
!dependents ? onParentChange() : scope.$on('selectUpdate', function (e, data) {
if ($.inArray(data.name, dependents) >= 0) {
onParentChange();
}
};
//Monitor the current value and broadcast it when it changes
parentScope.$watch(scope.modelName, function (newValue, oldValue) {
if (newValue || '' !== oldValue || '') {
scope.$root.$broadcast('selectUpdate', {
//Broadcast the name attribute of the changed menu to facilitate the identification of the menu that depends on it
name: scope.name
};
}
};
}
}
);