Preview function

Without further ado, a preview:

Demand background

The company had a previous project developed by Vue that used ElementUi’s cascading selector control. No, the product dad began to make demon, oops good oh, I want to use it to my project (project to Js + Php as the architecture, the front and back end is not separated).

Daily battle:

“Can you do multilevel linkage?” “, “No!

“But ElementUi depends on Vue, the two project architectures are different, so this control has to be developed separately.”

“This requirement is very simple, I don’t care how to achieve it.”

“MMP”

Function point analysis

Since battle is still available, there is no choice but to write a pure Js plugin. Here are a few function points:

  1. There is no limit on hierarchy. Multiple or single options can be selected by configuring parameters.
  2. Select the parent level, and all children are selected

2.1. Select the child level. If all the children of the same level are selected, then the parent level is selected. 2.2. Select children. If any child of the same level is not selected, the parent level is half selected. 2.3. Unselect the children. If no children of the same level are selected, the parent is unselected. 3. The complete path of the selected data can be displayed in the command output parent – Parent – Child (you can customize the separator by configuring the configuration item) 4. Supports deleting a single selected data item. 5. Supports one-click deletion of all selected data items (you can select whether to enable it through the configuration item).

Train of thought to sort out

Here’s how Vue data binding works:

Each piece of data in the data, in a way of data hijacking, has a setter and a getter bound to it so that it's responsive and in conjunction with the observer pattern, each piece of data maintains an observer list, and as soon as it's used, it's added to the observer list. When this data changes, all observers are notified and new data is passed.Copy the code

Going back to the requirement, the parent changes and the child changes, and the child changes and the parent changes.

Combined with Vue data binding practice, consider whether we can learn from its ideas?

Each parent is the target object, manages a list of observers, and within that list is a list of children, and if the child has any children, the children then become the target object, manages a list of observers, and so on.

At the same time, the child, as the target object, should not only have its children in the observer list, but also its parent. When it changes, all internal observers are notified to update their data.

That is, in the observer or publish/subscribe model, the children should be both publishers and subscribers.

Taking this form into consideration, it is difficult to adopt this form. The whole control not only needs to maintain the cascading panel class, but also needs to add the publish and subscribe lists for each data at the same time, and the later maintenance cost is also large. pass~

In fact, no matter how the idea is, the central point remains the same, that is: change the original operation DOM, to operate data. Turn view-driven thinking into data-driven thinking. First, let’s see what the initial data looks like (in the same format as the data passed in to ElementUi) :

var tags = [
  {
    id: 1.label: 'central'.children: [{id: 5.label: 'the shanxi'.children: [{id: 6.label: 'in taiyuan'}]}]}, {id: 8.label: 'the northwest'.children: [{id: 9.label: 'the shaanxi'}}]]Copy the code

The data format is very simple. If you don’t consider cascading options, simply render the DOM, there is no logic at all, and recursively loop the data to create the DOM.

How do I know if the current data is selected? How do I manipulate the parent or child selected state?

If it is view-driven, directly render the above data recursively into the body. Click the parent level to operate all the children below it. Click the child level to obtain the sibling state. Using Jquery is much easier, just a series of siblings() parent(). But this is a departure from the original purpose of the control, and does not extend the function at all. So let’s look at how we can be driven by data.

First of all, if we look at the data that’s passed in, it’s not going to meet our requirements. We want every node to have an indication of whether it’s selected, its parent, its child, its current path, and so on. The preliminary plan is as follows:

{
  id: 1.checked: false.// Whether to select
  hasChildren: true.// Whether there are children
  indeterminate: false.label: 'central'./ / text
  level: 1.// Node level
  parent: null./ / parent
  path: [1].// The path of the current node ID
  pathLabels: ['central'].// The current node text path
  value: 1./ / the same ID
  children: [ / / child nodes
    {
      id: 2.checked: false.hasChildren: false.indeterminate: false.label: 'the shanxi'.level: 1.parent: null.path: [1.2].pathLabels: ['central'.'the shanxi'].value: 2}},Copy the code

So that’s easy to do. Again, create the DOM recursively, and if the current checked node is true, it will be checked. So how do you do event binding? How do I know which data node is being clicked on?

When binding to the DOM, set the UID of the checkbox corresponding to the current data (in multiple selection mode) to the ID of the current data by setAttribute, click to find the corresponding ID in the matched data, and set its CHECKED to! Can be checked. There are three specific cases:

1. Case 1: If the current node is a level 1 node recursively traversing all the children, set the child checked attribute 2. Case 2: If the current node is not a tier 1 node and has child nodes, there are two steps: 1. Recursive traversal of all child nodes, setting the child checked attribute (2) to find and at the same level of the current node, according to the checked attribute of the node at the same level, set the parent node to select the | | | half chosen | uncheck three. Case 3: if the current node is the last level of child nodes: find and at the same level of the current node, according to the checked attribute of the node at the same level, set the parent node to select the | | half selected | | deselectCopy the code

Design idea

Above, we said that the raw data is processed, and all subsequent operations are based on the processed data. Is it a bit of an adaptor pattern? Yes, you could, but not quite. In this example, two classes are created.

A class for the node CascaderNode, each level in the original data nodes, packaging innovation (similar to the adapter), at the same time, add the appropriate method (such as: the text path, id path, set the parent | | child state, etc.), that is, each level of nodes, is an instance of the node class.

One is the cascading panel class EoCascader, this class is also the final outward exposed interface class, the main role is to accept user parameters, call the above mentioned node class, node instance creation, according to the processed node DOM rendering, binding DOM events…

In this way, each node is functionally decoupled from the DOM. The DOM does not need to care about what the node is, and the node does not need to care about how the outside world uses its instance.

The difficulties in analysis

How does the child operate on the parent

If you noticed that after processing the raw data, each node has an Indeterminate property, which is used by the child to operate on the parent. The default value is False, that is, not selected. If the default value is true, that is, half selected.

First get the length of all children under the parent of the current node (the node that is currently clicked). Let’s say it is 3. Every method of the Array is used to know whether all children are selected.

At the same time, set a flag variable flag to record the number of children selected

If flag is the same as the number of children (assume 3 here), select all the parents

If flag is not 0 and the value is not equal to the number of children, it indicates that one or more children are not selected and the parent is half-selected. Set indeterminate to true.

Have you finished? No, what about the parent and the parent here? You can recursively set indeterminate for the parent. The code is as follows:

CascaderNode.prototype.onChildCheck = function onChildCheck(checked) {
  this.checked = checked
  var parent = this.parent
  // Get whether the sibling is selected
  var isChecked = parent.children.every(function (child) {
    return child.checked
  })
  this.setCheckState(this.parent, isChecked);
}

// A recursive function to set the parent state
CascaderNode.prototype.setCheckState = function setCheckState(parent, isChecked) {
  parent.checked = isChecked
  // Number of nodes at the same level
  var totalNum = parent.children.length;
  // Record the number of selected nodes at the same level
  var checkedNum = parent.children.reduce(function (c, p) {
    var num = p.checked ? 1 : p.indeterminate ? 0.5 : 0;
    return c + num;
  }, 0); parent.indeterminate = checkedNum ! == totalNum && checkedNum >0;
  // If the parent still has a parent, pass in the parent and recurse
  parent.parent && this.setCheckState(parent.parent, isChecked)
}
Copy the code

How is the cascading panel presented?

So let’s think about first level data how do we present it? Just to recall the data format, this is directly pseudocode:

varData = [{Level 1 data -1
    child: [Secondary data -1-1}, {level 1 data -2
    child: [Secondary data -2-1]}]Copy the code

Want to render cascading panels? Good to say:

function render(data) {
  / /... traverse
  return 
  ` < div class = "menu - wrap" > < li > class data - 1 < / li > < li > class data - 2 < / li > < / div > `
}
Copy the code

That’s fine for the first level, but have you thought about how to render the second level? Or you can keep recursing and iterating, and the code is horrible.

The rendering idea of ElementUi has to be explained here. It maintains a separate two-dimensional array of cascading panels for rendering. The specific idea is as follows:

Sets an array of menus that stores the node data to be rendered. In the initial case, it is a tier 1 node:

[[Level 1 data -1, level 1 data -2]]
// Remember that each data is processed by the * node class *, which contains the data of the child nodes
Copy the code

Render the level 1 panel through the render method. When you click on some data in the first-level panel, get the current data under the child node, push to the menus

[[Level 1 data -1, level 1 data -2], [Secondary data -1-1, secondary data -1-2]]
Copy the code

The advantage of this is that there is a dedicated data for rendering, and there is no need to recursively render the data processed by the node class. The node class only wraps the data and returns the node instance. Once again, the data and rendering layers are decoupled.

How is the selected data stored, deleted, and echoed?

Storage and Deletion

In this example, objects are stored. The selected node ID is used as the key and the text path is used as the value. When rendering the selected list, you simply iterate over the object.

How do you delete it? Just deleting the ID from the storage list is not enough. You also need to deal with the selected status of the current item in the cascading panel and the selected status of its parent. Just call the onChildCheck method written above.

The echo data

If the current page is the edit page, entering the page cascading panel should display the last submitted data

Note: The output consists of two parts, one is the list of selected data (and text path), the other is the cascading panel and its node selected status

When using this control, the developer can pass in the selected data from the last data submission, as follows:

var cascader = new eo_cascader(tags, {
    // Other parameters...
    // For non-edit pages, pass null for checkedValue
    // When editing, pass the last level ID in the checkedValue
    checkedValue: [4.7.10.11.21.31.33] | |null
})
Copy the code

Now that I have the ID of the last level node, it’s easy to do.

  1. Recursively find the child node corresponding to the current ID, obtain the text path of the child node, and store it in the data list to render the selected text;
  2. At the same time, obtain the data of all the parent nodes of the child node, set their selected state (including the parent of the parent, layer up, until the level 1 node), and form a newmenusArray for cascading panel rendering.

conclusion

Although this control is not as powerful as ElementUi intermediate connection selector, such as asynchronous access to data, data search and other functions, but basically meet the needs of the product father, and will continue to iterate.

With the completion of the entire control, the data drive is found to be so powerful. If you’re based on the DOM driver, manipulating the selected state in the cascading panel is enough for you. If when you complete a function, you can start from the data layer, based on the DOM data manipulation, will be another world. Attached control source code, if there are errors, welcome to put forward, timely correction.

😘 Finally, must say: fried sauce noodles, refueling!