Résoudre les sélections en cascade indésirables dans el-tree à partir des nœuds feuilles

Lorsqu’on utilise el-tree d’Element UI avec des cases à cocher pour représenter une hiérarchie de type province / ville / quartier, un nœud intermédiaire coché entraîne souvent la sélection automatique de tous ses descendants. L’API peut pourtant indiquer que seuls certains enfants sont actifs. Le problème vient du fait que el-tree propage l’état de sélection aux nœuds parents et enfants. Pour éviter ce comportement, il faut déterminer les clés à cocher uniquement à partir des nœuds feuilles.

Le principe est le suivant : au lieu de faire confiance à un champ selected ou tick présent à tous les niveaux, on parcourt l’arbre jusqu’aux feuilles et on ne conserve que les identifiants des feuilles cochées. Ces identifiants deviennent les default-checked-keys du composant, et les chemins correspondants sont utilisés pour l’affichage ou l’envoi.

Utilisation du composant ecnapsulé

<region-tree
  :items="treeData"
  enable-search
  show-checkbox
  plus-icon
  :node-props="nodeProps"
  :default-checked-keys="defaultKeys"
  @selection-change="onSelectionChange"
/>
nodeProps: {
  children: 'list',
  label: 'name',
  isLeaf: 'leaf'
}

La propriété isLeaf est importante, car elle aide el-tree à identifier correctement les feuilles. Si on remplit default-checked-keys avec des identifiants de nœuds intermédiaires, le composant cochera en cascade tous leurs descendants.

Extraction des clés à partir des feuilles

methods: {
  // Parcours récursif de l'arbre
  collectLeaves(source, keys, depth, prefix) {
    depth += 1;

    source.forEach((entry) => {
      if (depth <= 3) {
        if (depth === 1) {
          prefix = '';
          entry.regionPath = String(entry.id);
        } else {
          entry.regionPath = `${prefix},${entry.id}`;
        }
        entry.type = 'region';
      }

      // On ne garde que les feuilles du niveau 3
      if (entry.tick && depth === 3) {
        keys.push(String(entry.id));
        this.checkedPaths.push(entry.regionPath);
      }

      if (entry.list) {
        this.collectLeaves(entry.list, keys, depth, entry.regionPath);
      }
    });
  },

  async fetchRegionTree() {
    const response = await getAreaConnectList(this.chosen);

    if (response.code === 200) {
      this.treeData = response.data || [];
    } else {
      this.$message.error('Échec du chargement des régions');
      return;
    }

    const keys = [];
    this.checkedPaths = [];
    this.collectLeaves(this.treeData, keys, 0, '');
    this.defaultKeys = keys;
  }
}

La fonction collectLeaves incrémente la profondeur à chaque niveau et construit le chemin regionPath. Seules les entrées du niveau 3 (les feuilles) et dont le champ tick est vrai sont ajoutées à la liste des clés cochées. Ainsi, aucun nœud intermédiaire n’est passé à default-checked-keys, et la propagation automatique n’apparaît pas.

Récupération des éléments cochés

onSelectionChange(payload) {
  this.checkedPaths = payload.checkedNodes.map((node) => node.regionPath);
}

Exemple complet du composant encapsulé

Voici une version refactorisée du composant, avec des noms de variables et une structure modifiés, mais conservant le même comportement.

<template>
  <div class="region-tree">
    <el-input
      v-if="searchable"
      v-model="query"
      size="small"
      placeholder="Rechercher"
      clearable
      @input="filterTree"
    />

    <el-tree
      ref="regionTree"
      class="region-tree__body"
      :data="items"
      :props="nodeProps"
      :show-checkbox="showCheckbox"
      :node-key="idField"
      :default-checked-keys="checkedKeys"
      :default-expand-all="expandAll"
      :indent="indicator ? 0 : 16"
      :icon-class="plusIcon ? 'el-icon-circle-plus-outline' : ''"
      :expand-on-click-node="expandOnClick"
      :filter-node-method="filterNode"
      highlight-current
      @check="emitSelection"
      @node-click="emitClick"
    >
      <span slot-scope="{ node, data }">
        <i v-if="data.icon" :class="['region-tree__icon', data.icon]" />
        <span>{{ node.label }}</span>
      </span>
    </el-tree>
  </div>
</template>
<script>
export default {
  name: 'RegionTree',
  props: {
    items: {
      type: Array,
      default: () => []
    },
    nodeProps: {
      type: Object,
      default: () => ({
        children: 'children',
        label: 'name',
        isLeaf: 'leaf'
      })
    },
    idField: {
      type: String,
      default: 'id'
    },
    showCheckbox: {
      type: Boolean,
      default: false
    },
    searchable: {
      type: Boolean,
      default: false
    },
    plusIcon: {
      type: Boolean,
      default: false
    },
    indicator: {
      type: Boolean,
      default: false
    },
    expandAll: {
      type: Boolean,
      default: false
    },
    expandOnClick: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      query: '',
      checkedKeys: []
    };
  },
  watch: {
    items: {
      immediate: true,
      handler(value) {
        this.checkedKeys = this.extractLeafKeys(value);
      }
    }
  },
  methods: {
    filterNode(value, data) {
      if (!value) return true;
      const label = data[this.nodeProps.label];
      return String(label).toLowerCase().includes(value.toLowerCase());
    },

    filterTree(value) {
      this.$refs.regionTree.filter(value);
    },

    extractLeafKeys(nodes, depth = 0) {
      const keys = [];

      nodes.forEach((node) => {
        const children = node[this.nodeProps.children];
        const isLeaf = !children || children.length === 0;

        if (node.tick && isLeaf) {
          keys.push(String(node[this.idField]));
        }

        if (children) {
          keys.push(...this.extractLeafKeys(children, depth + 1));
        }
      });

      return keys;
    },

    emitClick(data, node) {
      if (this.showCheckbox) return;
      this.$emit('node-click', data, node);
    },

    emitSelection(data, list) {
      this.$emit('selection-change', { data, checkedNodes: list.checkedNodes });
    }
  }
};
</script>
.region-tree {
  &__body {
    margin-top: 10px;
  }

  &__icon {
    margin-right: 6px;
  }
}

Avec cette approche, l’état de sélection reste fiable et ne se propage plus aux enfants d’un parent. L’essentiel est de ne jamais passer des clés de nœuds intermédiaires à default-checked-keys, mais uniquement les identifiants des feuilles effectivement cochées.

Étiquettes: Element UI el-tree Vue.js sélection en cascade nœuds feuilles

Publié le 23 septembre à 17h22