My Idea Factory - By Ludovic Perrichon

JavaScript, SharePoint, Word

Update Docx with JS and optianally upload it to SP

Let's say you have a customer, for a project who need to auto-generate some word documents. And of course you don't have the right to do some code server side.
My situation was while working an a SharePoint site, a customer wanted to auto-generate document but the document content has to change depends of right and data into list. Of course the internal rules of his company avoid the installation of .wsp files.
I show him some solutions with the native function of the SharePoint content types and a word template. But it didn't awser to his needs. So below is the solution I have found.

But don't worry if you are not working on SharePoint but you need to work with docx client side. You can use the solution below.

  1. Requierement
  2. Javascriprt solution
    2.1 Properties
    2.2 Load Method
    2.3 Search Method
    2.4 Replace Method
    2.5 Use docxtamplater variables Method
    2.6 Set name Method
    2.7 Download Method
    2.8 Upload to SharePoint Method
    2.9 All together
  3. How to use it

1. Requierement

To make this solution work together you need jQuery, JSZip but a version 2.x, JSZip utils, Docxtemplater, FileSaver and DocxReader.js which is the JS I made and the one I am going to explain below.

Why do you need JSZip, which is used to read into zip file with Javascript? Because like I say in a previous article about how to generate docx files with C#, a docx (or any Microsoft office documents finishing with x) is a zip file with xml files inside. The goal here is to read this docx/zip file then get and update xml files inside.

The call of your javascript file will be :

<script type="text/javascript" src="lib/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="lib/jszip.min.js"></script>
<script type="text/javascript" src="lib/jszip-utils.min.js"></script>
<!--[if IE]>
<script type="text/javascript" src="lib/jszip-utils-ie.min.js"></script>
<![endif]-->
<script type="text/javascript" src="lib/docxtemplater-latest.min.js"></script>
<script type="text/javascript" src="lib/fileSaver.min.js"></script>
<script type="text/javascript" src="DocxReader.js"></script>

2. Javascript solution

Firstable let's create our Javascript "Class" where we are going to put all our vars and methods in :

var DocxReader = function(){  
    // Vars and methods go here
};

2.1 Properties

this.url = "";  
this.error = null;  
this.zipContent = null;  
this.file = {  
    contentAsString : null,
    contentAsXml : null
};
this.docxtemplater = null;  
this.name = "output.docx";  

Here are the properties we are going to use.
Url is the word document url.
Error is the return if an error happens.
ZipContent is the JSZip object.
File :
- contentAsString : Your document content xml as string
- contentAsXml : Your document content as xml Docxtemplater is the docxtemplater object.
Name is the name of your document. Set to output.docx at the begining.

2.2 Load Method
This method takes as parameters, the docx url you want to work with and a function to call when your file is loaded.
This method load your docx file, get the xml word/document.xml, which is the xml of your content, set your docxtemplater object and set all the properties.

this.Load = function(url, loaded){  
    this.url = url;

    var splitUrl = this.url.split('/');
    this.name = splitUrl[splitUrl.length - 1];

    var docThis = this;
    JSZipUtils.getBinaryContent(this.url, function(error,content){
        if(error){
            docThis.error = error;
            return;
        }

        docThis.zipContent = new JSZip(content);
        var documentxml = docThis.zipContent.file("word/document.xml");
        var strDocumentxml = documentxml.asText();
        docThis.file.contentAsString = strDocumentxml
        docThis.file.contentAsXml = $(strDocumentxml);

        docThis.docxtemplater = new Docxtemplater();
        docThis.docxtemplater.loadZip(docThis.zipContent);

        loaded();
    });
};

2.3 Search Method
This method is used to search a word or a sentence into your document. It takes as parameters the text you want to search. If it found it, it return true, otherwise it return false.

this.Search = function (txt){  
    var lowerCaseTxt = txt.toLowerCase();
    var innerDocument = this.file.contentAsXml.find('w\\:body').text().toLowerCase();
    if(innerDocument.indexOf(lowerCaseTxt) != -1){
        return true;
    }

    return false;
};

2.4 Replace Method
This method is used to replace a text by another in your document. Be careful, it will look into all your xml. Do not replace a tag otherwise you word file won't work anymore. It will take as parameters the text to replace and the text to replace by.

this.Replace = function(oldtxt, newtxt){  
    // Be careful, it will look in all the xml as text.
    // Don't replace tags otherwise your docx will not work anymore !
    var oldContent = this.file.contentAsString;
    var newContent = oldContent.replace(new RegExp(oldtxt, 'g'), newtxt);
    this.zipContent.file("word/document.xml", newContent);

    // Update object
    this.docxtemplater.loadZip(this.zipContent);
    this.file.contentAsString = newContent;
    this.file.contentAsXml = $(newContent);
};

2.5 Use docxtamplater variables Method
This method is to use the docxtemplate datas. It is used to make templates and replace data into your template. It will take as parameters, a data docxtemplater for tags.
To have a look how to works, have a look at the Docxtamplater types of tags.

this.ReplaceVariable = function(variables){  
    this.docxtemplater = setData(variables);
    this.docxtemplater.render();

    // Update object
    this.zipContent = this.docxtemplater.getZip();
    var documentxml = this.zipContent.file("word/document.xml");
    var strDocumentxml = documentxml.asText();
    this.file.contentAsString = strDocumentxml
    this.file.contentAsXml = $(strDocumentxml);
};

2.6 Set name Method
This method change your word file name. It takes as parameters the new name. Complete it with .docx.

this.SetName = function(name){  
    this.name = name;
};

2.7 Download Method
This method is used to download your word file after update.

this.Download = function(){  
    var out = this.docxtemplater.getZip().generate({
        type:"blob",
        mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    }) 
    saveAs(out,this.name);
};

2.8 Upload to SharePoint Method
This method is optional if you don't use SharePoint.
It takes as parameters your SharePoint JSOM ClientContext, your JSOM SharePoint list, your function when your file has been uploaded and your function when the upload has failed.
The code is an adaptation from the following article http://www.c-sharpcorner.com/blogs/how-to-uploads-document-in-document-library-using-jsom-in-sharepoint-2013-o365.

this.UploadToSharePoint = function(clientContext, jsomListObject, onSucceed, onFailed){  
    var base64 = this.zipContent.generate();
    var listRootFolder = jsomListObject.get_rootFolder();
    clientContext.load(listRootFolder);
    clientContext.executeQueryAsync(Function.createDelegate(this, function(){
         var fileName = listRootFolder.get_serverRelativeUrl() + '/' + this.name;

        //Create FileCreationInformation object using the read file data  
        var createInfo = new SP.FileCreationInformation(); 
        createInfo.set_content(base64); 
        createInfo.set_url(fileName); 

        //Add the file to the library  
        var uploadedDocument = jsomListObject.get_rootFolder().get_files().add(createInfo);

        //Load client context and execcute the batch  
        clientContext.load(uploadedDocument); 
        clientContext.executeQueryAsync(Function.createDelegate(this, onSucceed), Function.createDelegate(this, onFailed));
    }), Function.createDelegate(this, onFailed));

};

2.9 All together

var DocxReader = function(){  
    // Var
    this.url = "";
    this.error = null;
    this.zipContent = null;
    this.file = {
        contentAsString : null,
        contentAsXml : null
    };
    this.docxtemplater = null;
    this.name = "output.docx";

    // Methods
    this.Load = function(url, loaded){
        this.url = url;

        var splitUrl = this.url.split('/');
        this.name = splitUrl[splitUrl.length - 1];

        var docThis = this;
        JSZipUtils.getBinaryContent(this.url, function(error,content){
            if(error){
                docThis.error = error;
                return;
            }

            docThis.zipContent = new JSZip(content);
            var documentxml = docThis.zipContent.file("word/document.xml");
            var strDocumentxml = documentxml.asText();
            docThis.file.contentAsString = strDocumentxml
            docThis.file.contentAsXml = $(strDocumentxml);

            docThis.docxtemplater = new Docxtemplater();
            docThis.docxtemplater.loadZip(docThis.zipContent);

            loaded();
        });
    };

    this.Search = function (txt){
        var lowerCaseTxt = txt.toLowerCase();
        var innerDocument = this.file.contentAsXml.find('w\\:body').text().toLowerCase();
        if(innerDocument.indexOf(lowerCaseTxt) != -1){
            return true;
        }

        return false;
    };

    this.Replace = function(oldtxt, newtxt){
        // Be careful, it will look in all the xml as text.
        // Don't replace tags otherwise your docx will not work anymore !
        var oldContent = this.file.contentAsString;
        var newContent = oldContent.replace(new RegExp(oldtxt, 'g'), newtxt);
        this.zipContent.file("word/document.xml", newContent);

        // Update object
        this.docxtemplater.loadZip(this.zipContent);
        this.file.contentAsString = newContent;
        this.file.contentAsXml = $(newContent);
    };

    this.ReplaceVariable = function(variables){
        this.docxtemplater = setData(variables);
        this.docxtemplater.render();

        // Update object
        this.zipContent = this.docxtemplater.getZip();
        var documentxml = this.zipContent.file("word/document.xml");
        var strDocumentxml = documentxml.asText();
        this.file.contentAsString = strDocumentxml
        this.file.contentAsXml = $(strDocumentxml);
    };

    this.SetName = function(name){
        this.name = name;
    };

    this.Download = function(){
        var out = this.docxtemplater.getZip().generate({
            type:"blob",
            mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        }) 
        saveAs(out,this.name);
    };

        this.UploadToSharePoint = function(clientContext, jsomListObject, onSucceed, onFailed){
        var base64 = this.zipContent.generate();
        var listRootFolder = jsomListObject.get_rootFolder();
        clientContext.load(listRootFolder);
        clientContext.executeQueryAsync(Function.createDelegate(this, function(){
             var fileName = listRootFolder.get_serverRelativeUrl() + '/' + this.name;

            //Create FileCreationInformation object using the read file data  
            var createInfo = new SP.FileCreationInformation(); 
            createInfo.set_content(base64); 
            createInfo.set_url(fileName); 

            //Add the file to the library  
            var uploadedDocument = jsomListObject.get_rootFolder().get_files().add(createInfo);

            //Load client context and execcute the batch  
            clientContext.load(uploadedDocument); 
            clientContext.executeQueryAsync(Function.createDelegate(this, onSucceed), Function.createDelegate(this, onFailed));
        }), Function.createDelegate(this, onFailed));

    };
}

3. How to use it

For exemple I have got a word document like this one :

var docx = new DocxReader();  
docx.Load("Path/to/file.docx", function(){

    // Search
    var found = docx.Search("Test"); // Return true
    var found2 = docx.Search("Testa"); // Return false

    // Replace
    docx.Replace("Read my", "Read your");

    // Change var inside document
    var docxvar = {
        Variable : "Change my var inside doc"
    };

    docx.docxtemplater.setData(docxvar);

    try {
        // render the document (replace all occurences of {first_name} by John, {last_name} by Doe, ...)
        docx.docxtemplater.render();
    }
    catch (error) {
        var e = {
            message: error.message,
            name: error.name,
            stack: error.stack,
            properties: error.properties,
        }
        console.log(JSON.stringify({error: e}));
        // The error thrown here contains additional information when logged with JSON.stringify (it contains a property object).
        throw error;
    }

    // Change file name
    docx.SetName("Awesome changes by JS.docx")

    // Download
    docx.Download();

    // OPTIONAL : Upload to SharePoint document library
    var ctx = new SP.ClientContext(_spPageContextInfo.webAbsoluteUrl);
    var spDocsLib = ctx.get_web().get_lists().getById("DOCUMENT_LIBRARY_GUID");
    // OR
    // var spDocsLib = ctx.get_web().get_lists().getByTitle("DOCUMENT_LIBRARY_TITLE");
    docx.UploadToSharePoint(ctx, spDocsLib, function(sender, args){
        // Success
        alert('File Uploaded');
    },
    function(sender, args){
        // Error
        alert('Upload Failed');
        console.log(args.get_message());
    });

});

The result is :