Tag

Extensions

Browsing

Why would you want to inject CSS?

Since Microsoft introduced Modern Pages to Office 365 and SharePoint, it is really easy to create beautiful sites and pages without requiring any design experience.

If you need to customize the look and feel of modern pages, you can use custom tenant branding, custom site designs, and modern site themes without incurring the wrath of the SharePoint gods.

If you want to go even further, you can use SharePoint Framework Extensions and page placeholders to customize well-known areas of modern pages. Right now, those well-known locations are limited to the top and bottom of the page, but I suspect that in a few weeks, we'll find out that there are more placeholder locations coming.

But what happens when your company has a very strict branding guideline that requires very specific changes to every page? When your customization needs go beyond what's supported in themes? When you need to tweak outside of those well-known locations?

Or, what if you're building a student portal on Office 365 and you need to inject a custom font in a page that is specifically designed to help users with dyslexia?

That's when I would use a custom CSS.

Here be dragons!

Before you go nuts and start customizing SharePoint pages with crazy CSS customizations, we need to set one thing straight:

With SharePoint, you should always colour within the lines. Don't do anything that isn't supported, ever. If you do, and you run into issues, you're on your own.

A badly coloured version of the SharePoint logo.
With SharePoint, you should always colour within the lines

Remember that Microsoft is constantly adding new features to SharePoint. The customizations you make with injecting custom CSS may stop working if the structure of pages change.

What's worse, you could make changes to a page that prevents new features from appearing on your tenant because you're inadvertently hiding elements that are needed for new features.

With custom CSS (and a CSS zen master), you can pretty much do anything you want. The question you should ask yourself is not whether you can do it, but whether it is the right thing to do.

Enough warnings! How do I inject custom CSS?

It is very easy. In fact, I'm probably spending more time explaining how to do it than it took me to write the code for this. If you don't care about how it works, feel free to download the source and install it.

Using SharePoint Framework Extensions, you can write code that you can attach to any Site, Web, or Lists. You can control the scope by how you register your extensions in your SharePoint tenant.

With an extension, you can insert tags in the HTML Head element.

I know what you're thinking: we can just insert a STYLE block at in the HEAD element and insert your own CSS. Sure, but what happens when you need to change your CSS? Re-build and re-deploy your extension? Nah!

Instead, how about inserting a LINK tag and point to a custom CSS that's located in a shared location? That way, you can modify the custom CSS in one place.

You can even have more than one custom CSS and use your extension properties to specify the URL to your custom CSS. In fact, you can add more than one extension on a site to combine multiple custom CSS together to suit your needs.

Building your custom CSS injection extension

You too can design a beautiful SharePoint site that looks like this:

sampleresults
I'm really a better designer than this. I just wanted a screen shot that smacks you in the face with a bright red bar and a custom round site icon. It hurts my eyes.
  1. Start by creating your own custom CSS (something better than I did, please). For example, the above look was achieved with the following CSS:
    .ms-compositeHeader {
        background-color: red;
    }
    .ms-siteLogoContainerOuter {
        border-radius: 50%;
        border-width: 3px;
    }
    .ms-siteLogo-actual {
        border-radius: 50%;
    }
  2. Save your custom CSS to a shared location on your SharePoint tenant. For example, you could save it in the Styles Library of your root site collection. You could also add it to your own Office 365 CDN. Make note of the URL to your CSS for later. For example, if you saved your custom CSS as contoso.css in the Styles Library of your tenant contoso.sharepoint.com, your CSS URL will be:
https://contoso.sharepoint.com/Style%20Library/contoso.css

which can be simplified to:

/Style%20Library/custom.css
  1. Create an SPFx extension following the instructions provided in the Build your first SharePoint Framework Extension (Hello World part 1) article. (Hey, why improve what's already perfect?).
  2. Change the props interface that was created for your ApplicationCustomizer class and replace the description property to cssurl. For example, my ApplicationCustomer class is called InjectCssApplicationCustomizer so my props interface is going to be called IInjectCssApplicationCustomizerProperties. Like this:
  1. Change your onInit method to insert a LINK element pointing to your cssurl property.
  1. In your serve.json located in the config folder, change the pageUrl to connect to a page on your tenant. Also change the cssurl property to pass the URL to the custom CSS you created in steps 1-2, as follows:
    1. Test that your extension works by running gulp serve. When prompted to allow debug scripts, select Load debug scripts.

DebugScriptWarning

You can now tweak your custom CSS to suit your needs, continuing to hit refresh until you're happy with the results.

Deploying to your production tenant

When ready to deploy, you need to bundle your solution, upload it to the app catalog, and enable the extension on every site you want to customize.

To make things easy, you can add an elements.xml file in your SharePoint folder and pre-configure your custom CSS URL. Here's how:

  1. In your solution's sharepoint/assets folder, create a new file called elements.xml. If you don't have a sharepoint folder or assets sub-folder, create them.
  2. Paste the code below in your elements.xml:
  1. Make sure to replace the custom action TitleClientSideComponentId to match your own extension. You can find those values in your InjectCssApplicationCustomizer.manifest.json, under id and alias.
  2. Change the ClientSideComponentProperties to point to your CSS URL. Pay attention to URL encode the values (e.g.: a space becomes %20).
  3. Run gulp bundle --ship to bundle your solution/
  4. Run gulp package-solution --ship
  5. Drag and drop the .sppkg file that was created in your sharepoint/solution folder to your tenant's app catalog.

If you selected to automatically deploy to all site collections when building the extension, you're done. If not, you'll need to go to every site and add the extension by using the Site Contents and Add an App links.

Conclusion

You can easily inject custom CSS in every modern page of your SharePoint tenant by using an SPFx extension, but be careful. With great CSS power comes great SharePoint responsibility.

You can get the code for this extension at https://github.com/hugoabernier/react-application-injectcss

I'd love to see what you're doing with your custom CSS. Let me know in the comments what you have done, and -- if you're interested -- share the CSS.

I hope this helps?

In part 1 of this article, I introduced the concept for an SPFx extension that adds a header to every page, showing the classification information for a site.

In part 2, we created an SPFx extension that adds a header that displays a static message with the security classification of a site.

In part 3, we learned more about property bags and learned a few ways to set the sc_BusinessImpact property (a property we made up) of our test sites to LBI, MBI, and HBI.

In part 4, we wrote the extension that reads from a site's property bags and displays the classification in the header.

In this part, we will clean up a few things, package and deploy the extension.

Preparing to deploy to production

The extension we wrote in parts 1-4 of this article works, but it isn't really production ready.

First, we'll want to change the code to only display the extension if a web can find a site's information security classification in its property bag. That way, if you chose to deploy the extension to production, you won't have to worry about affecting sites that do not have a security classification (although, it is recommended that every site has a classification, even if it is LBI by default).

Second, we'll change the hard-coded hyperlink to point to a page on your tenant that provides handling instructions for each security classification.

Then we'll remove all those hard-coded strings and replace them with localized strings.

Let's get started!

Conditionally display the extension

So far, our code assumes that every site has a security classification -- which is the right thing to do if you want to be compliant.

However, there are cases where you may want to deploy this extension in production and not display a security classification until you've actually applied a classification to a site.

To do this, we'll change our code a little bit.

  1. In ClassificationHeader.types.ts, we'll change the default classification to be undefined. So, we're changing this line:
    export const DefaultClassification: string = "LBI";
    

    to this line:

    export const DefaultClassification: string = undefined;
    
  2. Now let's change the render method in ClassificationHeader.tsx to handle an undefined value and skip rendering if there is no security classification. Change this code:
    var barType: MessageBarType;
        switch (businessImpact) {
          case "MBI":
            barType = MessageBarType.warning;
            break;
          case "HBI":
            barType = MessageBarType.severeWarning;
            break;
          default:
            barType = MessageBarType.info;
        }
    

    to this code:

        // change this switch statement to suit your security classification
        var barType: MessageBarType;
        switch (businessImpact) {
          case "MBI":
            barType = MessageBarType.warning;
            break;
          case "HBI":
            barType = MessageBarType.severeWarning;
            break;
          case "LBI":
            barType = MessageBarType.info;
            break;
            default:
            barType = undefined;
        }
    
        // if no security classification, do not display a header
        if (barType === undefined) {
          return null;
        }
    

When you're done, the code should look like this:

Test your extension again, making sure to try with an LBI, MBI, and HBI site, as well as any other site that hasn't been classified yet (i.e.: that doesn't have a security classification property bag value defined yet).

Linking to handling procedures

Since the first part of this article, I have been using a fake URL instead of an actual link to handling instructions. Let's set a default URL to display proper handling procedures.

  1. Start by creating a page on your SharePoint site that explains to your users how they should properly handle information based on their security classification. You can create one page, or (ideally) create a separate set of URLs for each classification.
  2. In ClassificationHeader.types.ts, we'll add a new constant to store the URL to the new handling procedures page you created. If you created more than one, feel free to add more than one constant. If you don't want to use a hyperlink, just set it as undefined. Add this line of code, with the URL of your choice:
    export const DefaultHandlingUrl: string = "/SitePages/Handling-instructions.aspx";
    

    Remember that your URLs should be absolute (e.g.: https://yourtenant.sharepoint.com/sitepages/handling-instructions.aspx) or at least relative to the root (e.g.: /sitepages/handling-instructions.aspx), because your links will get rendered on every page in the site.

  3. Now let's change the render method in ClassificationHeader.tsx to use the handling URL in the hyperlink. Change this code:
 public render(): React.ReactElement {
    // get the business impact from the state
    let { businessImpact } = this.state;

     // change this switch statement to suit your security classification
     var barType: MessageBarType;
     switch (businessImpact) {
       case "MBI":
         barType = MessageBarType.warning;
         break;
       case "HBI":
         barType = MessageBarType.severeWarning;
         break;
       case "LBI":
         barType = MessageBarType.info;
         break;
         default:
         barType = undefined;
     }
 
     // if no security classification, do not display a header
     if (barType === undefined) {
       return null;
     }
     
    return (
      
        This site is classified as {this.state.businessImpact}. Learn more about the proper handling procedures.
      
    );
  }

to this code (note that you'll need to add an import for DefaultHandlingUrl at the top (not shown here):

public render(): React.ReactElement {
    // get the business impact from the state
    let { businessImpact } = this.state;

    // ge the default handling URL
    let handlingUrl: string = DefaultHandlingUrl;

    // change this switch statement to suit your security classification
    var barType: MessageBarType;
    switch (businessImpact) {
      case "MBI":
        // if you'd like to display a different URL per classification, override the handlingUrl variable here
        // handlingUrl = "/SitePages/Handling-instructions-MBI.aspx"
        barType = MessageBarType.warning;
        break;
      case "HBI":
        barType = MessageBarType.severeWarning;
        break;
      case "LBI":
        barType = MessageBarType.info;
        break;
      default:
        barType = undefined;
    }

    // if no security classification, do not display a header
    if (barType === undefined) {
      return null;
    }

    return (
      
        This site is classified as {this.state.businessImpact}.
        {handlingUrl && handlingUrl !== undefined ?
           Learn more about the proper handling procedures.
          : null
        }
      
    );
  }

When you're done, the code should look like this:

Localizing resources

There are a few places in our code where we display some text that is hard-coded in the code.

Being of French-Canadian origins, I am especially sensitive to the aspect of localization; you shouldn't hard-code text, dates, numbers, currencies, and images in code if you can avoid it. Not only because it makes it easier to support easily support another language, but also because it makes it easy to maintain the text in your solution without wading through code.

Flashback: I remember working on a project where the geniuses in the marketing department changed the name of the product about 17 times while we were building it. Every time, the team would have to scour through the code to change the references to the product name. Once they learned the wonders of localization and string resources, they could change all references to the product name in a few seconds (they still gave the marketing department a hard time, though) 🙂

You only need to localize the code where something that is displayed could potentially change in a different locale. It's not just a different language, dates, numbers and currencies are displayed differently depending on where you live, even if you speak English. You don't need to worry about debugging code (e.g.: when you write to the console) unless you want people who speak in a different language to debug your code too.

Luckily, our code has only a few strings literals to worry about, and they're all in the ClassificationHeader.tsx.

You don't have to localize your code. But you should. So follow these instructions if you want to be a better SPFx developer:

  1. In the myStrings.d.ts file, located in the loc folder (source | extensions | classificationExtension | loc), add the following two lines to the
    IClassificationExtensionApplicationCustomizerStrings interface:
        "ClassifactionMessage": "This site is classified as {0}. ",
        "HandlingMessage": "Learn more about the proper handling procedures."
  2. In the en-us.js file, add two more lines below the "Title" line, making sure to add a comma at the end of the line that already exists:
    ClassifactionMessage: string;
    HandlingMessage: string;
  3. Now go to the ClassificationHeader.tsx file and add a reference to your localized strings at the top of the file, below all the other import statements:
    import * as strings from "ClassificationExtensionApplicationCustomizerStrings";
  4. Finally, replace the code in the render method to use the localized strings. Note that we're replacing the placeholder in the localization string with the classification label. We could have simply concatenated the values, but every language has a different syntax structure, and doing it this way makes it easier to deal with different language syntax.
    return (
            {strings.ClassifactionMessage.replace("{0}",this.state.businessImpact)}
            {handlingUrl && handlingUrl !== undefined ?
               {strings.HandlingMessage}
              : null
            }
        );

You code should look like this:

Optional: using configuration properties

The eagle-eyed reader may have noticed two things:

  1. There is a testMessage property that is defined in the ClassificationExtensionApplicationCustomizer.ts that we never use.
  2. The ClassificationPropertyBag, DefaultClassification, and
    DefaultHandlingUrl are all hard-coded. If you ever need to change any of the configuration items, you'd have to change the code, re-build, and re-deploy.

Thankfully, the SPFx team did a great job and designed SPFx extensions to support configuration properties. I don't know if that's what they're actually called, but that's what I call them 🙂

The testMessage is a sample configuration property that is created for us when we use the Yeoman generator. We can replace this property to anything that suits us. In our case, the ClassificationPropertyBag, DefaultClassification, and DefaultHandlingUrl.

To do this, let's follow these steps:

  1. Open ClassificationExtensionApplicationCustomizer.ts and replace the IClassificationExtensionApplicationCustomizerProperties interface code so that it looks like this:
    export interface IClassificationExtensionApplicationCustomizerProperties {
      ClassificationPropertyBag: string;
      DefaultClassification: string;
      DefaultHandlingUrl: string;
    }
  2. In the ClassificationHeader.types.ts file, add the same properties to the IClassificationHeaderProps interface by replacing the code to this:
    export interface IClassificationHeaderProps {
        context: ExtensionContext;
        ClassificationPropertyBag: string;
        DefaultClassification: string;
        DefaultHandlingUrl: string;
    }
  3. While you're in there, make sure to remove the other definitions of ClassificationPropertyBag, DefaultClassification, and DefaultHandlingUrl.
  4. Now back in ClassificationExtensionApplicationCustomizer.ts pass the properties to the ClassificationHeader props by replacing this code:
    const elem: React.ReactElement = React.createElement(ClassificationHeader, {
            context: this.context
          });

    to this:

    const elem: React.ReactElement = React.createElement(ClassificationHeader, {
            context: this.context,
            ClassificationPropertyBag: this.properties.ClassificationPropertyBag,
            DefaultClassification: this.properties.DefaultClassification,
            DefaultHandlingUrl: this.properties.DefaultHandlingUrl
          });
    
  5. To prevent any issues from not having any configuration information, let's add some code at the top of the onInit method:
    if (!this.properties.ClassificationPropertyBag) {
          const e: Error = new Error("Missing required configuration parameters");
          Log.error(LOG_SOURCE, e);
          return Promise.reject(e);
        }
  6. Finally, find any references to ClassificationPropertyBag, DefaultClassification, or DefaultHandlingUrl in ClassificationHeader.tsx and replace them to this.props.[property]. For example, replace ClassificationPropertyBag to this.props.ClassificationPropertyBag.

When you're done, the code should look like this:

This will allow you to pass configuration properties to the extension without having to change code.

To test this:

  1. Find serve.json in the config folder.
  2. Replace the "properties" attribute to pass the configuration we need, from this:
    "properties": {
                "testMessage": "Test message"
              }
    

    to this:

    "properties": {
                "ClassificationPropertyBag": "sc_x005f_BusinessImpact",
                "DefaultClassification": "",
                "DefaultHandlingUrl":"/SitePages/Handling-instructions.aspx"
              }
  3. Launch the extension by using gulp serve and test that the extension still works.

Note: if you're planning on debugging the extension, don't forget that the URL has now changed with these new properties. Follow the instructions earlier to copy the URL to the launch.json file.

Deploying to production

Assuming that everything works, we're only a few steps away from deploying to production:

  1. When you deploy the solution that includes the extension, SharePoint looks for the default configuration in the elementx.xml and uses whatever it found.  Since we changed the default properties, let's go change the elements.xml file (you can find it in the sharepoint folder) to the following:
    <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
        <CustomAction
            Title="ClassificationExtension"
            Location="ClientSideExtension.ApplicationCustomizer"
            ClientSideComponentId="4017f67b-80c7-4631-b0e5-57bd266bc5c1"
            ClientSideComponentProperties="{"ClassificationPropertyBag":"sc_x005f_BusinessImpact","DefaultClassification":"","DefaultHandlingUrl":"/SitePages/Handling-instructions.aspx"}">
        </CustomAction>
    </Elements>
    
  2. From the Terminal pane type:
    gulp bundle --ship
  3. Followed by:
    gulp package-solution --ship
  4. Navigate to your tenant's App Catalog  (e.g.: https://yourtenant.sharepoint.com/sites/apps) site and navigate to the Apps for SharePoint library.
  5. Find the folder where the package was created by going to Visual Studio Code and finding the sharepoint | solution folder, right-clicking and selecting Reveal in explorer.
  6. Drag and drop the classification-extension.sppkg solution package to the Apps for SharePoint library.

You should be able to go visit your classified sites and see the extension at work. If it doesn't work, you may have elected to not automatically deploy the solution to every site when you built the extension. If that's the case, you'll need to add the extension to the sites by using Add an App.

Conclusion

It took 5 parts to describe how to build the extension, but we successfully created an extension that reads a site's security classification from its property bag and displays the site's classification in a label.

In our article, we manually set the classification by modifying the property bag, but in the real world, we'll want to use an approach that automatically classifies sites when they are created.

The code for this application (including any modifications I may have made to it since publishing this article) can be found at: https://github.com/hugoabernier/react-application-classification.

If you're interested in seeing how we might approach automatically classification, let me know in the comments and maybe I'll create another (series of) article(s).

I hope this helps!?

 

In part 1 of this article, I introduced the concept for an SPFx extension that adds a header to every page, showing the classification information for a site.

In part 2, we created an SPFx extension that adds a header that displays a static message with the security classification of a site.

In part 3, we learned more about property bags and learned a few ways to set the sc_BusinessImpact property (a property we made up) of our test sites to LBI, MBI, and HBI.

In this part, we will finally get to add code to our extension that reads the property bag of the current site and displays the appropriate site classification label.

Reading the classification from the site's property bag

You can get the property bag of a site using a simple REST call to https://yourtenant.sharepoint.com/sites/yoursite/_api/web/allProperties  but it is even easier to use the SP PnP JS library make queries like these.

Adding the SP PnP JS library to your project

Open the Visual Studio Code solution you created in part 2 and perform the following steps:

  1. Open the terminal pane (CTRL-`).
  2. From the terminal pane, type:
    npm i sp-pnp-js --save
  3. We'll need to update the ExtensionContext in the IClassificationHeaderProps interface. It will allow the ClassificationHeader component to access the context used to make PnP calls. We'll also add a couple variables to the IClassificationHeaderState interface: one to keep the classification we'll retrieve from the property bag, and one to keep track if we're still loading the page.
    The code also defines the classification property bag name (sc_BusinessImpact) and the default classification ("LBI") for when it doesn't find a classification for a site. Feel free to change either of those values to what makes sense for your needs.
    Simply copy and paste the following code to ClassificationHeader.types.ts:
  1. Now we need to pass the ExtensionContext to the ClassificationHeader component. Open the ClassificationExtensionApplicationCustomizer.ts file and paste the following code (line 53 is the only line that was updated):
  1. Now we just need to make the ClassificationHeader component query the property bag when component mounts, save the classification in the state variable and change the render code to display the classification. Just copy the code below to ClassificationHeader.tsx:

That should be it, let's try it!

  1. From the Terminal pane in Visual Studio Code, type:
    gulp serve
  2. It should launch the browser to the page you had set up in part 2, in serve.json. If prompted to run debug scripts, accept.
  3. Assuming that the default page is not one of your LBI, MBI, or HBI test pages, you should get the default value classification (e.g.: LBI).
  4. Change the first part of the browser's URL to point to your HBI page (change the part before ?debugManifestsFile=...), and it should tell you that the site is classified HBI.
  5. Repeat step 4 with your LBI and MBI sites and make sure that you get the right messages.

If everything went well, your sites displayed the right classification, but the message bar didn't change from the default yellow warning. Let's change that.

Changing the message bar type based on the site classification

  1. Change the render method of the ClassificationHeader.tsx to display a message bar type "warning" for MBI, and "severeWarning" for HBI, and "info" for everything else. The render method should look like this:

Try the LBI, MBI, and HBI test pages again just like you did before, except this time, you should get the following:

TestMBI2
MBI Test Site
TestHBI
HBI Test Site

Help! The extension stops loading when I changed pages and it stopped prompting me if I want to load the debug scripts!

You most likely forgot to include the part after ?debugManifestsFile=… in the URLTry to launch the extension again (gulp serve) and copy the part of the URL with the ? to your test pages.

(I know because I did this a few times)

How to debug the extension

In theory, the extension should work and load at least the default LBI message. But what if you want to debug the extension?

Here is a simple trick:

  1. Launch your extension by using gulp serve as you did above.
  2. Copy the everything in the URL from the ?. It should look like something like this:
    ?debugManifestsFile=https%3A%2F%2Flocalhost%3A4321%2Ftemp%2Fmanifests.js&loadSPFX=true&customActions=%7B%224017f67b-81c7-5631-b0e5-57bd266bc5c1%22%3A%7B%22location%22%3A%22ClientSideExtension.ApplicationCustomizer%22%2C%22properties%22%3A%7B%22testMessage%22%3A%22Test%20message%22%7D%7D%7D
  3. In your Visual Studio Code project, find launch.json under the .vscode folder.
  4. If you don't have such a file, you probably need to install the Chrome Debugger Extension for Visual Studio Code. Just go to https://aka.ms/spfx-debugger-extensions and follow the instructions to install it.
  5. Find the configuration entry that starts with "name": "Hosted Workbench" and paste the ugly URL you got in step 2 at the end of the URL marked "url". This will add the instructions to load the extension in debug mode.
  6. From the Terminal pane, type:
    gulp serve --nobrowser
  7. This will start the local web server but won't launch the browser.
  8. Set a few breakpoints where you want to debug the code by using F9. For example, the render method of the ClassificationHeader component.
  9. From the Debug menu in Visual Studio Code, select Start Debugging and it should launch Chrome to the page you specified in launch.json, prompt you to login, then prompt you to run Debug scripts. Accept and you should be able to debug through the code.

This should be all for today. Next part of this article will clean up some of the code, add localized strings, and prepare the code for production and deploy it!.