Sunday, 11 June 2017

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

[Updated September 2017 for SPFx 1.2 RC0]

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 (N.B. these are the names from SPFx 1.2 onwards):

  • Top
  • Bottom
  • DialogContainer

N.B. We can expect more zones in the future! Here’s what the Top (header) and Bottom (footer) zones 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.

SPFX 1.2 changes

  • Changes to placeholder names – “Top” and “Bottom” insteaad of “PageHeader” and “PageFooter”
  • The onRender() method is deprecated/should no longer be used in SPFx extensions

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 Top and Bottom zones

In earlier versions of SPFx, some pages only had the Top zone but missed the Bottom zone. That’s now been fixed and it seems that if the Top zone exists on a page type (e.g. modern page, Site Contents page, document library or list page etc.), the Bottom one will too:

SNAGHTML53485bb

I showed a relatively narrow bar above, but there’s nothing to stop you making that top zone larger if you want to with CSS (this image is zoomed out):

SNAGHTML2418631

But 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.placeholderProvider.tryCreateContent() method to get a reference to the appropriate placeholder and it’s content - and the fact that it gives you the DOM element to manipulate (e.g. set innerHTML)

Deployment options – global or site-by-site

    In terms of what associates your customizer to the site, there are two ways of doing this in production:

    • Site-by-site – in this approach, you add some declarative XML to your app packaging, and then ensure the app is installed from the App Catalog to each site where your extension should operate. Specifically, 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.
    • Global/scripted – in this approach, you set the skipFeatureDeployment attribute to “true” in youre package-solution.json file, and then use CSOM or REST to add a CustomAction programmatically to each web as you need (i.e. by iterating, or including into some provisioning code). See https://dev.office.com/sharepoint/docs/spfx/tenant-scoped-deployment for more details. When using this approach, the admin has the option of making the SPFx web part/extension globally available when installing to the App Catalog:

      SNAGHTMLf7281c2

      SPFx web parts will show up in every site, but as I say, for SPFx extensions you also need to take care of the programmatic association/registration to each site/web you require, using CustomAction/ClientSideComponentId. See my post Manage tenant-scoped SPFx extensions across your SharePoint sites for some PowerShell/C# code to do this.

    But before packaging for production, 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 (which may look a little different to this in later versions of SPFx):

    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 (site-by-site/declarative approach)

    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 (remember, you can skip this if you plan to use skipFeatureDeployment=true and globally deploy via script):

      <?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 

    • 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!

    1 comment:

    Chunkyfeather said...

    This is fantastic!!! Now I can possibly embed a megamenu, which has been stopping us moving to use modern pages.