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:
 
SPFx Application Customizer To Generate Document QR Code Using Command Sets 
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. 
  1. npm install --save qrcode  
SPFx Application Customizer To Generate Document QR Code Using Command Sets
 
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.
  1. import { BaseDialog, IDialogConfiguration } from '@microsoft/sp-dialog';  
  2. import { SPComponentLoader } from '@microsoft/sp-loader';    
  3. import styles from './DocumentQrCodeExtensionCommandSet.module.scss';  
  4.   
  5. import Download from './download';  
  6.   
  7. interface ICustomDialogContentProps {  
  8.     itemUrl: string;  
  9.     close: () => void;  
  10.     submit: (color: string) => void;  
  11.       
  12.   }  
  13. export default class CustomDailog extends BaseDialog {  
  14.     public itemUrl: string;  
  15.     public base64Image: string;  
  16.     public filename:string;  
  17.     public render(): void {  
  18.        this.domElement.innerHTML +=   `<div id="popup1" class="${ styles.mainpopup }">  
  19.         <div class="${ styles.popup }">  
  20.         <h2>QR Code</h2>  
  21.         <div class="${ styles.content }"> ` +  
  22.         `<img src="` + this.base64Image  + `">   
  23.         </br>  
  24.         <button id="downloadQR">Download QR Code</button>  
  25.         <button id="close">Close</button>  
  26.         </div>  
  27.     </div>  
  28. </div>`;  
  29. this._setButtonEventHandlers();  
  30.     }  
  31.     
  32.      // METHOD TO BIND EVENT HANDLER TO BUTTON CLICK  
  33.  private _setButtonEventHandlers(): void {    
  34.     const webPart: CustomDailog = this;    
  35.     this.domElement.querySelector('#downloadQR').addEventListener('click', () => {    
  36.         Download(this.base64Image, this.filename, "image/png");  
  37.      });   
  38.      this.domElement.querySelector('#close').addEventListener('click', () => {    
  39.       this.close();  
  40.    });   
  41.  }   
  42.     public getConfig(): IDialogConfiguration {  
  43.       return {  
  44.         isBlocking: false  
  45.       };  
  46.     }  
  47.       
  48.     protected onAfterClose(): void {  
  49.       super.onAfterClose();  
  50.     }  
  51.   }  
  52.     
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'
  1. .overlay {  
  2.     position: fixed;  
  3.     top: 0;  
  4.     bottom: 0;  
  5.     left: 0;  
  6.     right: 0;  
  7.     background: rgba(0, 0, 0, 0.7);  
  8.     transition: opacity 500ms;  
  9.     opacity: 0;  
  10.   }  
  11.   .overlay:target {  
  12.     visibility: visible;  
  13.     opacity: 1;  
  14.   }  
  15.   .mainpopup {  
  16.     text-align: center;  
  17.   }  
  18.   .popup {  
  19.     padding: 20px;  
  20.     background: #fff;  
  21.     border-radius: 5px;  
  22.     position: relative;  
  23.     transition: all 5s ease-in-out;  
  24.   
  25.     .h2 {  
  26.       margin-top: 0;  
  27.       color: #333;  
  28.       font-family: Tahoma, Arial, sans-serif;  
  29.     }  
  30.     .content {  
  31.       overflow: auto;  
  32.     }  
  33.   }  
ExtensionCommandSet.ts
 
Now, go to your extensioncommanset.ts file, mine is 'DocumentQrCodeExtensionCommandSet.ts' 
 
Import CustomDialog 
  1. 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.
  1. @override  
  2. public onListViewUpdated(event: IListViewCommandSetListViewUpdatedParameters): void {  
  3.   const compareOneCommand: Command = this.tryGetCommand('COMMAND_1');  
  4.   if (compareOneCommand) {  
  5.     // This command should be hidden unless exactly one row is selected.  
  6.     compareOneCommand.visible = event.selectedRows.length === 1;  
  7.   }  
  8. }  
Replace your Execute method with below
  1. @override  
  2.   public onExecute(event: IListViewCommandSetExecuteEventParameters): void {  
  3.     switch (event.itemId) {  
  4.       case 'COMMAND_1':  
  5.           var docurl = window.location.origin + event.selectedRows[0].getValueByName("FileRef");  
  6.           // With promises  
  7.           QRCode.toDataURL(docurl)  
  8.           .then(url => {  
  9.             console.log(url)  
  10.               const dialog: CustomDailog = new CustomDailog();  
  11.               dialog.itemUrl = event.selectedRows[0].getValueByName("FileRef");  
  12.               dialog.base64Image = url;  
  13.               dialog.filename = event.selectedRows[0].getValueByName("FileName");  
  14.               dialog.show().then(() => {  
  15.               });  
  16.                 })  
  17.                 .catch(err => {  
  18.                   console.error(err)  
  19.                 })  
  20.         break;  
  21.       default:  
  22.         throw new Error('Unknown command');  
  23.     }  
  24.   }  
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. 
  1. //download.js v3.0, by dandavis; 2008-2014. [CCBY2] see http://danml.com/download.html for tests/usage  
  2. // v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime  
  3. // v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs  
  4. // v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support  
  5.   
  6. // data can be a string, Blob, File, or dataURL                                                       
  7. function download(data, strFileName, strMimeType) {  
  8.       
  9.     var self = (window as any), // this script is only for browsers anyway...  
  10.         u = "application/octet-stream"// this default mime also triggers iframe downloads  
  11.         m = strMimeType || u,   
  12.         x = data,  
  13.         D = document,  
  14.         a = D.createElement("a"),  
  15.         z = function(a){return String(a);},  
  16.           
  17.           
  18.         B = self.Blob || self.MozBlob || self.WebKitBlob || z,  
  19.         BB = self.MSBlobBuilder || self.WebKitBlobBuilder || self.BlobBuilder,  
  20.         fn = strFileName || "download",  
  21.         blob,   
  22.         b,  
  23.         ua,  
  24.         fr;  
  25.   
  26.     //if(typeof B.bind === 'function' ){ B=B.bind(self); }  
  27.       
  28.     if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback  
  29.         x=[x, m];  
  30.         m=x[0];  
  31.         x=x[1];   
  32.     }      
  33.     //go ahead and download dataURLs right away  
  34.     if(String(x).match(/^data\:[\w+\-]+\/[\w+\-]+[,;]/)){  
  35.         return navigator.msSaveBlob ?  // IE10 can't do a[download], only Blobs:  
  36.             navigator.msSaveBlob(d2b(x), fn) :   
  37.             saver(x) ; // everyone else can save dataURLs un-processed  
  38.     }//end if dataURL passed?  
  39.       
  40.     try{  
  41.       
  42.         blob = x instanceof B ?   
  43.             x :   
  44.             new B([x], {type: m}) ;  
  45.     }catch(y){  
  46.         if(BB){  
  47.             b = new BB();  
  48.             b.append([x]);  
  49.             blob = b.getBlob(m); // the blob  
  50.         }     
  51.     }   
  52.     function d2b(u) {  
  53.         var p= u.split(/[:;,]/),  
  54.         t= p[1],  
  55.         dec= p[2] == "base64" ? atob : decodeURIComponent,  
  56.         bin= dec(p.pop()),  
  57.         mx= bin.length,  
  58.         i= 0,  
  59.         uia= new Uint8Array(mx);  
  60.   
  61.         for(i;i<mx;++i) uia[i]= bin.charCodeAt(i);  
  62.   
  63.         return new B([uia], {type: t});  
  64.      }  
  65.         
  66.     function saver(url, winMode = false){  
  67.         if ('download' in a) { //html5 A[download]            
  68.             a.href = url;  
  69.             a.setAttribute("download", fn);  
  70.             a.innerHTML = "downloading...";  
  71.             D.body.appendChild(a);  
  72.             setTimeout(function() {  
  73.                 a.click();  
  74.                 D.body.removeChild(a);  
  75.                 if(winMode===true){setTimeout(function(){ self.URL.revokeObjectURL(a.href);}, 250 );}  
  76.             }, 66);  
  77.             return true;  
  78.         }  
  79.         //do iframe dataURL download (old ch+FF):  
  80.         var f = D.createElement("iframe");  
  81.         D.body.appendChild(f);  
  82.         if(!winMode){ // force a mime that will download:  
  83.             url="data:"+url.replace(/^data:([\w\/\-\+]+)/, u);  
  84.         }  
  85.         f.src = url;  
  86.         setTimeout(function(){ D.body.removeChild(f); }, 333);      
  87.     }//end saver   
  88.     if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL)  
  89.         return navigator.msSaveBlob(blob, fn);  
  90.     }     
  91.     if(self.URL){ // simple fast and modern way using Blob and URL:  
  92.         saver(self.URL.createObjectURL(blob), true);  
  93.     }else{  
  94.         // handle non-Blob()+non-URL browsers:  
  95.         if(typeof blob === "string" || blob.constructor===z ){  
  96.             try{  
  97.                 return saver( "data:" +  m   + ";base64,"  +  self.btoa(blob)  );   
  98.             }catch(y){  
  99.                 return saver( "data:" +  m   + "," + encodeURIComponent(blob)  );   
  100.             }  
  101.         }  
  102.         // Blob but not URL:  
  103.         fr=new FileReader();  
  104.         fr.onload=function(e){  
  105.             saver(this.result);   
  106.         };  
  107.         fr.readAsDataURL(blob);  
  108.     }     
  109.     return true;  
  110. /* end download() */  
  111. 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.
  1. "items": {  
  2.   "COMMAND_1": {  
  3.     "title": { "default""QR Code" },  
  4.     "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",  
  5.     "type""command"  
  6.   }  
  7. }  
That's it, we have added all our files, below is how extension folder should look.
 
SPFx Application Customizer To Generate Document QR Code Using Command Sets
 
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.,  
  1. {  
  2.   "$schema""https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",  
  3.   "port": 4321,  
  4.   "https"true,  
  5.   "serveConfigurations": {  
  6.     "default": {  
  7.       "pageUrl""https://warnerbros.sharepoint.com/sites/joker/mydocs/Forms/AllItems.aspx",  
  8.       "customActions": {  
  9.         "323203d-5f80-46232-aed3-2c0342338": {  
  10.           "location""ClientSideExtension.ListViewCommandSet.CommandBar",  
  11.           "properties": {  
  12.             "sampleTextOne""One item is selected in the list",  
  13.             "sampleTextTwo""This command is always visible."  
  14.           }  
  15.         }  
  16.       }  
  17.     }  
  18.   }  
  19. }  
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.
 
SPFx Application Customizer To Generate Document QR Code Using Command Sets
 
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
Complete code can be found at this github repo. I hope you enjoyed reading, feel free to comment with any queries.