SPFx Extension To Generate Document QR Code Using Command Sets

In this article, we will create a SPFx extension of type Command Sets which will generate QR code for selected item url. Below is the output you can expect:

Pre-requisites
  • Create a SPFx extension command set using this link, this link has step by step approach on How to build the first Simple command set.
Once you have successfully created an SPFx extension using the above link, follow the below steps to change command set logic to implement QR code generator for the selected document.
The first thing we will do is install the required npm package which can be used to generate QR code.
npm install --save qrcode

 

Above is the output of package updating, but you should see similar output.
CustomDailog.ts
The next thing to do here is create CustomDailog.ts, if you wanted to know how to create a custom dialog in SPFx extension, you can follow my article where I have explained how to create custom dialogs and how to pass back and forth parameters to custom dialog and extension.
Below is code for CustomDailog.ts, create this file in the extension folder.
import { BaseDialog, IDialogConfiguration } from '@microsoft/sp-dialog';  
import { SPComponentLoader } from '@microsoft/sp-loader';    
import styles from './DocumentQrCodeExtensionCommandSet.module.scss';  
  
import Download from './download';  
  
interface ICustomDialogContentProps {  
    itemUrl: string;  
    close: () => void;  
    submit: (color: string) => void;  
      
  }  
export default class CustomDailog extends BaseDialog {  
    public itemUrl: string;  
    public base64Image: string;  
    public filename:string;  
    public render(): void {  
       this.domElement.innerHTML +=   `<div id="popup1" class="${ styles.mainpopup }">  
        <div class="${ styles.popup }">  
        <h2>QR Code</h2>  
        <div class="${ styles.content }"> ` +  
        `<img src="` + this.base64Image  + `">   
        </br>  
        <button id="downloadQR">Download QR Code</button>  
        <button id="close">Close</button>  
        </div>  
    </div>  
</div>`;  
this._setButtonEventHandlers();  
    }  
    
     // METHOD TO BIND EVENT HANDLER TO BUTTON CLICK  
 private _setButtonEventHandlers(): void {    
    const webPart: CustomDailog = this;    
    this.domElement.querySelector('#downloadQR').addEventListener('click', () => {    
        Download(this.base64Image, this.filename, "image/png");  
     });   
     this.domElement.querySelector('#close').addEventListener('click', () => {    
      this.close();  
   });   
 }   
    public getConfig(): IDialogConfiguration {  
      return {  
        isBlocking: false  
      };  
    }  
      
    protected onAfterClose(): void {  
      super.onAfterClose();  
    }  
  }  

 

Now let us create scss file, which is being referred in CustomDailog.ts
SCSS file
Create a file named ‘<yourextensioname>.module.scss’ , mine is ‘DocumentQrCodeExtensionCommandSet.module.scss’
.overlay {  
    position: fixed;  
    top: 0;  
    bottom: 0;  
    left: 0;  
    right: 0;  
    background: rgba(0, 0, 0, 0.7);  
    transition: opacity 500ms;  
    opacity: 0;  
  }  
  .overlay:target {  
    visibility: visible;  
    opacity: 1;  
  }  
  .mainpopup {  
    text-align: center;  
  }  
  .popup {  
    padding: 20px;  
    background: #fff;  
    border-radius: 5px;  
    position: relative;  
    transition: all 5s ease-in-out;  
  
    .h2 {  
      margin-top: 0;  
      color: #333;  
      font-family: Tahoma, Arial, sans-serif;  
    }  
    .content {  
      overflow: auto;  
    }  
  }

 

ExtensionCommandSet.ts
Now, go to your extensioncommanset.ts file, mine is ‘DocumentQrCodeExtensionCommandSet.ts’
Import CustomDialog
import CustomDailog from './CustomDialog';

 

Replace your onListViewUpdated method with below, here we are writing a condition that command should be visible only when one row is selected.
@override  
public onListViewUpdated(event: IListViewCommandSetListViewUpdatedParameters): void {  
  const compareOneCommand: Command = this.tryGetCommand('COMMAND_1');  
  if (compareOneCommand) {  
    // This command should be hidden unless exactly one row is selected.  
    compareOneCommand.visible = event.selectedRows.length === 1;  
  }  
}

 

Replace your Execute method with below

@override  
  public onExecute(event: IListViewCommandSetExecuteEventParameters): void {  
    switch (event.itemId) {  
      case 'COMMAND_1':  
          var docurl = window.location.origin + event.selectedRows[0].getValueByName("FileRef");  
          // With promises  
          QRCode.toDataURL(docurl)  
          .then(url => {  
            console.log(url)  
              const dialog: CustomDailog = new CustomDailog();  
              dialog.itemUrl = event.selectedRows[0].getValueByName("FileRef");  
              dialog.base64Image = url;  
              dialog.filename = event.selectedRows[0].getValueByName("FileName");  
              dialog.show().then(() => {  
              });  
                })  
                .catch(err => {  
                  console.error(err)  
                })  
        break;  
      default:  
        throw new Error('Unknown command');  
    }  
  }

 

Download.ts
Next thing we will be doing is creating a typescript file which will provide functionality of Download QR code image. Basically, I did a little trick here, converted download.js to ts file for our use. We are using below library for download functionality.
http://danml.com/download.html 
Create download.ts file in the same folder. Add the below code.
//download.js v3.0, by dandavis; 2008-2014. [CCBY2] see http://danml.com/download.html for tests/usage  
// v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime  
// v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs  
// v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support  
  
// data can be a string, Blob, File, or dataURL                                                       
function download(data, strFileName, strMimeType) {  
      
    var self = (window as any), // this script is only for browsers anyway...  
        u = "application/octet-stream", // this default mime also triggers iframe downloads  
        m = strMimeType || u,   
        x = data,  
        D = document,  
        a = D.createElement("a"),  
        z = function(a){return String(a);},  
          
          
        B = self.Blob || self.MozBlob || self.WebKitBlob || z,  
        BB = self.MSBlobBuilder || self.WebKitBlobBuilder || self.BlobBuilder,  
        fn = strFileName || "download",  
        blob,   
        b,  
        ua,  
        fr;  
  
    //if(typeof B.bind === 'function' ){ B=B.bind(self); }  
      
    if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback  
        x=[x, m];  
        m=x[0];  
        x=x[1];   
    }      
    //go ahead and download dataURLs right away  
    if(String(x).match(/^data\:[\w+\-]+\/[\w+\-]+[,;]/)){  
        return navigator.msSaveBlob ?  // IE10 can't do a[download], only Blobs:  
            navigator.msSaveBlob(d2b(x), fn) :   
            saver(x) ; // everyone else can save dataURLs un-processed  
    }//end if dataURL passed?  
      
    try{  
      
        blob = x instanceof B ?   
            x :   
            new B([x], {type: m}) ;  
    }catch(y){  
        if(BB){  
            b = new BB();  
            b.append([x]);  
            blob = b.getBlob(m); // the blob  
        }     
    }   
    function d2b(u) {  
        var p= u.split(/[:;,]/),  
        t= p[1],  
        dec= p[2] == "base64" ? atob : decodeURIComponent,  
        bin= dec(p.pop()),  
        mx= bin.length,  
        i= 0,  
        uia= new Uint8Array(mx);  
  
        for(i;i<mx;++i) uia[i]= bin.charCodeAt(i);  
  
        return new B([uia], {type: t});  
     }  
        
    function saver(url, winMode = false){  
        if ('download' in a) { //html5 A[download]            
            a.href = url;  
            a.setAttribute("download", fn);  
            a.innerHTML = "downloading...";  
            D.body.appendChild(a);  
            setTimeout(function() {  
                a.click();  
                D.body.removeChild(a);  
                if(winMode===true){setTimeout(function(){ self.URL.revokeObjectURL(a.href);}, 250 );}  
            }, 66);  
            return true;  
        }  
        //do iframe dataURL download (old ch+FF):  
        var f = D.createElement("iframe");  
        D.body.appendChild(f);  
        if(!winMode){ // force a mime that will download:  
            url="data:"+url.replace(/^data:([\w\/\-\+]+)/, u);  
        }  
        f.src = url;  
        setTimeout(function(){ D.body.removeChild(f); }, 333);      
    }//end saver   
    if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL)  
        return navigator.msSaveBlob(blob, fn);  
    }     
    if(self.URL){ // simple fast and modern way using Blob and URL:  
        saver(self.URL.createObjectURL(blob), true);  
    }else{  
        // handle non-Blob()+non-URL browsers:  
        if(typeof blob === "string" || blob.constructor===z ){  
            try{  
                return saver( "data:" +  m   + ";base64,"  +  self.btoa(blob)  );   
            }catch(y){  
                return saver( "data:" +  m   + "," + encodeURIComponent(blob)  );   
            }  
        }  
        // Blob but not URL:  
        fr=new FileReader();  
        fr.onload=function(e){  
            saver(this.result);   
        };  
        fr.readAsDataURL(blob);  
    }     
    return true;  
} /* end download() */  
export default download;

 

Now let us add icon and rename command text. Go to your extension.manifest.json file. Mine is ‘DocumentQrCodeExtensionCommandSet.manifest.json’.
We would be using the SVG icon as taken from sample by Hugo Bernier . Here he has created similar functionality using the react framework.
"items": {  
  "COMMAND_1": {  
    "title": { "default": "QR Code" },  
    "iconImageUrl": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='32' height='32' viewBox='0 0 401.994 401.994' style='enable-background:new 0 0 401.994 401.994;' xml:space='preserve' class='qrcodecommandsetcustomicon' %3E %3Cg%3E %3Cg%3E %3Cpath d='M0,401.991h182.724V219.265H0V401.991z M36.542,255.813h109.636v109.352H36.542V255.813z'/%3E %3Crect x='73.089' y='292.355' width='36.544' height='36.549'/%3E %3Crect x='292.352' y='365.449' width='36.553' height='36.545'/%3E %3Crect x='365.442' y='365.449' width='36.552' height='36.545'/%3E %3Cpolygon points='365.446,255.813 328.904,255.813 328.904,219.265 219.265,219.265 219.265,401.991 255.813,401.991 255.813,292.355 292.352,292.355 292.352,328.904 401.991,328.904 401.991,219.265 401.991,219.265 365.446,219.265 '/%3E %3Cpath d='M0,182.728h182.724V0H0V182.728z M36.542,36.542h109.636v109.636H36.542V36.542z'/%3E %3Crect x='73.089' y='73.089' width='36.544' height='36.547'/%3E %3Cpath d='M219.265,0v182.728h182.729V0H219.265z M365.446,146.178H255.813V36.542h109.633V146.178z'/%3E %3Crect x='292.352' y='73.089' width='36.553' height='36.547'/%3E %3C/g%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3C/svg%3E",  
    "type": "command"  
  }  
}

 

That’s it, we have added all our files, below is how extension folder should look.

The next step to test this extension without deploying. Go to ./Config/serve.json
Change pageUrl properties to point to any View of document library of your SharePoint online site.,
{  
  "$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",  
  "port": 4321,  
  "https": true,  
  "serveConfigurations": {  
    "default": {  
      "pageUrl": "https://warnerbros.sharepoint.com/sites/joker/mydocs/Forms/AllItems.aspx",  
      "customActions": {  
        "323203d-5f80-46232-aed3-2c0342338": {  
          "location": "ClientSideExtension.ListViewCommandSet.CommandBar",  
          "properties": {  
            "sampleTextOne": "One item is selected in the list",  
            "sampleTextTwo": "This command is always visible."  
          }  
        }  
      }  
    }  
  }  
}

 

Once you are done with the above changes, go to Node JS command prompt and run ‘gulp serve’, It would open url mentioned in the above pageurl property. You should be able to see the below output. You can use the download button, which will download a png file that can be shared.

In this article, we have learned or implemented the below concepts:
  • Creating SPFx command set Extension
  • Using a QrCode npm package to generate QR code image
  • Creating CustomDailog box for an extension to show custom html, pass parameters between extension and Dialog.
  • Converting download.js library to typescript library

Note – This article was originally published at this link.

(Visited 5 times, 1 visits today)