[O365] Deploying a SharePoint theme / branding using JavaScript only

With provider hosted add-ins being introduced in SharePoint 2013, the world of SharePoint devs shifted to using provisioning schemes to get their stuff in SharePoint sites. And this worked, quite well i might add. You might have read my post on SPMeta2 vs PnP (which is a bit outdated I must add). These provisioning engines allow your to provision “stuff” (files, folders, lists, contenttypes, whatever) to SharePoint. Amongst other things, they have one thing in common: they’re built on top of the CSOM (Client Side Object Model) C# SDK. This means that you are forced to run them as a stand-alone task, or deploy a provider hosted app which includes having a server up and running somewhere. So what if you do not want that? 

In my case, I wanted to deploy some branding to a site, plain and simple. Not something you’d really would want to have to spin up a server for. But I didn’t want to create a console app for private use either. And so I ventured into the world of JavaScript once again, trying to leverage the JSOM (JavaScript Object Model) to deploy things.

At this moment, there are no provisioning frameworks like SPMeta2 or PnP available for JSOM yet. The SPMeta guys let me know that they’re considering a JSOM port, but it’s not there at this point in time.

Ok so we’re left without a framework at this point in time, but we can still do what’s needed! In short, there’s three things I wanted to do:

  1. Copy the required content (css, javascript, images, theme file) to my site.
  2. Set the CSS to be the alternate CSS for the site
  3. Apply the theme to the site

Here’s the good news: there are API’s for all of this available!

Copying content

For copying content, I inspired myself with the bulk upload sample of the PnP team you can find here. This provided the basis for an uploader class. The most important code (all samples are in TypeScript by the way):

Upload(sourcePath: string, targetPath: string): JQueryPromise<void> {

    var deferred: JQueryDeferred<void> = jQuery.Deferred<void>();

    this.hostWebContext = new SP.ClientContext(Utils.getRelativeUrlFromAbsolute(this.hostWebUrl));
    var web = this.hostWebContext.get_web();

    this.hostWebContext.load(web);
    this.hostWebContext.executeQueryAsync(
        // in case of success
        () => {
            console.log("Host Web successfully loaded");

            var sourceFile = this.appWebUrl + sourcePath;
            //logMessage("Reading file from App Web <a href='" + sourceFile + "' target='_blank'>" + sourcePath + "</a><br /><br />", state.SUCCESS);
            //logMessage("<img src='" + sourceFile + "'><br />");
            // Read file from app web
            $.ajax(<JQueryAjaxSettings>{
                url: sourceFile,
                type: "GET",
                dataType: "binary",
                processData: false,
                responseType: 'arraybuffer',
                cache: false
            }).done((contents: number) => {

                var fileName: string = Utils.getFilenameFromUrl(targetPath);
                var folder: string = Utils.getPathFromUrl(targetPath);

                // new FileCreationInformation object for uploading the file
                var createInfo = new SP.FileCreationInformation();
                createInfo.set_content(Utils.arrayBufferToBase64(contents));
                createInfo.set_overwrite(true);
                createInfo.set_url(fileName);

                var targetFolder = Utils.getRelativeUrlFromAbsolute(this.hostWebUrl) + folder;

                // ensure the target folder has been created 
                this.ensureTargetFolder(Utils.getRelativeUrlFromAbsolute(this.hostWebUrl), folder).then((folder) => {

                    // add file to the folder
                    var files = folder.get_files();
                    this.hostWebContext.load(files);
                    files.add(createInfo);

                    // upload file
                    this.hostWebContext.executeQueryAsync(() => {
                        deferred.resolve();
                        var loadImage = this.hostWebUrl + "/" + folder + fileName;
                    }, (sender, args) => {
                        deferred.reject();
                    });
                });;
            }).fail((jqXHR, textStatus) => {
                deferred.reject();
            });

        },
        // in case of error
        (sender, args) => {
            deferred.reject();
        });

    return deferred.promise();
}

But I also needed a way to make sure the target path exists. This code takes care of that:

ensureTargetFolder(relativeUrl: string, folderPath: string): JQueryPromise<SP.Folder> {
    // to find the root folder, we need to traverse down the path until we find a 
    // folder that actually exists

    var parts = folderPath.split('/').filter((value) => { return value.trim() != '' });
    parts = parts.reverse();

    var deferred: JQueryDeferred<SP.Folder> = jQuery.Deferred<SP.Folder>();

    var folder = this.hostWebContext.get_web().getFolderByServerRelativeUrl(relativeUrl);
    this.hostWebContext.load(folder);
    this.hostWebContext.executeQueryAsync(() => {
        this.ensureChildFolders(folder, parts).then((folder) => {
            deferred.resolve(folder);
        });
    }, (sender, args) => {
        deferred.reject();
    });

    return deferred.promise();
}

ensureChildFolders(parentFolder: SP.Folder, folderStructure: string[]): JQueryPromise<SP.Folder> {
    // try to get the current path... when that succeedes; execute the function appending 
    // the next folder, if it doesn't; first create that folder

    var deferred: JQueryDeferred<SP.Folder> = jQuery.Deferred<SP.Folder>();

    if (folderStructure.length == 0) {
        deferred.resolve(parentFolder);
    }
    else {
        var folderUrl = folderStructure.pop();

        var folderRelativeUrl = Utils.appendPath(parentFolder.get_serverRelativeUrl(), folderUrl);

        var childFolder = this.hostWebContext.get_web().getFolderByServerRelativeUrl(folderRelativeUrl);
        this.hostWebContext.load(childFolder);

        this.hostWebContext.executeQueryAsync(() => {
            // folder exists; continue with the next part
            this.ensureChildFolders(childFolder, folderStructure).then((folder) => {
                deferred.resolve(folder);
            });
        }, (sender, args) => {
            // folder doesn't exist; create it and then continue
            childFolder = parentFolder.get_folders().add(folderUrl);

            this.hostWebContext.load(childFolder);
            this.hostWebContext.executeQueryAsync(() => {
                this.ensureChildFolders(childFolder, folderStructure).then((folder) => {
                    deferred.resolve(folder);
                });
            }, (sender, args) => {
                deferred.reject();
            });
        });
    }

    return deferred.promise();
}

Set the alternate CSS URL / theme

This was easier than I expected it to be. There simply is a method on the web object that allows you to set the URL. Same goes for the theme. Than there’s some plumbing, mainly the jQuery promises you’ll need to make sure all of the async stuff in done in the correct sequence. I found that I couldn’t combine setting the alternate css url + theme in one execution, it would then complain that the web was being altered by another process. So this is the safe way to get there:

ApplyTheme(): JQueryPromise<void> {
    var hostWebContext = new SP.ClientContext(Utils.getRelativeUrlFromAbsolute(this.hostWebUrl));
    var web = hostWebContext.get_web();

    var deferred: JQueryDeferred<void> = jQuery.Deferred<void>();

    hostWebContext.load(web);
    hostWebContext.executeQueryAsync(() => {
        var webRelativeUrl = web.get_serverRelativeUrl();

        var themeUrl = webRelativeUrl + "/_catalogs/theme/15/Atos.spcolor";
        var bgUrl = webRelativeUrl + "/_catalogs/masterpage/Atos/images/aeirial-view-of_traffic-and_overpasses.jpg";
        var cssUrl = webRelativeUrl + "/_catalogs/masterpage/Atos/atos.css";

        web.applyTheme(themeUrl, null, bgUrl, true);
        web.update();

        hostWebContext.executeQueryAsync(() => {

            web.set_alternateCssUrl(cssUrl);
            web.update();

            hostWebContext.executeQueryAsync(() => {
                deferred.resolve();
            }, (sender, args) => {
                deferred.reject("Setting alternate CSS failed: " + args.get_message());
            });

        }, (sender, args) => {
            deferred.reject("Setting theme failed: " + args.get_message());
        });
    }, (sender, args) => {
        deferred.reject("Loading the host web context failed: " + args.get_message());
    });

    return deferred.promise();
}

 

That’s all folks!

I’ve pushed the entire project to a GitHub repo you can find here: https://github.com/atosorigin/sharepoint-theming-app. Feel free to reuse it, feel just as free to brand all of your sites with the Atos branding although unless you’re a colleague I wouldn’t know why you would want to do that 😉

, ,

Related posts

Latest posts

Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *