Drag and Drop Image Files into a FlexGrid Cell in Vue
Background
Wijmo FlexGrid does not include a built-in setting that automatically inserts files dragged from File Explorer into a cell. However, in Vue applications, you can handle the browser’s dragover and drop events on the FlexGrid host element.
The FlexGrid hitTest method identifies the target cell. If the drop target is in the Image column, the dropped image can be read with FileReader, stored in the row data as a data URL, displayed with CellMaker.makeImage, and later sent to the server.
Steps to Complete
- Install Wijmo Vue packages.
- Register the Wijmo Grid module and import styles.
- Create the FlexGrid data and image template.
- Initialize the grid and attach drag-and-drop handlers.
- Add the FlexGrid markup and Image column.
- Save the updated grid data to the server.
Getting Started
Install Wijmo Vue packages
npm install @mescius/wijmo.vue2.grid @mescius/wijmo.grid @mescius/wijmo.grid.cellmaker @mescius/wijmo.styles
- These packages provide the Vue FlexGrid wrapper, the core grid APIs, the
CellMakerhelper, and the Wijmo stylesheet.
Register the Wijmo Grid module and import styles
// src/main.js
import { createApp } from 'vue';
import { registerGrid } from '@mescius/wijmo.vue2.grid';
import '@mescius/wijmo.styles/wijmo.css';
import App from './App.vue';
const app = createApp(App);
registerGrid(app);
app.mount('#app');
registerGrid(app)makes thewj-flex-gridandwj-flex-grid-columncomponents available in Vue templates.
/* src/style.css */
body {
font-family: Arial, sans-serif;
}
.wj-flexgrid {
height: 500px;
}
.wj-flexgrid .cell-img img {
max-width: 100%;
max-height: 64px;
object-fit: contain;
}
Create the FlexGrid data and image template
<!-- src/App.vue -->
<script setup>
import { ref } from 'vue';
import * as wjGrid from '@mescius/wijmo.grid';
import { CellMaker } from '@mescius/wijmo.grid.cellmaker';
const data = ref(getSampleData(50));
const imageCellTemplate = CellMaker.makeImage({
label: 'image not available'
});
function getSampleData(count) {
const countries = 'US,Germany,UK,Japan,Italy,Greece'.split(',');
const products = 'Phones,Computers,Cameras,Stereos'.split(',');
const rows = [];
for (let i = 0; i < count; i++) {
rows.push({
id: i,
country: countries[i % countries.length],
product: products[i % products.length],
image: null,
downloads: Math.round(100 + Math.random() * 10000),
sales: Math.random() * 10000,
expenses: Math.random() * 5000
});
}
return rows;
}
</script>
- The
imagefield stores the dropped image value.CellMaker.makeImagedisplays the stored data URL as an image in the grid cell.
Initialize the grid and attach drag-and-drop handlers
function initGrid(grid) {
grid.allowResizing = wjGrid.AllowResizing.Both;
grid.rows.defaultSize = 72;
const host = grid.hostElement;
if (host.dataset.imageDropInitialized === 'true') {
return;
}
host.dataset.imageDropInitialized = 'true';
host.addEventListener('dragover', e => {
const ht = getImageCellHitTest(grid, e);
e.preventDefault();
if (!e.dataTransfer || !ht || !hasImageItem(e.dataTransfer)) {
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'none';
}
return;
}
e.dataTransfer.dropEffect = 'copy';
});
host.addEventListener('drop', e => {
const ht = getImageCellHitTest(grid, e);
e.preventDefault();
if (!e.dataTransfer || !ht) {
return;
}
const file = Array.from(e.dataTransfer.files).find(isImageFile);
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = () => {
grid.setCellData(ht.row, ht.col, reader.result);
};
reader.readAsDataURL(file);
});
}
function getImageCellHitTest(grid, event) {
const ht = grid.hitTest(event);
if (ht.panel !== grid.cells || ht.row < 0 || ht.col < 0) {
return null;
}
const column = ht.panel.columns[ht.col];
return column.binding === 'image' ? ht : null;
}
function hasImageItem(dataTransfer) {
return Array.from(dataTransfer.items || []).some(item => {
return item.kind === 'file' && item.type.startsWith('image/');
});
}
function isImageFile(file) {
return file.type.startsWith('image/') || /\.(png|gif|bmp|jpe?g|webp)$/i.test(file.name);
}
- The
dragoverhandler allows dropping only when the user is over theImagecolumn and the dragged item is an image. Thedrophandler reads the file as a data URL and saves it into the bound grid cell.
Add the FlexGrid markup and Image column
<template>
<p>Drag an image from File Explorer into the Image column.</p>
<button type="button" @click="saveGrid">
Save Grid
</button>
<wj-flex-grid
:itemsSource="data"
:autoGenerateColumns="false"
:initialized="initGrid"
>
<wj-flex-grid-column binding="id" header="ID" />
<wj-flex-grid-column binding="country" header="Country" />
<wj-flex-grid-column binding="product" header="Product" />
<wj-flex-grid-column
binding="image"
header="Image"
:isReadOnly="true"
:width="140"
cssClass="cell-img"
:cellTemplate="imageCellTemplate"
/>
<wj-flex-grid-column binding="downloads" header="Downloads" />
<wj-flex-grid-column binding="sales" header="Sales" format="n2" />
<wj-flex-grid-column binding="expenses" header="Expenses" format="n2" />
</wj-flex-grid>
</template>
- The
initializedcallback provides the FlexGrid instance, where the drag-and-drop event listeners are attached.
Save the updated grid data to the server
async function saveGrid() {
const response = await fetch('/api/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data.value)
});
if (!response.ok) {
throw new Error('Unable to save grid data.');
}
}
- Because the dropped image is stored in each row’s
imagefield, the updated grid data can be sent to the server with the rest of the row values.
For large images, consider uploading the original image file separately and storing the returned image URL in the grid instead of storing a Base64 data URL.
Happy coding!