Category

SharePoint

Category

Introduction

Microsoft has an awesome web site called SharePoint Design. It provides design guidance on beautiful and fast sites, pages, and web parts with SharePoint in Office 365.

However, it does not tell you how to create those beautiful web parts.

This blog series is intended as a companion to the SharePoint Design site. It provides code samples and detailed how-to information for every design topic.

It should help you create web parts that look exactly like the ones on the SharePoint Design site.

So far, we have discussed the following topics:

In today's post, we'll continue our discussion about the web part layout patterns and discuss the compact layout.

What is the Compact layout?

Compact layout

According to the SharePoint Design site:

The compact layout is designed to show content in a smaller format and works the best in a one-third column. This layout can support a small image or icon and a few rows of text for a title, description, and/or metadata.

However, it seems that the compact layout is not only used in 1/3 columns. The SharePoint look book and the SharePoint Online Provisioning Service both make use of the compact layout with the Quick Links web part.

When to use the compact layout

Use the compact layout when the items in your web part have very little information. A title and an icon or a small thumbnail is pretty much all you'll be able to fit it.

How is it made?

The compact layout is simply a Grid layout, but where the individual items use a compact document card layout instead of the full document card.

To create your own, you would use an Office UI Fabric List control and render each item inside the list with a DocumentCard control where the type of the DocumentCard control would be set to DocumentCardType.compact.

Or, you can simply use the component I built to make my life easier.

How to create a web part with the compact layout

Note: The source code for this sample can be found in the WebPartLayouts sample on my repo.

  1. Create your own web part solution. For this sample, we'll assume that your web part is called CompactWebPart and that the component which renders the content of the web part is called Compact.

  2. Copy the content of the src\components\compactLayout from my sample code to your own solution. You may need to create a components and compactLayout folder under src to do so.

  3. In your web part, load the items you wish to display. For this example, we'll set them in your web part's state and hard-code them in your web part's constructor, but feel free to load any data you want to use. We'll want to have at least a title attribute and a thumbnail attribute, but -- as you'll see later -- you can really display any information you want when you render each item.

    constructor(props: ICompactProps) {
    super(props);
    
    // Sample data generated at https://mockaroo.com/
    this.state = {
      items: [{
        thumbnail: "https://robohash.org/nostrumquiiure.png?size=48x48&set=set1",
        title: "Aerified"
      }, {
        thumbnail: "https://robohash.org/minimafugitenim.png?size=48x48&set=set1",
        title: "Viva"
      }, {
        thumbnail: "https://robohash.org/nihilbeataeculpa.png?size=48x48&set=set1",
        title: "Overhold"
      }, {
        thumbnail: "https://robohash.org/essequiquo.png?size=48x48&set=set1",
        title: "Latlux"
      }, {
        thumbnail: "https://robohash.org/inipsumtotam.png?size=48x48&set=set1",
        title: "Biodex"
      }, {
        thumbnail: "https://robohash.org/utmodiet.png?size=48x48&set=set1",
        title: "Bitchip"
      }, {
        thumbnail: "https://robohash.org/undeenimvel.png?size=48x48&set=set1",
        title: "Rank"
      }, {
        thumbnail: "https://robohash.org/pariaturoditdolore.png?size=48x48&set=set1",
        title: "Opela"
      }, {
        thumbnail: "https://robohash.org/nullaullamincidunt.png?size=48x48&set=set1",
        title: "Rank"
      }, {
        thumbnail: "https://robohash.org/accusantiumnonvoluptatibus.png?size=48x48&set=set1",
        title: "Bitchip"
      }, {
        thumbnail: "https://robohash.org/culpaeossapiente.png?size=48x48&set=set1",
        title: "Sonsing"
      }, {
        thumbnail: "https://robohash.org/harumnihilvelit.png?size=48x48&set=set1",
        title: "Duobam"
      }, {
        thumbnail: "https://robohash.org/quianesciuntet.png?size=48x48&set=set1",
        title: "Prodder"
      }, {
        thumbnail: "https://robohash.org/aliquidipsamrem.png?size=48x48&set=set1",
        title: "Keylex"
      }, {
        thumbnail: "https://robohash.org/dignissimoseosaccusamus.png?size=48x48&set=set1",
        title: "Span"
      }, {
        thumbnail: "https://robohash.org/exomnisexcepturi.png?size=48x48&set=set1",
        title: "Stringtough"
      }, {
        thumbnail: "https://robohash.org/occaecatimolestiaererum.png?size=48x48&set=set1",
        title: "Prodder"
      }, {
        thumbnail: "https://robohash.org/consequaturinquis.png?size=48x48&set=set1",
        title: "Alpha"
      }, {
        thumbnail: "https://robohash.org/sapienteofficiisest.png?size=48x48&set=set1",
        title: "Job"
      }, {
        thumbnail: "https://robohash.org/similiquesuntiusto.png?size=48x48&set=set1",
        title: "Cookley"
      }, {
        thumbnail: "https://robohash.org/sitnequequi.png?size=48x48&set=set1",
        title: "Stronghold"
      }]
    };
    }
  4. In your web part's component, add an import for the compactLayout. You'll also need an import for the UI Fabric DocumentCard component:

    // Used to render document cards
    import {
    DocumentCard,
    DocumentCardPreview,
    DocumentCardDetails,
    DocumentCardTitle,
    IDocumentCardPreviewProps,
    DocumentCardType
    } from 'office-ui-fabric-react/lib/DocumentCard';
    import { ImageFit } from 'office-ui-fabric-react/lib/Image';
    import CompactLayout from '../../../components/compactLayout/CompactLayout';
  5. Add a _onRenderGridItem method:

    private _onRenderGridItem = (item: any, _index: number): JSX.Element => {
    const previewProps: IDocumentCardPreviewProps = {
      previewImages: [
        {
          previewImageSrc: item.thumbnail,
          imageFit: ImageFit.centerCover,
          height: 48,
          width: 48
        }
      ]
    };
    
    return <div
      data-is-focusable={true}
      data-is-focus-item={true}
      role="listitem"
      aria-label={item.title}
    >
      <DocumentCard
        type={DocumentCardType.compact}
        onClick={(ev: React.SyntheticEvent<HTMLElement>) => alert(ev)}
      >
        <DocumentCardPreview {...previewProps} />
        <DocumentCardDetails>
          <DocumentCardTitle
            title={item.title}
            shouldTruncate={true}
          />
        </DocumentCardDetails>
      </DocumentCard>
    </div>;
    }
    }
  6. Update your component's render method to render the compact layout as follows:

    public render(): React.ReactElement<ICompactProps> {
    return (
      <div className={styles.compact}>
        <CompactLayout
          items={this.state.items}
          onRenderGridItem={(item: any, index: number) => this._onRenderGridItem(item, index)}
        />
      </div>
    );
    }

That's really all there is to it! You web part will render something like this:
The compact layout

The great thing is that it is entirely up to you how you want to render each item. I chose to use the DocumentCard control, but you can replace any part of your _onRenderGridItem method to suit your needs.

For example, if you wanted to render a date instead of an thumbnail, you could use something like the DateBox component that I wrote for the React Calendar Feed sample.

The Datebox control

Adding pagination

When I first built the React Calendar Feed sample, the Events web part showed little Previous and Next buttons at the bottom of the web part when displaying in the compact mode.

It seems that the pagination has since been removed from the standard web part design. Nevertheless, I have included a sample control to show how you can add pagination to your compact web part.

Use it at your discretion.

To add pagination to your web part, follow these steps:

  1. Copy the content of the src\components\paging from my sample code to your own solution. You may need to create a paging folder under src\components to do so.
  2. Add a variable to store the current page number in your component's state, as follows:
    export interface ICompactState {
    items: any[];
    currentPage: number;
    }
  3. In your component's constructor, set the current page to 1, as follows:

    constructor(props: ICompactProps) {
    super(props);
    
    // Sample data generated at https://mockaroo.com/
    this.state = {
      currentPage: 1,
      items: [{
        thumbnail: "https://robohash.org/nostrumquiiure.png?size=48x48&set=set1",
        title: "Aerified"
      }, {
        thumbnail: "https://robohash.org/minimafugitenim.png?size=48x48&set=set1",
        title: "Viva"
      }, {
        thumbnail: "https://robohash.org/nihilbeataeculpa.png?size=48x48&set=set1",
        title: "Overhold"
      }, {
        thumbnail: "https://robohash.org/essequiquo.png?size=48x48&set=set1",
        title: "Latlux"
      }, {
        thumbnail: "https://robohash.org/inipsumtotam.png?size=48x48&set=set1",
        title: "Biodex"
      }, {
        thumbnail: "https://robohash.org/utmodiet.png?size=48x48&set=set1",
        title: "Bitchip"
      }, {
        thumbnail: "https://robohash.org/undeenimvel.png?size=48x48&set=set1",
        title: "Rank"
      }, {
        thumbnail: "https://robohash.org/pariaturoditdolore.png?size=48x48&set=set1",
        title: "Opela"
      }, {
        thumbnail: "https://robohash.org/nullaullamincidunt.png?size=48x48&set=set1",
        title: "Rank"
      }, {
        thumbnail: "https://robohash.org/accusantiumnonvoluptatibus.png?size=48x48&set=set1",
        title: "Bitchip"
      }, {
        thumbnail: "https://robohash.org/culpaeossapiente.png?size=48x48&set=set1",
        title: "Sonsing"
      }, {
        thumbnail: "https://robohash.org/harumnihilvelit.png?size=48x48&set=set1",
        title: "Duobam"
      }, {
        thumbnail: "https://robohash.org/quianesciuntet.png?size=48x48&set=set1",
        title: "Prodder"
      }, {
        thumbnail: "https://robohash.org/aliquidipsamrem.png?size=48x48&set=set1",
        title: "Keylex"
      }, {
        thumbnail: "https://robohash.org/dignissimoseosaccusamus.png?size=48x48&set=set1",
        title: "Span"
      }, {
        thumbnail: "https://robohash.org/exomnisexcepturi.png?size=48x48&set=set1",
        title: "Stringtough"
      }, {
        thumbnail: "https://robohash.org/occaecatimolestiaererum.png?size=48x48&set=set1",
        title: "Prodder"
      }, {
        thumbnail: "https://robohash.org/consequaturinquis.png?size=48x48&set=set1",
        title: "Alpha"
      }, {
        thumbnail: "https://robohash.org/sapienteofficiisest.png?size=48x48&set=set1",
        title: "Job"
      }, {
        thumbnail: "https://robohash.org/similiquesuntiusto.png?size=48x48&set=set1",
        title: "Cookley"
      }, {
        thumbnail: "https://robohash.org/sitnequequi.png?size=48x48&set=set1",
        title: "Stronghold"
      }]
    };
    }
  4. Add an import for the paging component at the top of your file:
    import { Paging } from '../../../components/paging';
  5. Change your render method to get a subset of items to show, as follows:

    public render(): React.ReactElement<ICompactProps> {
    let pagedItems: any[] = this.state.items;
    const totalItems: number = pagedItems.length;
    let showPages: boolean = false;
    const maxEvents: number = 5; // Use any page size you want
    const { currentPage } = this.state;
    
    if (true && totalItems > 0 && totalItems > maxEvents) {
      // calculate the page size
      const pageStartAt: number = maxEvents * (currentPage - 1);
      const pageEndAt: number = (maxEvents * currentPage);
    
      pagedItems = pagedItems.slice(pageStartAt, pageEndAt);
      showPages = true;
    }
    
    return (
      <div className={styles.compact}>
        <CompactLayout
          items={pagedItems}
          onRenderGridItem={(item: any, index: number) => this._onRenderGridItem(item, index)} />
    
        {showPages &&
          <Paging
            showPageNumber={true}
            currentPage={currentPage}
            itemsCountPerPage={maxEvents}
            totalItems={totalItems}
            onPageUpdate={this._onPageUpdate}
            nextButtonLabel={strings.NextLabel}
            previousButtonLabel={strings.PreviousLabel}
          />
        }
      </div>
    );
    }
  6. Finally, add a method to store the current page number in your state every time the page changes, as follows:
    private _onPageUpdate = (pageNumber: number): void => {
    this.setState({
      currentPage: pageNumber
    });
    }

Test your web part. You should now get a next and previous buttons at bottom of your web part.

Compact pagination

Conclusion

The compact layout is simply a grid control which renders compact document cards.

In our next post, we'll discuss the list layout!

Introduction

According to Microsoft documentation, you can use a base64-encoded image for your web part icons. However, base64-encoded SVG images -- as is shown in the documentation -- don't work as expected. And by the looks of it, this hasn't worked for a while.

Since the SPFx team has been working tirelessly to deliver more than 15 releases of SPFx since its general availability, I'm sure we can forgive them for letting something so minor fall through the cracks.

However, I really don't like how icons that were converted from PNG look slightly blurry. (I tend to obsess about little things like that sometimes).

As I was writing another post for my SharePoint Framework Design series, I decided to get to the bottom of the issue.

As it turns out, it was pretty easy to solve.

TL;DR

This is an interactive post: we'll convert your SVG so that you can use it as an SPFx web part icon.

If you don't care about the explanation, you can go straight to the solution

The Challenge

My challenge was that I wanted to use SVG images in my web part's manifest, but I didn't want to have a separate SVG asset. I wanted to define a base64-encoded SVG file, as is demonstrated in the SharePoint documentation.

I knew it didn't work (believe me, I've tried), so I wanted a solution that would not rely on any unsupported hacks.

Since I don't have access to change the SharePoint code, I needed a solution that would work without requiring any changes from SharePoint.

How SPFx web part icons work

According to the Microsoft documentation, for your SPFx web part icon you can use either an Office UI Fabric icon, an external icon image, or a base64-encoded image.

To change your icon to an Office UI Fabric icon, you simply open your web part's [YourWebPartName].manifest.json and change the "officeFabricIconFontName" node to the name of the icon you want from the UI Fabric site.

The default icon is Page. If you wanted to change your icon to the Sunny icon, you would simply change the following line:

"officeFabricIconFontName": "Page",

To this:

"officeFabricIconFontName": "Sunny",

Note that you have to re-build the web part (run gulp build) and reload your workbench (press [F5] on your keyboard) before you'll see the new icon. Trust me, I wasted a lot of time trying to figure out why my new icons weren't showing up.

If you want to set the icon to an external image, you'll want to find the URL of a publicly-accessible image and change your web part's manifest "officeFabricIconFontName" attribute from the following:

"officeFabricIconFontName": "Page",

For a "iconImageUrl" image URL, as follows:

 "iconImageUrl": "https://assets.contoso.com/weather.png",

You can also use a CDN, or use your SharePoint CDN to store your images.

If you don't want to use an external image, you'll want to use a base64-encoded image.

To do so, you'll have to first convert your image using a tool like this web site. Once you have your base64-encoded image, you'll change your web part's manifest from this:

"officeFabricIconFontName": "Page",

To this:

 "iconImageUrl": "data:...",

And you would put the entire base64-encoded string as the web part's URL. Because you see, the base64-encoded string is actually a valid URL.

It is just a special kind of URL, called a data URI.

About data URI

Base64-encoded images, whether they are SVG or PNG, JPG, or GIF, rely on data URI. Data URI allow you to embed a resource in a URI which would normally result in a separate HTTP request.

Most data URI consists of the data type of the item (e.g.: image/png), followed by a semicolon, and the data of that file.

The cool thing about data URI is that -- on most modern browsers -- you can pretty much use them anywhere you would normally use a link to a resource.

Parker

For example, to show the above image of Parker in HTML, you would embed an img element and point the src attribute to the URL of the image, as follows:

<img src="https://avatars0.githubusercontent.com/u/31443929?s=200&v=4" />

Using a data URI, you could also do this:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAABpUUlEQVR4nOxdBVgU2xc/S3d3d0iDtKgI2N3d+tBnd+uzu7D924mJGIgoIAooKiGIIp1KLsvS+f/mbrAxu+xiofL75JvZmTt3Ztd75vQ5Qi0tLdCJTnQCHwK/+gE60YmOjE4C6...

Which produces this image:

The first version of this image had a URL pointing to the original source of the image, while the second one contained the actual image!

To generate the super-long gibberish, I used this online base64-encoder and uploaded a copy of Parker's avatar, then copied that super-long string it generated into the src attribute of my img tag.

You have to base64-encode the image because you're trying to embed a binary file inside of HTML.

Except that SVG files are not binaries...

Using data URI without base64-encoding

SVG files are simply text markup files, like HTML.

During my research, I found an article entitled Probably don't base64 SVG which explains that when you base64-encode an image...

"It takes 4 characters per 3 bytes of data, plus potentially a bit of padding at the end."

Essentially, base64-encoding SVG takes more room than the original file! Files are essentially 133% bigger!

Fortunately, you can actually use your UTF-8 encoded SVG file inside a data URI as follows:

<img src="data:image/svg+xml; ... ">

Now, you need to make sure that you don't embed unsupported characters inside your src attribute. For example, if your SVG has a single quote (') and your src attribute uses single-quotes to define it, you'll want to escape the '.

Fortunately, Taylor Hunt wrote a great article explaining how to encode SVGs in data URI while optimizing them to help you with that.

Taylor's awesome article inspired someone to write a codepen to encode SVGs.

I forked Jakob-e's codepen to make my own which makes it easy to encode SVGs to work as a data URI icon for a web part's manifest.

Using a data URI SVG as your icon

To use your custom SVG as an icon, take the following steps:

  1. Design an icon so that it fits within a square, ideally 64x64, or at least 32x32.

  2. Make sure that your icon <svg> element sets the width and height attributes -- otherwise, your icon will be cropped and/or will not appear centered. To do so, simply change your SVG opening tag from:

    <svg>

    To this:

    <svg height="64" width="64">
  3. Consider optimizing your SVG by using SVGOMG or a similar tool.

  4. Paste your SVG in the box below to encode your SVG

VariableValue
Original SVG

  1. In your web part's [YourWebPartName].manifest.json, change the following line:

    "officeFabricIconFontName": "Page",

    To this:


    Don't forget the extra , at the end of the line.

  2. Rebuild your web part by running gulp build
  3. Refresh your workbench

Your new SVG icon should appear!

Conclusion

You still can't use base64-encoded icons as your web part icon -- at that's probably a good thing, because base64-encoded SVG files are bigger! -- but you can use a data URI image instead.

This article shows you how to convert your SVG so that it will produce beautiful web part icons.

Regardless of whether Microsoft ever fixes the issue with SVG icons for web parts, you should still consider optimizing your SVG before using it in your solutions.

I hope this helps?

Updates

  • September 1, 2019: This approach will also work for application extension.

Sources

Introduction

I recently wrote a post called My GitHub cheat sheet for PnP contributions -- an interactive cheat sheet which explains the GitHub commands that I use when I start a contribution. The post is interactive: you just tell it your GitHub username and what repository you want to contribute to, and it customizes the instructions for you.

I also wrote another post talking about how impressed I am with David Warner II's offer to help anyone with their first contribution.

After briefly chatting with David I realized that the biggest hurdle for people is that they just don't know where to get started. In my post, I recommend that you read the contribution guidelines for every repo, but I found that they are often hard to find in each repository.

I also say that most repositories want you to start from the dev branch, but as the Chris Kent pointed out in yesterday's PnP community call, some repositories prefer you use the master branch.

So, with David's help, we compiled a list of the most common PnP repositories to help you get started.

The list contains the following:

  • Repo: Name of the repository
  • What is it?: Description of the repository
  • Getting started: Links to the most likely resource if you want to get started contributing to that repository
  • Branch: The branch you should target when submitting your pull requests

Please note that the information in each of the repositories can change and that you should always refer to the repository for the latest information.

If you find that we forgot a repository, or that something is wrong, let us know in the comments of via Twitter and we'll get it updated!

Popular repositories

RepoWhat is it?Getting startedBranch
SharePoint/PnP-PowerShellSharePoint PnP PowerShell CmdLets https://aka.ms/sppnp-powershellContribution guidancedev
SharePoint/sp-dev-docsSharePoint Developer Documentation https://docs.microsoft.com/en-us/sharepoint/dev/Contribute to SharePoint developer documentationmaster
SharePoint/sp-dev-fx-controls-reactReusable React controls for SPFx solutions https://sharepoint.github.io/sp-dev-fx-controls-react/- Contribution guidelines
- Submitting a PR
dev
SharePoint/sp-dev-fx-extensionsCode samples and developer content targeted towards SharePoint Framework client-side extensions. http://dev.office.com/sharepointContribution guidanceCreate from:master
Submit to: dev
SharePoint/sp-dev-fx-library-componentsSample solutions from community around the SharePoint Framework library componentContribution guidanceCreate from: master
Submit to: dev
SharePoint/sp-dev-fx-property-controlsReusable SPFx property pane controls - Open source initiative https://sharepoint.github.io/sp-dev-fx-property-controls/- Contribution guidelines
- Submitting a PR
dev
SharePoint/sp-dev-fx-webpartsCode samples and developer content targeted towards SharePoint Framework client-side web parts. http://aka.ms/spfxContribution guidelinesCreate from: master
Submit to: dev
SharePoint/sp-dev-list-formattingSharePoint List Formatting Samples https://sharepoint.github.io/sp-dev-list-formatting/Contribution guidelinesmaster
SharePoint/sp-dev-modernizationAll modernization tooling and guidance http://aka.ms/sppnp-modernizeThe modernization repositorydev
SharePoint/sp-provisioning-serviceCode for the provisioning service hosted at http://provisioning.sharepointpnp.comContributingdev
SharePoint/sp-usage-docsSharePoint Documentation on usage and feature patterns for site owners and citizen developersAdding contentmaster
Sharepoint/PnP-sites-coreOffice 365 Dev PnP Core component (.NET) targeted for increasing developer productivity with CSOM based solutions.Contribution guidancedev
pnp/office365-cliManage Microsoft Office 365 and SharePoint Framework projects on any platform https://aka.ms/o365cli- Contribution guidelines
- Adding a new command
- Submitting a PR
dev
pnp/pnpjsSharePoint Patterns and Practices Reusable Client-side Libraries https://pnp.github.io/pnpjsContribution guidedev

Thanks

Thanks to David Warner II with putting together this list, and for always making yourself available to help people in this community.

This list wouldn't be possible without the hard work of all of those who contributed (and continue to contribute) to the above repositories. Thank you for your contributions!

Photo credit

Image by StockSnap from Pixabay

Updates

  • November 25, 2019: David Warner II has launched a new initiative to help anyone who wants to create their first PnP contribution. It is called Sharing Is Caring and you can register to attend a live online hands-on session where he walks you through step-by-step instructions to create your first pull request.
  • August 25, 2019: Thanks to Bert Jansen for providing us with details for the SharePoint/sp-dev-modernization repository.
  • August 23, 2019: Erwin van Hunen Tweeted to remind us about SharePoint/PnP-Sites-Core. It is so foundational to other components, I don't know how we missed it. Urgh! I hate to disappoint someone I hold in such high regard! Sorry!
  • August 23, 2019: Waldek Mastykarz rightly pointed out that we forgot the PnPjs repository.

Introduction

In my previous post I showed how you can use Flow to trigger a scheduled flow and retrieve an iCal feed over HTTP.

I also spent a bit of time explaining how to parse the text returned from the iCal feed into individual events and extract the information required to be able to import and synchronize the events in a SharePoint list. I mostly covered at a high-level and didn't give you detailed steps -- which is a good thing, because the article was already pretty long!

In today's post, we'll set up a SharePoint list and walk you through how to implement the flow, step-by-step.

Let's get going!

Setting up a SharePoint list

To synchronize an iCal feed to SharePoint, we need two things:

  • A feed
  • A SharePoint list

And we'll need a flow to combine the two.

For the feed, I'll use one of CalendarLabs.com's iCal feeds. Being Canadian, I'll use the Canadian Holidays feed, but feel free to use the US Holidays or any other that suits you. (Make sure that you get the URL from the Download button). I have to admit that I only learned about CalendarLabs.com when I was researching this post, but I'll definitely be recommending it to everyone!

There are all sorts of useful iCal feeds out there. For example, if your company uses People HR to for your HR software, you can get an iCal feed of employee vacations directly from People HR and synchronize with a list called Staff Calendar.

Let's assume you have found an iCal feed you want to sync and you tested it to make sure it returns something.

For the SharePoint list, we'll create one. Since the iCal feed I'm using is a list of Statutory Holidays, I'll call my new list Statutory Holidays (I know, right? How do I come up with this stuff?!)

To create the list, follow these steps:

  1. From a SharePoint site of your choice, go to Settings then Site contents
  2. From the Site contents page, select + New and pick App. We use App instead of List because we want to create an events list, not a regular list.
  3. From the Site contents > Your Apps page, pick Calendar
  4. From the Adding calendar dialog, in the Name field, enter Statutory Holidays (or whichever name you picked for your event list, I'm flexible!) then select Create.

Your list should automatically get created. Now we need to add one more field called UID to keep track of the unique identifiers for each event. The UIDs are provided by the iCal feed.

To add the column, follow these steps:

  1. From the list you just created, go to List settings
  2. In the Settings page, in the Columns section, select Create column.
  3. In the Settings > Create column page, type UID for the Column name.
  4. Un-check Add to default view, but leave everything else. We just want a simple text column to store the unique identifier, no need to get fancy.
  5. Select Ok to create the column.
  6. If you want, you can remove the columns that you won't use, but for the sake of brevity, I'll leave the existing fields as it.

To set up the Flow

Yesterday, I briefly touched how I build the flow to extract the data. Today, we'll do it step-by-step, starting with these steps:

  1. Log-in to https://flow.microsoft.com
  2. From the left navigation, select + Create
  3. From the Start from blank section, select Scheduled flow
  4. In the Build a scheduled flow dialog, pick a Flow name -- something like Sync Statutory Holidays from iCal Feed
  5. Under Run this flow, pick a Starting time that suits you, and select how often you want to repeat the flow. I picked 1 day under Repeat every.
  6. Select Create.
    Creating a Scheduled Flow

To retrieve the iCal feed

Once your flow is created, follow these steps:

  1. Select the + New step from the flow editor and type HTTP in the Choose an action window
  2. From the list of Actions, select HTTP
  3. Rename the new action you added to Get events from iCal
  4. In the HTTP action's details, set the Method to GET, and put your iCal feed's URL in the URI field.
  5. Leave everything else as is.
    HTTP Action

Try your flow by using the Test button in the upper right corner. Your Get events from iCal should return events. If it doesn't, check the URL.

To get the list of events

  1. Select the + New step and type Initialize in the Choose an action window.
  2. From the list of Actions, select Initialize variable
  3. Rename your new action to Get list of events
  4. In the initalize action's details, set the Name to Events, the Type to Array and the Value to:
    split(replace(body('Get_events_from_iCal'), '\\n', ''), 'BEGIN:VEVENT')

    Getting list of events

This will split the iCal feed into an array of events where BEGIN:VEVENT can be found.

To filter array elements that aren't events

  1. Select the + New step and type Filter in the Choose an action window
  2. From the list of Actions, select Filter array
  3. Rename the new action to Remove rows that are not events
  4. In the From field, select the Events variable using the Dynamic content
  5. Below the From field, select Edit in advanced mode and type:
    @not(startsWith(item(), 'BEGIN'))

Not events

This will keep only array items that don't start with BEGIN, thus removing all the iCal header information and leaving you with only events.

To create a loop to process all events

  1. Select the + New step button and type apply to each in the Choose an action window.
  2. From the list of Actions, select Apply to each
  3. Rename your action Loop through every event
  4. In the Select an output from previous steps field, select the Body of Remove rows that are not events in the Dynamic content picker.

This will create a loop for every event in the array.

Loop

To split every event into lines

  1. Within the Loop through every event loop, select Add an action
  2. In the Choose an action window, type compose
  3. From the list of Actions, select Compose
  4. Rename your new action Get all lines in event
  5. In the Input field, you'll want to use:
    split(item(), json('{"NL":"\n"}')?['NL'])

Splitting into lines

Now, all we need to do is extract the data from every relevant line

To get the Start Date

  1. Still within the loop, select Add an action and type Filter in the Choose an action window
  2. From the list of Actions, select Filter
  3. Rename the new action Find DTSTART
  4. In the Inputs field's Expression tab, type:
    @startsWith(item(), 'DTSTART')
  5. Add another action using Add an action and type Compose
  6. From the list of actions, select Compose
  7. Rename the action to Get DTSTART
  8. In the Inputs field, type the following expression:
    replace(first(body('Find_DTSTART')), 'DTSTART;VALUE=DATE:', '')

Getting Start Date

To get the End Date

Repeat the same steps as above except that you should call the Filter action Find DTEND and use the following expression:

@startsWith(item(), 'DTEND;VALUE=')

And in the Compose action, call it Get DTEND and use the following expression

replace(first(body('Find_DTEND')), 'DTEND;VALUE=DATE:', '')

Getting End Date

To get the Summary

You guessed it, repeat same as above, but name the Filter action Find SUMMARY and use the following expression:

@startsWith(item(), 'SUMMARY:')

And set your Compose action to Get SUMMARY and use this expression:

replace(first(body('Find_SUMMARY')), 'SUMMARY:', '')

Getting Summary

To get the Unique Identifier

One last time! Repeat same as above, but name the Filter action Find UID and use the following expression:

@startsWith(item(), 'UID:')

And set your Compose action to Get UID and use this expression:

replace(first(body('Find_UID')), 'UID:', '')

Getting UID

Now you have all the properties we need. You should test your flow to make sure everything works.

Everything we need

Note that if your iCal feed has different fields that you need, you may need to adjust your actions above. For example, some feeds will return DATE-TIME and DATE events, so you may want to add a little condition up there to deal with such events. To keep things simple, we won't do that here.

To verify if the event is already in SharePoint

  1. Still within the loop, select Add an action and type SharePoint in the Choose an action window
  2. From the list of Actions, select Get items
  3. In the Site Address field, type your site's URL
  4. From the List Name pick Statutory Holidays.
  5. Under Filter Query type UID eq ' then select the Output from Get UID in the Dynamic content window.
  6. Add ' to close the filter query
  7. Under Top Count, enter 1. We only care if there is already an event with a matching UID, so returning only 1 will do for us.

Getting SharePoint items

If you test your flow now, every Get Items action should return the following:

{
[]
}

Because there are no events to retrieve. Let's fix that.

To create a list item if there are no matches found

  1. Still within the loop, select Add an action and type Condition in the Choose an action window

  2. From the list of Actions, select Condition

  3. Rename the condition Any existing events found

  4. In the expression below type:

    length(body('Get_items')?['value'])
  5. Select by is greater than in the next field

  6. In the next field, type 0

  7. This will cause the condition to go to If yes when there is an existing event, and If no if there isn't an existing event. We'll only worry about If no for now, but you could always update the existing item in the If yes side if you wanted to. Just no today, ok?

  8. In the If no site, select Add an action

  9. In the Choose an action field, type Create item

  10. In the Actions list, select Create item from SharePoint

  11. In the Create item dialog, type your Site Address and pick Statutory Holidays from the List Name

  12. In the Title field, bind to the Output from Get SUMMARY

  13. We'll skip the Start Time and End Time for now. In the UID field, set it to the Output of the Get UID action.

  14. For the Start Time, we'll need to do some surgery because SharePoint expects the value to be formatted as yyyy-MM-ddThh:mm:ss. To do this, we'll use substring() to extract the year, month, and day from the event's date but using the following expression:

    concat(substring(outputs('Get_DTSTART'),0,4),'-',substring(outputs('Get_DTSTART'),4,2),'-',substring(body('Get_DTSTART'),6,2),'T00:00:00-00:00')

    Which essentially combines the first 4 characters of the Start Date with a -, followed by the next 2 characters of Start Date, followed by another -, and the last two characters of Start Date, followed by T00:00:00 to set the time to midnight. If your iCal returns DATE-TIME values, you'll also want to parse the time element instead of setting it to 00:00:00.
    The last part -00:00 is for the timezone. If you find that your events are coming in at the wrong time, you can adjust the time zone accordingly (e.g.: +01:00 or -01:00).

  15. Repeat the same formula for End Time, except that you should use GET_DTEND instead of GET_DTSTART.

SharePoint create items

Test your workflow, and you should have a whole bunch of new events! Then try again, and you should not get more events until the iCal feed adds a new event.

Conclusion

(Sigh of relief!) That was a long post!

This post showed you how to get an iCal feed in Flow and import events from that feed into a SharePoint list.

You can use a similar approach for other types of feeds.

There are a few more opportunities to improve the resiliency of this flow, and to deal with all-day events that span over two days when they should really last one day... but that'll have to be for another post.

I hope this helped?

Photo credit

Image by Free-Photos from Pixabay

Introduction

Let's say you have public iCal feed and you want to access the events from within SharePoint Online.

You could use the sample web part I created many months ago which allows you to display external event feeds from RSS, WordPress, Exchange, SharePoint and iCal. You'd get something that looks like it exists in SharePoint.

But what if you wanted to use the events from that iCal feed within SharePoint as a lookup for another list. Or what if that iCal feed contained statutory holidays and you wanted to skip scheduled Flows on those days?

In such situations, you need to actually import the data into SharePoint so that you can use it. My web part sample won't do!

Today, we'll explain how to use Flow to import events daily from an iCal feed into a SharePoint list.

The idea for this post came from an issue that Tejas kindly submitted. Thanks for the inspiration!

The problem with repetitive tasks

At first, I was very tempted to tweak my React Calendar Feed web part sample to display events and automatically add events to a SharePoint event list. After all, I was already parsing the iCal feeds in my code, so couldn't I just add the events that were not already in the list?

The problem with this idea is that it would require someone with sufficient permissions to add events to that event list to load that web part once a day -- or at least regularly enough to make sure the iCal events and the list were in sync.

Bad idea.

Any time you require someone to do a manual step regularly to keep a system going is not sustainable. It's like that countdown timer in the TV show Lost that required someone to enter a sequence of numbers or the world would end.

Lost countdown
The countdown clock from Lost -- I don't need that kind of responsibility!

It's what we called on a project I worked on "Death by a thousand cuts". It may not seem like a big deal to ask someone to do something every x days to keep a system going, but those little temporary solutions you take on eventually accumulate to a point where you need a person whose job is just to do those little things. It isn't a job that's very fulfilling for anyone.

Might as well use Homer Simpson's "stupid bird" to push a button every once in a while.

Homer Simpson's stupid bird
Homer's stupid bird

So, the lazy approach wouldn't work for me this time.

What could I use? A timer job on a server? Nah, I want a server-less solution.

How about a scheduled Azure web job? No, I want a no-code solution. Or at least low-code.

If only there was a way to schedule workflows to run regularly that wouldn't require any code...

Scheduled flows to the rescue!

Thankfully, Microsoft Flow allows you to create scheduled flows. We already discussed using schedule flows in a previous post, so I know with certainty that it will work.

Scheduled flows

I configured my flow to run every day. That's probably an overkill depending on how often your iCal feed gets updated, so feel free to adjust accordingly:

Schedule recurrence, every 1 day

Now here's the problem: Flow does not have a way to parse iCal feeds. I looked everywhere for a ready-to-use "import iCal" connector, but couldn't find any. As it turns out, I'm not the only one who wants this.

Note to Microsoft: if you accept open source contributions for connectors, let me know and I'll gladly submit an iCal connector.

But iCal feeds are really just text files with a very specific structure. Could we not just use the Flow HTTP connector and retrieve the feed as a simple string of text and parse it?

HTTP connector

Note: Unfortunately, HTTP is a Premium connector in Flow... but you get so much more with Flow Premium that it is worth it! Trust me!

This is how I configured my HTTP connector to retrieve my iCal feed:

HTTP call to iCal

Parsing the results to get a list of events

I configured an HTTP connector to do an HTTP GET with a public iCal feed. I used CalendarLabs.com's Canadian Holidays sample, but they have many other great sample calendar feeds.

And it worked. The response wasn't pretty, but it worked!

Here is an example of what I got. The full list is a lot longer, but you get the idea.

BEGIN:VCALENDAR
PRODID:fd29_Array_canada_country_holidays@calendarlabs.com
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Canada Holidays
X-WR-TIMEZONE:America/New_York
BEGIN:VEVENT
DTSTART;VALUE=DATE:20180101
DTEND;VALUE=DATE:20180102
DTSTAMP:20111213T124028Z
UID:5c60f18d0973d@calendarlabs.com
CREATED:20111213T123901Z
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. 
 Like us on Facebook: http://fb.com/calendarlabs to get updates.
LAST-MODIFIED:20111213T123901Z
LOCATION:Canada
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:New Year's Day
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20180212
DTEND;VALUE=DATE:20180213
DTSTAMP:20111213T124028Z
UID:5c60f18d09784@calendarlabs.com
CREATED:20111213T123901Z
DESCRIPTION:Visit https://calendarlabs.com/holidays/canada/family-day.php to know more about Family Day (BC). 
 Like us on Facebook: http://fb.com/calendarlabs to get updates.
LAST-MODIFIED:20111213T123901Z
LOCATION:Canada
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Family Day (BC)
TRANSP:TRANSPARENT
END:VEVENT

As you can see, iCal feeds use lines as a delimiter for each field. Every new event start with BEGIN:VEVENT on a new line.

So I used the Initialize variable to create an array variable which would contain every event in the feed. I called the variable Events.

Initialize variable

For the initial value of the variable, I used the split() function, and I specified that it should create a new array element every time it found a BEGIN:VEVENT.

Some iCal feeds that I tested also had extra newline characters, so I took the opportunity to remove those while I was parsing the feed, by using the replace() function to replace all \\n with nothing ('') as follows:

split(replace(body('Get_events_from_iCal'), '\\n', ''), 'BEGIN:VEVENT')

Splitting the events

The problem is that the split() function just blindly creates array elements wherever it finds the delimiter you specify. It doesn't care what comes before or after the delimiter.

For example, my sample feed contains 8 lines before the first event, meaning that this text:

BEGIN:VCALENDAR
PRODID:fd29_Array_canada_country_holidays@calendarlabs.com
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Canada Holidays
X-WR-TIMEZONE:America/New_York
BEGIN:VEVENT
DTSTART;VALUE=DATE:20180101
DTEND;VALUE=DATE:20180102
DTSTAMP:20111213T124028Z
UID:5c60f18d0973d@calendarlabs.com
CREATED:20111213T123901Z
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. 
 Like us on Facebook: http://fb.com/calendarlabs to get updates.
LAST-MODIFIED:20111213T123901Z
LOCATION:Canada
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:New Year's Day
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20180212
DTEND;VALUE=DATE:20180213
DTSTAMP:20111213T124028Z
UID:5c60f18d09784@calendarlabs.com
CREATED:20111213T123901Z
DESCRIPTION:Visit https://calendarlabs.com/holidays/canada/family-day.php to know more about Family Day (BC). 
 Like us on Facebook: http://fb.com/calendarlabs to get updates.
LAST-MODIFIED:20111213T123901Z
LOCATION:Canada
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Family Day (BC)
TRANSP:TRANSPARENT
END:VEVENT

When the text is split where BEGIN:VEVENT is found, I get the following array:

[ "BEGIN:VCALENDAR\nPRODID:fd29_Array_canada_country_holidays@calendarlabs.com\nVERSION:2.0\nCALSCALE:GREGORIAN\nMETHOD:PUBLISH\nX-WR-CALNAME:Canada Holidays\nX-WR-TIMEZONE:America/New_York\n",

"\nDTSTART;VALUE=DATE:20180101\nDTEND;VALUE=DATE:20180102\nDTSTAMP:20111213T124028Z\nUID:5c60f18d0973d@calendarlabs.com\nCREATED:20111213T123901Z\nDESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n Like us on Facebook: http://fb.com/calendarlabs to get updates.\nLAST-MODIFIED:20111213T123901Z\nLOCATION:Canada\nSEQUENCE:0\nSTATUS:CONFIRMED\nSUMMARY:New Year's Day\nTRANSP:TRANSPARENT\nEND:VEVENT\n",

"\nDTSTART;VALUE=DATE:20180212\nDTEND;VALUE=DATE:20180213\nDTSTAMP:20111213T124028Z\nUID:5c60f18d09784@calendarlabs.com\nCREATED:20111213T123901Z\nDESCRIPTION:Visit https://calendarlabs.com/holidays/canada/family-day.php to know more about Family Day (BC). \n Like us on Facebook: http://fb.com/calendarlabs to get updates.\nLAST-MODIFIED:20111213T123901Z\nLOCATION:Canada\nSEQUENCE:0\nSTATUS:CONFIRMED\nSUMMARY:Family Day (BC)\nTRANSP:TRANSPARENT\nEND:VEVENT\n",
...
]

Notice that the first array element is different than the others. It contains information about the calendar feed, which we don't care about for our needs.

To remove anything that isn't an event, I simply used the Filter action and kept only array elements that do not start with BEGIN:

Filtering array where the item doesn't start with BEGIN

(I could have filtered for events that start with DTSTART, but I didn't want to have to deal with the funny \n character at the start of every element)

This is the array the filter action returned:

[
"\nDTSTART;VALUE=DATE:20180101\nDTEND;VALUE=DATE:20180102\nDTSTAMP:20111213T124028Z\nUID:5c60f18d0973d@calendarlabs.com\nCREATED:20111213T123901Z\nDESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n Like us on Facebook: http://fb.com/calendarlabs to get updates.\nLAST-MODIFIED:20111213T123901Z\nLOCATION:Canada\nSEQUENCE:0\nSTATUS:CONFIRMED\nSUMMARY:New Year's Day\nTRANSP:TRANSPARENT\nEND:VEVENT\n",

"\nDTSTART;VALUE=DATE:20180212\nDTEND;VALUE=DATE:20180213\nDTSTAMP:20111213T124028Z\nUID:5c60f18d09784@calendarlabs.com\nCREATED:20111213T123901Z\nDESCRIPTION:Visit https://calendarlabs.com/holidays/canada/family-day.php to know more about Family Day (BC). \n Like us on Facebook: http://fb.com/calendarlabs to get updates.\nLAST-MODIFIED:20111213T123901Z\nLOCATION:Canada\nSEQUENCE:0\nSTATUS:CONFIRMED\nSUMMARY:Family Day (BC)\nTRANSP:TRANSPARENT\nEND:VEVENT\n",

"\nDTSTART;VALUE=DATE:20180214\nDTEND;VALUE=DATE:20180215\nDTSTAMP:20111213T124028Z\nUID:5c60f18d097c3@calendarlabs.com\nCREATED:20111213T123901Z\nDESCRIPTION:Visit https://calendarlabs.com/holidays/us/valentines-day.php to know more about Valentine's Day. \n Like us on Facebook: http://fb.com/calendarlabs to get updates.\nLAST-MODIFIED:20111213T123901Z\nLOCATION:Canada\nSEQUENCE:0\nSTATUS:CONFIRMED\nSUMMARY:Valentine's Day\nTRANSP:TRANSPARENT\nEND:VEVENT\n",
...
]

Now all I needed to do was to loop through every event and parse the values...

Processing each event

So far, I have a scheduled flow which retrieves an iCal feed, splits the text into an array of events, and filters out things that aren't events.

To process every item in the array of events, I just used the Apply for each action:

Apply for each

When it asked me what I wanted to loop through, I specified the Body of the filter action from before.
Looping through events

Now that I have the flow looping through every event, I need to split the event into individual lines, using the split() function, like before.

Except that this time, instead of storing the array of lines in a variable, I use the Compose action to temporarily build my array.

Compose

The only problem is, I found it very difficult to write a split() function to divide by the newline character. Luckily, someone who is way smarter than I am (tre4B) came up with a solution.

tre4B's solution is to temporarily define a JSON object which defines the newline character as a JSON element, and use that JSON element, as follows:

json('{"NL":"\n"}')?['NL']

I must admit, it's almost like voodoo to me, but it works.

So, to split every event into individual lines, I used:

json('{"NL":"\n"}')?['NL']

If your feed uses both a newline and a carriage return, you would use this instead:

split(item(), json('{"NL":"\r\n"}')?['NL'])

Splitting by newline

Now, every event returns an array of lines that look like this:

[
  "",
  "DTSTART;VALUE=DATE:20180101",
  "DTEND;VALUE=DATE:20180102",
  "DTSTAMP:20111213T124028Z",
  "UID:5c60f18d0973d@calendarlabs.com",
  "CREATED:20111213T123901Z",
  "DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. ",
  " Like us on Facebook: http://fb.com/calendarlabs to get updates.",
  "LAST-MODIFIED:20111213T123901Z",
  "LOCATION:Canada",
  "SEQUENCE:0",
  "STATUS:CONFIRMED",
  "SUMMARY:New Year's Day",
  "TRANSP:TRANSPARENT",
  "END:VEVENT",
  ""
]

Once I have every line as an array element, I can just use the same filter() technique I used before to find each line for the Start Date, End Date, Summary and anything else I need.

For example, to find the Start Date, I find the line which starts with DTSTART:

Filtering for DTSTART

And to get the date value, I use the replace() function to remove everything before the date, which looks like this:

replace(first(body('Find_DTSTART')), 'DTSTART;VALUE=DATE:', '')

All that's left is a string that represents the date. I won't bother converting the string to a date because SharePoint will expect a very specific date format later anyway.

I just repeat the same thing with DTEND for the End Date and SUMMARY for the event title.

Also, I'll retrieve every event's UID which is a unique identifier that I'll be able to use later to verify if the event has already been created in SharePoint.

Once completed, my event parsing looks like this:

My event parsing loop

It looks like it's a lot of work, but thanks to Flow's new clipboard functionality, it was able to copy and paste the Find DTSTART and Get DTSTART actions for the DTEND, SUMMARY and UID.

Easy!

To be continued

Sorry if this post was long (and probably boring). We had a lot of stuff to cover, and we're not finished yet!

So far, we have created a scheduled workflow which reads an iCal feed and parses each event to retrieve their individual attributes.

I should point out that my approach isn't the only way to do this, I'm sure. This is the way I did it. For example, instead of using replace(), I could have used the substring() function to extract the parts of the strings I wanted, but I prefer replace() because it makes it easier to read what the function is doing. Feel free to use whichever approach you like.

Tomorrow, we'll use the same technique we used before to see if every event exists in a SharePoint list. If it doesn't, we'll create it.

I'll also share the step by step instructions tomorrow.

I hope you'll come back tomorrow for the second part!

Photo credit

Image by Karolina Grabowska from Pixabay

Introduction

I use mind maps to understand things. I do this a lot. They're a great way to summarize things in a simple picture.

My goal is to take major SharePoint/Office 365 announcements, videos, and other relevant pieces of information which may require too much time for busy people to read and watch, and to boil it down to the essential.

This one highlights what's new in August 2019 with SharePoint pages and news authoring.

Just click on the mind map to see a larger image.

August 2019 Updates to SharePoint pages and news authoring mind map

Conclusion

Let me know if this was useful for you. Also, if I missed something, let me know.

Sources

Introduction

Do you find a spelling mistake in the SharePoint Documentation but you don't know how to fix it?

Do you have a cool SPFx web part or extension sample that you think other SharePoint developers would appreciate?

Do you want to add to the PnP reusable controls, PnP property controls, or write your own command for the PnP Powershell or Office365-CLI?

But you never got around to it because you just didn't know where to start?

At the 2019 SharePoint Conference, the always entertaining Vesa Juvonen presented a session about how to start contributing to the PnP community. (I can't find the actual session title or a video of it, but if anyone has a link to it, please let me know).

The whole point of the session was very clear: the SharePoint Development community (or PnP) is open for everyone to contribute.

However, it can be very overwhelming to get started.

Luckily, the community is here to help!

Together, as a community, we can achieve much more than alone.

It is scary for everyone

I'm sure if you asked any of the existing PnP contributors, they'll tell you the same thing: the first contribution is always scary.

Why is it scary? Gene Zelazny who taught me everything about public speaking says that being nervous about speaking in public is a good thing: it means that you respect for the audience.

I think that being scared of making your first contribution to the PnP community is a good thing; it means that you respect the members of the community and you don't want to introduce something that will break a solution, introduce bugs, or lower the quality bar.

That's a good thing!

It doesn't matter how much (or how little) experience you have with SharePoint. Newbies feel that maybe they have nothing new to add, or that it isn't their place to contribute, while more senior developers probably experience impostor syndrome.

If you're worried that you'll make a mistake and make a fool of yourself, don't be. I've written many love letters about the PnP community (here and here), but the fact is: the PnP community is filled with awesome people who will help you. If you make a mistake, they may either fix your mistake for you, or reject your submission, make suggestions to fix it, and kindly encourage you to re-submit.

David Warner II is one of those amazing PnP contributors who goes even further: he'll help everyone with their first contribution!

He recently tweeted this:

If you want to contribute & add your name to the contributors' list, but not sure how I’m offering my time to help! DM me and I will personally walk you thru your 1st contribution in docs! #SharePoint #SPFx #SPC19 #OfficeDev

-- David Warner II via Twitter

If you don't know who David Warner II, he's a prolific Microsoft Office Development MVP who is deeply focused on developing & branding -- two of my favourite topics.

During the day, David is a Managing Consultant at Catapult Systems, but after dark (or whatever spare time he has), he turns into a masked PnP contributor who has contributed to pretty much every PnP repo there is.

Somewhere in between, he also finds the time to write summaries for every PnP community call, complete with screenshots and links.

David Warner II
Come on, how could you not trust this guy?!

I have reached out to David before to seek his advice, and he could not have been more friendly or patient.

So, if you're still afraid to contribute but you want to do so, reach out to him via Twitter.

Paying it forward

The PnP community is so awesome that I feel it is also my responsibility to pay it forward and help.

I'm not a GitHub expert and I still get confused with pull, pushes, branches, commits and pull requests, so I'm not the right person to help you with making your first contribution to GitHub.

However, I'm pretty comfortable with SPFx web parts and extensions; If you have an idea for an SPFx web part that you'd like to build and you don't know where to start, reach out to me and I'll help you start your project and build it -- as long as you promise to share it with the rest of the community as your first contribution.

If the solution doesn't work, you can even blame me 🙂

Conclusion

The Office Dev PnP community is an awesome community that encourages sharing. It welcomes and celebrates newcomers just like you and me.
It can be overwhelming to get started, but the always awesome David Warner II has offered to help anyone get started with a new contribution.

Together, as a community, we can achieve much more than alone.

Welcome to the PnP community. I look forward to your first contribution!

Photo Credit

Photo credit by Esi Grünhagen from Pixabay

Introduction

The SharePoint Design is a beautiful web site that provides design guidance on beautiful and fast sites, pages, and web parts with SharePoint in Office 365.

Unfortunately, the SharePoint Design site does not tell you how to create the beautiful web parts they show you.

This series is intended as a companion to the SharePoint Design site, providing you with code samples and detailed how-to information for every design topic. It should help you create web parts that look exactly like the ones on the SharePoint Design site.

In our last post we discussed the filmstrip layout.

In today's post, we'll continue our discussion about the web part layout patterns and discuss the carousel layout.

What is the carousel

The carousel is a standard web part layout that presents your web part content, showing items one at a time.

A demo of the carousel layout

The carousel is best suited when you want to showcase the content instead of the metadata about the content. For example, if you want to show a large preview of your documents or a large picture.

By default, the carousel will expand your images to fit the whole width of your web part and maintain the aspect ratio of your images, with a minimal amount of metadata below the image (for example, the file name and location). It also shows how many items there are in the carousel and which "slide" you're currently viewing.

As your cursor approaches the carousel, two buttons appear, providing the users with the options to go to the next and/or the previous slide. The buttons are positioned halfway down the carousel, one button on each side of the image.

On touch-enabled devices, such as phones and tablets, users can also swipe left of right to indicate whether they like the image to move to the next/previous slide.

After the user has viewed all images, the carousel restarts with the first images, potentially keeping your users entertained for hours and hours.

The problem with carousels

If you mention carousels to most designers, they'll often groan and sigh heavily. That's because the carousel quickly became a trend that everybody wanted on their web sites without really understanding the impact on their users.

If you ask me, there were worse trends out there (like when suddenly everyone decided that you needed to enter your email address and then confirm your email, presumably to fool someone who was trying to enter a fake email address to give your the right email address the second time...? )

I already discussed the issues with carousels when I covered the filmstrip, but here is a brief summary:

  • Their design and frequent movement make people think that they are an ad, causing banner blindness.
  • Moving elements can hurt accessibility, especially for users with motor skill issues
  • People with cognitive difficulties can have difficulty reading all the text before the carousel moves to the next slide
  • Because they see only one slide at a time, users often miss content

The SharePoint carousel: the kinder, gentler carousel

Fortunately, the SharePoint carousel is better designed and less evil.

For one, it does not automatically scroll. It puts users in control of the navigation.

It also supports keyboard navigation and gestures, making it slightly easier for those with accessibility requirements to use the carousel.

It is designed to keep the amount of information on each slide simple (instead of overloading users with too much information)

How to use a carousel properly

Here are some tips for a better carousel design (backed by science!!!!):

  • Keep your slides to fewer than 5: It can be frustrating to swipe through too many items in a carousel. Also your users may not be able to keep a mental list of more than 5 items and may not recognize when they're seeing the same items over and over again. Keeping your items to fewer than 5 the number helps users discovering content, and makes it easy for them to find content again later.
  • Show how many items there are and which slide is currently displayed: It helps users understand that there are more items and helps them feel "in control".
  • Try to show items that are related: Because users only get to see one item at a time, stick to items that are related so that users can easily predict what the other items (the ones they can't see) are.

And probably the most important: make each slide a giant button. It's Fitts's law!

Fitts's law

In 1954 -- long before there were carousels and web sites -- Paul Morris Fitts was researching the human motor system. I'll spare you the boring math, but what he found is this:

  • The greater the distance between your starting point and your target, the harder it is to hit a target
  • The smaller the target is, the harder it is to hit
  • The faster you move from your starting point to your target, the harder it is to hit.

Nowadays, Fitts's law is used in user experience and user interface design. It is the reason why the menu on your Mac computer is located at the top of the screen without a top border (essentially making the menu a button of infinite size), and why Windows 8 tried to use the edges of your screen for the start menu, the share menu, and the context menus.

For example, here is how the distance between two buttons affect how easy it is to hit the target:
Further distance is hard to hit

And here is how size affects how easy it is to hit a target:

The reason why the SharePoint Framework web part layouts (the grid, the filmstrip, the carousel and the compact layout) mostly use large rectangular targets is not just because it looks cool, it is also because it makes it easier for users to hit the targets, regardless of where they started from on the page.

That's why you need to make sure that your entire carousel slide is a giant button that will lead to whatever item you're currently displaying.

Unfortunately, I still think that the next and previous buttons (and the little dots on the filmstrip layout) are a little too small, but since the layout controls support keyboard, and touch gestures, I'll stop complaining now.

For more information about Fitts's law, check out this cool interactive visualization

How carousel control is implemented

As is the case with the filmstrip control, the out-of-the-box carousel layout uses Ken Wheeler's Slick slider -- not just to provide the previous/next functionality, but to control how each slide is resized to fit within the viewable area.

The control I created for this sample aims to mimic the exact functionality of the out-of-the-box SharePoint carousel layout. It also uses the Slick slider. (Hey, if it's good enough for the SharePoint team, it's good enough for me). It the one I wrote for the React Calendar Feed sample in June 2018.

However, it is worth noting that Piotr Siatka recently submitted a carousel control to the Reusable React controls for SharePoint Framework solutions and I can't wait for it to become available in an upcoming version of the PnP controls. He actually built it without any third-party libraries and it looks really cool. I encourage you to check out his awesome work.

Piotr's awesome carousel control
Piotr's carousel control, coming soon to a PnP library near you

When it will become available, Piotr's control will give you the ability to control most aspects of carousel appearance and behavior.

My carousel layout control is designed to make it easy to create a web part with that mimics the carousel layout. It doesn't give you control over how the arrows look, where they are located, or anything else that deviates from the carousel layout standard.

Feel free to use whichever control is most suitable for you.

To create a carousel layout web part

To create your own web part using the carousel layout, follow these steps:

  1. Copy the folder src/components/carouselLayout from my sample code to your own web part project.
  2. In your src\webparts\[YourWebPartName]\components\[YourWebPartName].tsx, add an import for the CarouselLayout and the CarouselSlide controls.
    import { CarouselLayout, ICarouselItem } from '../../../components/carouselLayout';
  3. In your web part's component, retrieve the items you wish to display and store them where you can retrieve them later. For example, it your web part's state. For this example, I'll store sample items in the constructor:

    constructor(props: ICarouselProps) {
    super(props);
    
    this.state = {
      items: [{
        imageSrc: "https://lorempixel.com/744/418/technics/1/",
        title: "Adventures in SPFx",
        location: "SharePoint",
      }, {
        imageSrc: "https://lorempixel.com/744/418/technics/2",
        title: "The Wild, Untold Story of SharePoint!",
        location: "SharePoint",
      }, {
        imageSrc: "https://lorempixel.com/744/418/technics/4",
        title: "Not Your Grandpa's SharePoint",
        location: "SharePoint",
      }, {
        thumbnail: "https://lorempixel.com/744/418/technics/5/",
        title: "Get with the Flow",
        location: "Flow",
      }]
    };
    }

    This is pretty much the same thing we've done in all web part layouts samples so far, except that this time the items we pass must follow the ISlideItem interface and provide a title, a imageSrc, and a location (that's all the carousel has room to display).

  4. In the onRender method, add a CarouselLayout object and pass it the items you wish to display. You'll also need to provide a pagingTemplate string, which is template to control how you want to render the "1 of 5" items label. In your pagingTemplate, use a 0 for the current item placeholder, and 1 for the number of items placeholder.

    public render(): React.ReactElement<ICarouselProps> {
    
    return (
      <div className={styles.carousel}
    
      >
        <CarouselLayout
          pagingTemplate={'{0} of {1}'}
          ariaLabel={'Use right and left arrow keys to navigate between images in the carousel. Use up and down arrow keys to access the edit and remove buttons for any image.'}
          items={this.state.items}
          onSlideClick={(currentSlide) => { alert(`You clicked on slide ${currentSlide+1}`); }}
        >
        </CarouselLayout>
      </div>
    );
    }

    You should also provide an ariaLabel for accessibility's sake, and you must pass an event handler to handle when a user clicks on a slide.

If all goes well, you'll render the following carousel:

The end result

Conclusion

I'm not crazy about the fact that the carousel layout control I used in this sample isn't as flexible as the previous controls, but that's because all we can display is an image, a title and a location.

On the other hand, it makes it pretty easy to implement your own carousel layout web part.

The code for this sample can be found in my WebPartLayouts solution on my repo.

We're almost done with the layouts! Next stop: the compact layout.

Until then, let me know if you have any questions, comments, or concerns!

Note: There are a lot of screenshots in this post. Here's why: there is nothing that I dislike more than having to download/install something to find out exactly what's inside. Yes, there are a lot of screenshots, but you'll know exactly what to expect if you decide to download my PowerPoint template.

Introduction

As the self-proclaimed World's Laziest Developer, I always look for shortcuts and ways to avoid doing work.

When I start a SharePoint project where I have to design an Information Architecture, I like to use low fidelity wireframes instead of spending countless hours creating SharePoint sites.

I'm not a designer or a user experience specialist, but I'll take the time to do paper napkin drawings, whiteboard drawings, and any other available tools to help work through how my sites are going to be structured and laid out.

I'll even use tools like Balsamiq, UXPin, Adobe XD, and Visio, but I like to spend as little time as possible building my low-fidelity wireframes. Also, I find that few people have experience with those tools (even though they are awesome).

I do it because it is faster convey to my stakeholders what I'm planning on doing and getting feedback with a minimum effort. And I'm all about minimum efforts.

Since PowerPoint now has sketchy shapes, I decided to create a PowerPoint template that contains a sketchy design for SharePoint pages and a few of the commonly used web parts.

My template is not intended to replace the SharePoint Toolkit, but it requires Adobe XD and most client workstations I use do not have Adobe XD. If you can use Adobe XD and the SharePoint Toolkit, do so. If you can't feel free to use my PowerPoint template.

If you want to use it, you can download it and use it as you wish.

Note: This template will only work in the desktop version of PowerPoint. And it may require the Office Insider version of PowerPoint.

A few rules, though:

  • If you add more page designs and web parts that you think other people would enjoy, share it with me and I'll add it to the template
  • Don't go selling this template
  • Share it with others (#SharingIsCaring)

The components

Here are the elements you can use for now:

The Communication Site master

Communication site master

There is also a read-only version of the communication site if you want to show what the experience looks like for visitors:

Communication site read-only

Guided layouts

If you choose a slide master, you can pick a layout to help you place your web parts on the page.

1 column guide
1 column guide

2 column guide
2 column guide

3 column guide
3 column guide

Simply place your web parts so that they fit within the gray area. Once you're done moving the web parts, select the No guide version, which hides all the guides.

Hero web part

Hero web part

(I had a lot of fun drawing the placeholder pictures. Very therapeutic.)

News web part

News web part

Events web part

Events

Documents web part

Documents web part

People web part

People web part

Generic grid layout web part

If you want to write your web part with the grid layout and would like to mock them up in your wireframes, you can use the Generic Grid Layout Web Part. It comes in 1/3 column, 2/3 columns, and full-width variations.

Generic Grid Layout Web Part -- Full width
Generic Grid Layout Web Part -- Full width

Generic Grid Layout Web Part -- 2/3 columns
Generic Grid Layout Web Part -- 2/3 columns

Generic Grid Layout Web Part -- 1/3 column
Generic Grid Layout Web Part -- 1/3 column

Generic filmstrip layout web part

If you want to write your web part with the filmstrip layout and would like to mock them up in your wireframes, you can use the Generic Filmstrip Layout Web Part. It comes in 1/3 column, 2/3 columns, and full-width variations.

Generic Filmstrip Layout Web Part -- Full width
Generic Filmstrip Layout Web Part -- Full width

Generic Filmstrip Layout Web Part -- 2/3 columns
Generic Filmstrip Layout Web Part -- 2/3 columns

Generic Filmstrip Layout Web Part -- 1/3 column
Generic Filmstrip Layout Web Part -- 1/3 column

Generic carousel layout web part

If you want to design a custom web part that uses the carousel layout, use this template. Like the other generic web parts, it comes in 3 flavours: Full-Width, 2/3 columns and 1/3 column.

Generic Carousel Web Part -- Full Width
Generic Carousel Web Part -- Full Width

Generic Carousel Web Part -- 2/3 columns
Generic Carousel Web Part -- 2/3 columns

Generic Carousel Web Part -- 1/3 column
Generic Carousel Web Part -- 1/3 column

Generic list layout web part

I couldn't do the other generic layouts and omit the list layout! This one is also available in Full Width, 2/3 columns and 1/3 column.

Generic List Web Part -- Full Width
Generic List Web Part -- Full Width

Generic List Web Part -- 2/3 columns
Generic List Web Part -- 2/3 columns

Generic List Web Part -- 1/3 column
Generic List Web Part -- 1/3 column

Color palettes

Although many believe that low-fidelity wireframes should use grayscale and contrast instead of colors, sometimes I like to include colors. It really depends on my mood (and the audience).

Whether you're in the "no-colors" camp or in the "with colors" camp, every slide includes the SharePoint color palettes on the right of the page -- outside of the viewable area.

SharePoint color palettes

When you're editing your pages, you can pick the Eyedropper tool to pick the colors you need from the palette.
Eyedropper tool

Don't worry: because they are outside of the viewable area, the color palettes won't show up while you're presenting or when you print.

Color palettes won't print

If you need to find out more about what each color is, there is a section at the end of the PowerPoint template that explains the SharePoint colors.

SharePoint Colors Slide

Other stuff

There is also some token lorem ipsum placeholder text in case you need it, but I try to avoid it.

Also, there are sticky notes to help annotate your wireframes.

Sticky notes and wireframes

What else would you like to see? Let me know in the comments.

Using the template

Here are simple tips to use the template:

  • The template uses an 11"x17" page template. It may seem big, but it prevents you from having to deal with 4pt fonts. Don't worry, it presents well on a screen, and prints great posters for your team project room walls.
  • It uses the new sketchy line styles. It may not work in the web version of PowerPoint or if you don't have the Office Insider edition of PowerPoint. Let me know if you need a non-sketchy version and I'll try to oblige. If you get an error opening it in the web browser, try opening it on your desktop
    Error message with PowerPoint
    If you get this message, it is most likely because I used sketchy lines
  • To emphasize on the "work in progress" look, I used the Segoe Print font because I found that it was available on most workstations that I use. If you don't have this font, feel free to use any font you like. Ink Draft and Segoe Marker work well too. Just resist the urge to use Comic sans !
  • For now, every slide uses the communication site layout (and the read-only variation). I'll add the team site layout if I get requests to do so
  • The title of the slide is the site title
    Site title is the slide title
  • Every site element is grouped to make it easier to move them around. Feel free to un-group them and edit them as you need
    Grouping within the slide
  • I used some theme colors, but feel free to make the designs monochromatic if you want
  • If you need to edit the navigation, feel free to copy the navigation elements (called Site Navigation ) from the master slide and paste the customized navigation on your slide. It already has a white background to hide the navigation from the master slide
    Site Navigation

To edit site navigation

To edit the site navigation, follow these steps:

  1. From your PowerPoint slide, go to View then select Slide Master in the Master Views group
    Slide Master from the View ribbon
  2. PowerPoint should automatically take you to the master slide called Communication site. Select the site navigation by clicking on the site icon and click Copy.
    Copy site navigation
  3. From the Slide Master menu, select Close Master View to go back to your slide
    Close Master View
  4. Paste the navigation on top of the existing navigation. The white background behind the site navigation you just copied should hide the existing one from the master slide.
  5. Edit the navigation and site icon as you wish

To create your own wireframe

Although I have a sample wireframe in my template, you can create your own using these simple steps:

  1. In PowerPoint, insert a new slide in your presentation by going to New Slide and select the layout you want
    New Slide Menu
    You can pick from the Communication Site -- Edit Mode to show what an author would see, or Communication Site -- Read-only Mode to show what a visitor would see.
  2. Try to start with one of the guides. You can choose from 1 column guide, 2 column guide or 3 column guide.
  3. From the Web Part Templates section, find the web parts you want from the other slides in the PowerPoint template, copy them and paste them onto your new slide.
  4. Make sure to place the web parts within the gray areas on the guides.
  5. Edit the web part elements (like web part title and content). It may help to ungroup them first.
  6. Once you're done placing your web parts, go to Layout and pick the No guide version of whatever master you chose. It will remove the background guides, but will not affect where you placed your web parts.
    Layouts

Tips when preparing low fidelity wireframes

As Page Laubheimer from the Nielsen Norman Group puts it:

Sharing low fidelity user-interface prototypes with stakeholders is a great way to transfer knowledge and get buy-in early.

Tell your audience that this is work in progress. It may be obvious to you, but I have had clients get upset that their site was going to look "all wobbly" (and others who asked if the site was going to only be "in French" because I used lorem ipsum). Explain that you used this style of wireframe to help focus on the content and structure, not the look.

Explain to your audience that this is a great opportunity to share knowledge: for them to transfer their knowledge to you by getting their feedback early.

Don't worry about the look and feel, worry about the content/structure and function. Resist the urge to put lorem ipsum and try to put some text that is as real as you can make it for now.

Another way to use these wireframes is to print them and get your audience to mark them with their notes. You'd be amazed by what information you can get from watching people circle, underline, and annotate paper wireframes.

Conclusion

My PowerPoint template is available for you to use.

I'll continue adding web parts and designs. If you need anything else added to it, let me know in the comments.

Maybe I'll create a higher-fidelity version of these if people like them. I'd also love to create a version that you can print out and cut so that you can layout page designs on paper.

Let me know what you think?

Updates

  • August 5, 2019: Thank you Vesa Juvonen for pointing out the broken link. Moved files to GitHub repo to make it easier for those of you who wish to contribute.
  • August 3, 2019: Added more web parts (List, carousel, filmstrip) and explanation why I have so many damned screenshots. Also added introduction how to use the guides and section explaining color palettes.
  • August 2, 2019: Added more web parts (generic grid 1/3, 2/3, and full width) and guide layouts

Introduction

The SharePoint Design is a beautiful web site that provides design guidance on beautiful and fast sites, pages, and web parts with SharePoint in Office 365.

Unfortunately, the SharePoint Design site does not tell you how to create the beautiful web parts they show you.

This series is intended as a companion to the SharePoint Design site, providing you with code samples and detailed how-to information for every design topic. It should help you create web parts that look exactly like the ones on the SharePoint Design site.

In our last post we discussed layout patterns and showed how to create a Grid Layout web part.

In today's post, we'll continue our discussion about the web part layout patterns.

The Filmstrip layout

Like the grid layout, the filmstrip displays cards to display content. You can use any other rectangular content though. For example, you can use images of kittens, or event cards (as I did with my Calendar Feed web part).

Calendar feed web part
Calendar feed web part switches between filmstrip and compact mode

However, unlike the grid layout, the filmstrip displays items on a single row. As the screen resizes, the filmstrip layout resizes the items to keep the same number of items displayed at once. If there are more items than can be shown at once, the filmstrip will change to a carousel and break items into "pages".

Filmstrip layout resizing items as the screen resizes
The filmstrip layout resizes items as the page resizes

If you're a web designer, the word carousel may make you shudder. In fact, if you search online, you'll find over fifty-four million search results explaining why carousels are bad.

Banner blindness

You see, most people have issues with the giant photo carousels on web sites. For most users, the giant photos trigger our banner blindness because we automatically assume it is an advertisement, and we ignore it.

Since the carousel in the filmstrip layout does not show a giant image, but a series of smaller images (usually a preview of documents), this isn't something we have to worry about (although we'll have to have another conversation when we discuss the Carousel layout).

Mobile issues

Another issue is that giant carousels are notoriously bad on mobile devices.

One of the issues with carousels on mobile devices is that because of limited screen real-estate, it shows only one item at a time. This forces users to use sequential access: to view items in the order that is dictated by the control, not by the user's choice.

Research shows that -- with sequential access -- users will stop looking at items after the third or fourth item in a carousel.

Another issue with carousels that people have is with discoverability. If someone is in a hurry and they quickly glance at a typical carousel, they may not know that there are more items to see. Most carousels will use a teeny tiny little dot (which are often a bad design choice) to indicate that there are more items.

Fortunately, the mobile version of the filmstrip control uses bigger dots that are as big as the main text with a lot of colour contrast to help.

Sample mobile carousel
Mobile filmstrip. This isn't so bad?

If you plan on using the filmstrip layout, follow these guidelines:

  • Limit your items to less than 5: Otherwise, people may not see the other items.
  • Prioritize your items: Keep in mind that, depending on the screen size, some users may only see one item at a time. Make sure that you place the most important items first.
  • Make sure items are related: Because people may not see all items in your filmstrip, make sure that the items you show on the filmstrip are related so that users can predict/easily guess what items are hidden. In other words, don't put apples and oranges on the filmstrip.

Accessibility

For me, the biggest concern about most carousels has to do with accessibility.

You see, most evil carousels break a lot of accessibility guidelines:

  • The dots at the bottom are often too small to see and/or click
  • The dots often have a poor colour contrast
  • The slides often change automatically, without giving users a chance to read the content or slow it down
  • The left/right arrows often rely on the use of a mouse, with no keyboard alternatives
  • Left/right arrows often have poor colour contrasts
  • Carousels often do not provide alternative text

Luckily for us, the SharePoint filmstrip (and the carousel layout) doesn't have many of the typical carousel accessibility issues:

  • The slides do not change automatically
  • Users can navigate through the slides using the and keys on their keyboard and use TAB to cycle through the slides.
  • The left/right navigation arrows use a high contrast so that users with colour blindness and/or various forms of visual impairment can see them
    Left/right arrows have high contrast
  • The left/right navigation arrows aren't too small (but they could be bigger)
  • The filmstrip provides alternative text for the entire filmstrip (e.g.: "Highlighted content web part, showing Most recent Documents., Use right and left arrow keys to navigate between cards in the film strip."), as well as alternative text for every item in the filmstrip.
  • The bullets at the bottom of the filmstrip are still pretty small, but they have high contrast (21:1 on the theme I tested) exceeding the minimum required contrast (4.5:1)

It still isn't perfect, but it isn't as bad as most carousel controls out there.

How the filmstrip layout is implemented

Behind the scenes, the out-of-the-box SharePoint filmstrip and carousel layouts use Ken Wheeler's awesome Slick carousel.

Slick carousel
The slick carousel as a filmstrip

They simply made it more "Fabric UI" and fixed some accessibility issues.

Unlike the grid layout, which calculates the optimal card size as you resize the page, the filmstrip layout uses breakpoints to use pre-determined settings depending on the dimensions of the filmstrip control.

How to use the filmstrip layout

Unfortunately -- as was the case with the grid layout -- there aren't any ready-to-use controls to create a filmstrip layout web part.

I had to create my own filmstrip layout when I created the Calendar Feed web part for the SharePoint/sp-dev-fx-webparts repo.

React Calendar Feed Web Part with filmstrip
Custom filmstrip control in the calendar web part

And since I'm the world's laziest developer and I hate to write things more than once, I had created the filmstrip component as a re-usable component. (Ok, I had called it CarouselContainer back then, but it was really a filmstrip layout control).

For the rest of this post, we'll use my filmstrip layout control. If you don't like the one I created, feel free to use your own.

As we did for the grid layout, I'll show you how to insert a copy of my control into your own web parts -- that way, you can customize it to suit your own needs.

(Don't worry, I'll show you how to move the controls into a component library in a later post, so you won't have to always copy and paste my code into your solutions)

To create your own filmstrip layout web part, follow these steps:

  1. Create a web part solution with the Yeoman generator. For this sample, I'll call my web part filmstrip
  2. Copy the src\components\filmstripLayout folder from my project to yours
  3. In your project's terminal, install the slick-carousel and react-slick dependencies by using the following command:
    npm i slick-carousel react-slick
  4. In your web part's component, located at src\webparts\[YourWebPartName]\components\[YourWebPartName].tsx, add an import for the filmstripLayout control:
    import { FilmstripLayout } from '../../../components/filmstripLayout';
  5. In your code, load the items you wish to display. You can load an array of any items you wish, but for my sample, I used items with thumbnail, title, name, profileImageSrc, location, and activity props because I want to use the DocumentCard control to render my list items. For my sample, I initialized the state with a static list of items in my component's constructor:

    constructor(props: IFilmstripProps) {
    super(props);
    
    this.state = {
      items: [{
        thumbnail: "https://lorempixel.com/400/200/technics/1/",
        title: "Adventures in SPFx",
        name: "Perry Losselyong",
        profileImageSrc: "https://robohash.org/blanditiisadlabore.png?size=50x50&set=set1",
        location: "SharePoint",
        activity: "3/13/2019"
      }, {
        thumbnail: "https://lorempixel.com/400/200/technics/2",
        title: "The Wild, Untold Story of SharePoint!",
        name: "Ebonee Gallyhaock",
        profileImageSrc: "https://robohash.org/delectusetcorporis.bmp?size=50x50&set=set1",
        location: "SharePoint",
        activity: "6/29/2019"
      }, {
        thumbnail: "https://lorempixel.com/400/200/technics/3",
        title: "Low Code Solutions: PowerApps",
        name: "Seward Keith",
        profileImageSrc: "https://robohash.org/asperioresautquasi.jpg?size=50x50&set=set1",
        location: "PowerApps",
        activity: "12/31/2018"
      }, {
        thumbnail: "https://lorempixel.com/400/200/technics/4",
        title: "Not Your Grandpa's SharePoint",
        name: "Sharona Selkirk",
        profileImageSrc: "https://robohash.org/velnammolestiae.png?size=50x50&set=set1",
        location: "SharePoint",
        activity: "11/20/2018"
      }, {
        thumbnail: "https://lorempixel.com/400/200/technics/5/",
        title: "Get with the Flow",
        name: "Boyce Batstone",
        profileImageSrc: "https://robohash.org/nulladistinctiomollitia.jpg?size=50x50&set=set1",
        location: "Flow",
        activity: "5/26/2019"
      }]
    };
    }
  6. In your render method, add a filmstripLayout component. You should set the ariaLabel prop, but it is optional.

    public render(): React.ReactElement<IFilmstripProps> {
    return (
      <div className={styles.filmstrip}>
        <FilmstripLayout 
                ariaLabel={"Sample filmstrip layout web part, showing sample items., Use right and left arrow keys to navigate between cards in the film strip."}
    
        >
    
        </FilmstripLayout>
      </div>
    );
    }
  7. To render elements within your filmstrip layout, you simply add rectangular elements as children of the FilmstripLayout component. In my sample, I'll use a DocumentCard to render each item. To do so, you'll need to add the following imports:
    // Used to render document cards
    import {
    DocumentCard,
    DocumentCardActivity,
    DocumentCardPreview,
    DocumentCardDetails,
    DocumentCardTitle,
    IDocumentCardPreviewProps,
    DocumentCardLocation,
    DocumentCardType
    } from 'office-ui-fabric-react/lib/DocumentCard';
    import { ImageFit } from 'office-ui-fabric-react/lib/Image';
  8. Then, loop through your items and render a DocumentCard in your render method:

    public render(): React.ReactElement<IFilmstripProps> {
    return (
      <div className={styles.filmstrip}>
        <FilmstripLayout
        ariaLabel={"Sample filmstrip layout web part, showing sample items., Use right and left arrow keys to navigate between cards in the film strip."}
        >
          {this.state.items.map((item: any, _index: number) => {
            const previewProps: IDocumentCardPreviewProps = {
              previewImages: [
                {
                  previewImageSrc: item.thumbnail,
                  imageFit: ImageFit.cover,
                  height: 130
                }
              ]
            };
    
            return <div
              className={styles.documentTile}
              data-is-focusable={true}
              role="listitem"
              aria-label={item.title}
            >
              <DocumentCard
                type={DocumentCardType.normal}
                onClick={(ev: React.SyntheticEvent<HTMLElement>) => alert("You clicked on an item")}
              >
                <DocumentCardPreview {...previewProps} />
                <DocumentCardLocation location={item.location} />
                <DocumentCardDetails>
                  <DocumentCardTitle
                    title={item.title}
                    shouldTruncate={true}
                  />
                  <DocumentCardActivity
                    activity={item.activity}
                    people={[{ name: item.name, profileImageSrc: item.profileImageSrc }]}
                  />
                </DocumentCardDetails>
              </DocumentCard>
            </div>;
          })}
        </FilmstripLayout>
      </div>
    );
    }

If I didn't forget anything, you should see a web part that looks like this:

Custom filmstrip in action

Conclusion

The filmstrip layout is a great way to show more items than available screen, but keep in mind that there are accessibility/usability issues with it.

If you want to build a filmstrip layout web part, you can use my code sample to get started.

In our next post, we'll continue exploring the web part layout design patterns.

Photo Credits

Introduction

Sometimes, when working on a SPFx project, I just want to define a CSS class in my .scss file but I don't want the SASS pre-processor to append random strings to my class names.

For example, let's say I wanted to customize the DocumentCard elements within my SPFx web part to add a border. If I write my SCSS like this:

.myWebPart .ms-DocumentCard {
    border: 2px solid red;
}

It won't work.

That's because when building my solution, the SASS pre-processor will append random strings to my class names. So, my .myWebPart and .ms-DocumentCard CSS classes might become .myWebPart-223 and .ms-DocumentCard-242.

The problem is, I don't want my CSS classes to change from .ms-DocumentCard to .ms-DocumentCard-242 because the .ms-DocumentCard CSS class comes from another component (in this case, Microsoft's Fabric UI DocumentCard).

Luckily, there's a way around it. Every time I need to remember how to do it though, I find myself having to re-open old projects.

Using the :global pseudo selector

To prevent the SASS pre-processor from appending random strings to my CSS class name, just use the :global pseudo selector.

For example:

:global(.ms-DocumentCard) {
    border: 2px solid red;
}

You should be careful, though: global CSS changes apply, well, globally. This means that if you use global(.ms-DocumentCard) in your CSS, every single element with a CSS class of .ms-DocumentCard on the entire page will be affected -- not just the ones in your web part.

If you want to override styles within your web part, use a CSS selector that is a bit more restrictive; something like this:

.yourWebPart {
    :global(.ms-DocumentCard) {
        border: 2px solid red;
    }
}

If you need to define a whole bunch of CSS classes that you don't want to be renamed, you can define a global block, as follows:

:global {
  .ms-DocumentCard {
    border: 2px solid red;

    .ms-DocumentCard--compact {
      .ms-DocumentCardPreview {
        -ms-flex-negative: 0;
        flex-shrink: 0;
        width: 144px;
      }
    }

    .ms-DocumentCardPreview-icon img {
      width: 32px;
      height: 32px;
    }
  }

  .ms-DocumentCard:not(.ms-DocumentCard--compact) {
    ...
  }
}

More information

When you create a SPFx solution, the Yeoman generator creates a [YourWebPartName].module.scss file automatically for you.

You may have asked yourself why the file isn't just called [YourWebPartName].scss instead of [YourWebPartName].module.scss. Well, as it turns out, the .module part of the file name is what instructs the pre-processor to make every CSS class names unique.

If you changed your .scss file to [YourWebPartName].scss, the pre-processor would stop renaming the CSS class names, but you'd risk getting more issues; instead of being scoped to your web part, the CSS classes would be globally applied to the page.

Instead, it is better to continue using [YourWebPartName].module.scss and use the :global pseudo selector.

By the way, if you want to define a local CSS class name within a global block, simply use the :local pseudo selector. It works exactly the opposite of the :global pseudo selector.

For example:

:global {
  .ms-DocumentCard {
    border: 2px solid red;

        :local(.myDocument) {
            border: 2px solid green;
        }
    }
}

Conclusion

SCSS rocks, but sometimes it can be annoying how the CSS class names are automatically renamed to make them unique.

To prevent renaming a class name, use :global() or :global { } in your SCSS.

Whatever you do, resist the urge to make all your CSS classes global.

I hope it helps?

Photo Credit

Image by Christoph Meinersmann from Pixabay

Introduction

The SharePoint Design is a beautiful web site that provides design guidance on beautiful and fast sites, pages, and web parts with SharePoint in Office 365.

Unfortunately, the SharePoint Design site does not tell you how to create the beautiful web parts they show you.

This series is intended as a companion to the SharePoint Design site, providing you with code samples and detailed how-to information for every design topic. It should help you create web parts that look exactly like the ones on the SharePoint Design site.

In today's post, we'll begin discussing the web part layout patterns.

SharePoint web part layouts

If you look at the various web parts that are available out-of-the-box on SharePoint, you'll find that there are many different layouts for web parts.

In fact, there are 5 commonly-used layouts:

  • Grid
  • Filmstrip
  • List
  • Carousel
  • Compact

Five common web part layouts
5 common web part layouts

Each layout is best suited for different uses.

How to decide which layout to use

When planning your web part design, you can pick the best layout by considering the following criteria:

  • How "visual" is the content: does the content consist of a picture or a document preview (more visual), or is it mostly text (less visual)?
  • How much metadata to display: Do you need to simply provide a title, author, date and maybe a category (like most out-of-the-box web parts do) or do you need to provide more metadata?
  • Number of items to display: Do you want to display only a few items, or do you need to display many items at once.

To help the decision process, I use the following matrix which places the standard layouts against a grid of how visual and how much metadata you wish to display. The size of each circle helps to compare how many items you want to display -- the bigger the circle, the more items you can show.

Web part layout decision matrix

You should also consider how much space you'll have on a page and the size of the page.

"But I don't have control over the content and size of the page!"

...I can already hear some of you say. You're absolutely right! That's why you should consider giving your authors a few layout options so that they can pick the layout that is best suited for their page designs.

Luckily, you don't have to write multiple versions of the same code to offer multiple layouts; Most of the layouts re-use the same/similar components.

For example:

  • The Grid layout and the Filmstrip layout are essentially the same, except that the Grid wraps content over multiple rows, where the Filmstrip keeps items on a single row.
  • The Filmstrip turns into a Carousel when there are more items to show that what can be displayed on a single screen.
  • The Grid turns into a Compact layout when the screen is too small

The easiest way to demonstrate this is probably with code!

Grid layout

The grid layout presents content in rectangular areas in rows and columns from left to right and top to bottom. The grid layout will attempt to fit as many columns as possible and resize the grid items, or individual content elements within the grid, to fit the entire width of the grid.

When the grid resizes, it re-flows the grid items by keeping the same number of columns but making each column narrower. When the columns become too narrow, the grid will remove one column and resize the remaining columns to fit within the grid.

When there is only room for 1 column within the grid (e.g.: when viewed on a mobile device or if the web part is located in a one-third column), the grid layout will change to a compact layout.

Grid layout re-flowing contnet
The grid layout re-flows content in rows and columns

Creating a grid layout web part

Unfortunately, there isn't a readily-usable grid layout component that you can use in your web parts. The Office UI Fabric GitHub repo has a component called TileList which gives you a grid layout, but it is still in the experiments package which contains "not production-ready components and should never be used in [production code]". I'm not sure if Microsoft ever plans on moving the TileList component to the main Office UI Fabric component, but if they do, I'll update this article and code sample accordingly.

Instead, I created a GridList component that uses the Office UI Fabric List component and made a few modifications to make it look like a grid layout. I used the code from the List of 5000 grid items sample on the UI Fabric web site as the base for my code.

You can find the code in the src/components/GridList folder from my repository.

The control reacts the same way as the out-of-the-box grid layout. It even handles compact layouts:

Custom GridList control in action

Using the GridList component in your web part

Here's how to call the GridList component in your web part:

  1. If you haven't done so already, copy the content of the src/components/GridList folder from the sample code to your own project's src/components/GridList.
  2. In your src/webparts/[YourWebPartName]/components/[[YourWebPartName].tsx, retrieve the items you wish to display. To simplify this sample, I used a hard-coded list of items. I am too lazy (and unimaginative) to come up with my own sample data, so I used Mockaroo.com to generate a JSON structure that contains the items I wanted. I saved the items to my component's state in the constructor, as follows:
export default class GridLayout extends React.Component<IGridLayoutProps, IGridLayoutState> {
  constructor(props: IGridLayoutProps) {
    super(props);

    this.state = {
      items: [{
  thumbnail: "https://pixabay.com/get/57e9dd474952a414f1dc8460825668204022dfe05555754d742e7bd6/hot-air-balloons-1984308_640.jpg",
  title: "Adventures in SPFx",
  name: "Perry Losselyong",
  profileImageSrc: "https://robohash.org/blanditiisadlabore.png?size=50x50&set=set1",
  location: "SharePoint",
  activity: "3/13/2019"
}, {
  thumbnail: "https://pixabay.com/get/55e8d5474a52ad14f1dc8460825668204022dfe05555754d742d79d0/autumn-3804001_640.jpg",
  title: "The Wild, Untold Story of SharePoint!",
  name: "Ebonee Gallyhaock",
  profileImageSrc: "https://robohash.org/delectusetcorporis.bmp?size=50x50&set=set1",
  location: "SharePoint",
  activity: "6/29/2019"
}, {
  thumbnail: "https://pixabay.com/get/57e8dd454c50ac14f1dc8460825668204022dfe05555754d742c72d7/log-cabin-1886620_640.jpg",
  title: "Low Code Solutions: PowerApps",
  name: "Seward Keith",
  profileImageSrc: "https://robohash.org/asperioresautquasi.jpg?size=50x50&set=set1",
  location: "PowerApps",
  activity: "12/31/2018"
}, {
  thumbnail: "https://pixabay.com/get/55e3d445495aa514f1dc8460825668204022dfe05555754d742b7dd5/portrait-3316389_640.jpg",
  title: "Not Your Grandpa's SharePoint",
  name: "Sharona Selkirk",
  profileImageSrc: "https://robohash.org/velnammolestiae.png?size=50x50&set=set1",
  location: "SharePoint",
  activity: "11/20/2018"
}, {
  thumbnail: "https://pixabay.com/get/57e6dd474352ae14f1dc8460825668204022dfe05555754d742a7ed1/faucet-1684902_640.jpg",
  title: "Get with the Flow",
  name: "Boyce Batstone",
  profileImageSrc: "https://robohash.org/nulladistinctiomollitia.jpg?size=50x50&set=set1",
  location: "Flow",
  activity: "5/26/2019"
}]
    };
  }
  1. Define your component's props and state interfaces so that you have what you need to retrieve and store the items you wish to display (my props in this sample does not require anything so I left it empty):
export interface IGridLayoutProps {
// Add your own props here
}

export interface IGridLayoutState {
  items: IGridItem[];
}
  1. Define an interface to hold the properties for the items you wish to display. For my sample, I used the following interface:
export interface IGridItem {
  thumbnail: string;
  title: string;
  name: string;
  profileImageSrc: string;
  location: string;
  activity: string;
}
  1. In your src/webparts/[YourWebPartName]/components/[[YourWebPartName].tsx, onRender method, insert a GridList element, as follows:
public render(): React.ReactElement<IGridLayoutProps> {
    return (
      <div className={styles.gridLayout}>
        <GridList
          items={this.state.items}
          onRenderGridItem={(item: any, finalSize: ISize, isCompact: boolean) => this.onRenderGridItem(item, finalSize, isCompact)}
        />
      </div>
    );
  }
  1. Make sure to add an import for the GridList at the top of your file:
import { GridList } from '../../../components/gridList';

Note that your GridList element uses a method called onRenderGridItem which isn't defined yet. We'll do this next.

Using the DocumentCard component to render grid items

Typically, grids use cards to showcase content, but you can also use any rectangular content you wish to use.

In our sample code, we'll use Fabric UI DocumentCard components to render our content.

Our DocumentCard elements will contain two sub-elements:

  • DocumentCardPreview: which will contain a preview image of the item
  • DocumentCardDetails: which will contain further details

DocumentCard elements

The DocumentCardDetails itself contains more elements:

  • DocumentCardTitle: The title of the element
  • DocumentCardLocation: The "location" of the element. Often used to indicate an item's category or sub-title.
  • DocumentCardActivity: Used to indicate the item's latest activities, such as date last modified and last modified by

DocumentCardDetails elements

The DocumentCardActivity defines the following props:

  • activity: Describes the activity that has taken place, such as "Created Feb 23, 2020".
  • people: One or more people who are involved in this activity. Each person consists of the following two props:
    • name: The person's name you wish to display
    • profileImageSrc: The URL for the person's profile picture.

You can define more properties, but that's what I used for this sample. If you want to see the full list of properties, look at the IDocumentCardActivityPerson interface, and the IDocumentCardActivityProps interface on the Fabric UI documentation.

When the GridList wants to render a grid item, it will call your onRenderGridItem handler with three parameters:

  • item: The item it wants to render
  • finalSize: Size of the item to render
  • isCompact: Returns true if the grid is rendering in a compact mode.

If you return a DocumentCard element within every grid item, you'll do something like this:

private onRenderGridItem = (item: any, finalSize: ISize, isCompact: boolean): JSX.Element => {
    const previewProps: IDocumentCardPreviewProps = {
      previewImages: [
        {
          previewImageSrc: item.thumbnail,
          imageFit: ImageFit.cover,
          height: 130
        }
      ]
    };

    return <div
      className={styles.documentTile}
      data-is-focusable={true}
      role="listitem"
      aria-label={item.title}
    >
      <DocumentCard
        onClick={(ev: React.SyntheticEvent<HTMLElement>) => alert(ev)} >
        <DocumentCardPreview {...previewProps} />
        <DocumentCardLocation location={item.location} />
        <DocumentCardDetails>
          <DocumentCardTitle
            title={item.title}
            shouldTruncate={true}
          />
          <DocumentCardActivity
            activity={item.activity}
            people={[{ name: item.name, profileImageSrc: item.profileImageSrc }]}
          />
        </DocumentCardDetails>
      </DocumentCard>
    </div>;
  }

Make sure to add the following imports otherwise, your code will be sad:

// Used to render document cards
import {
  DocumentCard,
  DocumentCardActivity,
  DocumentCardPreview,
  DocumentCardDetails,
  DocumentCardTitle,
  IDocumentCardPreviewProps,
  DocumentCardLocation,
  DocumentCardType
} from 'office-ui-fabric-react/lib/DocumentCard';
import { ImageFit } from 'office-ui-fabric-react/lib/Image';

Note that in my code, I simply display an alert when someone clicks on a grid item. In your real code, you'll want to replace onClick={(ev: React.SyntheticEvent<HTMLElement>) => alert(ev)} with your own onClick handler.

Notice that, in our previous code, we don't really use the isCompact parameter. It is passed to tell us whether we should render the grid using a compact layout or not.

Luckily, the DocumentCard component has a built-in compact layout that you can use by simply passing type={DocumentCardType.compact} if the DocumentCard should be compact. The default type for the DocumentCard is DocumentCardType.normal.

To handle the compact rendering, I'll simply change the DocumentCard's type depending on whether we're rendering in compact mode or not:

   <DocumentCard
        type={isCompact ? DocumentCardType.compact : DocumentCardType.normal}
        onClick={(ev: React.SyntheticEvent<HTMLElement>) => alert(ev)}

      >

Finally, the out-of-the-box grid layouts tend to remove unnecessary information when rendering in compact mode (otherwise, there would just be too much stuff). The DocumentCardLocation is often omitted in compact mode. To mimic this behaviour, I only render the DocumentCardLocation if the layout isn't compact, using the following code:

{!isCompact && <DocumentCardLocation location={item.location} />}

Which makes the final code for your onRenderGridItem as follows:

private onRenderGridItem = (item: any, finalSize: ISize, isCompact: boolean): JSX.Element => {
    const previewProps: IDocumentCardPreviewProps = {
      previewImages: [
        {
          previewImageSrc: item.thumbnail,
          imageFit: ImageFit.cover,
          height: 130
        }
      ]
    };

    return <div
      className={styles.documentTile}
      data-is-focusable={true}
      role="listitem"
      aria-label={item.title}
    >
      <DocumentCard
        type={isCompact ? DocumentCardType.compact : DocumentCardType.normal}
        onClick={(ev: React.SyntheticEvent<HTMLElement>) => alert(ev)}

      >
        <DocumentCardPreview {...previewProps} />
        {!isCompact && <DocumentCardLocation location={item.location} />}
        <DocumentCardDetails>
          <DocumentCardTitle
            title={item.title}
            shouldTruncate={true}
          />
          <DocumentCardActivity
            activity={item.activity}
            people={[{ name: item.name, profileImageSrc: item.profileImageSrc }]}
          />
        </DocumentCardDetails>
      </DocumentCard>
    </div>;
  }

If all goes well, you get something like this:
Custom GridList control

Putting the finishing touches

If you replace the sample data from above with real data, your web part should be barely distinguishable from the out-of-the-box web parts.

For example: in the screen shot below, the top web part is the out-of-the-box Highlighted content web part, while the bottom one is my own, using (almost) the exact same code as show in this post.

Bottom part is custom

The only differences are:

  • My web part needs a title (that's easy to fix, just follow the instructions in my previous post)
  • In my onRenderGridItem, I added an iconSrc parameter to my previewProps to point to the document's icon, as follows:
const previewProps: IDocumentCardPreviewProps = {
      previewImages: [
        {
          previewImageSrc: item.thumbnail,
          imageFit: ImageFit.cover,
          height: 130,
          iconSrc: item.iconSrc
        }
      ]
    };

And, of course, I used an API call to retrieve the latest documents instead of returning hard-coded data.

Conclusion

There are many standardized web part layouts you can chose from. We discussed how to use the Grid layout in today's post. The code for today's post can be found in my GitHub repo.

In our next posts, we'll continue to discuss the standard web part layouts.

I hope this helps?

Update

  • July 29, 2019: Thanks everyone for the kind comments. I have submitted my grid layout control to the @pnp/spfx-controls-react library. Hopefully, the will find it as useful as you did!

Photo credits

Introduction

I love the SharePoint Design web site. It is a beautiful web site that provides design guidance on how to create beautiful and fast sites, pages, and web parts with SharePoint in Office 365.

However, the site does not provide you with enough code samples to tell you how to create the beautiful web parts they show you.

This series is intended as a companion to the SharePoint Design site. It provides you with code samples and detailed how-to information for every design topic.

It should help you create web parts that look exactly like the ones on the SharePoint Design site.

Today's post is the third on property panes. In part I, we covered the various types of property panes. In part II we showed how to create non-reactive web parts and how to add a loading indicator.

Today, we'll look at out-of-the-box web parts and we'll discuss how to replicate some of their property panes in your own web parts.

If you want to download the code samples used in this article, download the solution from the GitHub repo.

Create a choice group with images

One of the most frequently asked questions I see with property panes is about the property pane choice groups with images.

The out-of-the-box Highlighted content, Hero, Image gallery are examples of how the choice group property field is used.

Highlighted content choice groupo
Highlighted content web part with a choice group

Hero choice group
Hero web part choice group

You should use the property pane choice group when you have a small number of choices -- I recommend less than 7, no more than 9 choices -- where the user may not be immediately able to understand the differences between the choices.

For example, the difference between the Brick, Grid and Carousel layout in the Image gallery web part may not be immediately obvious to everyone. Adding images to represent each choice makes it easier to understand:

Image gallery choice group
Image gallery web part choice group

As it turns out, it's not very complicated to do.

Let's reproduce the Image gallery layout setting for our demo. To do so, follow these steps:

  1. In your [YourWebPartName]WebPart.ts file, create a property to store the new setting you want. We'll use layout. To make things easier to read, I'll just use possible values of Brick, Grid, or Carousel, but feel free to use an integer value, an enum, or any other data type you want:
    export interface IChoiceGroupWebPartProps {
    layout: 'Brick'|'Grid'|'Carousel';
    }
  2. At the top of the file, there should already an import statement for the @microsoft/sp-property-pane. To it, add an import for PropertyPaneChoiceGroup, which is the type of field we'll need to use:
    import {
    IPropertyPaneConfiguration,
    PropertyPaneChoiceGroup
    } from '@microsoft/sp-property-pane';
  3. You'll need an image or an icon for every choice you want to show in your choice group. I like to use SVG files with dimensions of 32px by 32px. I also like to store my web part images in a folder called assets in the [YourWebPartName] folder. For this sample, we'll use brick.svg, grid.svg and carousel.svg. Feel free to store the images wherever you like.
    The images within the web part
  4. The easiest way to include the image in your bundle is to use a require statement with the path to the file. Because I'll need to use each image twice (for each choice's imageSrc and selectedImageSrc), we'll define a variable for each image we'll need at the top of the getPropertyPaneConfiguration:
    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    const layoutBrick: string = require('./assets/brick.svg');
    const layoutGrid: string = require('./assets/grid.svg');
    const layoutCarousel: string = require('./assets/carousel.svg');
    ...
  5. Add a propertyPaneChoiceGroup if your property pane's groupFields, making sure to pass the same image for the imageSrc and selectedImageSrc. Of course, you can have a different image for the selected and un-selected images if you'd like, but I don't usually. You getPropertyPaneConfiguration method will look as follows:
    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    const layoutBrick: string = require('./assets/brick.svg');
    const layoutGrid: string = require('./assets/grid.svg');
    const layoutCarousel: string = require('./assets/carousel.svg');
    return {
      pages: [
        {
          header: {
            description: null
          },
          groups: [
            {
              //groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneChoiceGroup('layout', {
                  label: "Layout", // don't forget to localize your test in a real-world solution
                  options: [
                    {
                      key: 'Brick',
                      text: 'Brick',
                      selectedImageSrc: layoutBrick,
                      imageSrc: layoutBrick,
                    },
                    {
                      key: 'Grid',
                      text: 'Grid',
                      selectedImageSrc: layoutGrid,
                      imageSrc: layoutGrid,
                    },
                    {
                      key: 'Carousel',
                      text: 'Carousel',
                      selectedImageSrc: layoutCarousel,
                      imageSrc: layoutCarousel,
                    }
                  ]
                }),
              ]
            }
          ]
        }
      ]
    };
    }

    Note that in the above code, I did not localize the text to make the code easier to read. Please consider localizing all your text.

  6. I recommend that you define a default layout value in your [YourWebPartName]WebPart.manifest.json's preconfiguredEntries:
    "properties": {
      "layout": "Brick"
    }

    Remember that your changes to the manifest.json file will not take effect until you re-build your web part and re-add it to your workbench.

If you run gulp serve on your web part, you should see the following property pane:
Choice Group property pane

Create a choice group with Fabric icons

If you're lucky enough to find Office UI Fabric icons that suit your needs, it is even easier to create a choice group with icons.

To do so, follow these steps:

  1. Add a property to store your new setting. We'll use shape and we'll allow Circle, Square, or Triangle:
    export interface IChoiceGroupWebPartProps {
    layout: 'Brick'|'Grid'|'Carousel';
    // ADDED: For icon choice group
    shape: 'Circle'|'Square'|'Triangle';
    // END: added
    }
  2. Add a PropertyPaneChoicegroup control in your getPropertyPaneConfiguration method, making sure to pass a iconProps value instead of an imageSrc and selectedImageSrc. The iconProps should contain a single property officeFabricIconFontName, which should be set to the name of the icon you wish to use:
    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    const layoutBrick: string = require('./assets/brick.svg');
    const layoutGrid: string = require('./assets/grid.svg');
    const layoutCarousel: string = require('./assets/carousel.svg');
    return {
      pages: [
        {
          header: {
            description: null
          },
          groups: [
            {
              //groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneChoiceGroup('layout', {
                  label: "Layout", // don't forget to localize your test in a real-world solution
                  options: [
                    {
                      key: 'Brick',
                      text: 'Brick',
                      selectedImageSrc: layoutBrick,
                      imageSrc: layoutBrick,
                    },
                    {
                      key: 'Grid',
                      text: 'Grid',
                      selectedImageSrc: layoutGrid,
                      imageSrc: layoutGrid,
                    },
                    {
                      key: 'Carousel',
                      text: 'Carousel',
                      selectedImageSrc: layoutCarousel,
                      imageSrc: layoutCarousel,
                    }
                  ]
                }),
                // ADDED: For icon-based choice group
                PropertyPaneChoiceGroup('shape', {
                  label: "Shape", // don't forget to localize your test in a real-world solution
                  options: [
                    {
                      key: 'Circle',
                      text: 'Circle',
                      iconProps: {
                        officeFabricIconFontName: 'CircleShapeSolid'
                      }
                    },
                    {
                      key: 'Square',
                      text: 'Square',
                      iconProps: {
                        officeFabricIconFontName: 'SquareShapeSolid'
                      }
                    },
                    {
                      key: 'Triangle',
                      text: 'Triangle',
                      iconProps: {
                        officeFabricIconFontName: 'TriangleShapeSolid'
                      }
                    }
                  ]
                })
                // END: Added
              ]
            }
          ]
        }
      ]
    };
    }
  3. As before, I recommend that you define a default shape value in your [YourWebPartName]WebPart.manifest.json's preconfiguredEntries:
    "properties": {
      "shape": "Square"
    }

Running gulp serve will give you the following:
Property pane with layout and shape choice groups

Conditional field in property panes

Sometimes, you want to show settings that are dependent on each other. If a user selects one choice, you may want to show a dependent setting, but if they select another choice, you may want to hide that setting.

The out-of-the-box Yammer web part does this by hiding or showing the Number of conversations to show setting depending on the conversation source the user selects.

Yammer web part shows and hides fields based on selection

This is what I'll call conditional fields for the purpose of this conversation.

Making fields conditional can help improve the user experience by removing choices that do not apply, therefore reducing the complexity of choice (remember Hick's law).

However, there are some guidelines you should follow:

  • Make sure that the user easily understands the reason why a field is shown or hidden. For example, the Yammer web part lets you see plainly that the Number of conversations is impacted by the conversation source.
  • Make sure that fields that depend on each other are logically grouped/visible together. For example, do not place one field on a property pane page (or step) and the field that depends on it on another page.
  • If you find it difficult to convey the relationship between two fields, you may wish to enable and disable the dependent field instead of hiding it. For bonus points, you should add some text (or, at least, some form of tooltip) that explains why the field is disabled.
  • If you want some options to be available some times, and not available some other times, consider disabling the option instead of hiding it. If the option will never be available because of a specific condition (e.g.: user doesn't have sufficient permissions, the wrong source selected, etc.), consider hiding the field instead.

To demonstrate this concept, we'll recreate the Select conversation source, Search for a sourcem and Number of conversations to show fields from the Yammer web part. To do so, follow these step steps:

  1. In your [YourWebPartName]WebPart.ts, add properties to store the conversation source, the search criteria, and the number of conversations:
    export interface IConditionalFieldWebPartProps {
    conversationSource: 'Group'|'User'|'Topic'|'Home';
    searchCriteria: string;
    numberOfConversations: number;
    }
  2. In the getPropertyPaneConfiguration, add the following code to render the property pane fields. Again, code is not localized to make it easier to read, please localize text in a real-world scenario:
    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: "Select conversation source from groups, topics, users, or home."
          },
          groups: [
            {
              groupFields: [
                PropertyPaneDropdown('conversationSource',{
                  label: "Select conversation source",
                  selectedKey: this.properties.conversationSource,
                  options: [
                    {
                      key: "Group",
                      text: "Group"
                    },
                    {
                      key: "User",
                      text: "User"
                    },
                    {
                      key: "Topic",
                      text: "Topic"
                    },
                    {
                      key: "Home",
                      text: "Home"
                    },
                  ]
                }),
                PropertyPaneTextField('searchCriteria', {
                  label: "Search for a source",
                  placeholder: "Type to search"
                }),
                PropertyPaneDropdown('numberOfConversations',{
                  label: "Number of conversations to show",
                  selectedKey: this.properties.conversationSource,
                  options: [
                    {
                      key: 4,
                      text: "Small - 4 conversations"
                    },
                    {
                      key: 8,
                      text: "Medium - 8 conversations"
                    },
                    {
                      key: 12,
                      text: "Large - 12 conversations"
                    }
                  ]
                })
              ]
            }
          ]
        }
      ]
    };
    }
  3. To Make the Search for a source field hide if the conversation source field is set to Home, add this.properties.conversationSource !== "Home" && in front of the line that begins with PropertyPaneDropdown('numberOfConversations'. Doing so says "Only execute the next line if the conversation source is not equal to Home". The new getPropertyPaneConfiguration method should look like this:

    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: "Select conversation source from groups, topics, users, or home."
          },
          groups: [
            {
              groupFields: [
                PropertyPaneDropdown('conversationSource',{
                  label: "Select conversation source",
                  selectedKey: this.properties.conversationSource,
                  options: [
                    {
                      key: "Group",
                      text: "Group"
                    },
                    {
                      key: "User",
                      text: "User"
                    },
                    {
                      key: "Topic",
                      text: "Topic"
                    },
                    {
                      key: "Home",
                      text: "Home"
                    },
    
                  ]
                }),
                // ADDED: conditional rendering for field
                this.properties.conversationSource !== "Home" && PropertyPaneTextField('searchCriteria', {
                  label: "Search for a source",
                  placeholder: "Type to search"
                }),
                PropertyPaneDropdown('numberOfConversations',{
                  label: "Number of conversations to show",
                  selectedKey: this.properties.conversationSource,
                  options: [
                    {
                      key: 4,
                      text: "Small - 4 conversations"
                    },
                    {
                      key: 8,
                      text: "Medium - 8 conversations"
                    },
                    {
                      key: 12,
                      text: "Large - 12 conversations"
                    }
                  ]
                })
              ]
            }
          ]
        }
      ]
    };
    }
  4. No necessary for this example, but if you want the mimic the Yammer web part and enable/disable the Number of conversations field on the selection, add a isDisabled attribute to the PropertyPaneDropDown field, as follows:

    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: "Select conversation source from groups, topics, users, or home."
          },
          groups: [
            {
              groupFields: [
                PropertyPaneDropdown('conversationSource',{
                  label: "Select conversation source",
                  selectedKey: this.properties.conversationSource,
                  options: [
                    {
                      key: "Group",
                      text: "Group"
                    },
                    {
                      key: "User",
                      text: "User"
                    },
                    {
                      key: "Topic",
                      text: "Topic"
                    },
                    {
                      key: "Home",
                      text: "Home"
                    },
    
                  ]
                }),
                this.properties.conversationSource !== "Home" && PropertyPaneTextField('searchCriteria', {
                  label: "Search for a source",
                  placeholder: "Type to search"
                }),
                PropertyPaneDropdown('numberOfConversations',{
                //ADDED: To enable/disable field based on selection
                  disabled: this.properties.conversationSource !== "Home",
                  label: "Number of conversations to show",
                  selectedKey: this.properties.conversationSource,
                  options: [
                    {
                      key: 4,
                      text: "Small - 4 conversations"
                    },
                    {
                      key: 8,
                      text: "Medium - 8 conversations"
                    },
                    {
                      key: 12,
                      text: "Large - 12 conversations"
                    }
                  ]
                })
              ]
            }
          ]
        }
      ]
    };
    }
  5. Set default property values in your [YourWebPartName]WebPart.manifest.json, under preconfiguredEntries:
    "properties": {
      "conversationSource": "Group",
      "searchCriteria": "",
      "numberOfConversations": 8
    }

Running a new gulp serve should give you a web part pane that behaves like the Yammer web part:

The logic for enabling/disabling and hiding/showing property pane fields in Yammer is more complicated than what we cover in this example, but I hope it'll do for now.

In my opinion, the Yammer Number of conversations should probably tell you why it is disabled. If it is never supposed to be available from any other sources than Home, it should probably be hidden instead of disabled -- but that's my personal preference, not a hard rule.

Note: Don't worry, we'll cover how to create a drop-down box with icons in a later post.

Creating conditional property pane groups

The out-of-the-box Quick chart web part allows users to choose between a Column chart or a Pie chart. If the user selects a Column chart, the property pane will show a Data property group and a Layout property group; If they choose Pie chart, the property pane will hide the Layout property group and only show the Data property group.

Quick chart with conditional property groups

By hiding or showing only the property pane groups that are relevant to the user's selected chart type, the Quick chart web part helps the user quickly configure the web part without presenting unnecessary settings.

Imagine if -- instead of hiding the Layout property pane group -- we disabled the property group entirely. The screen would be needlessly cluttered.

A while ago, I created a Chartinator web part sample for the SharePoint sp-dev-fx-webparts samples repository, which extends on the Quick chart web part and allows users to configure 9 different types of charts. Depending on which chart type the user selects, multiple property pane groups appear and disappear to help users make sense of the options available to them. If we didn't hide some sections, the property pane would be much too complicated to use.

The Chartinator web part

For this code sample, we'll emulate the Quick chart property pane and allow users to select one of two types of charts. Depending on the selection, we'll hide or show the Layout property pane group. The Data property pane group will always be available.

To keep this sample simple, we won't add any property fields in either property pane group -- we'll just put some text to illustrate the concept. If you really want to see how to implement the full Quick chart property pane, check out the Chartinator sample.

To mimic the Quick chart web part property pane, follow these steps:

  1. As before, add a property to your web part to store the Chart type selection:
    export interface IConditionalGroupWebPartProps {
    chartType: "Column" | "Pie";
    }
  2. Change the getPropertyPaneConfiguration method to create an IPropertyPaneConfiguration variable to store the property pane configuration (instead of simply returning it) so that we can manipulate the content of the property pane configuration.

    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    const chartDescription: string = this.properties.chartType === "Column" ?
      "Use a column chart to show data changes over time or comparisons among items. Categories are typically shown along the horizontal axis and values along the vertical axis."
      : "Use a pie chart to show percentages of a whole. Best when used with fewer than seven categories.";
    
      const configuration: IPropertyPaneConfiguration = {
      pages: [
        {
          header: {
            description: "Select a chart type and then select a data source. You can enter up to 12 data points, or show up to 50 data points if you use a SharePoint list on this site as the data source."
          },
          displayGroupsAsAccordion: true,
          groups: [
            {
              groupName: "Chart type",
              groupFields: [
                PropertyPaneChoiceGroup('chartType', {
                  options: [
                    {
                      key: 'Column',
                      text: 'Column chart',
                      iconProps: {
                        officeFabricIconFontName: 'BarChart4'
                      }
                    },
                    {
                      key: 'Pie',
                      text: 'Pie chart',
                      iconProps: {
                        officeFabricIconFontName: 'PieDouble'
                      }
                    }
                  ]
                }),
                PropertyPaneLabel('chartType', {
                  text: chartDescription
                }),
    
              ]
            },
            {
              groupName: "Data",
              isCollapsed: false,
              groupFields: [
                PropertyPaneLabel('data', {
                  text: "This is some sample text for the data property group."
                }),
              ]
            }
          ]
        }
      ]
    };
    
    return configuration;
    }
  3. At the end of the same method, add some conditional logic to insert a new element in the groups array using push:

    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    const chartDescription: string = this.properties.chartType === "Column" ?
      "Use a column chart to show data changes over time or comparisons among items. Categories are typically shown along the horizontal axis and values along the vertical axis."
      : "Use a pie chart to show percentages of a whole. Best when used with fewer than seven categories.";
    
      const configuration: IPropertyPaneConfiguration = {
      pages: [
        {
          header: {
            description: "Select a chart type and then select a data source. You can enter up to 12 data points, or show up to 50 data points if you use a SharePoint list on this site as the data source."
          },
          displayGroupsAsAccordion: true,
          groups: [
            {
              groupName: "Chart type",
              groupFields: [
                PropertyPaneChoiceGroup('chartType', {
                  options: [
                    {
                      key: 'Column',
                      text: 'Column chart',
                      iconProps: {
                        officeFabricIconFontName: 'BarChart4'
                      }
                    },
                    {
                      key: 'Pie',
                      text: 'Pie chart',
                      iconProps: {
                        officeFabricIconFontName: 'PieDouble'
                      }
                    }
                  ]
                }),
                PropertyPaneLabel('chartType', {
                  text: chartDescription
                }),
    
              ]
            },
            {
              groupName: "Data",
              isCollapsed: false,
              groupFields: [
                PropertyPaneLabel('data', {
                  text: "This is some sample text for the data property group."
                }),
              ]
            }
          ]
        }
      ]
    };
    // ADDED: To insert a conditional group
    // If the selected type is not Column, we don't need to make any further changes
    if (this.properties.chartType !== "Column") {
      return configuration;
    }
    
    // Get the list of property groups
    const { groups } = configuration.pages[0];
    
    // Insert a property pane
    groups.push({
      groupName: "Layout",
      isCollapsed: false,
      groupFields: [
        PropertyPaneLabel('layout', {
          text: "This is some sample text for the layout property group."
        }),
      ]
    });
    
    // END: added
    return configuration;
    }
  4. Make sure to define some default properties in your web part's manifest.json file:
    "properties": {
      "chartType": "Column"
    }

On your next gulp serve, you'll have a web part property pane hides/shows conditional property groups just like the Quick chart web part:

Sample quick chart web part with conditional groups

At the risk of repeating myself: the sample does not localize the text to help keep the code easy to read. It is killing me to do this. Please localize your text in a real solution.

Conclusion

In today's post, we discussed how to create image choice groups, create conditional property pane fields and conditional property pane groups.

All the code samples for this post can be found in my sample solution from the GitHub repo.

Every time I created a web part for this series so far, I wanted to change the body of the web part to make it look better (instead of using the default styles rendered by the Yeoman generator). It took everything in me not to tinker with the web part body and create something that would be worthy of the SharePoint Design series.

In our next post, we'll focus on the web part body.

Finally!!!

Introduction

The SharePoint Design is a beautiful web site that provides design guidance on beautiful and fast sites, pages, and web parts with SharePoint in Office 365.

Unfortunately, the SharePoint Design site does not tell you how to create the beautiful web parts they show you.

This series is intended as a companion to the SharePoint Design site, providing you with code samples and detailed how-to information for every design topic. It should help you create web parts that look exactly like the ones on the SharePoint Design site.

Today's post continues on my previous post. It discusses various aspects of property panes that you may find useful.

Like the previous post, this is a companion to the Designing SharePoint web part page.

If you want to download the code samples used in this article, download the solution from the GitHub repo.

Reactive and non-reactive web parts

By default, when you change settings in a property pane, the changes are immediately reflected within the web part on the page; the web part is considered reactive.

Reactive web parts are recommended because it follows the WYSIWYG (what you see if what you get) user experience principles for authoring.

However, sometimes you may not want every change a user makes in the property pane to immediately be reflected in the web part.

For example, if your web part has to load data or call an API every when your user changes settings, you may want to make your web part non-reactive.

When you make your web part non-reactive, your property pane will add an Apply button at the bottom of your property pane. Any changes users make in your property pane will not take effect until they select Apply.

To make your web part non-reactive, all you need to do define a disableReactivePropertyChanges function in your web part, and return true. To do so, follow these steps:

  1. In your [YourWebPartName]WebPart.ts file, find the getPropertyPaneConfiguration function.
  2. Add the following code before or after (it's really up to you):
    protected get disableReactivePropertyChanges(): boolean {
    return true;
    }

That's it!

When you run your web part, the property pane will display the Apply button we discussed.

Non-reactive property pane

Showing a property pane loading indicator

You may have noticed that most web part property panes do not show a loading indicator. That's because we always want web parts to appear fast and responsive.

A loading indicator
This is what we mean by 'Loading indicator'

However, sometimes you need to make asynchronous requests to load some data for your property pane, which could take a long time. That's when you should display a loading indicator to the end-user, so they don't think your web part has crashed or has become unresponsive.

How long before a user thinks the system has crashed? According to the Doherty Threshold law of user experience, you should provide feedback within 400 milliseconds in order to keep your user's attention and increase their trust in the system.

If your async requests take longer than 400 milliseconds, you should display a loading indicator.

When I want to display a loading indicator in the property pane, I follow these steps:

  1. First, you'll need a variable to store whether you should display the loading indicator or not (otherwise, your loading indicator will always display!). To do so, add a boolean variable to you [YourWebPartName]WebPart class:

    export default class LoadingIndicatorWebPart extends BaseClientSideWebPart<ILoadingIndicatorWebPartProps> {
    
    // ADDED: To store whether we should display the loading indicator or not
    private showLoadingIndicator: boolean = true;
    // END: added
    ...

    In my code, I always initially set it to true because I usually only load my asynchronous data once -- the first time a user displays the property pane.

  2. Add an onPropertyPaneConfigurationStart method to your web part class if you don't already have one. It will get called when preparing the property pane to display. This is where I like to make my asynchronous calls:
    // ADDED: To display the loading indicator when preparing the property pane
    protected async onPropertyPaneConfigurationStart(): Promise<void> {
    // Call your service
    // Remember that this method gets called *every time* before a user
    // displays the property pane. You should probably verify that your
    // data isn't already loaded before calling your async method again
    this.loadedPlantList = await this.getPlantNames();
    }
    // END: Added

    Note that in my code, I use an await statement to wait until my async call is complete. If you prefer, you could use this.getPlantNames().then(...), but await calls are quickly becoming the preferred way to handle async calls in React.

  3. Once your data is loaded, set your showLoadingIndicator variable to false (line 10):

    protected async onPropertyPaneConfigurationStart(): Promise<void> {
    // Call your service
    // Remember that this method gets called *every time* before a user
    // displays the property pane. You should probably verify that your
    // data isn't already loaded before calling your async method again
    this.loadedPlantList = await this.getPlantNames();
    
    //ADDED: To stop displaying the loading indicator
    // When done loading, set the loading indicator to false and refresh
    this.showLoadingIndicator = false;
    //END: Added
    }
  4. To make sure that your property pane updates when you turn the loading indicator on or off, make sure to call refresh on the property pane -- otherwise it won't take effect (line 12):

    protected async onPropertyPaneConfigurationStart(): Promise<void> {
    // Call your service
    // Remember that this method gets called *every time* before a user
    // displays the property pane. You should probably verify that your
    // data isn't already loaded before calling your async method again
    this.loadedPlantList = await this.getPlantNames();
    
    // When done loading, set the loading indicator to false and refresh
    this.showLoadingIndicator = false;
    
    //ADDED: To force the property pane to refresh
    this.context.propertyPane.refresh();
    //END: Addd
    }
  5. Finally, in your getPropertyPaneConfiguration, set the property pane's showLoadingIndicator prop to the value of your showLoadingIndicator variable (line 4):
    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      // ADDED: To display a loading indicator
      showLoadingIndicator: this.showLoadingIndicator,
      // END: Added
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneDropdown('description', {
                  label: strings.DescriptionFieldLabel,
                  options: this.loadedPlantList
                })
              ]
            }
          ]
        }
      ]
    };
    }

That's all you should need to do (in theory). In my code sample, I call a fake service to get a list of plant names and load it in a property pane drop-down box. This is what the final product looks like (note that I exaggerated the delay for demo purposes):

Property pane with loading indicator

Delaying the loading indicator

As we discussed, we want to avoid showing the loading indicator if we can. We especially want to flash (i.e.: briefly show) a loading indicator every time we load the property pane.

But you also want to keep in mind the Doherty Threshold. Delay a response for longer than 400 milliseconds and people will start clicking around.

Luckily, you can delay the loading indicator and do both!

Let's pretend that you make a call to a service which always returns with results within 300 milliseconds -- except when everyone at work is watching YouTube, then it can take a lot longer. You may want to avoid showing a loading indicator as long as your async calls take less than 300 milliseconds, but start display an indicator if the calls take longer.

All you need to do is add a loadingIndicatorDelayTime value to your property pane configuration. The system will wait for whatever delay time you specified before displaying the loading indicator. If you turn off the loading indicator before the delay has elapsed (i.e.: when your async call returns within normal conditions), the loading indicator will never show. However, if you don't turn off the indicator before the delay period, the loading indicator will automatically start showing.

To do so, change your code as follows:

  1. In your web part's getPropertyPaneConfiguration method, add the loadingIndicatorDelayTime (line 5):
    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      showLoadingIndicator: this.showLoadingIndicator,
      // ADDED: To delay the loading indicator
      loadingIndicatorDelayTime: 300,
      // END: Added
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneDropdown('description', {
                  label: strings.DescriptionFieldLabel,
                  options: this.loadedPlantList
                })
              ]
            }
          ]
        }
      ]
    };
    }

Two things to note:

  • If you don't specify a loadingIndicatorDelayTime value, and your showLoadingIndicator is set to true, SharePoint will wait 500 milliseconds by default.
  • The loadingIndicatorDelayTime also seems to delay displaying the property pane until either the delay has elapsed or your showLoadingIndicator is set to false.

Conclusion

Today we expanded a little on how to create professional property panes by making them reactive (or not) and by adding a loading indicator when necessary.

There is a lot more to cover on property panes, but we'll covert it in our next post.

I hope this helps?

Introduction

The SharePoint Design is a beautiful web site that provides design guidance on beautiful and fast sites, pages, and web parts with SharePoint in Office 365.

Unfortunately, the SharePoint Design site does not tell you how to create the beautiful web parts they show you.

This series is intended as a companion to the SharePoint Design site, providing you with code samples and detailed how-to information for every design topic. It should help you create web parts that look exactly like the ones on the SharePoint Design site.

Today's post is a companion to the Designing SharePoint web part page.

What is a property pane?

The property pane is a panel that appears when users select Edit on a web part. It allows users to enable and disable features, select a layout, connect to another web part, or set other options.

Web Part with a property pane highlighted
The property pane

When visible, the property pane is 320 pixels, which makes it the perfect size to appear full-screen on a mobile device. On a tablet or desktop device, the page content area shrinks by 320 pixels and the content re-flows responsively to make room for the property pane.

Property pane in edit mode
The property pane in action

When to use a property pane

You should use a property pane when you need to offer options to your users to configure the web part.

You should not use a property pane to edit the web part's content.

Directness is a user interface design principle which says that your users should be able to achieve their goals through a minimal set of actions. It also says that users should be able to manipulate the objects they're working on in the most direct manner.

In other words, directness says that, whenever possible, you should allow users to edit the content of a web part where the content actually resides -- in the web part's content area.

That's why the out-of-the-box Text and Hyperlink web parts make you edit content directly within the web part.

Directness in text web part
Text web part in action

Directness in hyperlink web part
Hyperlink web part in action

In contrast, the Image web part opens a pane to insert and edit an image, because there isn't a natural way to create an image directly from the web part's content area.

Image web part uses a property pane
Image web part's use of the property pane

In some cases, you may wish to use the property pane to provide additional options for the selected content of the web part. For example, the Text web part uses the property pane to provide users with additional formatting options that would otherwise clutter the text editing toolbar. Users edit the content directly within the web part, but can also use additional formatting options in the property pane.

Text web part using the property pane to format text
The text web part uses the property pane to provide additional formatting options

The best way to leverage property panes it to use it to provide options to your page authors that you don't want other users (i.e.: readers) to see. Because SharePoint automatically hides or shows the Edit depending on the user's permissions, you don't have to build your own sophisticated security logic to hide or show options within your web part.

If in doubt about whether you should show an option within the web part body or within the property pane, ask yourself whether you want all users to be able to control that option. For example, if you want to allow all users to refresh a feed via a Refresh button, or if you want users to control whether they want their personal feed to be sorted in ascending or descending order, give the users the option to do that within the web part's body.

On the other hand, if you only want page authors to control options that will affect all users, you should probably place that option within the property pane.

Take a look at the various out-of-the-box web parts to see how they present their various options for inspiration.

Types of property panes

When designing a property pane, you can use one of three property pane types:

Single pane

Used for simple web parts with a few properties to configure. As the name implies, there is only one single pane without any grouping.

Single pane

Accordion pane

Contains a group or groups of properties with many options, and where the groups result in a long scrolling list of options.

For example, you might have three groups named Properties, Appearance, and Layout, each with many options.

Accordion pane

Steps pane

Group properties in multiple steps or pages. Used when web configuration needs to follow a precise order of steps, or when choices in the first step affect the options that display in next steps.

The property pane will display a Back and Next buttons at the bottom of the pane, along with an indication of how many steps there are, and which step is currently displayed.

Steps pane

For more information on each property pane type refer to the SharePoint Design site.

Create a single pane property pane

To create a single pane property pane, follow these steps:

  • Do nothing

That's because it is what the SPFx Yeoman generator automatically produces for you when you create a web part.

Let's take a second to explore the default code to understand it better.

When you create a web part using the Yeoman generator, this is the standard getPropertyPaneConfiguration method that you get in your [YourWebPartName]WebPart class:

 protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('description', {
                  label: strings.DescriptionFieldLabel
                })
              ]
            }
          ]
        }
      ]
    };
  }

If you aren't familiar with SPFx, Typescript, or React, it may not be obvious what each part of the code does. The biggest reason is that the SharePoint team is being really nice to us and provides us with a template that encourages you to localize all your web part resources (i.e.: the text in your web parts). That's why you see strings.PropertyPaneDescription as the description for the property pane. It is a great habit to follow, and I encourage you to keep localizing all your resources -- even if your web part is only ever going to be in one language. It makes it easier to change the text (like when fixing spelling mistakes) without having to touch the code.

To make things easier, let's replace the code and see what happens. Replace your existing getPropertyPaneConfiguration code with the following code:

protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: "This is the pane description"
          },
          groups: [
            {
              groupName: "This is the group name",
              groupFields: [
                PropertyPaneTextField('description', {
                  label: "This is the field label"
                })
              ]
            }
          ]
        }
      ]
    };
  }

When test your web part (using gulp serve), your property pane will look like this:

Property pane describing each component

If we use our browser's developer toolbar to explore the various elements, we can see the individual components:

Highlights each section of a property pane

You may have noticed that the property pane's title isn't configured anywhere. That's because it uses the web part's title, which is configured in the [YourWebPartName].manifest.json under preconfiguredEntries > title > default. If you change the title, remember that the settings won't take effect until you re-bundle the web part (by running gulp serve again, or gulp build) and re-add the web part to your page.

Property pane page description

I encourage you to use the page description to provide your users with instructions on what you expect them to do. For example, this is what the out-of-the-box Image property pane page description looks like:

Change your image and image options. Turn on or off the display of text over your image, add a link, and add or modify alternative text.

The File viewer web part gives you different directions based on what type of file you select. This is what it says if you select a PowerPoint presentation:

Select the slide you want people to see first.

And this is what you get if you selected a Word document:

Select the page you want people to see first.

If you didn't select a file, however, this is what you'll see:

Add a file to view on your page. You can select from a variety of file types including Excel, Word, PowerPoint, Visio, PDFs, 3D models, and more. You can also connect to a source to dynamically view files by selecting the ellipses (…) and Connect to source.

Last example: this is what the Highlighted content web part page description says:

Select the content you want to highlight, and choose layout options.

Use your property pane page description to provide users with useful guidance. If you don't have anything useful to say, you can simply omit the page description by removing the line in your code:

 protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          // REMOVED: to remove the description
          // header: {
          //   description: "This is the pane description" //strings.PropertyPaneDescription
          // },
          groups: [
            {
              groupName: "This is the group name", //strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('description', {
                  label: "This is the field label" //strings.DescriptionFieldLabel
                })
              ]
            }
          ]
        }
      ]
    };
  }

Which gets you this:

Property Pane without Description
Property pane without a description

For the record, though, I encourage you to provide some helpful text instead of removing the description.

Property pane group

Your property pane can have more that one group. However, when you use the single pane property pane when you have few options to present, otherwise you should use the accordion or steps property pane.

You may have noticed that some of the out-of-the-box web parts have no property pane groups. Or, rather, they have a single property pane group with no group header.

Image web part property pane
The image web part property pane has no group header

Text web part property pane
The text web part property pane has no group header either

To reproduce the same look, all you need to do is omit a group header in your code:

 protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          // REMOVED: To hide the property pane page description
          // header: {
          //   description: "This is the pane description"
          // },
          groups: [
            {
              //REMOVED: For single pane property pane
              //groupName: "This is the group name",
              groupFields: [
                PropertyPaneTextField('description', {
                  label: "This is the field label"
                })
              ]
            }
          ]
        }
      ]
    };
  }

Which produces the following web part property pane:

Single pane, no header, no group header

Create an accordion pane property pane

Sometimes you have too many options to present to your users at one time.

Take the out-of-the-box Highlighted content web part, for example. Imagine if showed all available options without any grouping. It would look like this:

Highlighted content web part with no grouping
Too many options at once!

Now take the same web part, but group the options logically. You get something like this:

Highlighted content web part with grouping
Ah! Much easier to understand!

It makes sense to group your choices into smaller, collapsible sections known as accordion panes.

Why should you do this? It's the law!

Hick’s Law, to be precise. Also known as the Hick-Hyman Law, it is named after a British and an American psychologist team of William Edmund Hick and Ray Hyman. In 1952, they found a relationship between the number of stimuli present and an individual's reaction time to any given stimulus.

Specifically, they found that the more stimuli to choose from, the longer it takes the user to make a decision.

(That's what I like to call the Cheesecake Factory Law, because of how long it takes to decide what to eat when at the Cheesecake Factory. Have you seen how long their menu is?!)

When you bombard your users with too many choices at once, they have to take time to interpret and decide, giving them work they don't want.

If you have a lot of choices to offer your users, consider grouping them into smaller choices. For example, instead of giving you 7 configuration choices to pick from, the Highlighted content web part gives you three simple choices:

  • Content
  • Filter and sort
  • Layout

Within each of those three, you have two or three smaller choices to make. Making it easier for users to make sense of the choices that are available to them.

Note that the rule only applies to choices that the user is unfamiliar with. For example, if you want to list the months of the year, don't group them into smaller choices. Otherwise, you'll actually increase the time it takes for a user to find the item they want.

For this sample, we have created 6 properties (demoProperty1 through demoProperty6) which we'll group into three categories (Group 1, Group 2 and Group 3). Feel free to use your own categories and properties.

If you want to create an accordion pane property pane, follow these steps:

  1. In your [YourWebPartName]WebPart.ts file, find the getPropertyPaneConfiguration function and create as many groups as you need, with fields within those groups.
    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: "This web part demonstrates how to use an accordion property pane"
          },
          groups: [
            {
              groupName: "Group 1",
              groupFields: [
                PropertyPaneTextField('demoProperty1', {
                  label: "Property 1"
                }),
                PropertyPaneTextField('demoProperty2', {
                  label: "Property 2"
                })
              ]
            },
            // ADDED: Group 2 and 3 for accordion support
            {
              groupName: "Group 2",
              groupFields: [
                PropertyPaneTextField('demoProperty3', {
                  label: "Property 3"
                }),
                PropertyPaneTextField('demoProperty4', {
                  label: "Property 4"
                })
              ]
            },
            {
              groupName: "Group 3",
              groupFields: [
                PropertyPaneTextField('demoProperty5', {
                  label: "Property 5"
                }),
                PropertyPaneTextField('demoProperty6', {
                  label: "Property 6"
                })
              ]
            }
            // END added
          ]
        }
      ]
    };
    }

    The groups and groupFields nodes expect arrays of items, which means that we can have many groups and many fields within the groups.

  2. To make the groups appear as accordions, add a displayGroupsAsAccordion property and set it to true, as follows (see line 9):
    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: "This web part demonstrates how to use an accordion property pane"
          },
          // ADDED: to turn groups into accordions
          displayGroupsAsAccordion: true,
          // END added
          groups: [
            {
              groupName: "Group 1",
              groupFields: [
                PropertyPaneTextField('demoProperty1', {
                  label: "Property 1"
                }),
                PropertyPaneTextField('demoProperty2', {
                  label: "Property 2"
                })
              ]
            },
            {
              groupName: "Group 2",
              groupFields: [
                PropertyPaneTextField('demoProperty3', {
                  label: "Property 3"
                }),
                PropertyPaneTextField('demoProperty4', {
                  label: "Property 4"
                })
              ]
            },
            {
              groupName: "Group 3",
              groupFields: [
                PropertyPaneTextField('demoProperty5', {
                  label: "Property 5"
                }),
                PropertyPaneTextField('demoProperty6', {
                  label: "Property 6"
                })
              ]
            }
          ]
        }
      ]
    };
    }
  3. If you wish to make the groups initially appear as expanded or collapsed, you can specify the isCollapsed property at the group level and set it to true or false, as desired:
    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: "This web part demonstrates how to use an accordion property pane"
          },
          displayGroupsAsAccordion: true,
          groups: [
            {
              groupName: "Group 1",
              // ADDED: to collapse group initially
              isCollapsed: true,
              // END added
              groupFields: [
                PropertyPaneTextField('demoProperty1', {
                  label: "Property 1"
                }),
                PropertyPaneTextField('demoProperty2', {
                  label: "Property 2"
                })
              ]
            },
            {
              groupName: "Group 2",
              // ADDED: to collapse group initially
              isCollapsed: true,
              // END added
              groupFields: [
                PropertyPaneTextField('demoProperty3', {
                  label: "Property 3"
                }),
                PropertyPaneTextField('demoProperty4', {
                  label: "Property 4"
                })
              ]
            },
            {
              groupName: "Group 3",
              // ADDED: to collapse group initially
              isCollapsed: true,
              // END added
              groupFields: [
                PropertyPaneTextField('demoProperty5', {
                  label: "Property 5"
                }),
                PropertyPaneTextField('demoProperty6', {
                  label: "Property 6"
                })
              ]
            }
          ]
        }
      ]
    };
    }
  4. If you run gulp serve, you should get an accordion pane property pane:
    A custom accordion pane

Note: To keep the code easier to read, we did not localize our text resources. Please don't do this at home.

Create a steps pane property pane

As we previously discussed, if you have a lot of configuration options in your web part, you should use an accordion pane and group your configuration options to make it easier for your users. Accordion panes work well when you want to allow your users to configure your web part in no particular order.

Sometimes, you need your users to configure your web part by following a specific sequence.

That's when you should use steps pane property panes.

To prepare this sample, we created demoProperty1 through demoProperty10. We'll show these options across 3 pages, divided into 5 groups. Feel free to use your own properties.

Just like the groups and groupField property, the pages property expects an array. You can define more than one page and they will be displayed in the same order that you defined them in the code.

To create our sample steps pane property pane, follow these steps:

  1. In your [YourWebPartName]WebPart.ts file, find the getPropertyPaneConfiguration function and create as many pages as you need, with groups and group fields within those pages:

    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: "This is the first page."
          },
          displayGroupsAsAccordion: false,
          groups: [
            {
              groupName: "Group 1",
              groupFields: [
                PropertyPaneTextField('demoProperty1', {
                  label: "Property 1"
                }),
                PropertyPaneTextField('demoProperty2', {
                  label: "Property 2"
                })
              ]
            },
            {
              groupName: "Group 2",
              groupFields: [
                PropertyPaneTextField('demoProperty3', {
                  label: "Property 3"
                }),
                PropertyPaneTextField('demoProperty4', {
                  label: "Property 4"
                })
              ]
            }
          ]
        },
        // ADDED: To add steps
        {
          header: {
            description: "This is the second page."
          },
          displayGroupsAsAccordion: false,
          groups: [
            {
              groupName: "Group 3",
              groupFields: [
                PropertyPaneTextField('demoProperty5', {
                  label: "Property 5"
                }),
                PropertyPaneTextField('demoProperty6', {
                  label: "Property 6"
                })
              ]
            },
            {
              groupName: "Group 4",
              groupFields: [
                PropertyPaneTextField('demoProperty7', {
                  label: "Property 7"
                }),
                PropertyPaneTextField('demoProperty8', {
                  label: "Property 8"
                })
              ]
            }
          ]
        },
        {
          header: {
            description: "This is the third and final page."
          },
          displayGroupsAsAccordion: false,
          groups: [
            {
              groupName: "Group 5",
              groupFields: [
                PropertyPaneTextField('demoProperty9', {
                  label: "Property 9"
                }),
                PropertyPaneTextField('demoProperty10', {
                  label: "Property 10"
                })
              ]
            }
          ]
        }
        //END added
      ]
    };
    }
  2. By default, the web part will start the property pane on the first page. If you'd like to override this (for example, if you want to bring attention to a specific page to encourage a user to solve an issue), specify the currentPage property as follows (see line 4):

    protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      // ADDED: To change the default current page
      currentPage: 3,
      // END added
      pages: [
        {
          header: {
            description: "This is the first page."
          },
          displayGroupsAsAccordion: false,
          groups: [
            {
              groupName: "Group 1",
              groupFields: [
                PropertyPaneTextField('demoProperty1', {
                  label: "Property 1"
                }),
                PropertyPaneTextField('demoProperty2', {
                  label: "Property 2"
                })
              ]
            },
            {
              groupName: "Group 2",
              groupFields: [
                PropertyPaneTextField('demoProperty3', {
                  label: "Property 3"
                }),
                PropertyPaneTextField('demoProperty4', {
                  label: "Property 4"
                })
              ]
            }
          ]
        },
        // ADDED: To add steps
        {
          header: {
            description: "This is the second page."
          },
          displayGroupsAsAccordion: false,
          groups: [
            {
              groupName: "Group 3",
              groupFields: [
                PropertyPaneTextField('demoProperty5', {
                  label: "Property 5"
                }),
                PropertyPaneTextField('demoProperty6', {
                  label: "Property 6"
                })
              ]
            },
            {
              groupName: "Group 4",
              groupFields: [
                PropertyPaneTextField('demoProperty7', {
                  label: "Property 7"
                }),
                PropertyPaneTextField('demoProperty8', {
                  label: "Property 8"
                })
              ]
            }
          ]
        },
        {
          header: {
            description: "This is the third and final page."
          },
          displayGroupsAsAccordion: false,
          groups: [
            {
              groupName: "Group 5",
              groupFields: [
                PropertyPaneTextField('demoProperty9', {
                  label: "Property 9"
                }),
                PropertyPaneTextField('demoProperty10', {
                  label: "Property 10"
                })
              ]
            }
          ]
        }
        //END added
      ]
    };
    }

Try to avoid skipping directly to another page without a valid reason, otherwise, your users may be a bit confused. If you choose to skip to a page other than page 1, help the users understand why you did it -- perhaps with a little message at the top of the page.

Once completed, you should get a property pane with three pages (I kept the default page to page 1, in case you're wondering:
A steps pane property pane

Conclusion

That's probably enough for one day.

As you hopefully noticed, creating property panes isn't very difficult. Most of the work is already done for you.

The code for this post is available on the SharePoint Framework Web Part Design Series repo.

In our next post, we'll continue discussing property panes with some advanced options, such as adding images to property pane headers, showing loading indicators, and making responsive/non-responsive property pane web parts. We'll also discuss what the text within these property panes should look like.

Then we'll discuss more exciting stuff, like creating conditional property pane groups and fancy property pane fields.

I hope this helps?

Introduction

The SharePoint Design is a beautiful web site that provides design guidance on beautiful and fast sites, pages, and web parts with SharePoint in Office 365.

Unfortunately, the SharePoint Design site does not tell you how to create the beautiful web parts they show you.

This series is intended as a companion to the SharePoint Design site, providing you with code samples and detailed how-to information for every design topic. It should help you create web parts that look exactly like the ones on the SharePoint Design site.

Today's post is a companion to the Titles and descriptions for SharePoint web parts

What is a Web Part Title?

A Web Part title is a heading that appears above a web part to visually separate the web part from other content and to provide information to users about the purpose of a web part.

Source: Microsoft

Most often, web parts provide a default title that page authors can override (or remove) to customize the web parts to suit their needs.

What is a Web Part Description

The Web Part description is often smaller text within the web part that describes the web part content. For example, the caption element in the Image web part.

The caption in the image web part is an example of a web part description

Just like the title, page authors can override or omit the web part description to suit their needs.

It is important to not confuse description with alternative text. The description is optional, but it is always visible.

The alternative text is something that you should always provide for visual content (such as images, charts, etc.). We'll discuss alternative text in a later post.

Why use titles and descriptions?

Understanding

Back in my McKinsey & Company days, a wise person told me:

When you leave it up to your audience to make an assumption, you lose control over that assumption

Seems kind of obvious, but what they meant was that if you present information to users and expect them to make a conclusion from it, they may make the wrong conclusion.

Don't assume your users are stupid, but don't assume that by just putting a web part on the page that they will understand the purpose of the web part is.

Titles and descriptions are a consistent way to indicate to your users the purpose of your web part.

Accessibility

When I tell people about accessibility requirements in software design, most people's initial reaction is often something like:

"We don't have any blind people [who work here], so we don't need to worry about accessibility"

Unfortunately, accessibility is more than about blindness. It is estimated that more than 10% of the population live with a disability.

Accessibility is actually broken down into 4 categories:

  • Visual: This can include blind (or non-sighted) users, but also includes users with low-vision, users with obstructed vision, or age-related visual impairment. This includes color blindness, which affects 1 in 12 men (or about 8%) and 1 in 200 women.
  • Auditory: This includes hearing-impaired users.
  • Motor: People with motor impairments includes people with Repetitive Stress Injuries, Cerebral palsy, Parkinson’s and Muscular dystrophy.
  • Cognitive: which relates to the ease processing of information. It includes people with autism, Down's syndrome, Dyslexia, or global developmental delay. Consider stroke victims and concussion victims as well.

Accessibility may be a permanent or temporary condition. People often (wrongly) assume that web accessibility is only for people with permanent or long-term disabilities, but accessibility benefits people with or without disabilities. For example, it may impact the following people:

  • People who are not fluent in English.
  • People who do not have or are unable to use a keyboard or mouse.
  • People with temporary disabilities due to accident or illness.
  • Older people.
  • New users.

Plus, in some countries, you are now legally obligated to provide accessible resources to your employees.

Lucky for you, the use of titles and descriptions in web parts help alleviate some of the accessibility issues by providing headings, plenty of white space on the page, visually grouping information together, and even tackling colour contrast requirements on the page -- to name only a few benefits.

When should you use titles

Let's pretend for a second that we are not talking about SharePoint, but Microsoft Word instead. You can add things in a Word document that will make the body of the document. Things like text, images, hyperlinks, page breaks, etc. When you insert such elements in a Word document, you want them to blend in together and print nicely.

If your web part is intended to blend in with other content within your SharePoint page, you probably don't need a title. For example, the following out-of-the-box web parts do not need a title:

  • Text
  • Image
  • File viewer
  • Link
  • Embed
  • Divider
  • Markdown

On the other hand, if your web part provides a new set of information that should be visually distinctive from the rest of your page content, it should probably provide a title.

For example, if you add the Events web part or the Document library web part in a page, you probably don't want to dump a list of events or documents in between two paragraphs without providing context to your users.

This decision tree may help. You can tell that it was written by a consultant because one of the answers is "it depends".

st=>start: Does my web part need a title?
e=>end: No title needed
cond=>condition: Does your web part 
need to blend in?
cond2=>condition: Does it need to visually
group information?
io=>parallel: No title needed
para=>end: Title is recommended
st->cond
e3=>end: It depends :-)
cond(yes)->e
cond(no)->cond2(yes)->para
cond2(no)->e3

When should you use descriptions

Unlike the web part title, the description is not intended to visually distinguish the web part. It is purely intended to help understand the purpose or context of a web part. I recommend providing the option for a description in your web parts if the body of your web part mostly consists of images or graphics. For example, a chart web part, or an image.

In my Comparer web part, which allows users to compare two images side-by-side, I allow editors to provide a description for each image.

How to add a web part title to your web part

If you want to see the sample web part I'm building in this post, visit the GitHub repo and open the WebPartTitles solution. The web part is called EditableTitle

I chose the web part title as the first in this series because it is probably the easiest one to implement. That's because the PnP Reusable Controls library provides a perfect Web Part Title component.

Web Part Title Control in Action

Here is how to add the web part title in your SPFx web part solution using React:

  1. Using your Node.js Command Prompt (or whatever terminal you wish to use), make sure that you're in your web part solution's root directory. (It should be the same folder where your solution's package.json is located).
  2. Type the following command to add the PnP Reusable Controls library to your project:
    npm install @pnp/spfx-controls-react

    > Note: most instructions tell you to use npm install @pnp/spfx-controls-react --save --save-exact, but the --save parameter is considered obsolete now. Feel free to use what you feel most comfortable with.

  3. In your web part class (the one that extends BaseClientSideWebPart), add a property to store your web part's title. If your web part is called EditableTitle, you would open the EditableTitleWebPart.ts file, and look for the IEditableTitleWebPartProps interface (usually located at the top of the file. Add the following code:
    export interface IEditableTitleWebPartProps {
    description: string;
    // BEGIN Add to support web part title
    title: string;
    // END Add
    }
  4. In your render method, your web part will need to pass the web part title, the displayMode (to determine whether the web part is in Edit Mode or Read Mode), and a function to handle changes to the title. All these things get passed to the component that is responsible for rendering the body of your web part. Your new render method will look like this:

    public render(): void {
    const element: React.ReactElement<ieditabletitleprops> = React.createElement(
      EditableTitle,
      {
        description: this.properties.description,
        // BEGIN: Add to support web part title
        // Don't forget that you need to add a comma at the end of the previous line
        title: this.properties.title,
        displayMode: this.displayMode,
        updateTitle: (value: string) => {
          this.properties.title = value;
        }
        //END: Add
      }
    );
    
    ReactDom.render(element, this.domElement);
    }

    If you get a nasty error message when you add the code, don't worry. We just haven't defined a title, displayMode, and updateTitle property for your component yet.

    Error message in code
    Don't worry about this error message, we'll fix it soon

  5. Open your component's I[YourWebPartName]Prop interface (located under src\webparts\[YourWebPartName]\components\I[YourWebPartName]Props.ts). Since my web part is called editableTitle, I'll open src\webparts\editableTitle\components\IEditableTitleProps.ts and add an import for the DisplayMode at the top of your file:
    import { DisplayMode } from '@microsoft/sp-core-library';
  6. Add the properties your component will need to support the web part title in your I[YourWebPartName]Props interface:
    export interface IEditableTitleProps {
    description: string;
    //BEGIN: Add support for web part title
    title: string;
    displayMode: DisplayMode;
    updateTitle: (value: string) => void;
    //END: Add
    }
  7. Open the web part's component TSX file (located under src\webparts\[YourWebPartName]\components\[YourWebPartName].tsx) and add an import for the WebPartTitle control:
    import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
  8. In your [YourWebPartName]'s render method, add the WebPartTitle control. You should add it immediately after the first opening <div>:
    public render(): React.ReactElement<ieditabletitleprops> {
    return (
      <div classname="{styles.editableTitle}">
        {/* BEGIN: Add to support web part title */}
        <webparttitle displaymode="{this.props.displayMode}" title="{this.props.title}" updateproperty="{this.props.updateTitle}">
        {/* END: Add */}
        <div classname="{styles.container}">
          <div classname="{styles.row}">
            <div classname="{styles.column}">
              <span classname="{styles.title}">Welcome to SharePoint!</span>
              <p classname="{styles.subTitle}">Customize SharePoint experiences using Web Parts.</p>
              <p classname="{styles.description}">{escape(this.props.description)}</p>
              <a href="https://aka.ms/spfx" classname="{styles.button}">
                <span classname="{styles.label}">Learn more</span>
              </a>
            </div>
          </div>
        </div>
      </webparttitle></div>
    );
    }

    Note: some people prefer to add the web part title within the second div -- the one with the container CSS class. I'll be removing that div in future code samples, but feel free to do as you please.

  9. Run gulp serve to test your web part and try editing your web part's title

The Web Part Title in action

Adding a default title

To make things easier for your users, you should provide a default title for your web part. Try to use a title that users won't immediately have to change (like Insert Title Here). For example, if your web part retrieves a list of recently added documents, try using Recent documents. If your web part displays events, try Upcoming events or Events.

Since the web part title is stored in a web part property (we called it title in our sample above), the easiest way to add a default title is to use your web part manifest's preconfiguredEntries to provide a default title.

To do so:

  1. Open the [YourWebPartName]WebPart.manifest.json file (located under the src\webparts\[YourWebPartName] folder)
  2. Locate the preconfiguredEntries JSON node
  3. Under the "properties" node, add the default web part title as follows:
    "properties": {
      "description": "EditableTitle",
      "title": "My Default Web Part Title"
    }
  4. Run gulp serve again. If your solution was already running, you need to stop the existing gulp serve and start a new one, otherwise, the updated manifest won't be reflected in your web part.
  5. If your web part was already running, remove it from the workbench, refresh the page, and re-add the web part. You should see your updated web part, now with a default title:

My Default Web Part Title

Adding a placeholder title

If your web part title is optional, or you prefer to start with a blank title, you should add a placeholder.

For example, the People web part uses People profiles for the placeholder title. It shows the title placeholder when your page is in Edit mode, but if you don't specify a title and save your page to view it in Read mode, the title will disappear.

People web part in edit mode shows placeholder
People web part in edit mode shows placeholder

People web part in reading mode hides placeholder
People web part in reading mode hides placeholder

To implement this functionality, follow the same steps you did to add the web part title, but don't provide a default title. Then:

  1. In the [YourWebPartName].tsx file, find your WebPartTitle component in the render method.
  2. Add the placeholder prop with the placeholder title you want to use.
    <webparttitle displaymode="{this.props.displayMode}" title="{this.props.title}" updateproperty="{this.props.updateTitle}" placeholder="{"Web" part="" placeholder"}="">

Note: please consider localizing your placeholder text. I'll explain localizing in a later post.

If you want to see the sample web part I'm building in this post, visit the GitHub repo and open the WebPartTitles solution. The web part is called PlaceholderTitle

How to add a description

Unfortunately, there isn't a WebPartDescription component that you can use to easily add a description section to your web part.

Fortunately, we can cheat and use the WebPartTitle component and apply some CSS magic to get the same results.

Here is how:

  1. If you haven't done so already, add the PnP Reusable Controls library to your project by typing the following command:

    npm install @pnp/spfx-controls-react
  2. In your web part class (located under src\webparts\[YourWebPartName]\[YourWebPartName]ts. Add a property to store your description. Since the default web part already has a description prop, I used webPartDescription, but in real life, I'd use the description prop for that purpose.

    export interface IEditableTitleWebPartProps {
    description: string;
    // BEGIN Add to support web part description
    webPartDescription: string;
    // END Add
    }
  3. In your render method, your web part will need to pass the web part description, the displayMode (to determine whether the web part is in Edit Mode or Read Mode), and a function to handle changes to the description. All these things get passed to the component that is responsible for rendering the body of your web part. Your new render method will look like this:

    public render(): void {
    const element: React.ReactElement<ieditabletitleprops> = React.createElement(
      EditableTitle,
      {
        description: this.properties.description,
        // BEGIN: Add to support web part description
        // Don't forget that you need to add a comma at the end of the previous line
        webPartDescription: this.properties.webPartDescription,
        displayMode: this.displayMode,
        updateDescription: (value: string) => {
          this.properties.webPartDescription = value;
        }
        //END: Add
      }
    );
    
    ReactDom.render(element, this.domElement);
    }
  4. Open your component's I[YourWebPartName]Prop interface (located under src\webparts\[YourWebPartName]\components\I[YourWebPartName]Props.ts). Since my web part is called webPartDescription, I'll open src\webparts\webPartDescription\components\IWebPartDescriptionProps.ts.

  5. If you haven't done so already, add an import for the DisplayMode at the top of your file:

    import { DisplayMode } from '@microsoft/sp-core-library';
  6. Add the properties your component will need to support the web part description in your I[YourWebPartName]Props interface:

    export interface IWebPartDescriptionProps {
    description: string;
    //BEGIN: Add support for web part description
    webPartDescription: string;
    displayMode: DisplayMode;
    updateDescription: (value: string) => void;
    //END: Add
    }
  7. Open the web part's component TSX file (located under src\webparts\[YourWebPartName]\components\[YourWebPartName].tsx).

  8. If you haven't done so already, add an import for the WebPartTitle control:

    import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
  9. In your [YourWebPartName]'s render method, add the WebPartTitle control where you would like the description to appear. We also add a placeholder (Add a description to override the default Web Part Title placeholder that comes with the WebParttitle component). In my sample, I'll add it below the container div:

    public render(): React.ReactElement<iwebpartdescriptionprops> {
    return (
      <div classname="{" styles.webpartdescription="" }="">
        <div classname="{" styles.container="" }="">
          <div classname="{" styles.row="" }="">
            <div classname="{" styles.column="" }="">
              <span classname="{" styles.title="" }="">Welcome to SharePoint!</span>
              <p classname="{" styles.subtitle="" }="">Customize SharePoint experiences using Web Parts.</p>
              <p classname="{" styles.description="" }="">{escape(this.props.description)}</p>
              <a href="https://aka.ms/spfx" classname="{" styles.button="" }="">
                <span classname="{" styles.label="" }="">Learn more</span>
              </a>
            </div>
          </div>
        </div>
         {/* BEGIN: Add to support web part description */}
         <webparttitle displaymode="{this.props.displayMode}" title="{this.props.webPartDescription}" updateproperty="{this.props.updateDescription}" placeholder="{"Add" a="" description"}="">
        {/* END: Add */}
      </webparttitle></div>
    );
    }
  10. To change the CSS for the web part description, we'll add some CSS. Open the [YourWebPartName].module.scss (located under src\webparts\[YourWebPartName]\components\[YourWebPartName].module.scss) and add the following CSS just above the .container class:

    .descriptionElement textarea {
    font-size: 14px;
    font-weight: 400;
    line-height: 1.6em;
    overflow-x: hidden;
    text-overflow: ellipsis;
    color: $ms-color-neutralPrimary;
    }
    
    .descriptionElement__NoMargin textarea  {
    margin-bottom: 0;
    }
    
    .descriptionElement__centerAlign textarea {
    text-align: center;
    }

    We're defining three CSS classes here: descriptionElement to render a smaller font, descriptionElement__NoMargin to remove the bottom margin, and descriptionElement_centerAlign to align the description at the center of the web part. You can use all three CSS styles, or only descriptionElement as it suits you.

  11. Go back to your [YourWebPartName].tsx.

  12. Add a reference to the css() function at the top of the file.

    import { css } from "@uifabric/utilities/lib/css";

    The css() function allows you to combine multiple CSS classes. To do so, I added. If you want to read more about the css() function, read my post.

  13. Add the CSS prop to the WebPartTitle you added in step 9. Use whatever combination of the three CSS classes we defined earlier as you desire (I use all three here):

         {/* BEGIN: Add to support web part description */}
         <webparttitle displaymode="{this.props.displayMode}" title="{this.props.webPartDescription}" updateproperty="{this.props.updateDescription}" placeholder="{"Add" a="" description"}="" classname="{css(styles.descriptionElement," styles.descriptionelement__nomargin,="" styles.descriptionelement__centeralign)="" }="">
        {/* END: Add */}
  14. Run gulp serve and test your web part.

You should get something that looks like this:

Web part description

Note: I really dislike the default shadowy border that comes with every Yeoman-generated SPFx web part because it isn't found anywhere else in the out-of-the-box SharePoint web parts. I always remove it from my projects. Although this is outside of the scope of this post, if you want to remove it, simply comment out the box-shadow line in the .container CSS class of your [YourWebPartName].module.scss. You'll get the following result:
Web parts without borders

If you want to see the code for this web part, visit the GitHub repo and open the WebPartTitles solution. The web part is called WebPartDescription. I know, I didn't think the web part name through when I built it.

UI text guidelines for Titles and descriptions

I've mostly copied this from the UI text guidelines for SharePoint web parts, but I include it here (and will continue to include the relevant sections throughout this series) because it is important to use consistent User Interface (UI) text within your web parts.

Capitalization

Use sentence casing (the first letter of the first word is capitalized, the rest all lowercase) for all titles.

Always capitalize:

  • The first word of a web part title or description
  • The word following a colon in a title. For example, "Step 1: Begin by entering your account information."
  • Proper nouns, such as the names of people, cities, and so on.

Punctuation

Don't use periods in titles. For other punctuation types, follow the basic rules of punctuation.

Voice and tone

If you want your users to fall in love with your web parts, make sure to use the right tone in your UI text. It will help build a strong, lasting relationship with your users.

Try to keep your words crisp and clear, warm and relaxed, and approachable.

Follow these simple tips:

  • Use a casual, conversational tone in the UI.
  • Use contractions. For example, use "can't" instead of "cannot".
  • Read your UI text out loud to test the tone. Does it sound like everyday language?
  • Use simple words.
  • Remove technical details if they're not relevant to the user experience.
  • Use "Please" only if you are inconveniencing the user. Avoid overuse.
  • Use "Sorry" only in error messages in SharePoint that result in serious problems for the customer.

Pronouns

Avoid pronouns if you can.

If you must use pronouns, follow these rules:

  • Use second person ("you" or "your") when you're presenting something that belongs to the user. For example, "Your drafts" or "Your images".
  • Use the first person ("me" or "my") for UI in which the user instructs the service to do something. For example, "Alert me when someone responds to my post."
  • Use "they" or "their" as a singular possessive modifier to avoid awkward "he/she" or "his/her" constructs. Ideally, rewrite the sentence as plural if possible.
  • Avoid using "them"; instead, use words like "someone" or "people". For example, "Enter a user name and domain to give someone permission to use this PC."

If you want to sound really cold and snobby, use third person references. Instead of saying "Users can change the layout", use a phrase like "You can change the layout".

Hint text

Hint text, or ghost text, is the text element you display in a UI element to help the user interact with the UI.

In the case of titles and descriptions, your hint text is your placeholder attribute. It should give information about what the user should enter.

You should try to use hint text sparingly, and only if it helps the user. Because titles and descriptions are not usually visible on a web part if they are empty, this is an instance where you should definitely use hint text.

Conclusion

This post described how to add titles and descriptions to your web parts in accordance with the SharePoint Design Principles.

In future posts, I'll explain the other design areas... but since the other design areas are a bit more complicated, I'll break each down into a few smaller posts.

I hope this helps?

Thanks

This post wouldn't have been possible without the amazing work from the SharePoint team. The technical writers don't often get the glory, so we should especially thank Linda Caputo and David Chestnut for their contributions.

More Information

I could have gone on for days about some design topics in this article, but I'll let the actual experts do the talking:

Update

  • July 12, 2019: Added the UI text section because it was driving me nuts that I had omitted it in the first place.
  • July 17, 2019: Fixed some embarassing spelling mistakes. I thought I was on a roll and writing well, but it turns out my spell checker was simply turned off.

Introduction

Sometimes you just need to figure out what version of the SPFx Yeoman generator is installed on someone's machine.

I got tired of having to look it up all the time, but I can never find the command (probably because it is too obvious for most people to write it down?).

So here is a note for myself, but I hope it helps somebody else too one day.

Getting the version number

To find the version of your SPFx Yeoman generator, follow these steps:

  1. Launch a Node.js command prompt command or whatever terminal you use
  2. Type the following command:
    npm ls -g --depth=0 @microsoft/generator-sharepoint
  3. Wait...
  4. The response should look a little like this:
    @microsoft/generator-sharepoint@1.8.2

The command isn't specific to the SPFx Yeoman generator. It can be used for any NPM package. Here is what it really does:

  • npm indicates a Node Package Manager command.
  • ls means to list packages
  • -g means that you want to list the global packages. If you don't use -g, you'll only list the packages installed in the current solution (assuming that you're currently in a folder that contains a solution)
  • --depth=0 means that you only want the top-level modules. In other words, you don't want to list all modules that includes the package you're looking for.
  • @microsoft/generator-sharepoint is the actual package you want to list. You can actually put whatever package you want here. For example, npm ls -g --depth=0 yo would tell you what version of the Yeoman generator is globally installed, and npm ls --depth=0 office-ui-fabric-react would tell you what version of Office UI Fabric React is currently installed in your current solution.

How to check if Yeoman has an update for you

As Stefan Bauer pointed out, if you don't want to know which version of the SPFx Yeoman generator you have installed, but you want to see if there is an update, you can follow these steps:

  1. Launch a Node.js command prompt command or whatever terminal you use
  2. Type the following command:
    yo
  3. Yeoman will greet you. If you have an update available, Yeoman should tell you right away (see how my Office generator has an update in the screen shot below)
    Office (Heart) Update Available!
  4. Select Update your generators
  5. Yeoman will prompt you to select the generators you want to upgrade. Use the spacebar to toggle which generators you want to update, then press Enter
    Selected generator-office
  6. Yeoman will do its thing, then will tell you I've just updated your generators. Remember, you can update a specific generator with npm by running npm install -g generator-______. Good to know Yeoman, good to know.
    Upgraded!

Conclusion

This article shows you how you can use a standard NPM command to query what version of the SPFx Yeoman generator is installed on a workstation.

You can use the same command for any NPM package, but in my particular case, I just wanted to remember how to diagnose the version of the SPFx Yeoman generator.

There may be an easier/faster way to do this. If you know a different way, please share with the rest of the class.

I hope it helps?

Update

  • Thanks to Stefan Bauer (https://n8d.at/) for confirming that there aren't any faster ways to do this, and for also suggesting that I explain how the command works. The section about using yo to update your generators was also his idea. Stefan is someone that I respect immensely and I truly appreciated his feedback!

Introduction

Anyone who has worked with me on a SharePoint project knows that I firmly believe that a good custom web part must be indistinguishable from the out-of-the-box SharePoint web parts. They need to look and behave like they were written by whichever awesome team at Microsoft is responsible for writing those things.

Someone at Microsoft must feel the same way as I do because they took the take to create a nice SharePoint Design web site dedicated to the SharePoint design principles; not just for web parts, but for sites and pages as well.

If you haven't visited it yet, I encourage you to do so now. Go ahead, I'll wait.

I like that Microsoft is documenting their design principles for web part look and feel, but they do not explain how to achieve the awesome look they show.

That's why I've created this series of posts on how to build SharePoint web parts, using SPFX, that follow the Microsoft design principles. Each post is intended as a companion to their respective section in the SharePoint Design site.

At the end of this series, you'll be able to build beautiful web parts that will conform to Microsoft's design principles and that will be indistinguishable from the out-of-the-box web parts.

In other words, the perfect web part.

Why should I follow Microsoft's design principles for SharePoint web parts?

Luke Wroblewski, a Product Director at Google once wrote:

“Getting in the way of a speeding freight train usually doesn’t end well. It takes a lot of effort to shift the course of something with that much momentum. Rather than forcing people to divert their attention from their primary task, come to where they are.”

Every web part with custom styles, fonts, and designs that are hosted within a page competes for your user's attention -- in a bad way. You're asking your users to learn a new user interface with every custom-layout web part you create.

Instead of focusing on your content, your users have to struggle just to make sense of your user interface.

In UX (user experience) circles, that's a concept called Cognitive overload.

Cognitive overload is often caused by overstimulation. If you want a good example of overstimulation, go visit LingsCars.com and notice how you'll have to struggle to take in all the information on that page.

A good example of overstimulation
LingsCars.com

In user experience, it is often said that:

The best user experience is the one the user doesn't notice

In SharePoint, your users are already familiar with the web part user interface and layout. They know where to look for the "Show all items" option, or what happens when they click on the pencil icon.

In this awesome article on cognitive load, they say that a way to reduce cognitive overload is to Follow time-proof conventions:

Don't reinvent the wheel. Users don't wanna take another driving lesson.
Dana Kachan

Following the SharePoint design principles for web parts is to follow an existing convention established in SharePoint and Office 365.

Building trust

Many years ago, I was working with a brilliant developer. Well, he did all the work while I attended meetings and demoed all his hard work, pretty much.

One day, he figured out a problem to a very difficult issue. I can't remember what it was, but it was one that most people we talked to said that it couldn't be solved.

When he demoed it to me, I was impressed, and I told him so. But then I pointed out that he was using the wrong font and colours, and that there was a spelling mistake on his screen.

(I'm talking comic sans with italic and ugly green fonts. Yuck!!!)

He was shocked. He had just solved an impossible problem and I was complaining about a minor user interface issue?!!?

I explained to him that when we'll demo his code to the client -- who has no appreciation for how complicated the issue was and how amazing the solution was -- all he will see is the ugly fonts, colours, and spelling mistakes. Instead of seeing a professional-looking solution, he'll see something that looks amateurish. It will break -- or at least chip away at -- the trust he has in us.

A more recent example of this is when Game of Thrones was in its last season, people got really upset about a coffee cup that was visible in one of the scenes.

Thar be mermaids!

Everybody knows that Game of Thrones was not really filmed in a fantasy time where dragons existed, right?

So why did people get upset?

Because the coffee cup that was carelessly forgotten in a shot chipped away at people's trust and respect for what was otherwise a beautifully produced show. It happened in the last season of the show when people were starting to criticize the writing and the rushed pace of the final episodes, and many people couldn't overlook it.

When you design your own look and feel within SharePoint, you're also chipping away at your user's trust.

Who should read this series?

If you're a designer who's anti-Microsoft and says "SharePoint looks like crap" and "I can do a better job myself", you're absolutely right. You don't need to read this series of blog posts.

If you're a developer who is new or somewhat experienced with creating SPFx web parts, but typically doesn't pay attention to how your web parts look -- as long as they work, you may find this series of posts useful.

Next article

Join me tomorrow for the first real article in the series: Web Part Titles.

Introduction

In my last two posts, I covered how to use the SharePoint Get items action in Flow and how to tell if the SharePoint Get items action returned items (by counting them).

I really wanted to provide a real-life sample how one would use the two concepts together in Flow.

Since we just had a national holiday and I completely forgot about it (got ready to go to work and everything), I thought I'd create a sample flow that automatically runs on a schedule and prompts users to do something, except when today's date is a holiday.

The workflow logic looks a little like this:

st=>start: Start (every n days)
e=>end: End
op2=>operation: Get today's date
op3=>operation: Find statutory holidays
with today's date
op4=>operation: Today is not a holiday
sub1=>subroutine: (Do something)
cond=>condition: Is today a holiday?
(Did you find any items)
io=>operation: Today is a holiday
(Do nothing)
st->op2->op3->cond
cond(yes)->io->e
cond(no)->op4->sub1->e

Let's get started by creating the environment we need for this workflow.

Creating a list of statutory holidays

In this example, we'll create a SharePoint list which will contain an entry for every statutory holiday. We'll use a list because it allows our HR folks to maintain it without needing a special app. We can also show the list on our SharePoint site so all employees can see what days are statutory holidays.

You can also use your own database, or an API, or even a static Excel spreadsheet if you want, but I wanted to use a SharePoint list to show how to use the SharePoint Get items action in Flow.

To create the list, follow these steps:

  1. From a SharePoint site, go to Site contents via the local navigation or the Settings option.
  2. In the Site contents, select + New then choose List from the drop-down menu.
  3. In the Create list pane, enter Statutory Holidays as the list's Name. Check or uncheck Show in site navigation depending if you want your users to see the list or not.
  4. Select Create to create the list
  5. In your newly created list, select + Add column then select Date from the drop-down list.
  6. In the Create a column pane, enter Date for the column Name. Set Include time to No and select Require that this column contains information to Yes under More options. This list doesn't make much sense if you don't require a date for each stat holiday.
  7. Select Save to create the column.

If you need to support statutory holidays for multiple states/provinces/countries, feel free to add more columns to your list to support your needs. I wanted to keep this list as simple as possible.

Why didn't I use a calendar list? I didn't want to add the extra columns that come with a calendar list. If you really want a calendar view, just add it as a custom view for your list.

Use your list's Quick edit to enter your statutory holidays. I use this site to get the list of statutory holidays for every year.

When you're done, you should have a list that looks like this:
Statutory Holidays, Canadian Style

Now let's create a scheduled flow that uses the list!

Creating a scheduled flow

  1. From https://flow.microsoft.com, go to My flows to view your list of flows.
  2. From the + New menu, select Scheduled--from blank
  3. In the Build a scheduled flow window, give your flow a descriptive Flow name. I named mine Prompt managers to approve timesheets.
  4. Under Run this flow, select the schedule that suits your needs. I want mine to go once a week on Mondays, so I selected 1 week under Repeat every, then selected M under On these days and unselected every other day.
    file.
  5. Select Create to create your workflow.

Your workflow will be created and open in the workflow editor. I renamed the Recurrence action to Every Monday because I always want my workflows to be easy to understand without having to expand every action.

Unfortunately, Flow won't let you save until you add another action.

Funny, cause that's exactly what we'll do next!

Connecting to the Statutory Holiday SharePoint list

Before we can access the Statutory Holidays list in SharePoint, we need to add a connection to SharePoint by following these steps:

  1. From within your flow editor, select +New step at the bottom of the flow.
  2. In the Choose an action prompt, type Get items in the Search connectors and actions. Search is case insensitive.
  3. Select the Get items action with a SharePoint logo from the list of Actions that appears. If the search query returns too many actions and you can't find the SharePoint Get items, you can filter out all other connectors by clicking on SharePoint just below the search bar.
  4. As soon as you select Get items, the Choose an action box will transform into the Get items box.
  5. If you haven't created a connection to SharePoint yet, you'll be prompted to Sign in to create a connection to SharePoint. Click Sign in to sign in with the account that you wish to use to access SharePoint.

    The account you use here specifies who will access SharePoint. Make sure that you use an account that can see the site and the list where you want to get items from. It is a good idea to use a service account that isn't using your own credentials to connect.

  6. Once connected, enter URL to the site that contains your list under Site address. If you experience problems typing or pasting the URL, try selecting Enter a custom value from the drop-down; it will turn the drop-down box into a text box.
  7. If the site URL you entered is valid and the credentials you supplied are correct, you should be able to pick the Statutory Holidays list from the List Name drop down.

Adding a filter to retrieve today's statutory holidays

If you ran the flow now, it would retrieve every statutory holiday in the list.

We want SharePoint to return only statutory holidays on the days the flow runs. To do this, we'll add an ODATA filter by following these steps:

  1. In the new Get items action you just created, select Show advanced options
  2. In the Filter Query field, enter Date eq datetime''.
  3. Place your cursor between the two single quotes you just typed and select Add dynamic content
  4. Select the Expression tab
  5. Scroll to the Date and time category and select See more, then select formateDateTime(timestamp, format) to insert it in the expression field.
  6. Making sure your cursor is between the two parentheses of the formatDateTime function, find the utcNow() function in the Date and time category.
  7. After utcNow() but before the last ), type ', 'yyyy-MM-ddT00:00:00') and select OK to insert the expression.
  8. In the Top Count field, enter 1 -- we only need to know if there is a statutory holiday or not, so we don't need to return more than one.
  9. In the Limit Columns by View, select All Items. This will ensure that we only return the Title and Date columns, instead of returning every single column in the list.

    If you want to test your flow, save it and use Test in the upper right corner. You can add a temporary list item in your statutory holidays list with today's date to see that SharePoint returned something.
    Test worked

If everything goes well, your flow is now able to retrieve statutory holidays from the SharePoint list every time your flow runs.

Now let's add logic to detect whether something was returned or not...

But before we do, let's rename the Get items action to Retrieve statutory holidays for today's date to make it easier to read. Hey, my blog, my naming conventions 🙂

Count how many statutory holidays were returned for today's date

As I explained in my previous post, I like using variables to make my flows easier to debug and easier to understand. We'll store the number of items returned in a variable called Number of statutory holidays.

Since this is the first time we set the variable, we'll use Initalize variable using the following steps:

  1. In the flow editor, select +New step
  2. From the Choose an action box, type variable in the search box.
  3. From the list of suggested actions, select Initialize variable.
    4.An Initialize variable box will replace the Choose an action box. Give your variable a descriptive Name. For example: Number of statutory holidays.
  4. In the Type field, select Integer -- because we'll be storing the number of items returned.
  5. We'll write the expression to calculate the number of items returned the Value field. If the dynamic content pane doesn't show, select Add dynamic content, the select Expression.
  6. Look for the length(collection) function in the Collection category and select it to insert it in the expression box. The length function is specifically designed to calculate how long a collection of items is -- and that's what the Get items action returns: a collection of items.
  7. Make sure your cursor is positioned between the two parantheses () in the length function. Select the Dynamic content tab and look for the value dynamic content for the Retrieve statutory holidays for today action.
  8. Flow will automatically insert body('Retrieve_statutory_holidays_for_today''s_date')?['value'] inside your length() function. The final expression should be:
    length(body('Retrieve_statutory_holidays_for_today''s_date')?['value'])
  9. Select OK to insert the value.

Save and test your flow. Mine returned 1 item:
1 item returned

Testing if any items were returned

Now that you have a variable that contains the number of statutory holidays, you can use it anywhere you want.

Let's create a conditional branch to do something if today is not a statutory holiday:

  1. In the flow editor, select +New step
  2. From the Choose an action box, select Control then Condition.
  3. A Condition box will replace the Choose an action box. Give your condition a descriptive name. For example: Is today a statutory holiday.
  4. If the Choose a value box, use Add dynamic content to select the variable you created earlier.
  5. In the next field, select is greater than
  6. In the next field (Choose a value) enter 0.
    Is today a stat holiday?

Save and test your flow. If everything worked well, the Expression value from your condition should return true if SharePoint found items, and false if nothing was found. My test returned true.

We have a stat holiday!

That's it! Now you can insert actions under If no to do something when today isn't a statutory holiday.

You could even add something under If yes to delay the flow until next day, but that's another post.

Conclusion

You can use Scheduled flows to run every n days and easily query a SharePoint list containing statutory holidays to skip running when the current date is a statutory holiday.

Note that in today's sample, I didn't deal with timezones by setting the start time of my workflow so that it is later than midnight in UTC time. If you run your workflow across multiple timezones, you should keep this into consideration.

I hope this helps you create workflows that know when to take it easy.

Because everyone deserves a vacation once in a while!

Photo credits

Image by Free-Photos from Pixabay

Introduction

Im my previous post, I explained how to use the SharePoint Get items action in Flow. As the name implies, it retrieves items from a SharePoint list.

Sometimes you need to know if your Get items action returned any items. For example, if you wanted to update an existing item or create a new item in none was found.

In this post, I'll show you how to count how many items were returned by SharePoint and how to test if any items were found.

And don't worry, this post won't be as long as the last one.

Counting results from SharePoint Get items

For the purpose of this example, we'll assume that you already created a flow with a SharePoint Get items action. If you haven't done so yet, take a look at my previous post.

Sample flow

When I have to use fancy formulas in many places within my flow, I like to define a variable. That way, I can just refer to the variable instead of re-entering the formula in many places.

You should always strive to make your flows easy to read so that if someone else has to maintain it (or if you have to come back to it later), it will be easy to understand what the flow does. Make sure to give your actions a descriptive name (not Get items like in my example, use something like Get existing responses from current user, for example). Using variables is another way to make your flows easier to use.

When using variables in flow, you use a different action to define a variable the first time (Initialize variable) than you would to set the variable or change its value (Set variable, Increment variable, and Decrement variable for example).

Since this is the first time we set the variable, we'll use Initalize variable using the following steps:

  1. In the flow editor, select +New step
  2. From the Choose an action box, type variable in the search box.
  3. From the list of suggested actions, select Initialize variable.
    Initialize variable
    4.An Initialize variable box will replace the Choose an action box. Give your variable a descriptive Name. For example: Number of existing items.
  4. In the Type field, select Integer -- because we'll be storing the number of items returned.
  5. We'll write the expression to calculate the number of items returned the Value field. If the dynamic content pane doesn't show, select Add dynamic content, the select Expression.
    Using an expression
  6. Look for the length(collection) function in the Collection category and select it to insert it in the expression box. The length function is specifically designed to calculate how long a collection of items is -- and that's what the Get items action returns: a collection of items.
    Length function
  7. Make sure your cursor is positioned between the two parantheses () in the length function. Select the Dynamic content tab and look for the value dynamic content for the Get items action (or whatever your SharePoint Get items action is called).
    Inserting body of get items
  8. Flow will automatically insert body('Get_items')?['value'] inside your length() function. The final expression should be:
    length(body('Get_items')?['value'])
  9. Select OK to insert the value.
    Formula inserted

Save and test your flow. Mine returned 1 item:
1 item returned

Testing if any items were returned

Now that you have a variable that contains the number of items, you can use it anywhere you want.

For example, if you wanted your flow to do something if any items were returned, and something else if nothing was returned you would follow these steps:

  1. In the flow editor, select +New step
  2. From the Choose an action box, select Control then Condition.
    Condition
  3. A Condition box will replace the Choose an action box. Give your condition a descriptive name. For example: Are there any existing items.
    Adding a condition
  4. If the Choose a value box, use Add dynamic content to select the variable you created earlier.
  5. In the next field, select is greater than
  6. In the next field (Choose a value) enter 0
    Condition for more than 0 items

Save and test your flow. If everything worked well, the Expression value from your condition should return true if SharePoint found items, and false if nothing was found.
We found items

Of course, you would want to add actions to your If yes and If no paths, but that's for another post.

Conclusion

The key to testing if the SharePoint Get items action returned items it to understand that Get items returns a collection of items. Using the length() function against the return value of your Get items action will tell you the length of your collection of items.

I could have avoided using a variable and just entered the length(body('Get_items')?['value']) formula directly in the condition, but I wouldn't be able to tell how many items were returned when I was testing the flow. This sample was an easy one, and I really didn't need to evaluate how many items were returned more than once -- so I really didn't need a variable -- but in more complicated flows, you'll find it a lot easier to define variables and use the variables throughout instead of copying the same formula every time.

I hope this helps?

Introduction

I love Flow (and Logic Apps)!

Favourite thing to do with Flow is doing demos and workshops!

When I meet a new customer who tells me they have a business problem, I love to put together a quick proof of concept how to solve their business problem using a no-code solution, involving SharePoint, PowerApps, and Flow -- right there, in front of them, while projecting. No safety nets, PowerPoint or scripted demos.

The SharePoint connector is by far my most frequently-used connector, because it allows me to quickly query, create, and update content in SharePoint as part of my solutions.

Even if I have used this connector many times, I sometimes get demo blindness and I forget how to use it when I'm in the middle of a demo.

This article explains how to use the SharePoint connector and the GetItems action to retrieve items from a SharePoint list.

Hopefully, next time I forget how to use it in the middle of a demo, this article will show up in the search results.

NOTE: This article focuses on Microsoft Flow, but you can use the connector in Logic Apps in (almost) the same way, and PowerApps with these instructions.

Creating a Test Flow

For this article, we'll assume you want to connect to SharePoint to get one or more items in a list from within an existing Flow.

If you already have an existing flow you can use to follow along, go ahead and skip to the next section.

If you don't have an existing flow, let's create a Flow that you can manually trigger by following these steps:

  1. From https://flow.microsoft.com, navigate to My Flows.
  2. Select New then Instant -- from blank to create a flow that we'll be able to trigger at any time to test. Feel free to use any other type of flow here.
    New | Instant -- from blank
  3. In the Build an instant flow dialog, enter a Flow name and select From Microsoft Flow when prompted Choose how to trigger this flow and select Create.
    Build an instant flow  dialog

Your new flow will be created. Note that you need to insert at least one step before you can save or test it.

Good thing that's what we're doing next!

Adding a SharePoint connection

Before we can access SharePoint items, we need to add a connection to SharePoint by following these steps:

  1. From within your flow editor, select +New step at the bottom of the flow. You can also click on the + button that appears between two existing flow steps.
    New step
  2. In the Choose an action prompt, type Get items in the Search connectors and actions. Search is case insensitive.
    Get items
  3. Select the Get items action with a SharePoint logo from the list of Actions that appears. If the search query returns too many actions and you can't find the SharePoint Get items, you can filter out all other connectors by clicking on SharePoint just below the search bar.
  4. As soon as you select Get items, the Choose an action box will transform into the Get items box.
  5. If you haven't created a connection to SharePoint yet, you'll be prompted to Sign in to create a connection to SharePoint. Click Sign in to sign in with the account that you wish to use to access SharePoint. If your instance of SharePoint is on-prem, you can check Connect via on-premises data gateway -- but that's for another post.
    Sign in to SharePoint

    The account you use here specifies who will access SharePoint. Make sure that you use an account that can see the site and the list where you want to get items from. It is a good idea to use a service account that isn't using your own credentials to connect.

  6. Once connected, enter URL to the site that contains your list under Site address. If you experience problems typing or pasting the URL, try selecting Enter a custom value from the drop-down; it will turn the drop-down box into a text box.
    file
  7. If the site URL you entered is valid and the credentials you supplied are correct, you should be able to pick the list you want to use from the List Name drop down. In my example, I only have one list called Parking Lot Passes.
    Selecting a list name

    If you get a GUID in your List Name instead of a friendly name, make sure that the connection you're using has permissions to access the list. You can change the connection by selecting the elipsis (***) at the top of the Get items box and using the My connections section to change or add a new connection.
    My connections

Let's test it!

  1. Select Test from the top toolbar. If it is disabled, you may need to Save it first.
    Test
  2. From the Test Flow pane, select I'll perform the trigger action and select Save & Test.
    Test Flow
  3. The Run Flow dialog will prompt you to confirm the conneciton information. Make sure everything is correct and select Continue
    Run Flow
  4. Once you confirmed the connection, you'll get prompted again. It usually only happens the first time you create or change a connection. Select Run flow.
    Run flow -- again
  5. If everything went well, you should see Your flow run successfully started. Select See flow run activity to see if everything went well.
    Your flow run successfully started.
  6. You'll get a Run history for your flow, usually sorted by newest at the top. Select the top (and most likely only) one by clicking on the start time.
    Run history
  7. From your flow history page, you should see green checkmarks next to every step in your workflow. If you get a red x, check your connection information.
    Success
  8. Click on the Get items action to see what SharePoint returned. You want to see a 200 Status code, and a Body that returns value items.
    Get Items results

You now have a connection to your SharePoint list. Now let's add a filter to get only the items you want from the list.

Building an ODATA filter

For this article, I'll use a list I had created for a Park Pass request application. It has a Text column called Make, a Person column called RequestedBy, and Date and Time columns called From and To. You can use whatever list you want in your workflow.

Pro Tip: when creating a column that has a space (or any other funny characters) in the name, create the column without the space first, then rename it with a space. That way, you'll avoid funny column names like Requestedx20By. In my example the Requested By, was created as RequestedBy (no spaces) first, then renamed it to Requested By.

The Get Items action allows you to specify an ODATA filter query to filter returned items from a list. To specify a filter, select Show advanced options from the Get items action.

Get Items Filter

If you aren't familiar with ODATA filters, you can read the article about using ODATA query operations in SharePoint REST requests, or read below to find how I build my ODATA filters.

In most cases, you can write your query as [columnname] [operator] [value]. Where [operator] is one of the following keywords:

  • Lt: Less than
  • Le: Less than or equal to
  • Gt: Greater than
  • Ge: Greater than or equal to
  • Eq: Equal to
  • Ne: Not equal to

For example, to retrieve all cars where the Make is Canyon Arrow, you would write:

Make eq 'Canyon Arrow`

If you wanted to retrieve any Make but the Canyon Arrow, you would write:

Make ne 'Canyon Arrow`

If you can't figure out why your ODATA filter doesn't work, here is a mostly foolproof way to build your ODATA filter:

  1. Using your browser, navigate to: https://[yourtenant].sharepoint.com/sites/[yoursite]/_api/lists/getbytitle('[Your list title']). For example, my if my tenant is ashbay16, my site is TestPowerApps and my list is titled Parking Lot Passes, my URL would be:
    https://**ashbay16**.sharepoint.com/sites/**TestPowerApps**/_api/lists/getbytitle('**Parking%20Lot%20Passes**').
  2. If you entered the right URL, you should see information about the list in XML format.
  3. Add /fields at the end of the URL you created in step 1 to get all the fields names in your list. For example, my URL is now https://ashbay16.sharepoint.com/sites/TestPowerApps/_api/lists/getbytitle('Parking%20Lot%20Passes')/fields
  4. You may want to use an XML editor (like Visual Studio Code) to view the XML results from the previous step. Find the field you want to filter on by searching the XML file for the field title.
  5. While you're looking at the XML definition for the field, take a look at the d:Filterable node to see if it is filterable (it should be true). Also, take a look at the d:TypeAsString node to see what type of field you're dealing with. Finally, look at d:EntityPropertyName -- that'll be how you refer to that field in your filter. For example, to filter by Approval Status, you would use OData__ModerationStatus. Note that the column names are case sensitive.
  6. Look at the ODATA query syntax chart below to see what possible filter you can use to build your filter. I've grayed out the parts that don't apply below:
    ODATA filter query
  7. Depending on your field's d:TypeAsString, you can use the following queries:
    • Text: lt, le, gt, ge, eq and ne, plus startswith() and substringof(). For example, substringof('S', Model) will return all entries where the Model column contains the letter S. Note that the field name is the second parameter with substringof and startswith.
    • Number: lt, le, gt, ge, eq or ne
    • DateTime: day(), month(), year(), hour(), minute(), second(). You can also use datetime to compare a date. For example SubmittedDate gt datetime'2019-06-14T00:00:00' to get items where the SubmittedDate column is greater than June 14, 2019.
    • User: Use lt, le, gt, ge, eq and ne, plus startswith() and substringof() to evaluate against the user's display name, or specify the user's attribute. For example: RequestedBy/EMail eq 'hugo.bernier@contoso.com' to find items where the email address of the RequestedBy user is hugo.bernier@contoso.com.
  8. Test your filter in your browser by replacing /fields with /items?$filter=[yourfilter]. For example, if my filter is Make eq 'Canyon Arrow', my URL would be https://ashbay16.sharepoint.com/sites/TestPowerApps/_api/lists/getbytitle('Parking%20Lot%20Passes')/items?$filter=Make%20eq%20%27Canyon Arrow%27. Notice that when I type spaces and single quotes, the browser will url encode the values for me -- meaning Canyon Arrow becomes Canyon%20Arrow.

Once you have built your ODATA filter, add the filter to your Get Items action.

Specifying the ODATA filter

Now that you have your ODATA filter, go back to your flow and:

  1. Select Show advanced options on your Get items action.
  2. In the Filter Query enter your ODATA filter. For example, I entered Make eq 'Canyon Arrow'
    Static filter
  3. Save and test again.

Your results should now be filtered!

Specifying a dynamic ODATA filter

Flow allows you to enter dynamic values pretty much anywhere. Let's say we wanted to filter items that were previously submitted by the user who triggered the workflow.

  1. In your Get items action, make sure that the advanced options are showing. If not, select Show advanced options.
  2. In the Filter Query field, enter SubmittedBy/EMail eq ''
  3. Position your cursor between the two single quotes and select Add dynamic content to show the list of possible dynamic values you can use.
  4. From the list of dynamic content, find User Email from the Manually trigger a flow category
    Adding dynamic content.
  5. Save and test your workflow.

Note, you could also have simply used SubmittedBy eq '[User name]' from the dynamic content list, but I specifically wanted to compare by email in this example.

Your results should contain only items that were submitted by you (since you triggered the workflow).

Dealing with results from Get Items

The SharePoint Get Items action always returns an array of items -- whether it found 1 record, zero records, or a whole bunch of records.

If you insert a new action that uses the results from Get Items, Flow will automatically wrap the action in a loop, iterating through each record that was returns from Get Items.

For example, let's pretend we wanted to email the person who submitted every SharePoint list item where the To column contains a date that is earlier than today (or utcNow() in Flow). You would first set your query as follows:

  1. In the Get items action, enter To lt datetime'' in the Filter Query field.
  2. Place your cursor between the two single quotes. This time, instead of using Dynamic content, select the Expression tab.
  3. In the list of possible expressions, select utcNow() from the Date and time category. Between the parentheses, type 'yyyy-MM-ddTHH:mm:ssZ'. Your final expression should be utcNow('yyyy-MM-ddTHH:mm:ssZ'). Click Update to insert your expression.
    Entering an expression
  4. Immediately below the Get items action, select +New step and insert a Send an email action from the Office 365 Outlook group.
    Send an email
  5. In the newly inserted Send an email action, select the To field and use Add dynamic content to select Requested by Email (or whatever field you want) from your Get items action.
  6. You'll notice that as soon as you select dynamic content from the Get items action, Flow converts your Send an email into an Apply to each loop.
    Apply to each loop.
  7. Finish writing your test email and test your workflow. (Be careful that you don't send emails to a whole bunch of people, they might not appreciate it!).

Caution: Throttling

When using the SharePoint Get items action, your Flow may get throttled (i.e.: slowed down) if you exceed more than 600 calls within 60 seconds. You may want to keep that in mind when designing your flow.

Conclusion

In this long post (too long!), I explained how to use the SharePoint Get items action within Flow.

There is still a lot to cover (for example, how to detect if any items were returned, how to reduce chances of throttling by specifying the columns to return and the number of items to return, etc.), but I hope that you'll be able to get started using Get Items.

Have fun!

Introduction

I think that custom SharePoint web parts should always look like they belong to SharePoint; I want my users to be unable to tell my custom web parts from the ones that come out-of-the-box (O.O.B.) with SharePoint. I think that it helps users by presenting a common interface that they are already familiar with.

Sometimes, I'll even go as far as examine the HTML, CSS and even analyze the network traffic an O.O.B. SharePoint web part produces to make sure mine behave consistently.

When testing a SharePoint web part on the SPFx workbench, analyzing network traffic using your web browser's developer tools can get pretty overwhelming; there are so many telemetry calls every few seconds that it becomes impossible to figure out what calls are real API calls, and which ones are telemetry calls.

For example, I added a test web part on my workbench page and refreshed the page, then took a screenshot of the number of telemetry calls the page made (non-telemetry calls are greyed out):

That's a lot of telemetry

I left the page running while I wrote this introduction, and took another screenshot of the network traffic the page logged; this is what it looks like now:

More telemetry calls

The page is filled with telemetry calls! Every little tick in the timeline pane above the list is a telemetry call.

Fortunately, you can temporarily disable telemetry while you're debugging your web parts on the SPFx workbench!

What is telemetry

SPFx uses telemetry to collect measurements (such as performance and usage) and automatically sends that data at regular intervals automatically. It normally does not affect performance and can be quite useful to the Engineering team to detect (and resolve) potential issues.

When using your browser's developer tools, on the Network tab, you can see a bunch of harmless Ajax calls to a link that looks like: https://spoprod-a.akamaihd.net/files/sp-client-prod_2019-06-21.008/3.vendors~sp-client-telemetry-aria_d335ca85ff1f5f8fcce5.js

DisableTelemetry=true

If you want to debug a web part on your SPFx workbench (https://localhost:5432/workbench or https://yourtenant.sharepoint.com/_layouts/15/workbench.aspx ), you can simply append ?disableTelemetry=true to the your query string to temporarily disable telemetry calls.

Notice how there are no more telemetry calls on my page's network traffic below? No more regular dots in the timeline means no more regular Ajax calls.

No more telemetry

That's all!

Conclusion

Telemetry can be super useful, but sometimes it can be annoying -- especially when you're trying to analyze a page's network traffic.

The disableTelemetry=true query string parameter only seems to work on SPFx workbench pages, but that's fine with me.

I hope it helps you?

Image Credit

Image by swooshed from Pixabay

Introduction

Sometimes, you need to create a view in a SharePoint list where the items are sorted using a custom sort order. For example, if you had a list of items that needs to be sorted by Rating where the possible choices are High, Medium, and Low, your list items would appear in the following order: High, Low, and Medium because SharePoint will want to sort your Rating values alphabetically.

Sorting a choice column alphabetically doesn't always work

Poor SharePoint, it doesn't know that you want a custom sort order!

First instinct is to change the possible choices so that the list gets sorted in the right order. For example, renaming Low to 1 - Low, Medium to 2 - Medium, and High to 3 - High. It will force SharePoint to sort by category in the right order.

But sometimes you don't want to change the values in your metadata just so that it sorts properly.

This post will explain a quick method I use to sort list items in a view using a custom sort order.

It is so easy, it is almost embarassing.

Solution

All you need to do is simply add a calculated column where you assign a numerical equivalent to the column you wish to sort on.

Here are the steps:

  1. From your list, select Add column
    Add column
  2. When prompted to choose a column type, select More...
    Select More...
  3. In the Create Column page, select a Column name that suits you. I usually call it [Choice column] Sort, where the [Choice column] is the name of the choice column you want to sort on. For example, to sort by Rating, I would call the column Rating Sort. Feel free to use whatever name you like.
  4. For The type of information in this column is:, select Calculated
  5. Let's skip the formula for a second, we'll get back to it. For The data type returned from this formula is:, select Number and select 0 for the Number of decimal places
    Select Number and Zero decimal places
  6. For the Formula, use a whatever logic you want to assign a numerical value to the item. For example, to sort Ranking, I would assign 1 to Low, 2 to Medium, and 3 to High by using the following formula:
    =IF(Rating="Low",1,If([Rating] = "Medium", 2, 3))
  7. Select OK to create your column.
  8. Test that your column is returning the right values:
    Does my formula work?
  9. If you're getting the right numerical values, change your view to sort by the column you just created by going to Settings | List settings and selecting the view you want to change under Views.
  10. In the Edit View page, make sure to de-select the column you created (we don't actually want to show it!)
    De-selecting your sorting column
  11. Scroll to the Sort section and select your new column as the sort order.
    Sort by Ranking Sort
  12. Select OK to save your view.
  13. Test your view. It should short your items sorted using your sort logic.
    Sorted!

Conclusion

I told you it was simple!

All you have to do is add a calculated column that calculates the sort value you want, and sort by that column!

I hope this helped?

More Information

The example in this post uses the IF function.

The IF syntax is as follows:

IF(logical_test,value_if_true,value_if_false)

So, if you want to return 1 if the [Rating] column is equal to Low, otherwise return 2 you would write:

IF([Rating] = "Low", 1, 2)

You can nest IF statements too. For example, if the value of [Rating] isn't Low, it could be Medium or High. To return the appropriate value, we nest a second IF in the first IF's value_if_false parameter, as follows:

IF([Rating] = "Low", 1, IF([Rating] = "Medium", 2, 3))

However, it'll suck if you have a lot of choices to pick from. For example, if you wanted to sort by month, you would have to do something ugly like this:

IF([Month] = "January",1 , IF([Month] = "February", 2, IF([Month] = "March", 3, IF([Month] = "April", 4, IF([Month] = "May", 5, IF([Month] = "June", 6, IF([Month] = "July", 7, IF([Month] = "August", 8, IF([Month] = "September", 9, IF([Month] = "October", 10, IF([Month] = 11, 12)))))))))))

But that's ugly!

You don't have to use IF to calculate your numerical equivalent. For example, I like to use FIND to find the value in a long string.

The FIND syntax is as follows:

FIND(find_text,within_text,start_num)

Where:

Find_text is the text you want to find.

Within_text is the text containing the text you want to find.

Start_num is the character at which to start the search. If you omit it, default is 1.

Since FIND returns the location in the string where it found the value you're searching for, you just need to make sure your possible values are in order (and unique). For example, this is how I would sort by month:

FIND([Month], "January February March April May June July August September October November December")

Which would return 1 for January, 9 for February, 18 for March, etc.

Sure, the numbers aren't sequential, but we don't care, as long as they get bigger with each value!

Examples of common formulas in SharePoint Lists
The IF function
The FIND function

Introduction

Every once in a while, I write a blog post as a note to myself about something that I couldn't find easily with the hope that next time I (or someone else in need) look for it, it'll be easy to find.

This is the case with this one.

Yesterday, I was helping to set up a new Office 365 tenant for a company that has their head offices in Toronto.

We created some sites and used the SharePoint Online Provisioning Service but found it annoying that the time zone for every site was UTC -8. We wanted UTC -5, or Eastern Standard Time as our default time zone.

It's almost as if the company that wrote this product was based in Seattle, or something.

But when it came time to set the default time zone, I couldn't remember where the setting was. I've done it so many times before, but it was as if all my past with all the previous versions of SharePoint got mixed together. Throw in the additional confusion by considering the differences between the on-premises and online versions, and I just could not remember where it was.

This post is to make sure I don't have to look it up anymore.

Finding the setting

My brain is too small to remember things that I can look up or easily deduce. The location of the setting for the default time zone for new SharePoint sites is not something easily deduced.

I clicked around and couldn't find the setting. It must've been a case of demo blindness, a condition that often occurs in professional consultants during demos where they can't see something that it plainly in front of them.

I eventually searched it online and found an article that Mark D. Anderson, someone I respect immensely, wrote in 2018.

The setting is in the SharePoint Admin Center (located at https://**yourtenant**-admin.sharepoint.com/), under Settings and Site Creation (site creation is the last setting on that page):

SharePoint Admin Settings

In the Site Creation pane, you'll find Default time zone:
Site Creation pane in SharePoint Admin Settings.

Select the time zone you want and select Save.

Note that this setting will not overwrite the time zone setting for existing sites. It will only affect new sites created after you apply the setting.

Conclusion

The setting is in the SharePoint Admin Center, under Settings and Site Creation.

Thanks Mark for saving my demo 🙂

Introduction

When migrating from SharePoint on-premises to SharePoint Online/Office 365, you may find that some users have a checked-out file called spcommon[1].png. If you ask users about it, they'll have no idea what you're talking about.

As it turns out, this isn't a bug. It is possible for users to check-out this file without knowing they did it.

But for this issue to occur, you need a "perfect storm" to happen: a series of things that occur that are seamingly unrelated that results in the issue we're discussing today.

What is spcommon[1].png

spcommon.png is an image that SharePoint uses to render things like checkboxes, arrows, gears, and pretty much any icon that you see on a SharePoint page.

If you download the file, it looks like a bunch of icons in one bigger image:
SPCOMMON.PNG

Those types of images are called sprites; they usually consist of many images grouped together as a single image. That image is bigger to download than individual images, but since most browsers try to avoid re-downloading a file it has already downloaded (something known as caching -- pronounced cashing) -- making the entire page load faster.

SharePoint uses the giant sprite and hides irrelevant parts of the image (by setting a background-url and background-position CSS styles).

On a typical SharePoint page, the spcommon.png image may be shown dozens of times.

So why do I get a spcommon[1].png file that gets checked out by users?

Perfect Storm

This issue will typically happen in document libraries that either Require check-out before editing files or that have a mandatory property.

I've seen this issue in SharePoint 2013 and SharePoint 2016 migrations, but it could potentially happen in SharePoint 2019 as well.

As you already know, users can drag and drop documents unto a document library using their browsers, and SharePoint will try to upload the document.

If you try drag and drop a document to a library that requires checking in or one that has mandatory properties, the newly uploaded document will be checked out until the user provides values for the mandatory properties and checks the document in.

Most browsers also allow you to select an image from a web page and drag and drop it. (Try it now with an image on this page!).

If you try to drag and drop an image onto a document library page, it will try to upload that image into the document library. If that library has mandatory properties, the image will be uploaded but checked-out.

Here is where it gets crazy: if a user tries to click on the checkmark next to a document and accidentally drags the mouse instead -- even for a few pixels -- the browser will think that the user meant to upload the image to the document library.

The checkmark

And the image used to display the checkbox is -- you guessed it --spcommon.png.

It often happens too quickly for users to notice, but here is what happens if you slow it down and take a screen shot of a user dragging the checkmark icon in a document library:

Dragging the checkmark

And since most browsers will try to uniquely name dragged files, the spcommon.png file is automatically renamed to spcommon[1].png.

Conclusion

The issue with a mysterious checked-out spcommon[1].png file in a document library requires a lot of factors to happen.

Fortunately, It seems that newer versions/patches of SharePoint prevent this issue from happening by giving users an error message saying "Folders and invalid files can't be dragged to upload", meaning that you're less likely to find this issue in future migrations.

Issue prevented?

If you see this error when migrating files to SharePoint Online, you can safely ignore it.

Introduction

Recently, Microsoft announced a plan to rename Office UI Fabric to Microsoft UI Fabric. The Fabric React Component that have become ubiquitous on Microsoft web applications will be updated to give the Fabric controls that new cool Fluent style -- eventually aligning the desktop and web app look and feel to give users a consistent experience.

To help understand how the controls will change with the Fluent style, the UI Fabric team created a Preview web site. On it, there is a component that allows you to compare how the controls will appear before and after the Fluent style update by dragging a slider left to right. Although it isn't a standard UI Fabric component, the comparer component looks and feels like it belongs to UI Fabric.

Fluent Comparer Control

At that time, I was putting together a demo for a client that needed to compare two images on their SharePoint site. I thought I'd re-create the comparer component into an SPFx web part.

As I was building the web part, I decided to add the ability for users to pick the Before and After images using a file picker like the one available in SharePoint.

The SharePoint File Picker

The out-of-the-box file picker allows users to pick files from their recent files, web search, OneDrive, the current site they're on, an uploaded file, or a hyperlink. Perfect for my needs!

Being the World's Laziest Developer, I looked for a standard control withint the SPFx libraries, Fabric UI, the PnP Reusable React controls for SPFx solutions, and the PnP Reusable SPFx property pane controls .

But I found nothing.

What I lack in being lazy, I make up in being stubborn. So I decided to write my own File Picker control.

The web part came out OK, but I was pretty happy with the File Picker.

Web Part in Action

This article will describe some of the techniques and approaches I used to reverse engineer the out-of-the-box File Picker to create my own.

If all you need is to see the code, you can find the React Comparer web part on the SharePoint SP-Dev-Fx-WebParts Sample Repository.

Design Criteria

Before I set out to build the File Picker, I defined some rules that I would have to stick to no matter what:

  1. To the best of my ability, the File Picker had to look and feel exactly like the out-of-the-box File Picker. That includes copying some of the weird inconsistencies in the out-of-the-box component -- I'll get to those later.
  2. Even though I only cared about picking images, the File Picker must be designed to eventually support picking documents as well as images. I wouldn't be spending any time testing the picker using documents, but I would spend every effort to make sure I would be able to add full support for documents later.
  3. The control should be easy to re-use in other solutions. Eventually, it should be easy to add it to the PnP Reusable SPFx property pane controls -- as long as anybody else shows interest in it.
  4. Using the File Picker control should not require setting custom permissions or prompt the user to enter their credentials -- or any other weird behavior (see #1).
  5. Should be designed to support mobile browsing in the future, but no testing on mobile device.
  6. The File Picker control should be easy to extend without disrupting the user experience. For example, I would like to add a Camera tab to allow users to insert an image from their camera, but I'd want the tab to look like the feature came out-of-the-box.

With all the above rules established (why do I do this to myself, again?), I started coding!

What kind of control?

When I first started trying to figure out how I'd go about doing this control, I thought I'd do a regular control that I'd launch when clicking on the property pane button. I thought I'd use the @microsoft/sp-webpart-base's PropertyPaneButton and the onClick handler to display a UI Fabric Panel control.

The problem with this approach is that I would need to track whether the panel was opened or not to determine when to render the panel. Since I had no control over the state of the PropertyPaneButton control, I would have to store that information in the web part properties -- something that I didn't want to do.

But if I created a custom property pane control, I could render the button to open the dialog and add the state of the dialog to the property pane control's state.

Luckily, there is a great training module called SharePoint Framework training package - Working with the Web Part Property Pane that teaches you how to use and develop your own property pane controls. The PnP
Reusable SPFx Property Pane Controls source code is also a great place to look for examples to get started.

Ultimately, I mimicked the code from PnP (after all, I'm planning on submitting the control to PnP!) and created the property control.

Taking inspiration from existing controls

Since my goal was to reproduce the exact same look and I feel, I started rebuilding the same component structure by using the out-of-the-box file picker and examining the HTML it produced.

Using your browser's Developer Toolbox (F12 on Chrome and Edge) Elements tab, you can navigate through the page elements and determine what kinds of controls are used.

Using the Dev Toolbox

For example, the navigation which allows users to select whether to pick a file from OneDrive, the Site, upload, etc. has the ms-Nav-group CSS class.

Navigation.

A quick search for ms-Nav-group in the UI Fabric source code pointed to the UI Fabric Nav component.

By comparing the samples, I was able to find the sample that matched the look of the out-of-the-box control the closest and started with that.

When to split components

Since React makes it easy to create controls that are made up of many components, it can be difficult to figure out when a group of HTML elements are from a single control, and when they aremade of individual controls.

Luckily, the majority of controls usually consist of the control's .tsx file, and a .scss file that contains all the CSS classes needed to render that control.

When React renders a control in a web page, it adds the control's CSS to the page. To prevent one's control's CSS from interfering with another control's CSS, React uniquely names each CSS class by appending a unique suffix to each class name.

In the example below, the nav CSS class is rendered as nav_b427d4d7. If you look closely at the parent elements of the nav_b427d4d7 element, you'll find there is an element with a class focusTrapZone_b427d4d7 -- with the same _b427d4d7 suffix.

file

Elements with the same class name suffix are generally rendered from the same React control. By navigating up and down the HTML elements, I was able to determine which items were rendered together.

Copying styles

While I was reverse engineering the controls I would need, I was also able to extract the styles I needed for each element by looking at the Developer Toolbar's Styles pane in the Elements tab:

Styles

I simply gave my HTML elements the same CSS class names that I would see in the Developer Toolbar (minus the unique suffix) and used the same styles.

With a bit of tweaking, I was able to make the control look exactly the same.

Listening In

Once I got the HTML to look the way I wanted, I had to figure out where SharePoint got the results.

Once again, the browser's Developer Toolbars came to the rescue. By loading the out-of-the-box file picker and using the Network tab, I was able to see what REST calls SharePoint makes to retrieve documents.

Using the Network Tab

After filtering through many XHR calls, I was able to find which calls returned the data I was looking for.

I used the @pnp/sp library to re-create the calls in my components and return the same data that the SharePoint file picker control returned.

Discrepancies

By analysing the file picker, I noticed some discrepancies how each tab was rendered. For example, the OneDrive tab and the Site tab may look very similar, but a closer look reveals minor differences between the two.

For example, the Site tab shows folders within the site with no file count:
Site Tab

Meanwhile, the OneDrive tab shows a file count within each file and offers users the ability to select which view they wish to use (in the uppoer right corner):
OneDrive Tab

The HTML behind each tab is also very different.

I chose to keep the same discrepancies between tabs, even if I was tempted to clean up and optimize the code. I suspect the each tab was created by a different team members (or teams?) within Microsoft, which explains the lack of consistency.

Conclusion

The value proposition of SPFx is that it gives third-party developers (like me) the same set of tools that first-party developers (like Microsoft) can use.

By using the browser's developer tools and a bit of elbow grease, you can reproduce the look and feel of SharePoint components and make them do whatever you want them to do.

If you want to find out more on how I wrote the File Picker control, take a look at the code.

I hope this helps?

Updates

  • The FilePicker control is now available in the Pnp reusable controls, woo hoo! I can't take the credit for submitting the re-usable control, but I'm super proud that members of the PnP community were able to get together to make this happen... and that I was able to help!

By Oguh Reinreb, Evil Consultant

NOTE: Today's post was written by Hugo's evil twin Oguh. The views, information, or opinions expressed in this guest post are solely those of the evil twin involved and do not necessarily represent those of Hugo Bernier.

Introduction

Everybody can migrate SharePoint to Office 365, right?

All you need is a credit card to create an Office 365 tenant and an old SharePoint instance and you can get started migrating your files!

But if you're looking for job protection -- or if you're offering professional services -- you probably don't want the migration to happen too fast.

Why would you want the migration to complete quickly when you can just let it drag longer than it should?

This article will give you pro tips to guarantee your migration from SharePoint to Office 365 will fail.

This list is a compilation of pearls of wisdom that I have learned over the years with dealing with people who know better.

Follow this list and you'll be able to bleed your employer/client of all their money and not deliver anything of value. And if you do deliver something, it'll be completely useless.

You can always blame Microsoft if it doesn't work.

What do we mean by failing?

The best way to fail is to avoid establishing any kind of success metrics. If people don't know how to measure your success, they'll never know you failed.

But to make sure you fail real good, let's establish some failure criteria:

  • Costs overruns and delays: a study by Deloitte (I think it was Deloitte, but who cares, 82% of statistics are made up on the spot -- who will check?) found that the common practice of awarding a contract to the lowest bidder usually results in project delays and spending over budget. In fact, the average cost is equivalent to 5 times higher than the price of the highest bidder. Now that's something to strive for!
  • Poor user adoption: if you improve your user's lives and made them more productive, they'll keep on expecting more from you. Make sure that the migration you did is so bad that your users won't want to use SharePoint and that they'll find alternate ways to store documents and collaborate. That's what email is for anyways.
  • Lost files and metadata: If your old SharePoint library had 1,000 files, just make sure that you migrate 1,000 files over. That's enough QA. Nobody needs metadata.

Don't involve your users

Ugh. Talking to users is such a waste. of. time.

Why should you spend your valuable web-surfing time at work to interview your users and find out what their pain points are? That's like saying "please whine some more".

Just because your users spend all their time doing their daily job doesn't mean that they understand what they need better than you. I mean, they don't know anything about IT, probably less about SharePoint, so how can they possibly add value?

Remember: you know better

The worst part about interviewing your users and asking them for their pain points is that it would give you something to measure your success with. Users might actually expect you to deliver everything they ask for.

My advice: don't talk to users. You do you. They'll just have to adapt.

And don't even think about nominating so-called "Champions". They're the worst. "Champions" (read with air quotes) might think of things that you didn't think of and identify gaps, just because they know sooooo much about how they need to do their jobs. They might even help change other users' opinion by talking to their peers (and then everybody will expect you to deliver stuff).

If someone insists that you need to identify "Champions" in the organization, get the Executives to name people that don't really know anything -- don't let them name people who will actually use the system.

And whatever you do, if a group of people approaches you and they want to be early testers on your migration, steer clear of them! I've seen it too many times before: they may find issues before everybody else gets to use the system (and then you'll be expected to fix the issues !).

Even worse: "keeners" (that's what I call them) might generate excitement in the organization. The may get other groups (who would otherwise leave you alone) to get jealous, to get excited about features they have been whining about for a long time, and demand to be early adopters.

Don't communicate

This is just common sense. If you tell people that a new version of SharePoint is coming, they'll just get worried about the coming changes and they'll make your life miserable.

Remember that your users have jobs to do. They have probably worked really hard to learn the last version of SharePoint (they change everything between versions, don't they?) and if you tell them about the upcoming SharePoint migration, they'll fight you.

Even worse if you tell them about moving to the cloud! Remember: users don't know anything about the cloud, and they probably have concerns over safety and security. If you tell them in advance what's coming, you're probably going to have to explain to them why the cloud is better, blah, blah, blah.

Let the rumours fester. If you let users guess what's happening, they'll make up stuff and may not even have to deliver anything because they'll fight the migration before you even get started!

Best strategy: hope that your users enter a deep coma until your migration is done. When they wake up, dump the new platform on them and let them fend to themselves.

If you're a consultant and you were brought in to migrate, try to can keep the rest of the IT department in the dark. It is fun to watch them squirm and think that they're going to get laid off.

And they're always trying to show off with all the stuff they know about their legacy systems and why you shouldn't do this and that. Yecch!!!

Probably the best way to make your life easy is to never explain to users about the benefits of moving to the cloud and using Office 365. Don't tell them what's in it for them. Because they'll expect stuff from you!

Don't waste your time on a content audit

Why would you take the time to list all the sites, subsites, document libraries and file shares that you'll have to migrate when a cursory glance at the data will give you enough to make up an estimate?

Trust me, if you do an audit, people will expect you to make educated decisions about what content needs to move. Or even worse, they might expect you to consult them to find out what you should move.

Status Quo is good

Copy and paste. Don't think!

Don't start going through the content and try to identify duplicates, or content that you should migrate. Don't look at permissions and folder structure. That would be using your brain, and we don't want that.

So what if your users have eleventy layers of folders and sub-folders, that's their problem.

And if you move from a file share to SharePoint and change the folder structure, your users might whine that you lost "implicit metadata" (i.e.: metadata that is implied by where a document resides in a file structure). You would have to take extra time to apply metadata to documents, and that sounds like too much work.

Just copy and paste the documents the exact same way they are at the source. Don't start spring cleaning -- you can always clean next time you migrate.

Estimates are for losers

Estimates are hard. Like "work" hard.

Why would you take the time to analyze the data thoroughly to estimate what the effort will be to migrate the content when all you have to do is give an estimate that your boss/client/stakeholders will be happy with? Go ahead, underestimate.

Most cloud migrations take 6 to 9 months to be done right, but if you tell your bosses that, they won't like it and they'll demand smaller estimates. It is better to give them an estimate that they want to hear (like 1 month or 3 weeks).

Once they signed off on starting the migration, it's not like they can do anything if you take longer, right?

You know as well as I do that they won't stop asking you for shorter and shorter estimates until you tell them what they want to hear. Why go through the trouble when you can just skip to the estimate they want.

For extra bonus points, once you gave them a short estimate that you have no hope of meeting, try to rush the migration to keep deliver to that ridiculous timeline you committed to. The best part about that approach is that nothing will be done right, and you may spend the next several months or year (usually more than your most pessimistic estimate) fixing all the issues.

You could try to give them a reasonable estimate based on actual calculations, and ask your stakeholders to give you enough time to perform a few test migrations so that you can improve your estimates, but that sounds like too much like adulting.

Oh, and whatever you do: don't keep a master risk list or, if you're using Agile, a risk registry.

If you tell people about the risks, you'll need to have uncomfortable discussions about why you think something may impede your migration.

They might even expect you to create a mitigation plan to prevent risks from happening, or contingency plans to do if the risk actually happens. Sounds like more work to me!

Best thing to do: cover you're a* and mention the issue once, casually, whenever you have a chance. The less formal the better. For example, tell your boss when he's late for a meeting. Don't offer a constructive way to solve the issue, just complain*.

That way, if something goes wrong, you can always say "I told you so!".

Gap analysis shmanalysis

Gap analyses are so overrated!

Who needs to look at the differences between the old version of SharePoint and the new version.

For example, if you found out that your old sandbox solutions are no longer supported, or that your mission-critical application built on Access Services won't work anymore, people are going to expect you to find an alternative solution.

Don't waste your time with migration tools

Why would you use a migration tool when you can just drag and drop the documents from the old SharePoint to the new SharePoint Online?

Arrgh! Those annoyingly demanding users are going to complain that all the documents' Last Modified date and Author have changed.

And you better hope they don't notice that you lost their version history either!

I guess you can always look at Microsoft's free SharePoint Migration Tools, which handles migrating sites and file shares. But how good can it be when it is free?

I mean, what incentive can Microsoft possibly have to spend time and money building a decent tool to migrate to Office 365?

My advice: don't even look at the tools. If someone tells you about the free SharePoint Migration Tools, you can always use the age-old IT consultant trick. Just say the following words:

I've heard that [INSERT PRODUCT NAME HERE] is slow and buggy

No one will ever argue with you. If you say it is slow and buggy, it must mean that you used it and nobody will ever doubt your IT knowledge ever again.

Don't waste time researching the various tools available. Don't look at how well they support their products or how they take time to educate their users to migrate successfully.

Don't evaluate whether the tools support scripting or automation. That way, no one will expect you to test your migrations and save them to a script that you can re-run without introducing issues.

Nah. If you have to use migration tools, pick the first one you heard of without worrying about why it is better than any other tool. It's not like you're spending your money, right?

If the tool doesn't work, you can always blame the tool.

Users love surprises

If you tell your users that you are going to migrate their stuff, they will pester you with questions.

It is better to hope that you never have to talk to your users, and just migrate stuff without telling them.

They can't stop what they don't know about, amiright?

If you start telling people what you're doing, I can promise you that a lot of the managers and directors of employees who may be affected by your migration will start asking you hard stuff. Like "how much time do you need from my staff to help?". They may even ask you to schedule meetings in advance!

Pro tip: Users don't have anything better to do. When you finally realize that you need their help, just expect them to drop everything. They'll be excited at the opportunity to take on additional duties to help you.

Don't schedule maintenance windows... that's what lunch breaks are for!

If you're pretty sure that nothing is going to go wrong, don't even bother telling people when you're migrating stuff. I mean, everything should go perfectly, so why worry users?

If they lose something important that they were working on, we have a backup, right?

Uh, I never really checked that the backups actually work.

Just wait until people are away from their desks (lunchtime or late Friday afternoon is a great time to do this), that way you can hope that if you have to reboot a server or if you affect the network or server performance, users won't notice it.

Training is for babies

We've discussed this before: anything that you can do to avoid telling your users what's coming will reduce the risk of them asking more questions and demanding answers.

The best training strategy is: throw them in the deep end and expect them to swim.

Don't waste money and time on training people. That's time you can't be Facebooking or Instagramming.

Sure, Microsoft has tons of customizable training materials available for free, but how good can it be, really? I mean: it's free.

Let users figure it out on their own. They're all technical experts, right?

Users are stupid.

Start with the assumption that users are stupid.

Also: feel free to make decisions to disable functionality because people won't understand it. Don't reward users who are more technologically savvy. Instead, assume that if you give your users too many options, they will spend so much brain power trying to understand that they will probably lose basic motor functions and be unable to maintain bladder control.

I always admire it when a manager or a director says "oh, our users will never understand that". They understand how you and they are intellectually superior to everyone else and don't even give users the opportunity to learn.

For example, don't give people access to Microsoft Teams, because they are too stupid to understand how to use chat applications.

Don't let people post profile pictures, because they'll instantly post pictures of their private parts. Instead of revising your "IT Acceptable Usage Policy", just disable those functions immediately.

And don't get me started about letting people use emoticons and comments in sites. So unprofessional! Better to spend all your efforts finding ways to disable those features, otherwise every single one of those previously professional people will suddenly lose all sense of proper conduct.

It's a wonder that they never thought of doing this with email? I mean, e-mail was invented in 1971 and we let people send emails to "All employees" and external email addresses and it hasn't occurred to them that they could send inappropriate emails before? Stupid people.

I know that the workplace landscape is changing and that some statistics (somewhere, can't be bothered to check) say that over the next 3 years, 48% of the working population will reach retirement age. It means that the new wave of people entering the workforce will be used to new modern tools, but what do they know -- they're young.

Keep your antiquated old views

You started using SharePoint back in 2001. How much can it have changed?

Don't waste time learning new features that Office 365 offers. True SharePoint gurus stick to the OG functionality.

For example, when you create a new Team Site or Communication Site in SharePoint Online, it creates a new Site Collection. Talk about overkill! They're called "Sites", not "Site Collections" for a reason!

Microsoft could just create one giant site collection with all your sites in one place.

If a feature didn't work well in a previous version of SharePoint, don't waste time to see if it has improved; just dismiss it completely and never go back to it. That feature is dead to me.

I mean, really, all that the SharePoint team has done since then is make it look uglier. So much whitespace!

If you wrote a book or a blog article even remotely related to SharePoint 10 years ago, you are absolutely absolved from having to learn anything new. Just hold on to those views, just like you've been holding on to your BlackBerry because let's face it, it's better.

Also, grey areas are for sissies. You need to have an unwavering opinion about every topic, and it needs to be absolutely black or white. No wiggle room to account for the customer's unique needs.

For example, folders are bad. Period. Immediately dismiss the opinion of anyone who says otherwise. Don't even waste time to think about whether there are acceptable scenarios where folders should be used.

I once worked with someone who refused to learn this new "SharePoint" thing we were implementing at her work (even though her new role was "SharePoint Administrator"). When I asked her why she didn't want to learn it, she said: "because I'm retiring in 5 years and I want things to stay the same". Be more like that: don't learn anything new.

More wisdom

Here are more pearls of wisdom I have learned from people.

Waste valuable time trying to rename SharePoint.com

When you had your old SharePoint on premises, you had full control over your SharePoint's domain name and server name. You could call it sharepointy_mc_sharepoint_face if you wanted to.

When you switch to Office 365, your new SharePoint domain will be yourcompanyname.sharepoint.com.

That will confuse everyone. People won't be able to handle it. There will be desks flipped over and people will set things on fire.

The best thing to do is to spend all your energy to find a creative way to rewrite your SharePoint online URL to something that won't confuse users.

Never mind that Microsoft doesn't support this, they just don't know any better.

Rebrand SharePoint completely

Office 365 and SharePoint support the use of themes and some limited organization profile colours.

Again, Microsoft doesn't know any better and hasn't spent any time researching how to optimize content for legibility, accessible contrast, etc.

If you don't completely rebrand your SharePoint site so that it meets your corporate designs, your users will not understand that SharePoint is a tool for work.

If they aren't constantly reminded of what company they work for with a giant logo occupying the top 1/3 of the screen, they may even forget who they work for.

Best advice: do everything you can to rebrand SharePoint to meet your corporate design.

Microsoft says that you shouldn't change the master pages or inject CSS in the page because (read this next part in a whiny/mocking voice) "they reserve the right to change the page structure at any time".

So what, for example, if you change the master page to suit your designs and it blocks you from receiving any new features/fixes Microsoft releases? It's not like they release new features, and updates twice a week, right?

I mean, can Microsoft really have this whole release thing down so that they technically have the ability to do multiple releases a day? I don't think so!

Don't worry about what's supported. Do what feels right.

Spend time disabling features you don't understand

If you don't understand a feature that Microsoft offers, and they don't provide you with a way to disable it, spend as much energy and resources as possible to disable that feature, regardless of the impact.

For example, if you don't understand the benefits of using the News feature in SharePoint, you should stop every single team from using it.

Even if you have practically unlimited storage space in Office 365, don't let users create groups. God forbid they create a collaboration space where they can do their work.

I mean, people are going to stop doing all their work and spend their entire days creating team sites and groups and put bad content in there, and then they'll leave it there.

No, instead of spending time trying to create a proper governance process, and quotas and other easy solutions to help prevent issues, disable those functions completely.

Don't use all the tools at your disposal

Don't even look at the tools and features that are included in your Office 365 subscription. That would require you to spend time understanding what they do and -- sigh -- explain to people how to use them.

For example, don't use the built-in voice, video, and whiteboard features of Microsoft Teams that integrate natively into pretty much everything. Instead, insist on using another third-party video conferencing tool that you have to pay extra for without understanding what the gaps are. (Remember, say it is slow and buggy).

Instead, spend all your efforts to try to get integrate a third-party tool.

Don't use Advanced Threat Analysis features of Data Loss Prevention features. Insist on forcing users to use your old, unpatched VPN solution.

That way, Microsoft's machine learning algorithms can't identify potential threats and immediately take counter-measures to protect your information. That's something that's best left to a human.

Don't worry about URL limits... or any other kinds of limits

When you're planning your migration, don't worry that the URL of your documents may be over 400 characters (because your old file structure is like a Russian nesting doll of folders within folders).

Your users won't know the difference. They'll find out that their files are missing way too late to force you to do anything about it.

Force every single person to go on your home page

When users log in to Office 365, they can choose what their landing page is going to be.

For example, their SharePoint page can get them the list of news and updated documents that are relevant to them based on their group memberships and other smart algorithms.

That means that you can't force people to go to your home page and see what you decided was important to them.

You could configure an organizational news source and use the Set-SPOOrgNewsSite PowerShell command to help push your corporate news in the user's news feeds where it will reach them (SharePoint Home, Team Sites, Communication Sites, Hub Sites, Microsoft Teams, Mobile App), but isn't it better to find an unsupported way to force everyone to go to whatever page you deem important for your users?

Even better, you should force your user's workstations to launch a browser to your mandatory home page. It's not like you can make SharePoint become such a mission-critical tool for them that they'll naturally want to log-in to it every day.

Remember, they don't know any better.

Conclusion

I hope that I have given you enough material today to help you ensure a failed SharePoint migration to Office 365.

Remember that if you succeed, people will expect you to deliver again and again. If you're busy working, when are you going to surf the internet and watch videos on Youtube?

If anyone accuses you of trying to fail a SharePoint migration, don't forget that according to CMS Wire, you're in good company: nearly 50 percent of all Enterprise Content Management programs fail just from a technology perspective. And of the 50 percent that succeeds, half of those fail to really provide value to the business.

You can hardly be blamed for failing.

What other tips do you have to help ensure your SharePoint migration to Office 365 fails? Let me know in the comments -- as long as you don't expect me to read it or anything.

Whatever you do, don't read these articles

Avert your eyes! You might actually learn something and I'll have written this post for nothing

5 Mistakes To Avoid When Migrating from SharePoint to Office 365. Pffft, what do they know?!
5 Common Mistakes Migrating to Office 365
Top 5 Mistakes to Avoid When Migrating to Office 365
25 Mistakes to Avoid in SharePoint or Office 365 (and How to Fix Them) -- they're actually trying to fix problems?! What's wrong with them?!!!
The Why of ECM Failure and the How of ECM Success

Update

Thanks, everyone for your feedback, I've added a section on keeping your antiquated views by popular (i.e.: more than zero) demand.

Introduction

Scott Hanselman is someone that I admire, both as a Developer and as a Human Being. I never miss a chance to attend his presentations.

I'm not smart enough to understand everything he talks about all the time, but he's always entertaining to watch.

Everyone once in a while, he creates an ultimate list of developer and power tools which always has one or two new tools I didn't know about. Any serious developer should look at his list as a great starting point to configure their workstation.

When SPFx first came out, I had never touched Node.js, React, or TypeScript. Visual Studio Code was (in my mind) just a free/lightweight version of Visual Studio -- why would anyone use VS Code over the full-on Visual Studio?! And don't get me started about GitHub!

I had a steep learning curve ahead of me. And I had a whole new set of development tools to install on my workstation.

It took me a while to figure out what I needed to install to be efficient at creating SPFx solution.

I'm in the process of updating my Windows 10 workstation and, as I was writing down the list of things to re-install, I thought I'd share my list.

This list is specifically for people who want to write SPFx solutions on Windows. It isn't as comprehensive as Scott's list, but I hope that it will help anyone getting started with SPFx development.

I also work with .NET, Azure, VR and AR, and Dynamics 365, so I have other tools on my workstation, but I wanted to focus on SPFx development.

If you develop SPFx solutions and see that I forgot anything here, I would love to hear from you!

NOTE: I'm a bit of a minimalist when it comes to installing stuff on my workstation. I hate installing things that will automatically start when I launch windows (that's why I never install anything from Adobe anymore). You'll rarely find anything in this article that will completely change how your operating system works or takes over your machine... and if you do, it is because I feel that the trade-off is worth it.

I also don't like to pay for things if there are free alternatives. For example, it would take me 5 minutes to create my animated GIF screenshots using SnagIt, but I foolishly spend 25 minutes using free tools because I'm too stubborn (or too cheap?). Every tool in this article is free, just keep in mind that there may be better alternatives if you're willing to spend money.

Mandatory

  • Node.JS: Find the .MSI file in the list that suits your workstation (x86 or x64). As tempting as it may be to download the latest version, don't. 10.x is the only version that is officially supported for SPFx development.
  • Gulp: Gulp is a tool that helps automate building your solutions. Once NodeJS is installed, install Gulp by launching the Node.js command prompt and type the following:
    npm install -g gulp
  • Yeoman: Yeoman is a tool that scaffolds solutions. Think of it as the Visual Studio new project wizard, if the new project wizard was command-line driven, open-sourced, and contained a bazillion project types. To install it, use your Node.js command prompt and type:
    npm install -g yo
  • SharePoint Framework Yeoman Generator: Now that Yeoman is installed, you need to add what is essentially your "SPFx Project Wizard". To do so, use your Node.js command prompt and type:
    npm install -g @microsoft/generator-sharepoint

Recommended

  • Visual Studio Code: When I first started writing SPFx solutions, I refused to use Visual Studio Code (real developers use Visual Studio, right?). Unfortunately, Visual Studio messed up my SPFx solutions more than once and I quickly learned to appreciate Visual Studio Code. Visual Studio no longer messes with your SPFx solutions, but I still use VS Code for all-things-SharePoint.
  • Git for Windows: Even if your company uses Azure DevOps or TFS for source control, you should install Git to make your life easier when downloading SPFx code samples.
  • Cmder for Windows: If you've ever wondered what that cool command-line they use on the SharePoint Development Community calls, you have found it! Follow my instructions if you need help installing it.
    Cmder
  • Fira Code Font: This font will add the cool visualization to the Cmder command prompt. You should also configure it as your default Visual Studio Code font Fira Code Font
  • Cmder Powerline: When paired with the Fira Code font (above), it will add that cool command-prompt to Cmder.
  • Postman: Test your APIs using this awesome tool. It can even intercept calls and replay them. I use this tool all the time when I'm trying to understand how SharePoint does something.
  • Fiddler: Use Fiddler to capture your workstation's network traffic and diagnose issues. For example, if you want to know how the Microsoft Teams app retrieves your list of groups, use Fiddler to capture what calls it makes.
  • TeraCopy: If you've ever copied your SPFx project files, you know how frustratingly slow Windows can be. TeraCopy is insanely fast (so fast that you'll think it didn't work at first). Don't take my word for it: next time you have to copy your SPFx project files, start your copy with Windows. Then, download TeraCopy for Windows, install it, and copy the exact same files using TeraCopy to a separate folder. Then go have a coffee, tell your co-workers that they should install TeraCopy, and go back to your desk. Windows will still be copying the first set of files.
    TeraCopy
  • Office 365 CLI: A command-line interface that lets you do tons of stuff in Office 365 and SPFx solutions. To install, use your Node.js command prompt and type the following:
    npm i -g @pnp/office365-cli
  • SharePoint Online Management Shell: Use it to create SharePoint Online sites and add users, you can quickly and repeatedly perform tasks much faster than you can in the Office 356 admin center. You can also perform tasks that are not possible to perform in the Office 356 admin center.
  • SharePoint PnP Cmdlets: SharePoint Patterns and Practices (PnP) contains a library of PowerShell commands (PnP PowerShell) that allows you to perform complex provisioning and artifact management actions towards SharePoint. The commands use CSOM and can work against both SharePoint Online as SharePoint On-Premises.
  • PnP Yeoman Generator: If you find yourself always adding the PnP developer controls, PnP Property Controls, PnPJs, unit testing, etc. to your SPFx solutions, you should probably try the PnP Yeoman Generator. It is built on top of the SPFx Yeoman generator, so you're not missing out on anything the SPFx generator will give you, but it automatically adds many other useful features. To install it, use your Node.js command prompt and type:
    npm install -g @pnp/generator-spfx

Visual Studio Code Extensions

  • GitHub Pull Requests: Allows you to review and manage GitHub pull requests in Visual Studio Code.
  • Paste JSON as Code: Convert JSON object to typescript interfaces as you paste into Visual Studio Code. What, you thought I typed all those classes?
    Past JSON as Code
  • Rencore Deploy SPFx Package: Easily deploy a SharePoint Framework solution package to SharePoint Online directly from Visual Studio Code.
    Rencore Deploy SPFx Package
  • Rencore SPFx Script Check: Using the Rencore Script Check Visual Studio Code extension you can easily reference external libraries in SharePoint Framework projects the right way. Additionally, you can ensure, that the CDN they are using is well performing.
    Rencore SPFx Script Check
  • Rencore Tenant-Wide SPFx Extension Deployment: Easily add tenant-wide deployment information for your SPFx extension directly from Visual Studio Code.
    Rencore Tenant-Wide SPFx Extension Deployment
  • SPFx Debug Configuration: This Visual Studio Code extension can be used to add the required configuration for debugging your SharePoint Framework.
  • SPFx Essentials: This is an extension pack that contains useful extension for SharePoint Framework projects. Most of the extensions I listed here are already included in Elio's awesome list.
  • SPFx Localization: This extension for Visual Studio Code makes it easier to work with locale resource files in SharePoint Framework projects. The extension has the ability to export all locale labels to a CSV file to make translations easier to process. With this extension, you have absolutely no excuse to hard-code your text in English within your solutions.
    SPFx Localization
  • SPFx Snippet: I never realized how much I use this extension until I tried to write spfx-rcc for a web part on a machine that didn't have the extension installed. Take it from the World's Laziest Developer, you need this extension.
    SPFx Snippet
  • SPFx Task Runner: This extension allows you to easily run SharePoint Framework tasks with a couple of mouse clicks. At the moment you can for example list all the available gulp tasks in your project, start the local development server and create debug or release solution packages or pick a task to run from the list of available tasks.
    SPFx Task Runner
  • SharePoint Typed Item: Sergei's awesome extension generates interfaces based on list and content type fields. Also check out Sergei's SPFx Rest Client
    SharePoint Typed Item
  • Prettier -- Code Formatter: This extension format your JavaScript / TypeScript / CSS using Prettier.
  • SPGo for Visual Studio Code: SPGo allows you to develop SharePoint web solutions from your local PC using Visual Studio Code. It pulls down remote folders from SharePoint to your local workspace and automatically publishes files when you save. It is one of those "where has this been all my (SharePoint) life?!" extensions.

Nice to have

  • Zoomit: Be a considerate presenter! If you do presentations and show your code, you should consider installing this tool. Those people in the back of the room (who sat in the back so that they can exit quickly if they find you boring) may not exit so fast if they can actually see what you're doing.
  • Paint.NET: A great image editing tool that's fully featured and free. I use it to edit icons and other assets.
  • Inkscape: A powerful SVG editing tool. I use it to create SVG icons. If only I could get SVG icons to work consistently in SPFx, it'd be great.
  • ScreenToGif: Do everyone a favour and show an animated GIF of what your SPFx solutions can do in the README.MD file. It saves them having to install your solution to see what it does. This tool is a screen, webcam and sketchboard recorder with an integrated editor that makes it super-easy to create animated GIFs. Available as a Microsoft Store App or a WPF install.
    ScreenToGif

Chrome Extensions

  • SP Editor: A Google Chrome Extension for creating and updating files (js, css), injecting files to sites, modifying web/list property bag values (add, edit, remove, index) and creating webhook subscriptions, edit/add/remove web parts from publishing pages and run sp-pnp-js typescript snippets in SP2013, SP2016 and SharePoint Online from Chrome Developer Tools. This tool will help you create amazing SharePoint applications fast from your browser from any computer which runs Chrome!
  • React Developer Tools: Allows you to inspect the React component hierarchies in the Chrome Developer Tools. You get a new tab called React in your Chrome DevTools which shows you the root React components that were rendered on the page, as well as the subcomponents that they ended up rendering. It is great when trying to understand how the SharePoint team built a component.
  • Screen Reader for Google Chrome: Test your web parts for accessibility by giving you the same experience your users will get when they use a screen reader.
  • AXE: No, not the body spray. Use this tool to check for accessibility for WCAG 2.0 and Section 508 accessibility. If you aren't testing for accessibility, you're possibly making it difficult for 10 to 20% of your users.

Websites

  • SharePoint Look Book: Get inspired with cool looking SharePoint sites.
    SharePoint Look Book
  • SharePoint Design Guidance: Learn how to create great looking SharePoint solutions.
    SharePoint Design Guidance
  • Office UI Fabric: See what components are at your disposal when building awesome SharePoint web parts.
  • SharePoint Dev Platform Uservoice: Request new features and see what the team is working on.
  • PnP/PnPJS: PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative and we encourage contributions and constructive feedback from the community.
  • Reusable property pane controls for the SharePoint Framework solutions: This repository provides developers with a set of reusable property pane controls that can be used in their SharePoint Framework (SPFx) solutions.
    Reusable Property Pane Controls
  • Reusable React controls for your SharePoint Framework solutions: Provides developers with a set of reusable React controls that can be used in SharePoint Framework (SPFx) solutions. The project provides controls for building web parts and extensions. You should really try the ChartControl 🙂
    Reusable React Controls
  • SharePoint PnP Provisioning Service: The SharePoint PnP Provisioning Service lets you add samples, templates and solutions to your Office 365 tenant.
    SP PnP Provisioning Service
  • SharePoint Developer Community - PnP (YouTube): YouTube channel with SharePoint Dev Weekly videos, SharePoint Framework Tutorials and Training videos, SharePoint Framework and Extensions Tutorials, Getting Started videos, PnP Webcasts, etc. If you no like read, you watch these good.
  • Base64 Image Encoder: Use this site to encode your web part icons to base 64.
    Base64 Encoder
  • EzGif.com: If you don't want to install ScreenToGif but want to create animated GIFs to help people see what your web part will look like without having to install it, I recommend using this web site. Create a video of your web part in action, then use EZ GIF's Video to animated GIF converter to create your GIF. Add the GIF to your README.MD file and people will see how cool your web part really is!
    EzGif

GitHub Repos

  • Office UI Fabric React: When I want to learn how to create awesome React components, I take my inspiration from Office UI Fabric.
  • SP Dev FX WebParts: Almost every SPFx project I create starts from one of the many samples available on this awesome repo.
  • PnP Property Controls: Back when most of us were still learning React and SPFx, they were already creating re-usable controls for SPFx. Take some time to read their code for inspiration.
  • PnP Developer Controls: From the brilliant minds that brought you the PnP Property Controls. Read the code to learn tons!
  • SharePoint Starter Kit: Look at some of the best solutions used to build the perfect demo environments.

Conclusion

This article listed the various tools I use when building SPFx solutions on a Windows 10 workstation.

As I stated previously, I'm kind of a minimalist when it comes to installing stuff on my machine. I'm sure that there are many other tools that I should install, but I haven't found a need for it yet.

What other tools do you install on your workstation? Let me know in the comments.

Update

  • December 06, 2019: Updated version of Node.js to 10.x.
  • October 10, 2019: I added SPGo after attending Beau Cameron and David Warner II's great SharePoint Saturday New England session about SPFx Development Tips from the trenches. Watching those two present together reminded me that there is always room to learn more.
  • July 2, 2019: Thanks to Denis Molodtsov for suggesting ScreenToGif instead of using EzGif. It makes it so much easier to capture an animated GIF of your web part in action!
  • April 4, 2019: Added SharePoint Developer Community - PnP YouTube channel
  • March 28, 2019: Added [Sergei Sergeev's] cool SPFx Typed Item extension, which he demoed in the March 28th SharePoint Dev Ecosystem call. Watch out for those Kung-fu Gophers.
  • March 25, 2019: Thanks to Thomas Lamb for suggesting Prettier -- Code Formatter.
  • March 20, 2019: Thanks to Sam Culver for pointing out that Fira Code makes a great font in Visual Studio.
  • March 16, 2019: Added Paint.NET and Inkscape as graphical tools.
  • March 14, 2019: Thanks to Miguel Isidoro for pointing out that I had the wrong link to the Rencore SPFx Script Check.

    Credits

    Header image by Rudy and Peter Skitterians from Pixabay

Introduction

What happens when you file to plan

When migrating content from a network file share to SharePoint Online (or SharePoint on premises), remember SharePoint's URL length limitations.

This article discusses the current limitation with URL lengths in SharePoint. It discusses how this limitation manifests itself, and how it can impact you during your SharePoint migration.

A matter of legacy

In SharePoint, the limit for a document's URL is 400 characters long. We'll discuss this in greater length (see what I did there?) later, but for now, let's discuss how we get long path names.

To begin with, nobody in their right mind ever says "I think I'll name my file SuperInsaneLongFileNameThatIDontEverWantToTypeAgainBecauseItIsTooLong.docx".

That'd be just annoying.

Instead, people get long path names because of the legacy infrastructure dictated by file shares.

You see, on a network file share, users don't get the luxury of using metadata or versioning. As a result, users tend to get creative and create folder structures as a substitute to convey metadata.

We've used this example before. Imagine that your users store case files on a network file share. They want to group case files by status, so they case folders for each status under the Cases folder:

\Cases
    \Open
    \Closed
    \Pending review

They also want to group cases by year, so they create a folder for every year in each Cases > Status folder:

\Cases
    \Open
        \2017
        \2018
        \2019
    \Closed
        \2017
        \2018
        \2019
    \Pending review
        \2017
        \2018
        \2019

Each case is placed in the folder matching the year it was received, within the folder for the case status.

Now imagine that, in the process of managing a case, your staff receive electronic evidence, via USB sticks, removable drives, or email attachments -- or whatever way. To keep these files together, users start creating folders. Of course, they want to keep track of when they received the files, so they put the files in a folder called John Smith files received Jan 21, 2019

Also, because our users want to keep incoming documents (stuff they received during the case management process) from outgoing documents (letters and documents they send out through the case management process), they create separate folders, aptly named Incoming documents and Outgoing documents.

For example, let's pretend we have an open case, numbered CA-12345678, from 2019. The folder structure would look like this:

\Cases
    \Open
        \2017
        \2018
        \2019
            \CA-12345678
                \Incoming documents
                    \John Smith files received Jan 21, 2019
                        \Photo evidence 1.png
                        \Letter from complainant.pdf
                        \...
    \Closed
        \2017
        \2018
        \2019
    \Pending review
        \2017
        \2018
        \2019

Of course, they also want to keep track of outgoing documents they work on, so they name the files accordingly:

\Cases
    \Open
        \2017
        \2018
        \2019
            \CA-12345678
                \Incoming documents
                    \John Smith files received Jan 21, 2019
                        \Photo evidence 1.png
                        \Letter from complainant.pdf
                        \...
                \Outgoing documents
                    \Case Summary
                        \Case review v1 Feb 21.docx
                        \Case review v2 Feb 25.docx
                        \Case review v3 Feb 27.docx
                    \Case interviews
                        \Interview with John Smith Jan 23, 2019 raw.mp4
                        \Interview with John Smith Jan 23, 2019 transcript v1.docx
                        \Interview with John Smith Jan 23, 2019 transcript final.docx
                        \Interview with John Smith Jan 23, 2019 transcript final final.docx
    \Closed
        \2017
        \2018
        \2019
    \Pending review
        \2017
        \2018
        \2019

And so on.

Users don't do this to be annoying. They do this because they need a way to manage their information with the tools they have. Without metadata at their disposal, they use long file and folder names.

As long as they keep their file paths shorter than 255 characters, most Windows applications will be able to handle opening and saving files from the file share. This isn't a limitation of UNC paths, it is a limitation within the apps that try to open and save files (for example, your PDF reader). Depending on the version of Windows you use, and assuming you use applications from this century, you can probably get away with longer file paths.

Typically, organizations will map file shares to a drive to make it easier for users. So, instead of having a path that starts with:

\\servername\networksharename

They'll have a single letter pointing to the file share. For example, S:\. It helps keep the file paths shorter, thus preventing any issues.

Everyone is happy.

Until they try moving the files to SharePoint.

SharePoint URL Length Limitations

Prior to May 2017, the maximum URL length for a file stored in SharePoint was 255 characters.

Since then, Microsoft kindly increased the maximum path size to 400 characters.

When I say 400 characters, I really mean 400 unicode units. If you use International characters with multibyte values, you actually get less that 400 characters.

"So what? Our file names are less than 400 characters", you may ask.

You see, the URL is for the entire URL of the file, including:
https:\\yourtenant.sharepoint.com/sites/yoursitename/yourdocumentlibraryname/yourfolderpath/subfolderpath/Filename.extension/?parameters

For example, let's say your tenant is called Contoso, your site called Case Management, and your document library called Shared documents. On your file share, that file path that started with:

S:\

...when migrated to SharePoint, will become:

https://contoso.sharepoint.com/sites/case%20management/shared%20documents/

That's a whopping extra 71 characters added to your file names. Those file paths that didn't cause any issues on a network file share can suddenly be too long once migrated to SharePoint.

How long URLs manifest themselves

No issues when uploading documents

Here is the problem: you may be able to migrate your documents to SharePoint -- even if they have long file names that exceed the maximum path length.

Errors opening documents online

However, when you try to open the file, you get an error. For example, here is what happens when I try to open a document with Word online:
Word File Path Too Long

Errors opening documents using desktop applications

What's worse, if your users are on an older version of Windows, or if they use a Mac, they may experience issues with file paths around the 255 character length!

Also, if they try to Sync the files to OneDrive for Business, and they try to open documents using older desktop applications, they may get an error indicating that the "file could not be opened" or the ever-mysterious "an error has occurred".

"Some files work, some files don't"

Users may experience an issue when only a few documents (or folders) won't open, but other files open without problems.

This usually happens with file names that have different lengths. Shorter file names open without issues because the full paths are less than 400 characters. Longer file names don't open because the file names are longer than 400 characters.

To regular users, it seems like a random issue.

But you and I know better, don't we?!

How to solve the URL length issue

You migrated your file share to SharePoint without getting any errors while uploading the documents.

Everything looks good.

Then a user tries to open a document with a URL that exceeds the limit. And they get an error. Of course, this always happens with someone very high up in your company, usually minutes before a very important event.

Move files to a shorter URL

The easiest way to solve this problem: use SharePoint's Move functionality within a document library to move the file to a shorter URL.

Move to

For example, if the file is 5 folders deep in a document library, try moving the file to the root of that document library.

It may shorten the URL with enough precious characters to allow you to open the file.

Then have the talk (you know, the one about why you should use shorter folder names and file names) with your users.

Errors with custom code

If you use custom code to connect to SharePoint using REST, you may also run into issues when the full URL for the REST call exceeds the limit.

One way to solve this problem is to use GetFileById and pass the unique file id instead of trying to use the file name:

https://yourtenant.sharepoint.com/sites/yoursitename/_api/web/GetFileById('fileid')

How to test URL length limits

When I tested the maximum URL length for this article I manually created super-long file names.

Fortunately, you don't have to do that. Thanks to Rene Modery, you can use a script to create long file paths. It uses the SharePoint PnP cmdlets to connect to SharePoint and create long paths for you.

Conclusion

When planning a SharePoint migration, pay attention to long path names. You may be able to migrate the documents without issues, but your users will run into issues opening documents.

If your users work with older desktop applications, older versions of Windows, or Macs, you may want to stick to a maximum URL length of fewer than 255 characters.

If your users have the latest version of Windows and Office, you can safely plan for maximum server-relative URL lengths (i.e.: the URL without the https://[yourtenant].sharepoint.com) of 400 characters.

Of course, after your content is migrated, help your users understand why they shouldn't use silly long file and folder names anymore and show them how to use metadata.

I hope this will save you some headaches in the future.

Update

Contrary to Microsoft's own article, it appears the 400 character limit no longer counts the https://[yourtenant].sharepoint.com/ or the parameters (e.g.: ?web=1) in the URL length. The limit of 400 characters applies to the server-relative file path.

When a URL contains special characters, like space, %, #, etc., the characters get URL-encoded. For example, space will be encoded as %20. When verifying if your file URL to shorter than 400 characters, SharePoint uses the un-encoded URL (i.e.: special characters count as 1 character).

For more information

Introduction

Information Architecture (IA) is the structural design of shared information environment (source: Wikipedia).

In this series on Information Architecture, we discussed how a bad IA can affect your SharePoint success.

If your SharePoint users can't find the information they need quickly, they'll get frustrated.

In our first article, we explained why trying to create an IA that has only one dimension leads to creating the lowest common denominator IA instead of giving your users the best-of-breed experience they deserve.

Our second article explained how you should build an IA on 3 dimensions. We described how the physical IA should cater to authors. If you don't create an IA that allows your authors to place content in the right place, you'll end up with messy, unstructured data. Yes, it is data, not information, because it loses all meaning.

In the last article, we looked at the logical IA. While the physical IA caters to your authors, the logical IA should cater to your readers.

But there is one more dimension to consider: the metadata dimension.

The metadata dimension

The metadata dimension of IA helps the system deliver information for specific purposes.

For example, imagine that the HR department has a document library for their HR policies. Before employees can see the latest version of a policy document, they must be approved.

Marie the HR Manager is responsible for approving HR policy documents.

When she launches SharePoint, her HR site tells her how many documents are waiting for her approval.

If Mary clicks on a document waiting for her approval, it takes her directly to the document. From there, she can approve or reject the updated policy document.

SharePoint does not keep documents waiting for Mary's approval in a separate physical location. Yet, from Mary's perspective, SharePoint appears to bring all documents waiting for her approval in one convenient place for her.

In other words, the physical location of her documents doesn't change. SharePoint uses the metadata to identify which documents need Mary's approval. SharePoint then presents the documents in a separate logical location.

That's what creating the metadata IA is all about: making sure that your documents have the metadata they need to support specific goals.

You might say "Ok, I get it, so we just use the goals we identified when we [designed to logical IA]( (https://tahoeninjas.blog/2019/02/22/information-architecture-in-sharepoint-the-logical-dimension/) and create the metadata we need to support it?"

Well, yes ...and no.

(Remember, I'm a consultant. The answer is always "It depends")

Progressive Disclosure

In user experience (UX) design, there is a design pattern called Progressive Disclosure. It is a strategy for managing information complexity by gradually (or progressively) revealing more information as users indicate they wish to see more.

In other words, show only the information that is necessary at every point of interaction.

By reducing the amount of unnecessary information you show to users down to the essential, you make it easier for users to make sense of that information.

You see this all the time when using SharePoint. For example, the More button in the document library toolbar is a form of progressive disclosure. We don't need to show every single option in the toolbar. We only need to show the most common options. If users tell us they want to see more choices, we reveal more options.
Progressive Disclosure Example with the More menu

The News web part in SharePoint works the same way. When you go to a team site or communication site, you see the latest news. If users want to see more news, they can use the See all link, in the upper right corner.
SharePoint News

Summary, List, Details

When designing your metadata IA, you should consider creating the metadata structure required to build progressive disclosure in your design.

An easy way to do this is to present the information in 3 distinct views:

  • Summary
  • List
  • Details

We'll describe each view below.

Summary

As you evaluate every actor goal, ask yourself:

What is the least amount of information that this actor will need to meet their goal.

Another way to ask this question is:

How do I summarize the information this actor needs to meet their goal quickly

In our example, Mary the HR Manager needs to know quickly when documents need her approval.

We don't need to show her every single document. We just need the documents that have not been approved yet.

Keeping this in mind, we don't even need to show the Size, or Date Created. We probably only need the document's Title, Date Modified, and Modified By.

When building a summary view, consider providing the user information that will help them prioritize their tasks. For example, we can show Mary the list of documents that have been waiting for her approval the longest. Or, we can show the list of documents by how recently they were submitted for approval.

Every summary view should allow users to do at least two things:

  • Get the full information about an item in the summary (a.k.a. the Details view)
  • Get the full list of items (a.k.a. the List view)

Both are discussed further below.

How do I sort my summary view?

Almost every client engagement I work on, there is at least one business stakeholder who insists that their summary view of [whatever] must be sorted alphabetically -- from A to Z.

Their argument is usually "people need to be able to find [whatever] quickly. Sorting by alphabet is the fastest way to let them find it".

It may be a good approach for a list view of [whatever], but the summary view should boil it down to what matters now.

Instead, consider showing the list of latest [whatever]. Sorted in reverse descending date (i.e.: newest first). Doing so will allow repeat visitors to see the newest [whatever]. If they scan down the list, they may eventually see an item that they've seen before and assume that they have seen everything below that item.

Consider the alternative: users have to scan through the entire list of [whatever] to see if there is anything new.

How many items in my summary view?

There are no set rules for the number of items to show in a summary view. Luckily, there are brilliant people who have done some research on this.

The first rule is known as Miller's Law. In summary:

The average person can only keep 7 (plus or minus 2) items in their working memory.

Miller's Law is often misinterpreted. It doesn't mean that you should only present 7 (+/- 2) items only. If you need to present more, consider chunking the information into groups of 7 (+/- 2) items.

The second rule is Hick's Law. Hick's law says:

The time it takes to make a decision increases with the number and complexity of choices.

In other words: the more choices you give people, the longer it takes for them to make a decision.

So, when I build a summary view, I try to limit it to 7 +/- 2 items.

Sometimes I show 5, sometimes I show 7, and rarely I'll show 9 items. If I need more than 9 items, I'll always try to group the items into subsets of 5-9 items.

The more information I need to present with each item, the fewer items I'll show in my summary view.

Doing so will ensure that every user can make sense of the entire list and that they will make a decision quickly.

List

The list view allows users to view all items, possibly without filters or restrictions.

But don't think that you can just create a All items or All documents view and be done with your list view.

Keeping your user's goals in mind, you should design one or more list views to help your users accomplish their goals quickly without having to scan through the entire list of items.

For example, I'll often create views like:

  • Latest [whatever]
  • My [whatever]
  • [Whatever] waiting for my approval
  • [Whatever] by approval status
  • [Whatever] about to expire

...and the list goes on.

As with the summary view, you need to consider what metadata you'll need to support each list view.

Every item in the list should provide users with a link to the Details view for that item.

Details

The details view should provide all the information needed on an item so that users can achieve their goals.

Most often, the details view for a document is really the document itself -- because, most often, the user's goal is to read the document.

Sometimes, you need a different details view. For example, let's say we want to allow executives to do a second-level approval for expenses that approved by managers who report to them.

Instead of showing the full document, we'd need to show the expense summary, amount, manager's approval, and approval date. The view would probably make it easy to approve the expense without having to open it.

Keeping in mind progressive disclosure, and providing users with Summary, List, and Details views of information will help your users make decisions quickly and accomplish their goals.

Implicit metadata

If you're planning an IA to migrate a network file share to SharePoint, heed this warning:

Don't simply relocate document. The location of a document in a file share is a form of metadata that you may lose if you relocate documents.

That's implicit metadata.

Let's say that your file share contains legal cases for case management purposes. You have a folder for every case your company has ever processed.

Because of the large number of cases your company processes every year, there is a folder for every year in your file share. Cases are then placed in their respective folders, according to when you received the case.

Your company gives each case a unique number, e.g.: CA123456. To ensure privacy, case numbers do not convey any information about who the case parties are.

To make things even easier for your users to work through open cases only, your network file share groups every year folder into an Open cases, Closed cases, and Pending approval.

The folder structure looks as follows:

  • Cases
    • Open case
      • 2017
        • CA123456
          • CA123459
          • CA123464
        • ...
      • 2018
        • CA133456
          • CA123559
          • CA121464
          • ...
      • 2019
        • CA233456
          • CA323359
          • CA124464
          • ...
    • Closed cases
      • 2017
        • ...
      • 2018
        • ...
      • 2019
        • ...
    • Pending approval
      • 2019
        • ...

Because the documents you want to migrate are in a network file share, there may not be a lot of metadata -- if any -- for your documents.

But if you look carefully, the location of each document provides implicit metadata about each document: the status of the case, the year the case was received, and the case the document belongs to.

If you migrate your file share to SharePoint, you need to consider a way to apply the implicit metadata to each document.

You might say: "If I keep the same folder structure, I won't lose the implicit metadata". Sure, but what happens when users are looking for a document and use search? The only way they will be able to see that implicit metadata about every document in their search results is if they look at the document location for every. single. document.

Conclusion

As I have hopefully conveyed in today's post, you need to consider your metadata IA structure to support your users' goals.

The metadata dimension also needs to support both the physical and logical IA.

In my next post, I will explain how to put all this theory together using SharePoint's capabilities.

For More Information

Introduction

"One size fits all" doesn't work for Information Architecture.

Especially not in SharePoint.

In a previous post, I discussed a lot of bad SharePoint implementations are due to bad Information Architecture.

As a general rule, if your users complain that they can't find the information they need, it is most likely because your Information Architecture (IA) needs tweaking.

Your IA may have been great when it was first implemented. But things change.

Your customers' needs change, and your business needs to change to adapt -- if you want to stay in business.

When your business changes, your IA should change as well.

In this article, I'll discuss how to avoid the "One size fits all" approach for IA, and how it should constantly evolve.

IA does not equal the Org Chart

The first instinct when building an IA is to mimic your company's Org Chart.

That would be good... if your company suddenly became sentient and started browsing your SharePoint site. If Contoso Inc became a living thing, it would feel right at home navigating an IA that looks like its Org Chart.

But you don't build IAs for companies.

You build them for people.

Your SharePoint users are those who will need to find information. They're the ones that need to be able to get to the stuff they need to do their jobs.

So, here is my bold statement:

Information Architectures need to be User-centric, not Organization-centric.

Now many of you are probably saying: "Do you expect us to build different navigation for every single person in my company?"

Ideally, yes! But it isn't as simple as that.

And how do you fit different navigations for every user in your IA? Won't it get too complicated to make sense?

Limitations of file structures

In the olden days, when people tried to build IAs for network file shares, there was only one dimension to work with. The folder structure.

With folders, if you want to create a folder structure that caters to different types of people, you don't have a lot of flexibility.

Let's take an example: employee benefits.

Most organizations offer various types of employee benefits. Here are some examples:

  • Health care
  • Retirement
  • Workplace flexibility
  • Wellness program
  • Tuition reimbursement

The Human Resources department handles some of the benefits above. Finance department handles some others.

When creating the folder structure, you want to give a place for our HR folks to share benefits information. Things like brochures, forms, standard operating procedures (SOPs), etc.

But you don't want people from the Finance dept to mess with HR's files. And you don't want HR people to mess with Finance's files.

So you create another folder for the Finance department to share information about the benefits they take care of. Another folder with brochures, forms, SOPs, etc.

But how do employees find information about benefits if all you have is a file folder structure? They need to know that there are files in both the HR and Finance department benefits folders.

That's the problem with using a file folder structure. If you don't want to duplicate documents, you need to put them in one place.

So you end up creating the lowest common denominator structure that will cater to most people.

But what if you could use more that one Information Architecture?

To be continued

As it turns out, your SharePoint Information Architecture has 3 different dimensions! That's 3 different types of Information Architecture at your disposal.

In our next article, we'll discuss the 3 dimensions of Information Architecture in SharePoint.

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 Perry! (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, the 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 versions 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 dont seem to take effect when debugging your solution, you may need to stop debugging, rungulp 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 ominous 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 set 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

Introduction

If you write SPFx web parts or extensions using React, you may have had to assign more than one or more CSS classes to the same element. To do so, you simply list all the CSS class names inside a string, separated by spaces; Like this:

public render(): React.ReactElement<IDemoProps> {
    return (
      <div
        className={"myClass mySelectedClass myEnabledClass"}>
    ...
    </div>);
}

However, if you want to dynamically assign CSS classes, the string gets a bit more complicated.

For example, if I wanted to add a CSS class only if the state of the element is selected, and also have a different CSS class for whether the object is enabled or not, you would combine a whole bunch of conditional operators inside your string.

Something like this:

public render(): React.ReactElement<IDemoProps> {
    const {
        selected,
        enabled } = this.state;

    return (
      <div
        className={"myClass " 
            + selected ? "mySelectedClass "
            : undefined 
            + enabled ? "myEnabledClass"
            : "myDisabledClass"}>
    ...
    </div>);
}

Note that I had to include a space after myClass and mySelectedClass because, if they get concatenated in a string and I forget to include the space, the className attribute will be:

myClassmySelectedClassmyEnabledClass

instead of:

myClass mySelectedClass myEnabledClass

Which is obvious now that I write it, but when it is 3 in the morning and you're trying to figure out why your CSS class isn't working properly, it is a small mistake that can be very annoying.

And if your logic gets even more complicated, your CSS class name concatenation can be pretty unruly.

Luckily, the standards SPFx solution has a built-in helper.

@uifabric/utilities/lib/css

Courtesy of our Office UI Fabric friends, there is a helper function that takes an array of CSS class names and concatenates it for you.

And the best part is: it is already included inside your SPFx solution!

To use it, start by importing the CSS utilities:

import { css } from "@uifabric/utilities/lib/css";

And replace all that concatenation ugliness with a simple call to css, as follows:

public render(): React.ReactElement<IDemoProps> {
    const {
        selected,
        enabled } = this.state;

    return (
      <div
        className={css("myClass", 
            selected &amp;&amp; "mySelectedClass", 
            enabled ? "myEnabledClass" : "myDisabledClass")}>
    ...
    </div>);
}

The class takes care of adding spaces between the classes. For example, the following code:

className={css('a', 'b', 'c')}

will produce:

className={'a b c'}

It also skips the "falsey" values (according to comments in their code). In other words, you can evaluate class names that result in a null, undefined, or false value and it will skip it.

For example the following code:

className={css('a', null, undefined, false, 'b', 'c')}

Will produce:

className={'a b c'}

You can even pass a dictionary of class names, each with a true/false value, and css will concatenate all the class names that are true, as follows:

className={css('a', { b: true, z: false }, 'c')}

Produces:

className={'a b c'}

<strong>But wait! If you order now, you'll also get</strong> the ability to pass serializable objects (objects that have a <strong>toString()</strong> method) -- at no extra charge!

```TypeScript
const myObject = { toString: () => 'b' };
...
className={css('a', myObject, 'c')}

Will result in:

className={'a b c'}

Conclusion

As a self-proclaimed World's Laziest Developer, I tend to avoid extra work at any cost. The css helper function, which is already in your SPFx solution helps avoid writing extra CSS class name concatenation logic by provided something that is versatile, sturdy and -- best of all -- tested!

I know that this isn't an earth-shattering technique or original, but I find myself constantly re-opening old SPFx solutions to remember where that css function is defined. This article may save me some searching in the future... and hopefully, help you as well!

This is an easy one, but I keep Googling it.

When you create an SPFx web part, the default Property Pane automatically submits changes to the web part. There is no "Apply" button.

Property Pane without Apply
Default property pane -- no Apply button

But sometimes you don't want changes to the property pane fields to automatically apply.

All you have to do is to add this method in your web part class (just before

getPropertyPaneConfiguration is where I like to place it):
protected get disableReactivePropertyChanges(): boolean {
	return true;
}

When you refresh the web part, your property pane will sport a fancy Apply button!

 

PropertyPaneWithApply.png
Property pane with an Apply button

Property changes in the property pane will only get applied when users hit Apply.

 

That's it!

Hub sites?

Unless you're a SharePoint geek like me, you may not have been eagerly waiting for this new feature announced at Ignite 2017 in Orlando. Hub sites are a special site template that allows you to logically group team sites and communication sites under another site, with a shared navigation, theme, and logo.

Hub sites will also aggregate news and activities from any sites associated to it, and you can search within a scope of a hub site and it's associated sites.

The picture Microsoft used in their announcement explains it best:

hubbahubba

The Problem

The typical corporate intranet is often nothing more than a re-hash of the company's corporate organization structure, blindly copied to a web site accessible to employees. If that intranet is done using SharePoint or Office 365, it'll consist of a bunch of site collections with some sub-sites.

(By the way, I completely disagree with using the org chart for your intranet structure, but I'll save it for another blog post).

What happens when your company restructures for (insert official reason here)? Let's say that you had a whole bunch of Divisions, each with their own site (or site collection) and they completely change the divisions every quarter (like the CEO of a former client of mine liked to do).

What happens when the IT, Finance, and HR team are no longer in the same groups?

You end up having to either:
a) Move sites around, break a lot of people's favourite shortcuts and links; or
b) Leave everything the way it is and give up hope

Or, you could create a structure that doesn't need to change with the org-chart-of-the-week by using a flat structure. Since the new modern sites in Office 365, it is a lot easier to create groups, team sites and communication sites in a rather "flat" structure (every site is created in their own site collection, located under https://yourtenant.sharepoint.com/sites/ or https://yourtenant.sharepoint.com/teams/).

So, now you end up with a flat site structure that doesn't need to change when your information architecture changes again, but there is no easy way to navigate through this flat structure.

You can hack together some sort of global navigation with custom code and/or scripts, but every time someone wants to add a new site, you need to change the code.

The Solution

SharePoint Hub Sites allows you to continue creating a flat structure and logically group sites together in a semi-hierarchical fashion.

There are caveats:

  • As of this writing, you can only have up to 50 hub sites on your tenant.
  • You can add sites to hub sites, but you can't add hub sites to hub sites. And don't get me started about hub sites under hub sites under hub sites.
  • You need to be a SharePoint admin to create hub sites, but you can control who can add sites to what hub sites.
  • You'll need to do some Powershell.

Demonstration

We are going to create an Employee Matters hub, which will be the go-to place for employees to find resources related to being an employee of [XYZ Corp].

It will contain the following sites:

  • Benefits
  • Jobs
  • Training

Before you start

Download and install the latest SharePoint Online Management Shell.

Create "Sub" Communication Sites

  1. From your Office 365 environment, create a Communication site by going to the waffle
    waffle
    | SharePoint | Create site.
    createsite1
  2. From the Create site panel, select Communication site. It also works with Team sites.create site 2
  3. Choose the Topic layout and name the site Benefits. Give it a description if you'd like. Select Finish.
    Createsite3
  4. Repeat steps 1-3 above with Jobs and Training (or anything else you'd like to do), making sure to remember the url of every site you create (you'll need to go back to the sites you just created later).

Create a (future) hub site

Repeat steps 1-3 above again, but this time call the site Employee Matters. This will be the site that will be converted to a hub site. Make note of the site's url.

Register the hub site

  1. Start the SharePoint Online Management Shell.
    SPOMS
  2. From the PowerShell command prompt, type:
    Connect-SPOService -url https://-admin.sharepoint.com

    where is your own SharePoint tenant. Note that we're connecting to the Admin site, not the regular .sharepoint.com site.

  3. Once connected (you'll be prompted to login, probably), type:
    Register-SPOHubSite -site https://.sharepoint.com/sites/employeematters

    ...making sure to use the url of the Employee Matters you created earlier. Note that this time, we are not using the -admin.sharepoint.com domain, just the regular .sharepoint.com domain.

  4. If all goes well, you'll get something like this:
    ID : 2be153d3-0fe8-4fb8-8fa0-b41dfdd8bd3f
    Title : Employee Matters
    SiteId : 2be153d3-0fe8-4fb8-8fa0-b41dfdd8bd3f
    SiteUrl : https://.sharepoint.com/sites/EmployeeMatters
    LogoUrl :
    Description :
    Permissions :
  5. Memorize the GUIDs. Just kidding! You can pretty much ignore the response -- as long as it didn't start spewing red text, you're doing fine.

At this point, if you got an error saying Register-SPOHubSite is not a valid command, you probably haven't installed the latest version of the SharePoint Online Management Shell.

If it gives you an error saying that hub sites aren't yet supported, go have a big nap and try again tomorrow.

You can go visit your newly created hub site. It should look like this:
employeematters1.png

It doesn't look much different than any other communication site, but it has an extra navigation bit at the top:

hubsite2

If your site hasn't updated yet, wait a little bit. Some of the changes take up to 2 hours, but every time I have done this, it was instant.

Optional: Set your hub site icon and description

You don't have to do this, but it is generally a good idea to label your sites and give them a custom icon. To do so:

  1. Upload an icon of your choice to a library of your choice (for this demo, I created a document library called Site Assets in the Employee Matters site). Make note of the url to the icon. The icon should be 64x64 pixels.
  2. From the SharePoint Online Management Shell thingy, enter the following:
    Set-SPOHubSite -Identity https://.sharepoint.com/sites/employeematters -LogoUrl https://.sharepoint.com/sites/employeematters/site%20assets/employeemattersicon.png -Description "Find resources for employees"

    Making sure to replace the LogoUrl for the url to the icon you want (and making sure that you put whatever description you want for the site hub).

  3. Your site hub will eventually get updated. Go take a look.

By the way, there is a user interface to change the site hub logo, but there isn't one to change the description. You can get to it by following these steps:

  1. Using your browser, go to your site hub.
  2. From the site hub home page, select the settings gear and select Hub site settings
    hubsite3.png
  3. From the Edit hub site settings pane that appears, you can change the icon or the site hub title. Not the description.
    hubsite4
  4. Select Save and your changes will (eventually) be reflected.

Associate "sub" sites to hub site using your browser

  1. Go to the Benefits site you created what seems like a million years ago.
  2. From the settings gear icon, select Site information
    sitesettings1
  3. From Edit site information pane that appears, select the Employee Matters hub site from the Hub site association, then select Save.
    Note thasitesettings2Note that, in real life, only users who have been granted the rights to join a site will be able to do this -- but that's another blog post. Also, note that changing the hub site will change the site theme to match the hub site and add its navigation (as is clearly indicated on the Edit site information pane).

You should notice that your Benefits site will now have the Employee Matters navigation added at the top. That means it worked.

Associate "sub" site to hub site using PowerShell

  1. From the SharePoint Online Management Shell, enter the following:
    Add-SPOHubSiteAssociation -Site https://.sharepoint.com/sites/Jobs -HubSite https://.sharepoint.com/sites/EmployeeMatters

It will associate the Jobs site to the Employee Matters hub. Note that the -Site parameter is the site you want to add to the hub site, while the -HubSite parameter is the hub site.

Use either the PowerShell method or the browser method to add the Training site to the hub site.

Add links to the hub site navigation

The sites associated to your hub site now sport the new fancy hub site navigation, showing Employee Matters, but you'll notice that the navigation did not get updated to show the newly associated sites.

To fix this:

  1. Go to your hub site's home page. You can do so by clicking on Employee Matters from any of your associated sites.
  2. From the hub navigation (top left corner of the hub site, where it says Employee Matters) select Edit.
  3. From the navigation editing pane that appears, select the button to add a new link.
    fancyplus
  4. In the Add a link pop-up that appears, enter the url to the Jobs site in the Address field, and type in Jobs for the Display name, then select OK.addlink
  5. Repeat until you have added Jobs, Benefits, and Training then hit Save.hubsitenav

Your hub navigation will contain links to each associated site.

News, activities and search results from the hub home will include results from all associated sites, provided that the current user has permissions to each site. It takes a while before the results appear, but they will!

Conclusion

Hub sites are going to be a great addition to SharePoint in Office 365. They aren't going to solve every navigation issues, but they are certainly a step in the right direction.

There is still a lot to cover with theming and security, but that's probably enough for today.

(OR: How to solve the "this property cannot be set after writing has started." error when calling OpenBinaryDirect)

The Problem

I was trying to write a little app to programmatically download files from a SharePoint instance on Office 365 to a local folder on my hard-drive/network file share -- something I've probably done a thousand times -- using this code:

/*
* This code assumes you already have filled the following variables
* earlier in the code
* Code has been simplified for 
*/
var webUrl = "https://yourtenantgoeshere.sharepoint.com/site/yoursitename";
var username = "yourusernamegoeshere@yourtenantgoeshere.com";
var password = "pleasedonteverwriteyourpasswordincode";
var listTitle = "yourdocumentlibrarytitle";
var destinationFolder = @"C:temp";

var securePassword = new SecureString();
//Convert string to secure string
foreach (char c in password) {
    securePassword.AppendChar(c);
}
securePassword.MakeReadOnly();

using (var context = new ClientContext(webUrl))
{
    // Connect using credentials -- use the approach that suits you
    context.Credentials = new SharePointOnlineCredentials(userName, securePassword);

    // Get a reference to the SharePoint site
    var web = context.Web;

    // Get a reference to the document library
    var list = context.Web.Lists.GetByTitle(listTitle);

    // Get the list of files you want to export. I'm using a query
    // to find all files where the "Status" column is marked as "Approved"
    var camlQuery = new CamlQuery
    {
        ViewXml = @"
            Approved
            1000
        "
    };

    // Retrieve the items matching the query
    var items = list.GetItems(camlQuery);

    // Make sure to load the File in the context otherwise you won't go far
    context.Load(items, items2 => items2.IncludeWithDefaultProperties
        (item => item.DisplayName, item => item.File));

    // Execute the query and actually populate the results
    context.ExecuteQuery();

    // Iterate through every file returned and save them
    foreach (var item in items)
    {
        // THIS IS THE LINE THAT CAUSES ISSUES!!!!!!!!
        using (FileInformation fileInfo = Microsoft.SharePoint.Client.File.OpenBinaryDirect(context, item.File.ServerRelativeUrl))
        {
	    // Combine destination folder with filename -- don't concatenate
            // it's just wrong!
            var filePath = Path.Combine(destinationFolder, item.File.Name);

            // Erase existing files, cause that's how I roll
            if (System.IO.File.Exists(filePath))
            {
                System.IO.File.Delete(filePath);
            }

            // Create the file
            using (var fileStream = System.IO.File.Create(filePath))
            {
                fileInfo.Stream.CopyTo(fileStream);
            }
        }
    }
}

The "usings" at the top of the file were:

using System;
using System.Collections.Generic;
using System.Security;
using Microsoft.SharePoint.Client;
using System.IO;

And every time I ran the code, I'd get a really annoying error on the OpenBinaryDirect method:

this property cannot be set after writing has started.

If I wasn't already bald, I would be after searching everywhere how to solve it.

The Solution

As it turns out, when I created my console application, I followed these steps:

  1. Launch Visual Studio
  2. File | New Project... | Console Application and saved the project
  3. On the newly created project, added Microsoft.SharePoint.Client references by right-clicking on the project's References and selecting Manage Nuget Packages and selecting the first nuget reference that had Microsoft.SharePoint.Client that looked semi-official -- you know, the one that says "by Microsoft"

Wrote the code and quickly ran into the aforementioned error.

As it turns out, I needed to use the Nuget package that said Microsoft.SharePointOnline.CSOM (also by Microsoft).

I removed the Microsoft.SharePoint.Client Nuget package and added Microsoft.SharePointOnline.CSOM instead. It automatically included the right Microsoft.SharePoint.Client and Microsoft.SharePoint.Client.RunTime dependencies it needed.

After recompiling, it worked perfectly.

The way it should have done several hours ago.

After a lot of cursing, mostly directed at myself, I decided to write this down as a #NoteToSelf. Next time I run into this issue, at least I'll find a blog entry describing the solution.

My own.