Integración de Autocompletado en Columnas de Tabla con Element UI

Imlpementación de edición inline con autocompletado

Cuando se trabaja con tablas que requieren edición directa en las celdas, es común necesitar funcionalidades como autocompletado para campos específicos. Este ejemplo muestra cómo integrar el-autocomplete dentro de una columna de el-table.

<!-- Definición de columnas dinámicas -->
<jr-table-column
  :label="col.label"
  v-for="col in headers"   
  :key="col.field">
  
  <template slot-scope="context">
    <template v-if="mode === 'editing'">
    	
      <!-- Campo de autocompletado para nombres -->
      <el-autocomplete
        v-if="col.field === 'partyName'"
        popper-class="custom-suggestions"
        style="width: 100%"
        clearable
        ref="autoCompleteRefs"
        v-model="context.row[col.field]"
        :fetch-suggestions="loadSuggestions"
        placeholder="Nombre del participante"
        @select="(selected) => onSelect(selected, context.$index)">
        
        <el-button
          slot="append"
          style="width: 60px"
          :loading="searching"
          @click="triggerSearch(context.$index)">
          <i class="el-icon-search" v-if="!searching"></i>
        </el-button>
      </el-autocomplete>

      <!-- Otros campos como inputs simples -->
      <el-input
        v-else
        v-model="context.row[col.field]"
        size="mini"/>
    </template>

    <template v-else>
      {{ context.row[col.field] }}
    </template>
  </template>
</jr-table-column>

data() {
  return {
    headers: [
      { field: "date", label: "Fecha" },
      { field: "partyType", label: "Tipo" },
      { field: "partyName", label: "Nombre" },
      { field: "idCode", label: "Código ID" },
      { field: "investment", label: "Inversión" }
    ],
    mode: "",
    currentQuery: "",
    searching: false,
    cacheResults: false,
    suggestionsData: []
  };
},

methods: {
  loadSuggestions(input, callback) {
    this.currentQuery = input;
    
    if (this.cacheResults) {
      callback(this.suggestionsData);
      this.cacheResults = false;
    } else {
      callback(this.suggestionsData || []);
    }
  },

  onSelect(selection, rowIndex) {
    const record = this.tableRows[rowIndex];
    record.partyCode = selection.code;
    record.idCode = selection.idNumber;
  },

  triggerSearch(rowIndex) {
    const params = {
      name: this.currentQuery,
      type: this.tableRows[rowIndex].partyType || ""
    };

    this.searching = true;
    
    searchParties(params).then(response => {
      this.cacheResults = true;
      this.searching = false;
      
      this.suggestionsData = response.data.map(item => ({
        code: item.code,
        idNumber: item.documentId,
        value: item.name
      }));

      if (!this.suggestionsData.length) {
        this.$message.warning("No se encontraron coincidencias");
      } else {
        // Acceso al elemento correcto considerando duplicados en refs
        const targetIndex = rowIndex * 2;
        this.$refs.autoCompleteRefs[targetIndex].focus();
      }
    });
  }
}

Ejemplo completo de tabla editable

<template>
  <div>
    <div class="toolbar">
      <el-button type="primary" size="mini" @click="enterEditMode">
        Editar
      </el-button>
      <el-button type="success" size="mini" @click="saveChanges">
        Guardar
      </el-button>
    </div>

    <jr-table :showPagination="false" height="300" :tableData="tableRows">
      <jr-table-column label="#" type="index" width="50"></jr-table-column>
      
      <jr-table-column
        :label="column.label"
        v-for="column in headers"
        :key="column.field">
        
        <template slot-scope="ctx">
          <template v-if="mode === 'editing'">
            <el-autocomplete
              v-if="column.field === 'partyName'"
              popper-class="suggestions-popup"
              style="width: 100%"
              clearable
              ref="autoRefs"
              v-model="ctx.row[column.field]"
              :fetch-suggestions="loadSuggestions"
              placeholder="Ingrese nombre"
              @select="(sel) => onSelect(sel, ctx.$index)">
              
              <el-button
                slot="append"
                style="width: 60px"
                :loading="searching"
                @click="triggerSearch(ctx.$index)">
                <i class="el-icon-search" v-if="!searching"></i>
              </el-button>
            </el-autocomplete>

            <el-input
              v-else
              v-model="ctx.row[column.field]"
              size="mini"/>
          </template>

          <template v-else>
            <span>{{ ctx.row[column.field] }}</span>
          </template>
        </template>
      </jr-table-column>

      <el-table-column fixed="right" label="Acciones" width="190" align="center">
        <template slot-scope="ctx">
          <el-button icon="el-icon-plus" size="mini" type="primary"
                     @click="addNewRow(ctx.$index)">
            Añadir
          </el-button>
          <el-button size="mini" type="danger" icon="el-icon-delete"
                     @click="removeRow(ctx.$index)">
            Eliminar
          </el-button>
        </template>
      </el-table-column>
    </jr-table>
  </div>
</template>

<script>
import { updateRecords, searchParties } from "@/services/api";

export default {
  props: {
    tableRows: Array
  },

  data() {
    return {
      headers: [
        { field: "date", label: "Fecha" },
        { field: "partyType", label: "Tipo Participante" },
        { field: "partyName", label: "Nombre" },
        { field: "idCode", label: "Identificación" },
        { field: "investment", label: "Monto Invertido" }
      ],
      mode: "",
      currentQuery: "",
      searching: false,
      useCache: false,
      suggestionsData: []
    };
  },

  methods: {
    loadSuggestions(query, cb) {
      this.currentQuery = query;
      cb(this.useCache ? this.suggestionsData : []);
      this.useCache = false;
    },

    onSelect(option, idx) {
      const row = this.tableRows[idx];
      row.partyCode = option.code;
      row.idCode = option.idNumber;
    },

    triggerSearch(idx) {
      const request = {
        partyName: this.currentQuery,
        partyType: this.tableRows[idx].partyType || ""
      };

      this.searching = true;
      searchParties(request).then(res => {
        this.useCache = true;
        this.searching = false;
        
        this.suggestionsData = res.data.map(item => ({
          code: item.partyCode,
          idNumber: item.taxId,
          value: item.fullName
        }));

        if (!this.suggestionsData.length) {
          this.$message.warning("Sin resultados disponibles");
        } else {
          this.$refs.autoRefs[idx * 2].focus();
        }
      });
    },

    saveChanges() {
      if (this.tableRows?.length) {
        updateRecords(this.tableRows).then(() => {
          this.mode = "";
          this.$emit("onSave");
          this.$message.success("Datos guardados correctamente");
        });
      } else {
        this.$message.warning("No hay datos para guardar");
      }
    },

    enterEditMode() {
      if (!this.tableRows?.length) {
        this.tableRows.push(this.createEmptyRecord());
      }
      this.mode = "editing";
    },

    addNewRow() {
      this.mode = "editing";
      this.suggestionsData = [];
      this.tableRows.push(this.createEmptyRecord());
    },

    removeRow(index) {
      this.$confirm("¿Eliminar este registro?", "Confirmación", {
        confirmButtonText: "Sí",
        cancelButtonText: "Cancelar",
        type: "warning"
      }).then(() => {
        this.tableRows.splice(index, 1);
        this.$message.success("Registro eliminado");
      });
    },

    createEmptyRecord() {
      return {
        date: "",
        partyType: "",
        partyName: "",
        partyCode: "",
        idCode: "",
        investment: ""
      };
    }
  }
};
</script>

Nota: Al usar componentes anidados dentro de bucles de tabla, Vue genera múltiples referencias por cada fila renderizada. Es importante calcular el índice correcto al acceder meidante $refs.

Etiquetas: element-ui vuejs JavaScript frontend-development ui-components

Publicado el 9-5 18:18