Frontend/vue3

[Vue] 컴포넌트 동적 생성 (agGrid.vue)

dddzr 2023. 3. 31. 09:16

agGrid

그림판 같은 툴에서 요소를 화면에 동적으로 생성하는 기능이 필요한데 외부 라이브러리를 쓰는 AgGrid인 경우

vue의 컴포넌트로 등록되어 있어서 일반 요소와 다른 생성 로직이 필요했습니다.

 

1. 라이브러리 파일

//AgGrid.vue
<template>
  <div style="height: 100%; width: 100%">
    <div class="example-wrapper">
      <div class="example-header" style="background: #ffdddd">Rows in this example do not move, only events are fired</div>
      <ag-grid-vue
        class="ag-theme-alpine"
        style="height: 500px"
        :column-defs="columnDefs"
        :row-data="rowData"
        :default-col-def="defaultColDef"
        row-selection="multiple"
        :animate-rows="true"
        :row-drag-managed="true"
        :enable-cell-change-flash="true"
        @cell-clicked="onCellWasClicked"
        @cell-changed="onCellWasChanged"
        @cell-double-clicked="onCellWasDoubleClicked"
        @cell-context-menu="onCellWasContextMenu"
        @grid-ready="onGridReady"
        @row-drag-enter="onRowDragEnter"
        @row-drag-end="onRowDragEnd"
        @row-drag-move="onRowDragMove"
        @row-drag-leave="onRowDragLeave"
      >
      </ag-grid-vue>
    </div>
  </div>
</template>
<!-- animate-rows=true >> Moving Columns, Filtering Rows , Sorting Rows, Expanding , Collapsing Row Groups-->
<script>
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import { AgGridVue } from 'ag-grid-vue3';
//import { reactive, onMounted, ref } from 'vue';

export default {
  name: 'AgGrid',
  components: {
    AgGridVue
  },
  data: function () {
    return {
      columnDefs: [
        { field: 'athlete', rowDrag: true },
        { field: 'country' },
        { field: 'year', width: 100 },
        { field: 'date' },
        { field: 'sport' },
        { field: 'gold' },
        { field: 'silver' },
        { field: 'bronze' }
      ],
      gridApi: null,
      columnApi: null,
      defaultColDef: {
        width: 170,
        sortable: true,
        filter: true,
        flex: 1,
        minWidth: 100,
        resizable: true
      },
      rowData: null
      /*
      cellWasClicked: event => {
        // Example of consuming Grid Event
        console.log('cell was clicked', event);
      },
      cellWasChanged: event => {
        console.log('onCellWasChanged', event);
      },
      cellWasDoubleClicked: event => {
        console.log('cellWasDoubleClicked', event);
      },
      cellWasContextMenu: event => {
        console.log('cellWasContextMenu', event);
      },
      deselectRows: () => {
        gridApi.value.deselectAll();
      }
      */
    };
  },
  created() {},
  methods: {
    onGridReady(params) {
      this.gridApi = params.api;
      this.gridColumnApi = params.columnApi;

      let locale = this.$i18n.locale;
      for (let i = 0; i < this.columnDefs.length; i++) {
        let field = this.columnDefs[i]['field'];
        let headerName = this.$i18n.t(field);
        this.columnDefs[i]['headerName'] = headerName;
      }
      const updateData = data => params.api.setRowData(data);

      fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
        .then(resp => resp.json())
        .then(data => updateData(data));
    },
    onCellWasClicked(e) {
      // Example of consuming Grid Event
      console.log('onCellWasClicked', e);
    },
    onCellWasChanged(e) {
      console.log('onCellWasChanged', e);
    },
    onCellWasDoubleClicked(e) {
      console.log('onCellWasDoubleClicked', e);
    },
    onCellWasContextMenu(e) {
      console.log('onCellWasContextMenu', e);
    },
    deselectRows: () => {
      gridApi.value.deselectAll();
    },
    onRowDragEnter(e) {
      console.log('onRowDragEnter', e);
    },
    onRowDragEnd(e) {
      console.log('onRowDragEnd', e);
    },
    onRowDragMove(e) {
      console.log('onRowDragMove', e);
    },
    onRowDragLeave(e) {
      console.log('onRowDragLeave', e);
    }
  }
};
</script>
<!--
<style scoped>
@import '@css/ag-grid/ag-grid.css';
@import '@css/ag-grid/ag-theme-alpine.css';
</style>
-->
<style lang="scss"></style>

 

2. agGrid를 여러군데서 커스텀해서 사용하기 위해 따로 sample파일을 만든 것 같습니다.

//AgGridSample.vue
<template>
  <div>
    <ul class="aggridsample">
      <li style="height: 100px; width: 500px"><AgGrid></AgGrid></li>
    </ul>
  </div>
</template>
<script>
import AgGrid from '@/components/lib/aggrid/AgGrid.vue';

export default {
  name: 'AgGridSample',
  components: {
    AgGrid
  }
};
</script>

 

3. 컴포넌트를 사용하는 화면

v-for문을 사용하여 data()함수 안의 agGrids만큼 컴포넌트를 생성합니다.

//Editor.vue
<template>
  <div id="center-board">
    <div class="rulers">
      <div class="rulersTop"></div>
      <div style="display: flex; flex-direction: row">
        <div class="rulersRight"></div>
        <div id="workspace" @keydown="mainKeyDown($event)" @keyup="mainKeyUp($event)">
          <div
            id="htmleditormain"
            class="editor"
            tabindex="0"
            @mousemove.prevent="mainMouseMove($event)"
            @mouseup.prevent="mainMouseUp($event)"
            @mousedown.prevent="mainMouseDown($event)"
            @drop.prevent="dragDrop($event)"
            @dragenter.prevent
            @dragover.prevent
          >
            <div v-for="agGrid in agGrids" :id="agGrid.id" :key="agGrid.id">
              <AgGridSample></AgGridSample>
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- Board -->
    <div class=""></div>
  </div>
</template>

<script>
import AgGridSample from '@/components/menu/AgGridSample.vue';

export default {
  name: 'HTMLEditor',
  props: {
    command: {
      type: Object,
      default: null,
      required: false
    }
  },
  components: {
    AgGridSample
  },
  data: {
	return {
	 agGrids: [],
	}
  },
  methods: {
  	async createElement (      
	 name,
	 type,
	 top,
	 left) {
        if (type === 'agGrid') {
          let elemId = this.agGrids.length + 1;
          const newAgGrid = { id: elemId };
          this.agGrids.push(newAgGrid);
        }
    }
  }
  
 }

 

* 실패한 코드

1. defineCompoent 사용

Vue3의 defineCompoent를 이용해 컴포넌트를 동적으로 등록합니다.

mount하기 위해서 app전체를 다시 mount해야해서 실패.

//실패한 코드
	async function init() {
            const AgGridSample = await import('./AgGridSample.vue');
            const AgGridComponent = Vue.defineComponent({
              components: { AgGridSample },
              mounted() {
                const agGridInstance = this.$refs.newId.$el;
                this.$refs.agGridContainer.appendChild(agGridInstance);
              },
              template: '<AgGridSample ref="' + newId + '"></AgGridSample>'
            });
            //const app = Vue.createApp(AgGridComponent);
            //const mountedApp = app.mount('#app');
            const agGridInstance = new AgGridComponent();
            agGridInstance.$mount();
            this.$refs.agGridContainer.appendChild(agGridInstance.$el);
          }

          init();

 

2. defineAsyncCoponent 사용

Vue 3에서는 defineAsyncComponent 함수를 사용하여 비동기적으로 컴포넌트를 등록할 수 있습니다. 이 함수는 Promise를 반환하며, 비동기적으로 로딩된 컴포넌트를 렌더링합니다.

setup함수 실행 시점에서 vue의 import가 완료되지 않아서 실패.

//실패한 코드2
<template>
  <div>
    <button @click="loadAgGridComponent">Load AgGrid Component</button>
    <component :is="component"></component>
  </div>
</template>

<script>
import { defineAsyncComponent, ref } from 'vue';

export default {
  name: 'HTMLEditor',
  setup() {
    const component = ref(null);

    const loadAgGridComponent = async () => {
      const AgGridComponent = await defineAsyncComponent(() => import('./AgGridSample.vue'));
      component.value = AgGridComponent;
    };

    return {
      component,
      loadAgGridComponent
    };
  }
};
</script>

위 코드에서는 defineAsyncComponent 함수를 사용하여 AgGridSample.vue 파일을 비동기적으로 로딩합니다. loadAgGridComponent 메소드에서 이 함수를 사용하여 컴포넌트를 로딩하고, 이후 component 변수에 할당합니다. 마지막으로 component 변수를 <component> 태그의 is 속성에 바인딩하여 동적으로 컴포넌트를 렌더링합니다.

 

* :is는 동적 컴포넌트를 생성하기 위해 사용되는 속성입니다. 이 속성을 사용하면 런타임에 컴포넌트를 변경할 수 있습니다.

예를 들어, component 변수에 MyComponent라는 컴포넌트를 할당했다면 :is="component"는 MyComponent 컴포넌트를 렌더링합니다.

:is 속성 대신 다른 이름을 사용하려면 해당 속성을 props로 전달해야 합니다. 예를 들어, <MyComponent /> 대신 <DynamicComponent component="MyComponent" />와 같이 사용할 수 있습니다. 이 경우, DynamicComponent 컴포넌트는 component prop을 받아들이고, 해당 prop을 사용하여 동적으로 컴포넌트를 렌더링할 수 있습니다.