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.