Skip to content

Instantly share code, notes, and snippets.

@hmt
Created June 17, 2018 14:46
Show Gist options
  • Save hmt/8dbfee77b1e49c4c61234a7bc180cce6 to your computer and use it in GitHub Desktop.
Save hmt/8dbfee77b1e49c4c61234a7bc180cce6 to your computer and use it in GitHub Desktop.
electron report generator

electron report generator

Writing schild.report I had to find a solution for printing PDF from some sort of template.

I chose to use quasar framework as the app's foundation and decided to use its simple electron feature so that the app works natively on any desktop.

Data comes in via a simple DB api that fetches data which makes it available to Svelte templates which are similar to vue templates. The difference is that Svelte templates are built while vue is evaluated at runtime. Besides that the syntax for svelte is slightly easier in my opinion. But that was just a matter of taste.

Since the app uses electron it makes webviews available which let you run external web pages inside your app. These webpages can be anything from remote sites to local files. Here webview is used to display the rendered template, i.e. reports. Data is fed into the reports and rendered as complete documents.

Because webviews are more or less standalone pages electron allows you to directly print them without the host page. This is perfect to only print the reports to PDF without worrying about injected CSS or foreign elements. Only what is inside the webview is printed.

So much about the theory. schild.report shall be used as an example to see how it works:

dokument.vue

<template>
  <div>
    <webview src="statics/webview.html" :preload="preload" autosize></webview>
  </div>
</template>

<script>
import path from 'path'

export default {
  name: 'Dokument',
  watch: {
    components () {
      this.initiateSvelte()
      this.createSvelte()
    },
    $route (to, from) {
      this.createSvelte()
    }
  },
  data () {
    return {
      preload: 'file://' + path.join(__statics, '/preload.js'),
      webview: null,
    }
  },
  computed: {
    klasse () { return this.$store.state.data.klasse }
    },
    componentName () {
      return this.components[this.$route.params.id].bez
    },
    components () { return this.$store.state.data.components }
  },
  mounted () {
    this.webview = document.querySelector('webview')
    this.webview.addEventListener('dom-ready', () => {
      this.webview.openDevTools()
      this.initiateSvelte()
      this.createSvelte()
    })
  },
  methods: {
    initiateSvelte () {
      this.webview.send('loadComponents', {
        componentsPath: this.$store.state.data.componentsPath + '/bundle.js'
      })
    },

    createSvelte () {
      this.webview.send('updateComponents', {
        component: this.$route.params.id,
        klasse: this.klasse,
      })
      this.$root.$emit('pdfName', this.pdfName)
    }
  }
}
</script>

<style>
  webview {
    display: block;
    border: none;
    height: 100vh;
    width: 100vw;
    display: inline-flex !important;
  }
</style>

As can be seen the vue page has a webview tag that is used to load another file called webview.html which will host the report.

To communicate with the webview an IPC is used so that svelte components can be initiated and updated if necessary. For example if the user selects a different component. In this case components can be selected through a component tree that bear links. A route change therefore triggers a component change.

Components need to be placed in an accessible place which can be inside or outside the app. Here it is a bundled version outside the app. The app reads svelte components and bundles them in just one file. If plain svelte templates need to be used one could compile them on the spot via svelte.create() (svelte needs to be imported as such though).

The webview.html file is kept very simple:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <style>
      /* make print look good */
      @media print {
        .noprint * {
          display: none;
          height:0;
        }
      }
    </style>
  </head>
  <body>
    <script>
      // run the global ipc() function  
      ipc()
    </script>
    <svelte></svelte>
    </div>
  </body>
</html>

and the preload.js file is used to add IPC:

const ipcRenderer = require('electron').ipcRenderer

global.ipc = () => {
  let svelte
  let components

  // a simple hot module reloading mechanism.
  // Just ditch the required components and require them again
  ipcRenderer.on('loadComponents', (event, args) => {
    delete require.cache[require.resolve(args.componentsPath)]
    components = require(args.componentsPath)
  })

  ipcRenderer.on('updateComponents', (event, args) => {
    if (svelte) svelte.destroy()
    svelte = new components[args.component](
      {
        target: document.querySelector('svelte'),
        data: {
          klasse: args.klasse
        }
      }
    )
  })
}

Then if your reports look good to you you can print them. schild.report uses a button in the layout to trigger a pdf creation so there is a vue method that looks like this:

    openPdf (options = {}) {
      const webview = document.querySelector('webview')
      options = {
        // pageSize: options.pageSize || 'A4',
        // landscape: options.landscape || false,
        marginsType: options.marginsType || 1,
        printBackground: options.printBackground || true,
        pdfName: options.pdfName || 'print.html'
      }
      webview.printToPDF({ ...options }, (error, data) => {
        if (error) throw error
        fs.writeFile(`${app.getPath('userData')}/print.pdf`, data, error => {
          if (error) throw error
        })
        ipcRenderer.send('view-pdf')
      })
    }

The last line tells the main process to pick up this file and open it in a new window:

let pdfWindow = null

function createPDFWindow () {
  pdfWindow = new BrowserWindow({
    show: false,
    parent: mainWindow,
    width: 800,
    height: 600,
    webPreferences: {
      plugins: true
    }
  })

  pdfWindow.on('closed', () => {
    pdfWindow = null
  })
}

ipcMain.on('view-pdf', () => {
  if (pdfWindow === null) {
    createPDFWindow()
  }
  pdfWindow.loadURL(`file://${app.getPath('userData')}/print.pdf`)
  pdfWindow.show()
})

That's it. Sorry, no magic involved.

You can see the full app here on github: https://github.com/hmt/schild.report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment