âIt looks great, but could you just add Excel import?â
If youâve developed software for a long time, youâve probably heard that question from a manager more than once. To a non-technical crowd, asking for Excel import/export doesnât sound like a big deal. How hard could it be, right?
But all too often, this question strikes fear into the hearts of developers. On most platforms, working with Excel files requires a lot of work. Historically, this has been doubly true on the web. Working with Excel data in a web app has felt a bit like this xkcd comic: "It can be hard to explain the difference between the easy and the virtually impossible." Building your own in-browser spreadsheet with Excel import and Excel export feels like a problem that will take five years and a research team to solve.
Thatâs changing. We now have turnkey libraries that let you drop a fully functioning spreadsheet into your web app. SpreadJS is one of them. Weâll look at how to take an existing Vue app â a real-world app using a Vuex store â and enhance it using SpreadJS.
The rest of the article assumes that you already understand HTML, CSS, and JavaScript. It also assumes you have a working knowledge of the Vue.js progressive JavaScript framework for creating web UI. It will help if youâve used Vuex for state management, but donât worry if you havenât. Itâs intuitive and easy to understand, and youâll be able to figure out whatâs going on just by reading the code if youâve used Vue.
In this blog, we will cover how to add Excel Import and Export to your Vue App following these steps:
- Vue Application Structure
- Add SpreadJS to Your Vue App
- Add Excel Export
- Add Excel Import
- Test Your Vue App
The Vue App
The Vue app weâll be working on is a straightforward sales dashboard with a few summary panels and a table of data. Itâs the kind of app that falls into the ânot badâ category:
Although itâs just a demo, itâs exactly the kind of app that enterprise web developers have to create. Itâs also exactly the kind of app that weâre typically asked to add Excel functionality to, so it will be a perfect example for the rest of this article.
The code for this application can be found here.
If you want to see how to create a Vue application from scratch, check out this quick start guide.
To set the stage, the dashboard is a single-page application made with Vue. It uses the latest and greatest Vue best practices: single-file components and a Vuex data store. Itâs also using Bootstrap for its CSS components and grid system.
Bootstrap isnât as popular as it used to be, but in reality, Bootstrap is still everywhere â especially in enterprise web apps where Excel support is usually needed. Weâd wager that plenty of new enterprise web apps will still use Bootstrap in 2030.
If youâd rather use Bulma or Tachyons in your spreadsheet-enabled apps, go right ahead! SpreadJS will work well with either of them.
Vue Application Structure
Letâs take a look at how the code is structured. Our Vuex store and Vue application are both defined in main.js. We have several single-file Vue components, all located in the components folder.
If you look at our Vuex store, youâll see the following:
const store = new Vuex.Store({
state: {
recentSales
}
mutations: {
UPDATE_RECENT_SALES(state) {
state.recentSales.push([]);
state.recentSales.pop();
}
}
});
Our storeâs initial state is set to the value of recentSales, a set of dummy data we imported. We also have a function that handles updating the recent sales when they are changed.
Wait a minute. If weâve only got one set of data, how are we generating three charts and a table? To see whatâs happening, open up the Dashboard.vue component. In it, youâll see several computed properties are being generated based on the data in the Vuex store:
<template>
<div style="background-color: #ddd">
<NavBar title="Awesome Dashboard"/>
<div class="container">
<div class="row">
<TotalSales :total="totalSales"/>
<SalesByCountry :salesData="countrySales"/>
<SalesByPerson :salesData="personSales"/>
<SalesTable :tableData="salesTableData"/>
</div>
</div>
</div>
</template>
<script>
import NavBar from "./NavBar";
import TotalSales from "./TotalSales";
import SalesByCountry from "./SalesByCountry";
import SalesByPerson from "./SalesByPerson";
import SalesTable from "./SalesTable";
import { groupBySum } from "../util/util";
export default {
components: { NavBar, SalesByCountry, SalesByPerson, SalesTable, TotalSales },
computed: {
totalSales() {
const items = this.$store.state.recentSales;
const total = items.reduce((acc, sale) => (acc += sale.value), 0);
return parseInt(total);
},
chartData() {
const items = this.$store.state.recentSales;
const groups = groupBySum(items, "country", "value");
return groups;
},
personSales() {
const items = this.$store.state.recentSales;
const groups = groupBySum(items, "soldBy", "value");
return groups;
},
salesTableData() {
return this.$store.state.recentSales;
},
}
};
</script>
Now, it makes more sense! The single data set contains everything we need to generate all of the numbers and tables for our dashboard. Since the data is in a reactive Vuex store, if the data updates, then all of the dashboard panels will update automatically.
This reactivity will come in handy in the next section when we replace our boring old static table with an editable spreadsheet.
Adding SpreadJS to Your Vue App
Hereâs where the fun starts! We have our dashboard, but we want to eliminate that clunky old HTML table. So, weâll have to change things up a little bit. We have a great starting point, but we must run our app locally to use SpreadJS in development mode without a license.
You can download the finished code to see the final result.
Start by opening the original project without SpreadJS. Open a terminal, navigate to the directory where you cloned the repository, and run ânpm install.â This installs the dependencies needed to run the application. When the dependency installation finishes, run ânpm serveâ to see the updated app in action. If you want to specifically import the libraries used, you can use this command:
npm install @mescius/spread-sheets @mescius/spread-sheets-vue @mescius/spread-excelio file-saver bootstrap
Letâs walk through the changes weâll have to make to upgrade our old app to its new and improved version. Since weâre going to replace our sales table with a spreadsheet, we will put the sheet into our existing SalesTable.vue component, but first, weâll have to get rid of our old table. Once it is gone, our SalesTable template will look like this:
<template>
<TablePanel title="Recent Sales">
</TablePanel>
</template>
After eliminating the table, weâve got our table panel ready and waiting for a spreadsheet, so letâs add one! After adding a SpreadJS sheet, our template will look like this:
<template>
<TablePanel title="Recent Sales">
<gc-spread-sheets :hostClass='hostClass' @workbookInitialized='workbookInit'>
<gc-worksheet :dataSource='tableData' :autoGenerateColumns='autoGenerateColumns'>
<gc-column
:width='50'
:dataField="'id'"
:headerText="'ID'"
:visible = 'visible'
:resizable = 'resizable'
>
</gc-column>
<gc-column
:width='300'
:dataField="'client'"
:headerText="'Client'"
:visible = 'visible'
:resizable = 'resizable'
>
</gc-column>
<gc-column
:width="350"
:headerText="'Description'"
:dataField="'description'"
:visible = 'visible'
:resizable = 'resizable'
>
</gc-column>
<gc-column
:width="100"
:dataField="'value'"
:headerText="'Value'"
:visible = 'visible'
:formatter = 'priceFormatter'
:resizable = 'resizable'
>
</gc-column>
<gc-column
:width="100"
:dataField="'itemCount'"
:headerText="'Quantity'"
:visible = 'visible'
:resizable = 'resizable'
>
</gc-column>
<gc-column
:width="100"
:dataField="'soldBy'"
:headerText="'Sold By'"
:visible = 'visible'
:resizable = 'resizable'
></gc-column>
<gc-column
:width="100"
:dataField="'country'"
:headerText="'Country'"
:visible = 'visible'
:resizable = 'resizable'
></gc-column>
</gc-worksheet>
</gc-spread-sheets>
</TablePanel>
</template>
Thatâs a lot to take in, so letâs walk through it to understand what is happening.
First, we create a spreadsheet by using the gc-spread-sheets element and binding it to two of our componentâs properties: hostClass and workbookInit.
Inside the spreadsheet, we create a new worksheet with the gc-worksheet element and bind it to our componentâs tableData and autoGenerateColumns properties. Note that tableData is exactly the same tableData we used to generate our plain HTML table. We can put our data into SpreadJS as-is with no changes required!
Finally, inside the worksheet, we define columns that tell SpreadJS how to display our data. The dataField property tells us which property of the underlying dataset this column should display, and headerText gives SpreadJS a nicely formatted column name to use. The rest of the bindings for each column are straightforward. The SpreadJS documentation has a complete list of everything you can pass to a gc-column.
So, with our template in place, how much code will be needed to make this all work? Fortunately, not much at all! Hereâs our SalesTable.vue componentâs new script code:
import "@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css";
// SpreadJS imports
import GC from "@mescius/spread-sheets";
import "@mescius/spread-sheets-vue";
import Excel from "@mescius/spread-excelio";
import TablePanel from "./TablePanel";
export default {
components: { TablePanel },
props: ["tableData"],
data(){
return {
sheetName: 'Sales Data',
hostClass:'spreadsheet',
autoGenerateColumns:true,
width:200,
visible:true,
resizable:true,
priceFormatter:"$ #.00"
}
},
methods: {
workbookInit: function(_spread_) {
this._spread = spread;
var self = this;
spread.bind(GC.Spread.Sheets.Events.ValueChanged, function () {
const store = self.$store;
var sheet = self._spread.getSheetFromName("Sales Data");
var newSalesData = sheet.getDataSource();
store.commit('UPDATE_RECENT_SALES', newSalesData);
});
}
}
};
Due to Vueâs simplicity, it takes very little code to make this work. If thereâs anything here youâre unfamiliar with, the âComponents In-depthâ section of the Vue documentation explains Vue components in detail. The only things that have changed are a few imports, some data properties, and a couple of methods. The data properties should look familiar; we saw them a moment ago in the template. Theyâre configuration options we are binding to the components in our SpreadJS spreadsheet.
The workbookInit method is a callback that SpreadJS calls when the sheet is initialized. In this method, we save our SheetJS spreadsheet object as an instance variable on our component so we can interact with it directly if necessary. We also added a binding function for the ValueChanged event to automatically update data whenever values are changed in the SpreadJS instance.
One last change: We give our component a scoped style to help the spreadsheet style itself. We saw this earlier when we passed the hostClass to the gc-spread-sheets element. Since hostClass is set to âspreadsheet,â weâre going to create a CSS class named spreadsheet:
<style scoped>
.spreadsheet {
width: 100%;
height: 400px;
border: 1px solid lightgray;
}
</style>
At this point, if we make no other changes and load our Dashboard, it will look like this:
But wait, thereâs more!
Remember how we passed our table data to the spreadsheet without making any changes to the data set? Now that our data is in a spreadsheet, we can edit it.
What will happen if we change the value of sale #6 from $35,000 to $3500? If we go into the sheet and edit the value, we get a dashboard that looks like this:
Wow! What happened?
We updated the SpreadJS sheet, and it automatically updated our Vuex store.
It also looks like Angela went from having a spectacular sales month to having a mediocre one. Sorry about that, Angela!
We now have an enhanced dashboard that a manager would be happy with. They can modify the data and watch the dashboard update before their eyes, but we can do even better by adding the ability to import and export Excel files. Next, weâll learn how to do that.
Adding Excel Export
Adding Excel export to our sheet is easy. First, letâs add an export button to our dashboard. Weâre going to place it at the bottom of our table panel in the SalesTable.vue file, right after the gc-spread-sheets closing tag:
âŚ
</gc-spread-sheets>
<div class="dashboardRow">
<button class="btn btn-primary dashboardButton" @click="exportSheet">
Export to Excel
</button>
</div>
</TablePanel>
</template>
As you can see, our button is expecting a click handler named exportSheet. Weâll add it in a moment, but first, we will import a function from an NPM package named file-saver:
import { saveAs } from 'file-saver';
Next, letâs add exportSheet to our componentâs methods object:
exportSheet: function() {
const spread = this._spread;
const fileName = "SalesData.xlsx";
//const sheet = spread.getSheet(0);
const excelIO = new IO();
const json = JSON.stringify(spread.toJSON({
includeBindingSource: true,
columnHeadersAsFrozenRows: true,
}));
excelIO.save(json, (blob) => {
saveAs(blob, fileName);
}, function (e) {
console.log(e)
});
}
Hereâs what the code is doing: first, we get a reference to our Sales Datasheet. Since itâs the only sheet in our spreadsheet, it lives at index 0, and we access it by calling getSheet. This can be used elsewhere in the function should we need to interact directly with the sheet.
We then instantiate SpreadJSâ ExcelIO library, convert our sheet to JSON, and ask SpreadJS to save it. Voila! Weâve exported an Excel file from our spreadsheet-enabled Vue app!
Note that weâre passing two serialization options to the sheetâs toJSON call: includeBindingSource and columnHeadersAsFrozenRows. Together, these options ensure that the data we bound to the sheet is exported correctly and that the sheet contains our column headers. So, looking at the exported Excel file will understand each column.
Adding Excel Import
Next, itâs time to add the ability to import Excel files.
Right below our export button, weâre going to add the following bit of markup:
<div>
<b>Import Excel File:</b>
<div>
<input type="file" class="fileSelect" @change='fileChange($event)' />
</div>
</div>
As you can see, we will use a standard HTML file picker and trigger a component method named fileChange when a file is selected.
Now that weâve added the template, letâs add the change handler to our componentâs methods object:
fileChange: function (_e_) {
if (this._spread) {
const fileDom = e.target || e.srcElement;
const excelIO = new Excel.IO();
const spread = this._spread;
const store = this.$store;
/*const deserializationOptions = {
includeBindingSource: true,
frozenRowsAsColumnHeaders: true
};*/
excelIO.open(fileDom.files[0], (_data_) => {
// Used for simply loading the JSON from a file
//spread.fromJSON(data, deserializationOptions);
var newSalesData = extractSheetData(data);
store.commit('IMPORT_RECENT_SALES', newSalesData)
});
}
}
Importing an Excel file is much the same as exporting it, except in reverse. After a file is chosen, we ask ExcelIO to import it. When done, it passes the sheet information to a callback function as a JavaScript object. Next, we pass the imported data through a custom function to extract the data we need from it and then commit it back to the Vuex store.
Typically, importing a file is as simple as calling that ExcelIO open method but using the workbook âfromJSONâ method. In this case, we just want to parse the data from the imported file and update the store, which will then update SpreadJS.
In our extractSheetData function, which youâll find in the src/util.util.js file, youâll see that we pull data out of the JavaScript object returned by ExcelIO and restructure it to match the shape of the data in our Vuex store.
Our import function assumes that the data in the imported sheet will have the same columns as our original data set. If someone uploads a spreadsheet that doesnât meet this requirement, our app wonât be able to handle it. This is an acceptable limitation in most line-of-business apps. Since our dashboard is designed to display a specific data type, itâs reasonable to ask users to supply data in the format the app expects.
When data extraction is complete, we call commit on the Vuex store and send the updated sales transaction data. The SpreadJS sheet and the dashboard panels then update themselves to reflect the new data. We can actually use a different mutation function for importing versus a value being changed so that we can add that to the main.js file as âIMPORT_RECENT_SALESâ:
const store = new Vuex.Store({
state: {
recentSales
},
mutations: {
UPDATE_RECENT_SALES(state) {
state.recentSales.push([]);
state.recentSales.pop();
},
IMPORT_RECENT_SALES(state, sales) {
state.recentSales = sales;
}
}
});
Testing Your Vue App
Now that you have seen the code, letâs test out Excel import and export in our Vue app.
Start by clicking the âExport to Excelâ button. Your web browser will then download an Excel spreadsheet containing all of the data we saw in our dashboardâs spreadsheet.
Open the sheet in Excel, and add a couple of lines of data. Itâs okay if you use new countries or new salespeople; all our dashboard components can handle it. Just be careful not to change the column order or names. When youâre finished, click the âChoose Fileâ button at the bottom of the Recent Sales panel. Select the Excel file you just edited.
When you select the file, youâll see the updated dashboard components.
Conclusion
Weâre done! We took an ordinary Vue dashboard app and added a live spreadsheet to it. We can now edit the data in the sheet and watch our entire dashboard update itself. Our enhanced dashboard is also able to import and export Excel files.
Vue, Vuex, and SpreadJS complement each other well. With Vueâs easy templating and data binding, Vuexâs reactive data store, and SpreadJSâ interactive spreadsheets, complex enterprise JavaScript apps can be created in hours.
As great as this might seem, weâve barely scratched the surface of what SpreadJS can do. To better understand what SpreadJS can do for you, refer to the SpreadJS Demos, which include full demos of the different features of SpreadJS, explanations, and live code showcasing those features. If youâre looking to dive deeper into using SpreadJS in your own apps, the SpreadJS Documentation has the information you need.
Learn more about this JavaScript Spreadsheet Component: