Dynamic default properties in SPFx web parts11 min read

Dynamic default properties in SPFx web parts11 min read

February 13, 2019 0 By Hugo Bernier

Introduction

SharePoint client-side web parts (SPFx) allow you to define custom properties that your users can use to customize your web parts.

You can set default values in your web part’s manifest.json file so that the web part is already pre-configured when a user adds your web part to a page.

For example, the following (fictitious) Deflatinator web part — which allows you to shoot a beam that will deflate everything within the Tri-state area has three custom properties:

  • deflateBeachBalls (boolean, default true) controls if it will deflate beach balls
  • deflateBlimps (boolean, default true) controls if it will deflate blimps
  • maxMirrorBounce (number, default 3) controls if the beam can bounce of mirrors (and increase chances that something will go wrong)
  • curseYou (string, default Perrry! (what else?)) controls who will be cursed if your plans go wrong.

Your web part\’s props will be defined as follows:

export interface IDeflatinatorWebPartProps {
  deflateBeachBalls: boolean;
  deflateBlimps: boolean;
  maxMirrorBounce: number;
  curseYou: string;
}

Your Deflatinator.manifest.json file would include a preconfiguredEntries section that looks like this:

  "preconfiguredEntries": [{
    "groupId": "5c03119e-3074-46fd-976b-c60198311f70",
    "group": { "default": "Other" },
    "title": { "default": "Deflatinator" },
    "officeFabricIconFontName": "Pinned",
    "description": { "default": "Deflates everything within the Tri-state area." },
    "properties": {
      "deflateBeachBalls": true,
      "deflateBlimps": true,
      "maxMirrorBounce": 3,
      "curseYou": "Perry!"
    }
  }]

Every time a user adds your Deflatinator web part, it will have those default values. If you configured your custom properties, your users will be able to customize the values as they wish.

The default values defined in your manifest.json are static — that is, the default value your users will receive will always be the same unless you change your manifest.json.

But what if you want different pre-configurations to be available to users?

Better yet, what if you want default values that change dynamically, depending on the user\’s language, permissions, or preferences? How about the SharePoint environment, current date, content of a list, or anything else?

Luckily, SPFx supports this!

Specifying multiple (but static) pre-configured entries

The first — and easiest — way to offer different configurations is to define multiple pre-configured entries in your manifest.json file.

For example, here is my Deflatinator.manifest.json file with two version of the web part: one that deflates blimps by default (deflateBlimps is true), and one that does not (deflateBlimps is false):

  "preconfiguredEntries": [{
    "groupId": "5c03119e-3074-46fd-976b-c60198311f70",
    "group": { "default": "Other" },
    "title": { "default": "Deflatinator" },
    "officeFabricIconFontName": "Pinned",
    "description": { "default": "Deflates everything within the Tri-state area." },
    "properties": {
      "deflateBeachBalls": true,
      "deflateBlimps": true,
      "maxMirrorBounce": 3,
      "curseYou": "Perry!"
    }
  },
  {
    "groupId": "5c03119e-3074-46fd-976b-c60198311f70",
    "group": { "default": "Other" },
    "title": { "default": "Deflatinator -- No blimps" },
    "officeFabricIconFontName": "Pinned",
    "description": { "default": "Deflates everything except for blimps within the Tri-state area." },
    "properties": {
      "deflateBeachBalls": true,
      "deflateBlimps": false,
      "maxMirrorBounce": 3,
      "curseYou": "Perry!"
    }
  }]

When users open the web part catalog, they will see two entries: Deflatinator and Deflatinator — No blimps. Depending on which web part entry they choose, the web part will either deflate blimps by default or not.

This is a good approach if you have a web part that can be used in a lot of different ways (like a web part with different views, or an Embed web part that allows you to embed different types of things in a page).

It is also a good way to emphasize different functionality within your web part.

However, it can also lead to over-crowding of your web part catalog. Imagine if we needed one pre-configured Deflatinator web part for every possible first name in the curseYou property?)

Specifying dynamic defaults

Luckily, you can define default properties when the user adds your web part to their page at run-time using the onInit event in your web part code.

During the onInit event, you can set the default properties any way you want.

The only tricky bit is that onInit expects a Promise<void> response — but don’t let that scare you!

Here is some code that sets the same default values as above:

protected onInit(): Promise<void> {
    // create a new promise
    return new Promise<void>((resolve, _reject) => {

        // set a default if Deflate Beach Balls has not been defined
        if (this.properties.deflateBeachBalls === undefined) {
            this.properties.deflateBeachBalls = true;
        }

        // set a default if Deflate Blimps has not beed defined
        if (this.properties.deflateBlimps === undefined) {
            this.properties.deflateBlimps = true;
        }

        // set a default if Mirror Bounce has not beed defined
      if (this.properties.maxMirrorBounce === undefined) {
        this.properties.maxMirrorBounce = 3;
      }

        // set a default if Curse You name hasn't been defined
        if (this.properties.curseYou === undefined) {
            this.properties.curseYou = 'Perry!';
        }

        // resolve the promise
        resolve(undefined);
    });
}

Of course, make sure to update your manifest.json file as follows:

  "preconfiguredEntries": [{
    "groupId": "5c03119e-3074-46fd-976b-c60198311f70",
    "group": { "default": "Other" },
    "title": { "default": "Deflatinator" },
    "officeFabricIconFontName": "Pinned",
    "description": { "default": "Deflates everything within the Tri-state area." },
    "properties": {
    }
  }]

NOTE: If you find that your changes to the manifest.json file don`t seem to take effect when debugging your solution, you may need to stop debugging, run gulp bundle, then restart debugging.

Using localized default values

The code above does exactly the same thing as if you defined default values in your manifest.json. If that\’s all you need, stick to setting the default values the manifest.json.

Let\’s try setting the default curseYou property to a localized name:

// assumes that when you created your web part it defined your localized strings
// and that you added a DefaultCurseYouName property
import * as strings from 'DeflatinatorWebPartStrings';
...
protected onInit(): Promise<void> {
    // create a new promise
    return new Promise<void>((resolve, _reject) => {

        // set a default if Deflate Beach Balls has not been defined
        if (this.properties.deflateBeachBalls === undefined) {
            this.properties.deflateBeachBalls = true;
        }

        // set a default if Deflate Blimps has not beed defined
        if (this.properties.deflateBlimps === undefined) {
            this.properties.deflateBlimps = true;
        }

        // set a default if Mirror Bounce has not beed defined
        if (this.properties.maxMirrorBounce === undefined) {
            this.properties.maxMirrorBounce = 3;
        }

        // set a default if Curse You name hasn't been defined
        if (this.properties.curseYou === undefined) {
            // BEGIN CHANGED: use the localized default name
            this.properties.curseYou = strings.DefaultCurseYouName;
            // END CHANGED
        }

        // resolve the promise
        resolve(undefined);
    });
}

Using current date and time

Ok, let\’s make things a bit more complicated; Let\’s pretend that your web part has a countdown (to indicate when the Deflatinator will trigger, of course) and that you want to store the triggerTime in a web part property.

You could update your IDeflatinatorWebPartProps to include a triggerTime prop:

export interface IDeflatinatorWebPartProps {
  deflateBeachBalls: boolean;
  deflateBlimps: boolean;
  maxMirrorBounce: number;
  curseYou: string;

  // BEGIN ADDED: Add triggerTime
  triggerTime: Date;
  // END ADDED
}

Now let\’s pretend that you want the triggerTime to automatically default to one day from when the user adds the web part. You would change your onInit method as follows:

  protected onInit(): Promise<void> {
    // create a new promise
      return new Promise<void>((resolve, _reject) => {

      // set a default if Deflate Beach Balls has not been defined
      if (this.properties.deflateBeachBalls === undefined) {
        this.properties.deflateBeachBalls = true;
      }

      // set a default if Deflate Blimps has not beed defined
      if (this.properties.deflateBlimps === undefined) {
        this.properties.deflateBlimps = true;
      }

      // set a default if Mirror Bounce has not beed defined
      if (this.properties.maxMirrorBounce === undefined) {
        this.properties.maxMirrorBounce = 3;
      }

      // set a default if Curse You name hasn't been defined
      if (this.properties.curseYou === undefined) {
        this.properties.curseYou = strings.DefaultCurseYouName;
      }

      // BEGIN ADDED: set a default Trigger Date
      if (this.properties.triggerTime === undefined) {
        // Get the current date
        const defaultTrigger: Date = new Date();

        // Add one day
        // I know, I know, I could use momentjs, but this is
        // the cheesy way to do it without extra libraries
        defaultTrigger.setDate(defaultTrigger.getDate() + 1);

        // Set the default date
        this.properties.triggerTime = defaultTrigger;
      }
      // END ADDED

      // resolve the promise
      resolve(undefined);
    });
  }

When the user adds your web part, the default triggerTime will automatically calculate tomorrow’s date.

NOTE: you’ll notice all my code above tests that the property is not undefined before setting the value. It handles cases where there is a default value configured in the manifest.json. It is not necessary, but it doesn\’t hurt to be extra careful, right?

Using current user information

So far, we’ve used pretty simple tricks to set default properties to a dynamic value, but what if we wanted to do something a bit more difficult? What if we wanted to use (gasp!) Promises?! (Insert omimous music here)

Let us pretend that — for whatever reason — we wanted the web part`s default property to use the name of the user who inserted the web part.

For this, we will use the awesome PnP/PnPjs libraries.

First, start by installing the library to your project by using the instructions from the PnP/PnPjs getting started page:

npm install @pnp/common @pnp/sp @pnp/logging @pnp/odata

NOTE:
YOU: "Hey, everything I have seen — including the PnP documentation — says that I need to add --save in my npm install command. You did not do that! Did you forget it?"
ME: No, the --save parameter is no longer required with npm install (see the documentation). It does not hurt if you have it, but it does not do anything anymore … assuming, of course, that you have a current version of npm.

Then add PnP libraries to your imports at the top of your web part code:

import { sp } from "@pnp/sp";
import { CurrentUser } from '@pnp/sp/src/siteusers';

Then change your onInit as follows:

protected onInit(): Promise<void> {
    // create a new promise
    return new Promise<void>((resolve, _reject) => {
      // set a default if Deflate Beach Balls has not been defined
      if (this.properties.deflateBeachBalls === undefined) {
        this.properties.deflateBeachBalls = true;
      }

      // set a default if Deflate Blimps has not beed defined
      if (this.properties.deflateBlimps === undefined) {
        this.properties.deflateBlimps = true;
      }

      //MOVED: moved the code to set the default Curse You name to the end of this function

      // Set a default Trigger Date
      if (this.properties.triggerTime === undefined) {
        // Get the current date
        const defaultTrigger: Date = new Date();

        // Add one day
        defaultTrigger.setDate(defaultTrigger.getDate() + 1);

        // Set the default date
        this.properties.triggerTime = defaultTrigger;
      }

      // set a default if Mirror Bounce has not beed defined
      if (this.properties.maxMirrorBounce === undefined) {
        this.properties.maxMirrorBounce = 3;
      }

      // BEGIN CHANGED: If there is no one to curse, get the current user
      if (this.properties.curseYou === undefined) {
        // No default value, get the current user's name
        sp.web.currentUser
          .select("Title") // don't retrieve everytyhing, we just want the display name
          .get()
          .then((r: CurrentUser) => {
            // set a default if Curse You name hasn't been defined

            // I always set a default value in case I can't get the current user's name
            let curseYouUser: string = strings.DefaultCurseYouName;

            // If we got user properties
            if (r !== undefined) {
              console.log("Yes to current user", r["Title"]);
              curseYouUser = r["Title"];
            }

            this.properties.curseYou = curseYouUser;

            // resolve the promise when done
            resolve(undefined);
          });
      } else {
        // Resolve the promise
        resolve(undefined);
      }
      // END CHANGED
    });
  }

You could also use the same approach to retrieve data from a SharePoint list, or from an external API.

Bonus benefits

There is an added benefit to setting default values in the onInit method: if you are debugging and testing your code, and want to make changes to the default values, you can just change the code in your onInit and your changes will be reflected next time you add the web part to a page.

If you changed your default values in your manifest.json instead, you would need to stop debugging, run gulp bundle, restart debugging, remove the web part, refresh the page, re-add the web part.

For a lazy person like me, it is much easier to change the onInit method. Just keep in mind that there are valid scenarios (like when you need to offer multiple versions of your web part) where it is better to use the manifest.json preconfiguredEntries.

Also, it doesn’t need to be a one-size-fits-all scenario: you can combine some entries in the manifest.json with some code in your onInit. That is why my code above always verifies that the value is undefined before I attempt to apply default values.

Just keep in mind the onInit gets called often. You want the code to be as fast and optimized as possible. For example, make sure the value you want to set as default is really empty before you call an API to get a default value.

Conclusion

SPFx allows you to pre-configure default values for your web part custom properties that get applied when a user first adds the web part to a page.

When you want to dynamically set default values, you can override the onInit method to apply any logic you need.

In this article I used a completely nonsense web part to demonstrate the concepts, but you can apply the same principles in your own (hopefully, less nonsense) web parts.

I hope this helps?

For more information

Advertisements