Build a Document Scanner Web Component with LitElement and Dynamic Web TWAIN

Lit is a simple library for building fast, lightweight web components. At Lit’s core is a boilerplate-killing component base class that provides reactive state, scoped styles, and a declarative template system that’s tiny, fast and expressive.

In this article, we are going to build a web component with LitElement to scan documents in an HTML website based on Dynamic Web TWAIN.

What you’ll build: A reusable <document-scanner> web component using LitElement and Dynamic Web TWAIN that can scan, view, and save documents as PDF — embeddable in any HTML page.

Key Takeaways

  • LitElement lets you build a framework-agnostic document scanner web component that works in any HTML page with a single custom element tag.
  • Dynamic Web TWAIN handles TWAIN scanner communication, image buffering, and PDF export through its JavaScript SDK.
  • The firstUpdated Lit lifecycle hook is the correct place to initialize the Dynamic Web TWAIN object and bind it to the shadow DOM container.
  • The finished component exposes scan, save, and image-count features and dispatches a custom initialized event for parent-level control.

Common Developer Questions

  • How do I build a document scanner web component with LitElement?
  • How do I integrate Dynamic Web TWAIN with Lit and webpack?
  • How do I save scanned documents as PDF from a browser using a web component?

Prerequisites

Before you start, make sure you have:

  • Node.js (v14 or later) and npm installed
  • A physical scanner connected to your machine (or use the virtual scanner for testing)
  • A Dynamic Web TWAIN license key. Get a 30-day free trial license to follow along.

Step 1: Set Up a New Webpack Project

Although Lit can be used without a build system, in this article, we are going to use webpack with the following template (clone with git):

git clone https://github.com/wbkd/webpack-starter

Step 2: Install Lit and Dynamic Web TWAIN

  1. Install Lit.

    npm install lit
    
  2. Install Dynamic Web TWAIN.

    npm install dwt
    

    In addition, we need to copy the resources of Dynamic Web TWAIN to the public folder.

    1. Install ncp.

      npm install --save-dev ncp
      
    2. Modify package.json to copy the resources for the build and start commands.

       "scripts": {
         "lint": "npm run lint:styles; npm run lint:scripts",
         "lint:styles": "stylelint src",
         "lint:scripts": "eslint src",
      -  "build": "cross-env NODE_ENV=production webpack --config webpack/webpack.config.prod.js",
      -  "start": "webpack serve --config webpack/webpack.config.dev.js"
      +  "build": "ncp node_modules/dwt/dist public/dwt-resources && cross-env NODE_ENV=production webpack --config webpack/webpack.config.prod.js",
      +  "start": "ncp node_modules/dwt/dist public/dwt-resources && webpack serve --config webpack/webpack.config.dev.js"
       },
      
    3. Modify webpack.common.js to copy the files in the public folder to the output folder instead of the public folder inside the output folder.

       new CopyWebpackPlugin({
      -  patterns: [{ from: Path.resolve(__dirname, '../public'), to: 'public' }],
      +  patterns: [{ from: Path.resolve(__dirname, '../public'), to: '' }],
       }),
      

Step 3: Build the Document Scanner Web Component

  1. Create a new documentscanner.js file under src\scripts with the following template.

    import {LitElement, html, css} from 'lit';
    
    export class DocumentScanner extends LitElement {
      static properties = {
      };
      DWObject;
      static styles = css`
        :host {
          display: block;
        }
        `;
      constructor() {
        super();
      }
    
      render() {
        return html``;
      }
    }
    customElements.define('document-scanner', DocumentScanner);
    
  2. Add a div element as the container for the controls of Dynamic Web TWAIN (mainly to view documents).

    render() {
      return html`<div id="dwtcontrolContainer"></div>`;
    }
    
  3. Configure Dynamic Web TWAIN in the constructor. You need a license to use Dynamic Web TWAIN. You can apply for a license here.

    import Dynamsoft from 'dwt';
    export class DocumentScanner extends LitElement {
      constructor() {
        super();
        Dynamsoft.DWT.AutoLoad = false;
        Dynamsoft.DWT.ResourcesPath = "/dwt/dist";
        Dynamsoft.DWT.ProductKey = "LICENSE-KEY"; 
      }
    }
    
  4. Initialize Dynamic Web TWAIN in the firstUpdated lifecycle after the dom is first updated and bind it to the container in the previous step. In addition, dispatch a custom event with the object of Dynamic Web TWAIN so that we can control it in a parent node.

    DWObject;
    firstUpdated() {
      let pThis = this;
      let dwtContainer = this.renderRoot.getElementById("dwtcontrolContainer");
      Dynamsoft.DWT.CreateDWTObjectEx(
        {
          WebTwainId: 'dwtcontrol'
        },
        function(obj) {
          pThis.DWObject = obj;
          pThis.DWObject.Viewer.bind(dwtContainer);
          pThis.DWObject.Viewer.show();
          pThis.DWObject.Viewer.width = "100%";
          pThis.DWObject.Viewer.height = "100%";
          const event = new CustomEvent('initialized', {
            detail: {
              DWObject: pThis.DWObject
            }
          });
          pThis.dispatchEvent(event);
        },
        function(err) {
          console.log(err);
        }
      );
    } 
    
  5. Add one button to scan documents and one button to save the document images as a PDF file.

    render() {
      return html`
      <div class="buttons">
        <button @click=${this.scan}>Scan</button>
        <button @click=${this.save}>Save</button>
      </div>
      <div id="dwtcontrolContainer"></div>`;
    }
    scan(){
      let pThis = this;
      if (pThis.DWObject) {
        pThis.DWObject.SelectSource(function () {
          pThis.DWObject.OpenSource();
          pThis.DWObject.AcquireImage();
        },
          function () {
            console.log("SelectSource failed!");
          }
        );
      }
    }
    
    save(){
      if (this.DWObject) {
        this.DWObject.SaveAllAsPDF("Scanned.pdf");
      }
    }
    
  6. Add a reactive property named total to reflect how many documents are scanned. We can update its value in the OnBufferChanged event of Web TWAIN.

    export class DocumentScanner extends LitElement {
      static properties = {
        total: {},
      };
      constructor() {
        super();
        this.total = 0;
        //...
      }
      render() {
        return html`
        <div class="buttons">
          <button @click=${this.scan}>Scan</button>
          <button @click=${this.save}>Save</button>
        </div>
        <div id="dwtcontrolContainer"></div>
        <div class="status">Total: ${this.total}</div>`;
      }
         
      firstUpdated() {
        //...
        Dynamsoft.DWT.CreateDWTObjectEx(
          {
            WebTwainId: 'dwtcontrol'
          },
          function(obj) {
            //...
            pThis.DWObject.RegisterEvent('OnBufferChanged',function () {
              pThis.total = pThis.DWObject.HowManyImagesInBuffer;
            });
            //...
          },
          function(err) {
            console.log(err);
          }
        );
      } 
    }
    
  7. Set the styles for the component.

    static styles = css`
      :host {
        display: block;
      }
      .buttons {
        height: 25px;
      }
      #dwtcontrolContainer {
        width: 100%;
        height: calc(100% - 50px);
      }
      .status {
        height: 25px;
      }
      `;
    

Step 4: Embed the Scanner Component in Your Page

  1. Import the component in the index.js file.

    import { DocumentScanner } from './documentscanner'; // eslint-disable-line
    
  2. Add the component in the index.html.

    <document-scanner 
      style="width:320px;height:480px;"
    ></document-scanner>
    

All right, we can now scan documents from the browser.

Lit document scanner

Online demo

Common Issues and Edge Cases

  • “Dynamic Web TWAIN resources not found” error: This usually means the ncp copy step did not run before the dev server started. Make sure the start and build scripts in package.json include the ncp node_modules/dwt/dist public/dwt-resources prefix as shown in Step 2.
  • Scanner not detected in the browser: Dynamic Web TWAIN requires the Dynamsoft Service to be installed on the client machine for TWAIN communication. If SelectSource returns no scanners, verify the service is running and accessible.
  • Shadow DOM blocks the viewer container: Because LitElement renders into a shadow root, you must use this.renderRoot.getElementById() instead of document.getElementById() to locate the container — standard DOM queries will not find elements inside the shadow tree.

Source Code

Get the source code of the demo to have a try:

https://github.com/tony-xlh/document-scanner-lit-element