Sunday, 11 June 2017

Use an SPFx Application Customizer to add JavaScript (e.g. header) to every page in a site

New tools for customizing modern SharePoint sites and pages in Office 365 have arrived (in preview at the time of writing, June 2017).  These are known as “SharePoint Framework (SPFx) extensions”, and replace some tools that SharePoint developers have long used to deliver key scenarios such as:

  • Adding JavaScript to every page in a site/web
  • Injecting some content (e.g. a mega-menu/global navigation or message bar) into every page
  • Popping up dialog boxes in an integrated way
  • Adding items into certain toolbars/menus in SharePoint
  • Changing the rendering/behavior of a specific field in a list

In other words, SPFx extensions provide the equivalent of CustomActions and JSLink – previous dev approaches which didn’t necessarily translate to modern pages.

In this article I want to focus on the first two scenarios listed above (in bold) – referencing some JS on every page, and also running some code to put something in the header area of the page. The documentation provided by Microsoft does a good job on the 2nd scenario, but sometimes it’s good to have something a bit more visual so I’ll provide more screenshots. I’ll also talk about the scenario where you don’t necessarily want to add some *content* to the page, but you do want to add *some other form of script* to run on every page (e.g. analytics/whatever).

In terms of injecting content into the page, we now have the following zones in modern pages:

  • PageHeader
  • PageFooter
  • DialogContainer

N.B. We can expect more zones in the future! Here’s what the header and footer look like:

SNAGHTML4eec1a

Key information

Microsoft are currently saying that SPFx extensions will hit General Availability (i.e. fully-released in all tenants and suitable for production use) in fall/autumn of 2017. Until this time they are in preview.

Also be aware that what makes the new extensions possible is Microsoft's updates to tenants (only in developer tenants at the time of writing, not even in First Release), and updates to the Yeoman Generator that developers use to get started - this has a new set of component types which get you started with the right default code.

Previous limitations with modern pages

Modern pages have been frustrating because:

  • No possibility to run custom script
    • Global JS added with previous methods (CustomAction + JSLink) did not run here – only on “classic” pages
  • Corresponding lack of page extensibility
    • No way to inject content into the page

What’s changing here is that Microsoft are providing a hook to run your code, and are also providing named placeholders on modern pages – zones of the page which you can add content to. So long as you stick to these zones and don’t arbitrarily “hack” the page by changing other DOM elements (e.g. with jQuery or similar), then Microsoft effectively guarantee that updates to Office 365 will not impact your customizations.

The script you provide has to be installed to an app catalog and deployed that way, meaning that there is effectively an approval step. This means that simply editing the page to add a Script Editor web part no longer exists as the easy option – the script must be OK’d by an administrator. Lots of debate on this one of course, but ultimately it’s what Microsoft need to do to facilitate more governance and safeguard Office 365 as a stable platform.

Targeting placeholders such as the PageHeader and PageFooter zones

The earlier image showed where the PageHeader and PageFooter zones are. As shown above, these zones are present on “system” pages such as a list/library page, but note that some modern pages such as a regular Site Page (in a standard site or a Group site) don’t get all placeholders. For example, there is no footer here:

SNAGHTML4c4489

SNAGHTML5197b2

And there’s nothing to stop you making that header zone larger if you want to with CSS (this image is zoomed out):

SNAGHTML2418631

And of course, all this only applies to modern pages – classic pages do not have these zones or support SPFx extensions in general:

SNAGHTML48946ed

I’ll talk about the end-to-end process later, but to get straight to the code - with some minor tweaks/simplification to the suggested code in the documentation, mine looks like this:

And the CSS is implemented by adding an SCSS file in your extension’s directory – mine is named AppCustomizer.module.scss and has the following content:

Remember this is imported to the class for your customizer e.g:

import styles from './AppCustomizer.module.scss';

So, the key elements here are:

  • A class that derives from the ApplicationBaseCustomizer class
  • Use of the this.context.placeholders collection to get a reference to the appropriate placeholder - and the fact that it gives you the DOM element to manipulate (e.g. set innerHTML)

In terms of what associates your customizer to the site, this is done declaratively in the app packaging – your customizer has a manifest file which contains it’s ID ([MyCustomizer].manifest.json), and on top of this you actually add an elements.xml file with a CustomAction element (just like the old days!). This has a new "’ClientSideComponentId” attribute, and this must point to the ID of your customizer. But before then, there’s a mode when you can dev/test your customizer before worrying about packaging. This works by running a “gulp serve” locally and adding some querystring parameters to a modern page so that the manifest is loaded from localhost – it’s a bit like the “local SPFx workbench” equivalent but for SPFx extensions/customizers.

But I don’t need placeholders – I just want to reference some JavaScript on every page!

In this case, the code is somewhat simpler. If you have an external JS file you want to reference in a quick and dirty way, you could do this by dynamically adding a script tag to the <head> element of the page. My testing shows it seems safe to do this in the onInit method, but the onRender method would be fine also – in any case, it’s just the old-fashioned method like this:

But consider!

  • If the JS is hosted on another domain, you may need to enable CORS there (depending on what your JS is doing)
  • If you're referencing a module script, you could do this in a cleaner way by referencing it as an external module in the "externals" section of your config.json file (see Add an external library to your SharePoint client-side web part for more). I've tested and this approach does work with an Application Customizer
  • You could also choose to bundle your script if that made sense, and ensure it was referenced in the onRender method for your customizer. That should work too..

Process

The process is effectively the same whether you're targeting page placeholders or just referencing script on every page:

Update the SPFx Yeoman Generator if needed

The first step you might need to do is to update your SPFx Yeoman Generator – assuming you already have all the bits installed, you can do this by typing “yo” at the command-line and then going through the update process:

SNAGHTML38f4e932

Choose the “Update your generators” option and select “@microsoft/sharepoint”:

SNAGHTML38f64e11

SNAGHTML38f7128a

Creating an Application Customizer extension

[N.B. I’m essentially duplicating/walking through the main “Build your first extension” documentation here – you should reference that too.]

Once you’re ready to actually create your app customizer, do this by running that generator:

SNAGHTML3902e5e2

Give your solution a name, and ensure you select the “Extension” option:

SNAGHTML1dbcd6b

In this case, we’re using Application Customizer (rather than ListView Command Set Customizer [CustomAction/toolbar replacement] or Field Customizer [JSLink/field replacement]):

SNAGHTML1d772eb

Provide a name for your customizer and then a description:

SNAGHTML212d82d

The generator will then get busy creating your application with the appropriate files, and then you’ll see:

SNAGHTML2114dd4

Your application has now been created and you’ll get the boilerplate code:

SNAGHTML2152c30

It’s a good idea to test running this in debug mode before making any code changes, so do this by running a gulp serve with the “nobrowser” switch:

SNAGHTML216506c

The next step is to browse to a modern page, but adding some querystring parameters in the URL so that our *local* manifest for the customizer is loaded. First, open a browser to a modern page – a document library is a good choice:

SNAGHTML21b7c46

And then in Notepad or similar, build the querystring parameters you need. This basic format of this is:

?loadSPFX=true&
debugManifestsFile=https://localhost:4321/temp/manifests.js&
customActions={"badba93c-7f98-4a68-b5ed-c87ea51a3145":{"location":"ClientSideExtension.ApplicationCustomizer","properties":{"testMessage":"Hello as property!"}}}
  

However, you’ll need to replace the ID with the one from your customizer’s manifest file:

SNAGHTML224e8a1

SNAGHTML226aa94

If you paste that onto the end of the URL to the document library in your browser window and hit enter, you should see a warning message related to debug mode:

SNAGHTML228610e

Click the “Load debug scripts” button, and then your code should execute and you should see the results – in the case of the boilerplate code, it’s an alert box:

SNAGHTML22a488b

Success! You’ve now run an Application Customizer in debug mode.

Packaging for production

For this, I recommend following the steps in the documentation (start at Deploy your extension to SharePoint) – but below is an extract of the main steps. Ultimately it revolves around:

  1. Building your app, and deploying the bundled JS files to somewhere like a CDN (just like an SPFx web part)
  2. Adding some packaging files to your app, so that your customizer is called when the app is added to a site (a bit like feature activation – in fact, it IS feature activation ;))
  3. Deploying the app package to an App Catalog, and then adding the app to a site

In terms of the process, key steps are:

  • Create SharePoint/Assets folder and add an elements.xml file:

    SNAGHTML3954c9a1
  • Add the contents to elements.xml – set the “ClientSideComponentId” to identifier of your customizer i.e. the one found in the [MyCustomizer].manifest.json file:

    <?xml version="1.0" encoding="utf-8"?>
    <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
        <CustomAction
            Title="COB Global JS"
            Location="ClientSideExtension.ApplicationCustomizer"
            ClientSideComponentId="5dba1a34-6bbe-42ef-be72-94e01b527ce2">
        </CustomAction>
    </Elements>
          

  • Edit the config\package-solution.json file – add a “Features” node to reference your elements.xml file. It needs contents similar to the following:

      "features": [{
          "title": "COB AppCustomizer - global JS",
          "description": "Adds some JavaScript to every page in the site",
          "id": "456da147-ced2-3036-b564-8dad5c1c2e34",
          "version": "1.0.0.0",
          "assets": {        
            "elementManifests": [
              "elements.xml"
            ]
          }
        }]
  •    
  • Take care of some other steps related to CDN-hosting of your JS bundle (e.g. updating the ‘cdnBasePath’ property in the ‘write-manifests.json’ file), and then bundle and package your app using 'gulp bundle --ship' and 'gulp package-solution --ship' respectively.
  • As I say, head to the documentation for the full steps when you actually come to do this.

    The app is then upload to the app catalog:

    SNAGHTML2dc2e6a

    Notice that at this point, the admin needs to trust the application and will see where the remote files are hosted - in my case, I used the Office 365 public CDN:

    SNAGHTML9a5ac5

    You should then see your customizer take effect, and if you go looking you’ll see a web-scoped feauture (by default) which is binding your customizer to the site:

    SNAGHTML2cbe9ae

Other matters

  • Feature scope – it should be possible to adjust the feature scope from the default of “web” to “site”. I haven’t tried this yet, but expect it to be in the “features” node of package-solution.json
  • Property bag – as shown in the “Build your first extension” page, there’s a property bag of sorts that can be used with customizers. In production mode, properties are specified in the CustomAction element in your elements.xml file. In my example, I chose to use values specified directly in the code, but this property bag provides some level of separation (but it is still burnt into your package)

Happy customizing!