2023-01-05 23:33:49 +00:00
|
|
|
---
|
|
|
|
title: Quasar
|
2023-02-28 11:40:44 +00:00
|
|
|
pagination_prev: demos/static/index
|
2023-01-05 23:33:49 +00:00
|
|
|
pagination_next: demos/desktop/index
|
|
|
|
sidebar_position: 3
|
|
|
|
sidebar_custom_props:
|
|
|
|
summary: VueJS + Web View
|
|
|
|
---
|
|
|
|
|
|
|
|
The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
|
|
|
|
from the main entrypoint or any script in the project.
|
|
|
|
|
2023-04-08 08:42:15 +00:00
|
|
|
The "Complete Example" creates an app that looks like the screenshots below:
|
|
|
|
|
|
|
|
<table><thead><tr>
|
|
|
|
<th><a href="#demo">iOS</a></th>
|
|
|
|
<th><a href="#demo">Android</a></th>
|
|
|
|
</tr></thead><tbody><tr><td>
|
|
|
|
|
|
|
|
![iOS screenshot](pathname:///quasar/ios.png)
|
|
|
|
|
|
|
|
</td><td>
|
|
|
|
|
|
|
|
![Android screenshot](pathname:///quasar/and.png)
|
|
|
|
|
|
|
|
</td></tr></tbody></table>
|
2023-01-05 23:33:49 +00:00
|
|
|
|
|
|
|
### Integration Details
|
|
|
|
|
2023-04-08 08:42:15 +00:00
|
|
|
This demo will use the Quasar ViteJS starter project with VueJS and Cordova.
|
|
|
|
|
2023-01-05 23:33:49 +00:00
|
|
|
The complete solution uses `cordova-plugin-file` for file operations. It can
|
|
|
|
be installed like any other Cordova plugin:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
cd src-cordova
|
|
|
|
cordova plugin add cordova-plugin-file
|
|
|
|
cd ..
|
|
|
|
```
|
|
|
|
|
|
|
|
#### Reading data
|
|
|
|
|
|
|
|
The `q-file` component presents an API reminiscent of File Input elements:
|
|
|
|
|
|
|
|
```html
|
|
|
|
<q-file label="Load File" filled label-color="orange" @input="updateFile"/>
|
|
|
|
```
|
|
|
|
|
|
|
|
When binding to the `input` element, the callback receives an `Event` object:
|
|
|
|
|
|
|
|
```ts
|
|
|
|
import { read } from 'xlsx';
|
|
|
|
|
|
|
|
// assuming `todos` is a standard VueJS `ref`
|
|
|
|
async function updateFile(v) { try {
|
|
|
|
// `v.target.files[0]` is the desired file object
|
|
|
|
const files = (v.target as HTMLInputElement).files;
|
|
|
|
if(!files || files.length == 0) return;
|
|
|
|
|
|
|
|
// read first file
|
|
|
|
const wb = read(await files[0].arrayBuffer());
|
|
|
|
|
|
|
|
// get data of first worksheet as an array of objects
|
|
|
|
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
|
|
|
|
|
|
|
|
// update state
|
|
|
|
todos.value = data.map(row => ({id: row.Index, content: row.Name}));
|
|
|
|
|
|
|
|
} catch(e) { console.log(e); } }
|
|
|
|
```
|
|
|
|
|
|
|
|
#### Writing data
|
|
|
|
|
|
|
|
The API is shaped like the File and Directory Entries API. For clarity, since
|
|
|
|
the code is a "pyramid of doom", the error handlers are omitted:
|
|
|
|
|
|
|
|
```ts
|
|
|
|
import { write } from 'xlsx';
|
|
|
|
|
|
|
|
// on iOS and android, `XLSX.write` with type "buffer" returns a `Uint8Array`
|
|
|
|
const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"});
|
|
|
|
// Request filesystem access for persistent storage
|
|
|
|
window.requestFileSystem(window.PERSISTENT, 0, function(fs) {
|
|
|
|
// Request a handle to "SheetJSQuasar.xlsx", making a new file if necessary
|
|
|
|
fs.root.getFile("SheetJSQuasar.xlsx", {create: true}, entry => {
|
|
|
|
// Request a FileWriter for writing data
|
|
|
|
entry.createWriter(writer => {
|
|
|
|
// The FileWriter API needs an actual Blob
|
|
|
|
const data = new Blob([u8], {type: "application/vnd.ms-excel"});
|
|
|
|
// This callback is called if the write is successful
|
|
|
|
writer.onwriteend = () => {
|
|
|
|
// TODO: show a dialog
|
|
|
|
};
|
|
|
|
// writer.onerror will be invoked if there is an error in writing
|
|
|
|
|
|
|
|
// write the data
|
|
|
|
writer.write(data);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
|
|
|
### Demo
|
|
|
|
|
|
|
|
:::note
|
|
|
|
|
2023-04-08 08:42:15 +00:00
|
|
|
This demo was tested on an Intel Mac on 2023 April 08. `create-quasar@1.1.2`
|
|
|
|
was installed during app creation. The app used Quasar version `2.11.10`.
|
|
|
|
|
|
|
|
The iOS simulator runs iOS 16.2 on an iPhone 14 Pro Max.
|
|
|
|
|
|
|
|
The Android simulator runs Android 12.0 (S) API 31 on a Pixel 3.
|
2023-01-05 23:33:49 +00:00
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
The demo draws from the ViteJS example. Familiarity with VueJS and TypeScript
|
|
|
|
is assumed.
|
|
|
|
|
|
|
|
0) Ensure all of the dependencies are installed. Install the CLI globally:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
npm i -g @quasar/cli cordova
|
|
|
|
```
|
|
|
|
|
|
|
|
(you may need to run `sudo npm i -g` if there are permission issues)
|
|
|
|
|
|
|
|
1) Create a new app:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
npm init quasar
|
|
|
|
```
|
|
|
|
|
|
|
|
<!-- spellchecker-disable -->
|
|
|
|
|
|
|
|
When prompted:
|
|
|
|
|
|
|
|
- "What would you like to build?": `App with Quasar CLI`
|
|
|
|
- "Project folder": `SheetJSQuasar`
|
|
|
|
- "Pick Quasar version": `Quasar v2 (Vue 3 | latest and greatest)`
|
|
|
|
- "Pick script type": `Typescript`
|
|
|
|
- "Pick Quasar App CLI variant": `Quasar App CLI with Vite`
|
|
|
|
- "Package name": (just press enter, it will use the default `sheetjsquasar`
|
|
|
|
- "Project product name": `SheetJSQuasar`
|
|
|
|
- "Project description": `SheetJS + Quasar`
|
|
|
|
- "Author": (just press enter, it will use your git config settings)
|
|
|
|
- "Pick a Vue component style": `Composition API`
|
|
|
|
- "Pick your CSS preprocessor": `None`
|
|
|
|
- "Check the features needed for your project": Deselect everything
|
|
|
|
- "Install project dependencies": `No`
|
|
|
|
|
|
|
|
2) Install dependencies:
|
|
|
|
|
|
|
|
<!-- spellchecker-enable -->
|
|
|
|
|
|
|
|
```bash
|
|
|
|
cd SheetJSQuasar
|
|
|
|
npm i
|
|
|
|
npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
|
|
|
|
```
|
|
|
|
|
|
|
|
3) Set up Cordova:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
quasar mode add cordova
|
|
|
|
```
|
|
|
|
|
|
|
|
When prompted, enter the app id `org.sheetjs.quasar`.
|
|
|
|
|
|
|
|
It will create a new `src-cordova` folder. Continue in that folder:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
cd src-cordova
|
|
|
|
cordova platform add ios
|
|
|
|
cordova plugin add cordova-plugin-wkwebview-engine
|
|
|
|
cordova plugin add cordova-plugin-file
|
|
|
|
```
|
|
|
|
|
|
|
|
:::note
|
|
|
|
|
|
|
|
If there is an error `Could not load API for iOS project`, it needs to be reset:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
cordova platform rm ios
|
|
|
|
cordova platform add ios
|
|
|
|
cordova plugin add cordova-plugin-file
|
|
|
|
```
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
Return to the project directory:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
cd ..
|
|
|
|
```
|
|
|
|
|
|
|
|
4) Start the development server:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
quasar dev -m ios
|
|
|
|
```
|
|
|
|
|
|
|
|
:::caution
|
|
|
|
|
|
|
|
If the app is blank or not refreshing, delete the app and close the simulator,
|
|
|
|
then restart the development process.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
5) Add the Dialog plugin to `quasar.config.js`:
|
|
|
|
|
|
|
|
```js title="quasar.config.js"
|
2023-04-08 08:42:15 +00:00
|
|
|
framework: {
|
|
|
|
config: {},
|
|
|
|
// ...
|
2023-01-05 23:33:49 +00:00
|
|
|
// Quasar plugins
|
|
|
|
// highlight-next-line
|
|
|
|
plugins: ['Dialog']
|
2023-04-08 08:42:15 +00:00
|
|
|
},
|
2023-01-05 23:33:49 +00:00
|
|
|
```
|
|
|
|
|
2023-04-08 08:42:15 +00:00
|
|
|
6) In the template section of `src/pages/IndexPage.vue`, replace the example
|
|
|
|
with a Table, Save button and Load file picker component:
|
2023-01-05 23:33:49 +00:00
|
|
|
|
|
|
|
```html title="src/pages/IndexPage.vue"
|
2023-04-08 08:42:15 +00:00
|
|
|
<template>
|
|
|
|
<q-page class="row items-center justify-evenly">
|
2023-01-05 23:33:49 +00:00
|
|
|
<!-- highlight-start -->
|
2023-04-08 08:42:15 +00:00
|
|
|
<q-table :rows="todos" />
|
2023-01-05 23:33:49 +00:00
|
|
|
<q-btn-group>
|
|
|
|
<q-file label="Load File" filled label-color="orange" @input="updateFile"/>
|
|
|
|
<q-btn label="Save File" @click="saveFile" />
|
|
|
|
</q-btn-group>
|
2023-04-08 08:42:15 +00:00
|
|
|
<!-- highlight-end --> </q-page>
|
2023-01-05 23:33:49 +00:00
|
|
|
</template>
|
|
|
|
```
|
|
|
|
|
|
|
|
This uses two functions that should be added to the component script:
|
|
|
|
|
|
|
|
```ts title="src/pages/IndexPage.vue"
|
|
|
|
const meta = ref<Meta>({
|
|
|
|
totalCount: 1200
|
|
|
|
});
|
|
|
|
// highlight-start
|
|
|
|
function saveFile() {
|
|
|
|
}
|
2023-04-08 08:42:15 +00:00
|
|
|
async function updateFile(v: Event) {
|
2023-01-05 23:33:49 +00:00
|
|
|
}
|
|
|
|
return { todos, meta, saveFile, updateFile };
|
|
|
|
// highlight-end
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
The app should now show two buttons at the bottom:
|
|
|
|
|
|
|
|
![Quasar Step 6](pathname:///mobile/quasar6.png)
|
|
|
|
|
|
|
|
:::caution
|
|
|
|
|
|
|
|
If the app is blank or not refreshing, delete the app and close the simulator,
|
|
|
|
then restart the development process.
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
7) Wire up the `updateFile` function:
|
|
|
|
|
|
|
|
```ts title="src/pages/IndexPage.vue"
|
|
|
|
import { defineComponent, ref } from 'vue';
|
|
|
|
// highlight-start
|
|
|
|
import { read, write, utils } from 'xlsx';
|
|
|
|
import { useQuasar } from 'quasar';
|
|
|
|
// highlight-end
|
|
|
|
|
|
|
|
export default defineComponent({
|
|
|
|
// ...
|
|
|
|
// highlight-start
|
|
|
|
const $q = useQuasar();
|
2023-04-08 08:42:15 +00:00
|
|
|
function dialogerr(e: Error) { $q.dialog({title: "Error!", message: e.message || String(e)}); }
|
2023-01-05 23:33:49 +00:00
|
|
|
// highlight-end
|
|
|
|
function saveFile() {
|
|
|
|
}
|
2023-04-08 08:42:15 +00:00
|
|
|
async function updateFile(v: Event) {
|
2023-01-05 23:33:49 +00:00
|
|
|
// highlight-start
|
|
|
|
try {
|
|
|
|
const files = (v.target as HTMLInputElement).files;
|
|
|
|
if(!files || files.length == 0) return;
|
|
|
|
|
|
|
|
const wb = read(await files[0].arrayBuffer());
|
|
|
|
|
2023-04-08 08:42:15 +00:00
|
|
|
const data = utils.sheet_to_json<any>(wb.Sheets[wb.SheetNames[0]]);
|
2023-01-05 23:33:49 +00:00
|
|
|
todos.value = data.map(row => ({id: row.Index, content: row.Name}));
|
|
|
|
} catch(e) { dialogerr(e); }
|
|
|
|
// highlight-end
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
To test that reading works:
|
|
|
|
|
|
|
|
- Download <https://sheetjs.com/pres.numbers>
|
|
|
|
- In the simulator, click the Home icon to return to the home screen
|
|
|
|
- Click on the "Files" icon
|
|
|
|
- Click and drag `pres.numbers` from a Finder window into the simulator.
|
|
|
|
|
|
|
|
![Quasar Step 7 save file](pathname:///mobile/quasar7a.png)
|
|
|
|
|
|
|
|
- Make sure "On My iPhone" is highlighted and select "Save"
|
|
|
|
- Click the Home icon again then select the `SheetJSQuasar` app
|
|
|
|
- Click the "Load" button, then select "Choose File" and select `pres`:
|
|
|
|
|
|
|
|
![Quasar Step 7 load file](pathname:///mobile/quasar7b.png)
|
|
|
|
|
2023-04-08 08:42:15 +00:00
|
|
|
Once selected, the screen should refresh with new contents.
|
2023-01-05 23:33:49 +00:00
|
|
|
|
|
|
|
8) Wire up the `saveFile` function:
|
|
|
|
|
2023-04-08 08:42:15 +00:00
|
|
|
```ts title="src/pages/IndexPage.vue"
|
2023-01-05 23:33:49 +00:00
|
|
|
function saveFile() {
|
|
|
|
// highlight-start
|
|
|
|
/* generate workbook from state */
|
|
|
|
const ws = utils.json_to_sheet(todos.value);
|
|
|
|
const wb = utils.book_new();
|
|
|
|
utils.book_append_sheet(wb, ws, "SheetJSQuasar");
|
|
|
|
const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"});
|
2023-04-08 08:42:15 +00:00
|
|
|
const dir: string = $q.cordova.file.documentsDirectory || $q.cordova.file.externalApplicationStorageDirectory;
|
2023-01-05 23:33:49 +00:00
|
|
|
|
|
|
|
/* save to file */
|
|
|
|
window.requestFileSystem(window.PERSISTENT, 0, function(fs) {
|
|
|
|
try {
|
|
|
|
fs.root.getFile("SheetJSQuasar.xlsx", {create: true}, entry => {
|
2023-04-08 08:42:15 +00:00
|
|
|
const msg = `File stored at ${dir} ${entry.fullPath}`;
|
2023-01-05 23:33:49 +00:00
|
|
|
entry.createWriter(writer => {
|
|
|
|
try {
|
|
|
|
const data = new Blob([u8], {type: "application/vnd.ms-excel"});
|
|
|
|
writer.onwriteend = () => {
|
|
|
|
try {
|
|
|
|
$q.dialog({title: "Success!", message: msg});
|
|
|
|
} catch(e) { dialogerr(e); }
|
|
|
|
};
|
|
|
|
writer.onerror = dialogerr;
|
|
|
|
writer.write(data);
|
|
|
|
} catch(e) { dialogerr(e); }
|
|
|
|
}, dialogerr);
|
|
|
|
}, dialogerr);
|
|
|
|
} catch(e) { dialogerr(e) }
|
|
|
|
}, dialogerr);
|
|
|
|
// highlight-end
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
The page should revert to the old contents.
|
|
|
|
|
|
|
|
To test that writing works:
|
|
|
|
|
|
|
|
- Click "Save File". You will see a popup with a location:
|
|
|
|
|
|
|
|
![Quasar Step 8](pathname:///mobile/quasar8.png)
|
|
|
|
|
|
|
|
- Find the file and verify the contents are correct. Run in a new terminal:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx |
|
|
|
|
while read x; do echo "$x"; npx xlsx-cli "$x"; done
|
|
|
|
```
|
|
|
|
|
|
|
|
Since the contents reverted, you should see
|
|
|
|
|
|
|
|
```
|
|
|
|
SheetJSQuasar
|
|
|
|
id,content
|
|
|
|
1,ct1
|
|
|
|
2,ct2
|
|
|
|
3,ct3
|
|
|
|
4,ct4
|
|
|
|
5,ct5
|
|
|
|
```
|
|
|
|
|
|
|
|
- Use "Load File" to select `pres.numbers` again. Wait for the app to refresh.
|
|
|
|
|
|
|
|
- Click "Save File", then re-run the command:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx |
|
|
|
|
while read x; do echo "$x"; npx xlsx-cli "$x"; done
|
|
|
|
```
|
|
|
|
|
|
|
|
The contents from `pres.numbers` should show up now, with a new header row:
|
|
|
|
|
|
|
|
```
|
|
|
|
SheetJSQuasar
|
|
|
|
id,content
|
|
|
|
42,Bill Clinton
|
|
|
|
43,GeorgeW Bush
|
|
|
|
44,Barack Obama
|
|
|
|
45,Donald Trump
|
|
|
|
46,Joseph Biden
|
|
|
|
```
|
|
|
|
|
2023-04-08 08:42:15 +00:00
|
|
|
**Android**
|
|
|
|
|
|
|
|
9) Create the Android project:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
cd src-cordova
|
|
|
|
cordova platform add android
|
|
|
|
cd ..
|
|
|
|
```
|
|
|
|
|
|
|
|
10) Start the simulator:
|
2023-01-05 23:33:49 +00:00
|
|
|
|
2023-04-08 08:42:15 +00:00
|
|
|
```bash
|
|
|
|
quasar dev -m android
|
|
|
|
```
|
|
|
|
|
|
|
|
:::note
|
|
|
|
|
|
|
|
In local testing, the Quasar build process threw an error:
|
|
|
|
|
|
|
|
```
|
|
|
|
java.lang.IllegalArgumentException: Unsupported class file major version 63
|
|
|
|
```
|
|
|
|
|
|
|
|
This was resolved by rolling back to Java 1.8
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
To test that reading works:
|
|
|
|
|
|
|
|
- Click and drag `pres.numbers` from a Finder window into the simulator.
|
|
|
|
- Tap "Load", tap the `≡` icon, tap "Downloads" and select `pres.numbers`.
|
|
|
|
|
|
|
|
To test that writing works:
|
|
|
|
|
|
|
|
- Tap "Save File". You will see a popup with a location.
|
|
|
|
|
|
|
|
- Pull the file from the simulator and verify the contents:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
adb exec-out run-as org.sheetjs.quasar cat files/files/SheetJSQuasar.xlsx > /tmp/SheetJSQuasar.xlsx
|
|
|
|
npx xlsx-cli /tmp/SheetJSQuasar.xlsx
|
|
|
|
```
|