Tag

SPFx

Browsing

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

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!

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

I was building a sample SPFx Timeline web part and needed a timeline to test it with.

To celebrate the release of the SPFx Framework v1.9, I created this timeline which highlights the history the SharePoint Framework since it first became available.

This is not official documentation. Any errors are mine and mine alone. If I have made any mistakes, please leave a comment below and I'll make sure to fix them.

The timeline below looks much better in full screen.

I hope you'll enjoy!

Open full screen

Updates

  • Aug 14: Added reference to SPFx 1.9.1!
  • July 26: Added entry to show that SPFx 1.9 package was pulled from NPM.

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

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

This article was written in collaboration with Eric Skaggs

Introduction

In today's SharePoint Dev Ecosystem (PnP) Bi-Weekly Call, Eric Skaggs asked a question I've heard many times before:

Do you have an example that shows how to go from a Javascript SPFx web part to a SPFx React web part?

(I'm paraphrasing)

I had been looking for an opportunity to write such as article, so I told Eric to DM me on Twitter to see if he had an example of a web part he'd like to convert.

As it turns out, Eric has such a web part: his GitHub Badge WebPart is a great example of a Javascript-only SPFx web part.

Eric's Javascript Only SPFx Web Part

Eric's SPFx web part works great, and there is really no need to convert it to React. We'll convert it to React simply to demonstrate the process and to highlight some of the design differences between a Javascript only web part and a React web part.

It is also important to point out that every developer has their own coding styles and preferred approaches that do not affect the final product. In this article, I'll try to point out where I applied my own coding style.

This article is written as if you're following along and converting the application yourself. Feel free to skip to the end to get the code and compare Eric's Javascript-only SPFx web part with my React version of the same web part.

Starting from scratch

The SPFx framework is constantly improving. In fact, the framework went from 1.8 to 1.8.1 two days ago!

Because of this, I like to make sure that I create a new solution every time I start converting a web part (I do this a lot, as it turns out).

Start by making sure that your environment is configured to create SPFx solutions. If you haven't done so yet, follow these steps to get you started.

If you were already set up, make sure to update your version of the Yeoman generator to the latest. To do so, use the following command from your Node.js command prompt:

npm install -g @microsoft/generator-sharepoint

Once this is completed, I followed the instructions from the SharePoint Framework documentation except, you know, with React GitHub Badge as the solution name:

  1. Create a new project directory in your favourite location.

    md react-github-badge

    Your web part solution does not need to start with react-. I just named it that way because it is the naming convention in the SP-Dev-Fx-WebParts repository

  2. Go to the new folder you created:

    md react-github-badge
  3. Create the React GitHub Badget web part by running the Yeoman SharePoint Generator.

    yo @microsoft/sharepoint
  4. When prompted:

    • Accept the default react-github-badge as your solution name, and then select Enter.
    • Select SharePoint Online only (latest), and select Enter.
    • Select Use the current folder for where to place the files.
    • Select N to allow the solution to be deployed to all sites immediately.
    • Select N on the question if solution contains unique permissions.
    • Select WebPart as the client-side component type to be created.
  5. The next set of prompts ask for specific information about your web part:

    • Enter GitHub Badge as your web part name, and then select Enter.
    • Enter Displays information from GitHub for a specified user as your web part description, and then select Enter.
    • For framework you would like to use, select React, and then select Enter.

The Yeoman generator in action

You'll know it has completed when you see the following message:

Success

Once it has completed, run:

gulp serve

to test your web part. (It should work). You should see something like this:
I created an SPFx solution and all I got was this lousy web part

It isn't pretty, but it's a start.

Fixing potential vulnerabilities

If you paid attention as the Yeoman generator created the solution, you may have noticed a nasty message like this one:

added 1759 packages from 1071 contributors and audited 565045 packages in 63.66s
found 1957 vulnerabilities (1806 low, 36 moderate, 115 high)
  run npm audit fix to fix them, or npm audit for details

Unfortunately, that's the nature of building solutions with open-source components.

I don't like it, so I typically run the following command to fix as many issues as possible:

npm audit fix

Once completed, you should see less scary exploits:
After npm audit fix

Run your web part again and make sure it still works:

gulp serve

Separating Web Part and Component

Eric's no-framework web part has all the code for retrieving a user's GitHub profile, managing web part properties, and rendering in the GitHubBadgeWebPart.ts. That's how it is done with "no-framework" web parts.

In SPFx React solutions, the web part will be broken into smaller components:

  • GitHubBadgeWebPart.ts: The Web Part, which is responsible for storing and retrieving web part properties, displaying the property pane, and calling components to render the web part.
  • GitHubBadge.tsx: The main component, which renders the content of the web part.

We're going to take Eric's code and move the content to GitHubBadge.tsx, and leave the web part code in GitHubBadgeWebPart.ts.

Adding a web part property to store the GitHub user name

  1. Using your favourite code editor, open GitHubBadgeWebPart.ts (located under src\webparts\gitHubBadge and find the following code that was generated by Yeoman:

    export interface IGitHubBadgeWebPartProps {
    description: string;
    }
  2. Since we don't need a description property for our web part, let's rename it to gitHubUserName. The code should look like this:

    export interface IGitHubBadgeWebPartProps {
    gitHubUserName: string;
    }

    If you use Visual Studio Code, simply place your cursor over description and hit F2. Type in gitHubUserName and hit Enter
    Using F2 to rename variables

  3. If you do not use Visual Studio Code, you should look for a line that says:

    description: this.properties.description

    and replace it for:

    description: this.properties.gitHubUserName
  4. Find the code inside the getPropertyPaneConfiguration function that looks like this:

    groupFields: [
     PropertyPaneTextField('description', {
         label: strings.DescriptionFieldLabel
     })
    ]

    and rename the description property to gitHubUserName. The code should look as follows:

    groupFields: [
    PropertyPaneTextField('gitHubUserName', {
        label: strings.DescriptionFieldLabel
    })
    ]
  5. Finally, let's rename the localized label for the DescriptionFieldLabel to GitHubUserNameFieldLabel by using the F2 method. Doing so will also rename the localized variable in src\webparts\gitHubBadge\loc\mystrings.d.ts. If you don't use Visual Studio Code, make sure to rename the DescriptionFieldLabel to GitHubUserNameFieldLabel.
    As a general rule, I always name the localized variable for all my properties as [PropertyName]FieldLabel. So, GitHubUserName becomes GitHubUserNameFieldLabel. Feel free to use your own naming convention.

  6. We'll also need to change the localized text! Go to src\webparts\gitHubBadge\loc\en-us.js and find the line that looks like this:

    "DescriptionFieldLabel": "Description Field"

    And change it to:

    "GitHubUserNameFieldLabel": "GitHub user name"

As always, run gulp serve to test your changes and make sure your web part didn't self-destruct.

If you click on your web part's edit button, you should see the following:
Web part property

So far, so good.

Your web part's manifest contains a section for pre-configured properties in case you want to provide default properties when users add the new web part to their page. Let's go make Eric's username the default GitHub username:

  1. Open src\webparts\gitHubBadge\GitHubBadgeWebPart.manifest.json and find the properties in the preconfiguredEntries section. Replace the following line:
    "description": "GitHub Badge"

    to this:

    "description": "skaggej"
  2. JSON files don't support comments, and Visual Studio Code will kindly remind you of that by showing the file in red. For bonus points, find all the comments (starting with \\) in that file and remove them. Visual Studio Code will reward you with a nice green file name instead.

When you change the manifest, you won't notice the difference until you stop and restart gulp serve, remove then re-add the web part to your page. I wasted a lot of time trying to debug this issue before I learned this the hard way.

Disabling reactive property changes

By default, SPFx web parts apply property changes as soon as you make them.

In this example, we don't want the web part to retrieve the GitHub user's profile until we're done entering the name.

We can do this by adding an Apply button to the property pane. To do so, open the Web Part's code at src\webparts\gitHubBadge\GitHubBadgeWebPart.ts and add the following code just above the getPropertyPaneConfiguration() function:

protected get disableReactivePropertyChanges(): boolean {
    return true;
}

When you refresh your web part, you'll get a nice Apply button at the bottom of your property pane.

Adding GitHubUserName property to the GitHubBadge props

Now that we've renamed the web part's Description property to GitHubUserName, we need to do the same to the property that gets passed into the GitHubPage component.

Typically, your React component will define a I[ComponentName]Props to store properties, and a I[ComponentName]State to store the component's state.

By default, the Yeoman generator will have created the IGitHubBadgeProps interface for you, which should be placed in the src\webparts\gitHubBadge\components\IGitHubBadgeProps.ts file.

Because I learned React from reading the Office UI Fabric code, and they already have awesome Coding Style, React and TypeScript guidelines, I tend to follow their standards.

In Office UI Fabric, they often group all types related to a component in a file called [ComponentName].types.ts.

You don't have to do this, but I prefer to do the same by storing both my I[ComponentName]Props and I[ComponentName]State interfaces in the same file called [ComponentName].types.ts.

In this case, I'll just rename the IGitHubBadgeProps.ts to GitHubBadge.types.ts by selecting the file in the Visual Studio Code explorer pane and hitting F2 then typing GitHubBadge.types.ts followed by Enter.

We'll add the IGitHubBadgetState interface later.

For now, though, let's open the newly renamed GitHubBadge.types.ts file and find the following line in the IGitHubBadgeProps interface:

  description: string;

and rename the description property to gitHubUserName by using the trusty F2 rename shortcut.

If all goes well, you'll notice that both src\webparts\gitHubBadge\GitHubBadgeWebPart.ts and src\webparts\gitHubBadge\components\GitHubBadge.tsx will update where they refer to the description property to point to the new gitHubUserName property.

Rendering static HTML

So far, I haven't used any of Eric's code.

That's about to change.

  1. Open:
  2. Find the following code in the render function and select it:
    <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.gitHubUserName)}</p>
            <a href="https://aka.ms/spfx" className={ styles.button }>
            <span className={ styles.label }>Learn more</span>
            </a>
        </div>
    </div>
    </div>
  3. Notice that the line that looks like:
    <div className={ styles.gitHubBadge }>

    and the last </div> isn't included in the selected code.

  4. Copy the following code from Eric's sample over the selected code:
    <div class="${ styles.container }">
          <div class="${ styles.row }">
            <div class="${ styles.column }">
              <div id="gitHubUserProfilePic"></div>
              <div id="gitHubUserName" class="${ styles.title }">${this.properties.gitHubUserName}</div>
              <div id="login" class="${ styles.label }"></div>
              <div id="id" class="${ styles.label }"></div>
              <div id="node_id" class="${ styles.label }"></div>
              <div id="avatar_url" class="${ styles.label }"></div>
              <div id="gravatar_id" class="${ styles.label }"></div>
              <div id="url" class="${ styles.label }"></div>
              <div id="html_url" class="${ styles.label }"></div>
              <div id="followers_url" class="${ styles.label }"></div>
              <div id="following_url" class="${ styles.label }"></div>
              <div id="gists_url" class="${ styles.label }"></div>
              <div id="starred_url" class="${ styles.label }"></div>
              <div id="subscriptions_url" class="${ styles.label }"></div>
              <div id="organizations_url" class="${ styles.label }"></div>
              <div id="repos_url" class="${ styles.label }"></div>
              <div id="events_url" class="${ styles.label }"></div>
              <div id="received_events_url" class="${ styles.label }"></div>
              <div id="type" class="${ styles.label }"></div>
              <div id="site_admin" class="${ styles.label }"></div>
              <div id="name" class="${ styles.label }"></div>
              <div id="company" class="${ styles.label }"></div>
              <div id="blog" class="${ styles.label }"></div>
              <div id="location" class="${ styles.label }"></div>
              <div id="email" class="${ styles.label }"></div>
              <div id="hireable" class="${ styles.label }"></div>
              <div id="bio" class="${ styles.label }"></div>
              <div id="public_repos" class="${ styles.label }"></div>
              <div id="public_gists" class="${ styles.label }"></div>
              <div id="followers" class="${ styles.label }"></div>
              <div id="following" class="${ styles.label }"></div>
              <div id="created_at" class="${ styles.label }"></div>
              <div id="updated_at" class="${ styles.label }"></div>
              <div id="notfound" class="${styles.label}"></div>
            </div>
          </div>
        </div>

    You will get some errors. Don't panic.

  5. React doesn't like it when you use the word class to define the CSS class name. It is a reserved word. Instead, you must use className. Luckily, you can replace all instances of the word class by using a trick I've described in my multi-cursor editing in Visual Studio Code article. Select the first instance of the word class= (including the = sign) and hit CTRL-SHIFT-L, then type className= instead, followed by the ESC key to stop multi-cursor editing. This should replace all instances of class to className.
  6. The keyword properties is also unique to the WebPart-derived classes. In React, the properties for a component are called props. Find the line that looks like this:
    <div id="gitHubUserName" className="${ styles.title }">${this.properties.gitHubUserName}</div>

    and replace it for this:

    <div id="gitHubUserName" className="${ styles.title }">{this.props.gitHubUserName}</div>

In Eric's code, he defines an ID for all the elements he wants to populate with data and dynamically inserts the text once he has retrieved it by calling every element by ID. It is a really efficient way to dynamically update web part content.

Although you technical can refer to HTML elements by IDs in React, it is rarely encouraged. One of the reasons for this is that if you add the web part twice on the same page, you'll get conflicts.

Instead, we'll later bind each element to the component's state, then populate the state when we receive the data from GitHub.

For now, let's just get rid of all those ID on every element.

Thankfully, you can do this by using multi-cursor editing!

  1. From the code, select the first instance of id=". Make sure to include the = and the double quotes ".
  2. Just like you did before hit CTRL-SHIFT-L to select all instances of the currently selected text. You should see that all instances of id=" got selected.
  3. Now hold SHIFT and CTRL and press the RIGHT arrow key. It should automatically select the word to the right of id=". (SHIFT means to extend the selection, while CTRL-RIGHT selects the next word).
  4. Hold SHIFT again (you can let go of CTRL) and hit the RIGHT arrow again. That should select the last double-quotes (") to the right of the text you've already selected.
  5. Hit BACKSPACE to delete the text you have selected. If you want, hit BACKSPACE once more to remove the extra space that is left after every <div.

Isn't multi-cursor editing cool?

TSX files in React TypeScript projects make it easy to combine HTML with React. To insert dynamic text as an HTML attribute, you just need to use { }. You don't even need the quotes around the attribute. This means that every instance of className="${ in Eric's former Javascript code can be simply replaced by className={.

You can do so by using multi-cursor editing again:

  1. In the code, find the first instance of "${ and select it.
  2. Hit CTRL-SHIFT-L to select all instances.
  3. Hit DEL to delete the selected text.
  4. Type { instead and hit ESC to stop multi-cursor editing.
  5. Go to the end of the className attribute, and select }"
  6. Hit CTRL-SHIFT-L to select all instances of }".
  7. Hit DEL and type } instead, followed by ESC.

The final render function should look like this:

    public render(): React.ReactElement<IGitHubBadgeProps> {
    return (
      <div className={ styles.gitHubBadge }>
        <div className={ styles.container }>
          <div className={ styles.row }>
            <div className={ styles.column }>
              <div></div>
              <div className={ styles.title }>{this.props.gitHubUserName}</div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={ styles.label }></div>
              <div className={styles.label}></div>
            </div>
          </div>
        </div>
      </div>
    );
  }

Finally, open replace the content of the src\webparts\gitHubBadge\components\GitHubBadge.module.scss with Eric's original SCSS:

@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';

.gitHubBadge {
  .container {
    max-width: 700px;
    margin: 0px auto;
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
  }

  .row {
    @include ms-Grid-row;
    @include ms-fontColor-white;
    background-color: $ms-color-themeDark;
    padding: 20px;
  }

  .column {
    @include ms-Grid-col;
    @include ms-lg10;
    @include ms-xl8;
    @include ms-xlPush2;
    @include ms-lgPush1;
  }

  .title {
    @include ms-font-xl;
    @include ms-fontColor-white;
  }

  .subTitle {
    @include ms-font-l;
    @include ms-fontColor-white;
  }

  .description {
    @include ms-font-l;
    @include ms-fontColor-white;
  }

  .button {
    // Our button
    text-decoration: none;
    height: 32px;

    // Primary Button
    min-width: 80px;
    background-color: $ms-color-themePrimary;
    border-color: $ms-color-themePrimary;
    color: $ms-color-white;

    // Basic Button
    outline: transparent;
    position: relative;
    font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
    -webkit-font-smoothing: antialiased;
    font-size: $ms-font-size-m;
    font-weight: $ms-font-weight-regular;
    border-width: 0;
    text-align: center;
    cursor: pointer;
    display: inline-block;
    padding: 0 16px;

    .label {
      font-weight: $ms-font-weight-semibold;
      font-size: $ms-font-size-m;
      height: 32px;
      line-height: 32px;
      margin: 0 4px;
      vertical-align: top;
      display: inline-block;
    }
  }
}

You deserve a reward! Run gulp serve again from your Node.js command prompt and refresh your web part. You should see something that looks like this:
Static HTML Preview

We're getting there!

Let's retrieve the data next!

Creating an IGitHubServices interface

Eric's example keeps things simple by putting the code to retrieve the user's GitHub profile in the web part class.

However, React solutions benefit from breaking things into smaller components with a clear division of responsibilities.

For example, the code that calls the GitHub API to retrieve the GitHub user's profile can be separated from the code that is responsible for rendering the profile information.

This is done because React makes it easy to create individualized components that do specific things. By keeping the code that retrieves the data separate from the code that renders the data, we could re-use the GitHub profile component in different ways.

It also makes it easier to create unit tests and mock services without having to change your GitHub profile component.

It doesn't make the React code better than the no-framework code. It's just a different approach.

Since the purpose of this example is to demonstrate converting a no-framework web part to a React web part, I'll show you the extra steps of creating a separate IGitHubService interface, with a mock service and a real service.

  1. First, let's create a new folder called services under the src folder. It should be at the same level as the webparts folder.

You may find other examples that place their services under the web part folder for the web part that calls it, but I like to design my services so that they can be used by more than one web part -- hence placing it at the same level as the webparts folder. Feel free to place it where you prefer.

  1. Under the src\services folder, create another folder called GitHubServices.

  2. In the src\services\GitHubServices folder, create a new file called GitHubServices.types.ts

  3. Let's create an interface that represents the returned data from GitHub APIs. From your browser, visit https://api.github.com/users/skaggej. It will return the JSON for Eric's profile.

  4. Copy the content of the JSON. It should look like this:

    {
    "login": "skaggej",
    "id": 1846656,
    "node_id": "MDQ6VXNlcjE4NDY2NTY=",
    "avatar_url": "https://avatars1.githubusercontent.com/u/1846656?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/skaggej",
    "html_url": "https://github.com/skaggej",
    "followers_url": "https://api.github.com/users/skaggej/followers",
    "following_url": "https://api.github.com/users/skaggej/following{/other_user}",
    "gists_url": "https://api.github.com/users/skaggej/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/skaggej/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/skaggej/subscriptions",
    "organizations_url": "https://api.github.com/users/skaggej/orgs",
    "repos_url": "https://api.github.com/users/skaggej/repos",
    "events_url": "https://api.github.com/users/skaggej/events{/privacy}",
    "received_events_url": "https://api.github.com/users/skaggej/received_events",
    "type": "User",
    "site_admin": false,
    "name": "Eric Skaggs",
    "company": "http://www.catapultsystems.com",
    "blog": "http://www.ericskaggs.net",
    "location": "Phoenix, AZ",
    "email": null,
    "hireable": null,
    "bio": "Fuse Solution Architect at Catapult Systems",
    "public_repos": 29,
    "public_gists": 3,
    "followers": 8,
    "following": 33,
    "created_at": "2012-06-13T14:01:52Z",
    "updated_at": "2019-04-09T00:18:35Z"
    }

    (don't worry too much if there are minor differences)

  5. If you don't have the awesome JSON to TS extension for Visual Studio, go install it now. It will allow us to convert the JSON you just copied into a TypeScript interface. (Did I mention I'm the world's laziest developer?)

  6. Place your cursor in the GitHubServices.types.ts file. We're about to insert some code in there.

  7. With JSON to TS installed, hit F1 to launch the Visual Studio Code command line and start typing JSON to TS. You should see **JSON to TS: Convert from clipboard. Select it and be patient. The extension will insert a TypeScript Interface called RootObject` that contains a property for every attribute returned by the GitHub API. The code should look like this:

    interface RootObject {
    login: string;
    id: number;
    node_id: string;
    avatar_url: string;
    gravatar_id: string;
    url: string;
    html_url: string;
    followers_url: string;
    following_url: string;
    gists_url: string;
    starred_url: string;
    subscriptions_url: string;
    organizations_url: string;
    repos_url: string;
    events_url: string;
    received_events_url: string;
    type: string;
    site_admin: boolean;
    name: string;
    company: string;
    blog: string;
    location: string;
    email?: any;
    hireable?: any;
    bio: string;
    public_repos: number;
    public_gists: number;
    followers: number;
    following: number;
    created_at: string;
    updated_at: string;
    }
  8. Rename the RootObject to IGitHubUserProfile and export the interface so that it can be accessed in other files. The code should look like this:

    export interface IGitHubUserProfile {
    login: string;
    id: number;
    node_id: string;
    avatar_url: string;
    gravatar_id: string;
    url: string;
    html_url: string;
    followers_url: string;
    following_url: string;
    gists_url: string;
    starred_url: string;
    subscriptions_url: string;
    organizations_url: string;
    repos_url: string;
    events_url: string;
    received_events_url: string;
    type: string;
    site_admin: boolean;
    name: string;
    company: string;
    blog: string;
    location: string;
    email?: any;
    hireable?: any;
    bio: string;
    public_repos: number;
    public_gists: number;
    followers: number;
    following: number;
    created_at: string;
    updated_at: string;
    }

    We use the convention I[Something] to indicate that this is an interface, not a class. That way, we can take any TypeScript object that implements the properties defined in the interface (login, id, node_id, avatar_url, etc.) -- regardless of how it was created. That way, we'll be able to convert the JSON we retrieved from the GitHub API to the IGitHubUserProfile interface and pass it around.

  9. We'll add an IGitHubService interface to the same file. The interface will implement one method called getUserProfile that receives an alias string and returns an asynchronous promise of an IGitHubUserProfile. Just paste the following code below the IGitHubUserProfile:

    export interface IGitHubService {
    getUserProfile(alias: string): Promise<IGitHubUserProfile>;
    }

    Again, we use an interface so that we can later have a MockGitHubService and a (real) GitHubService that both implement the IGitHubService. Our component won't care whether it is using a real service or a mock service, because it will expect an object that implements IGitHubService.

For your reference, the entire content of GitHubServices.types.ts should look like this:

export interface IGitHubUserProfile {
  login: string;
  id: number;
  node_id: string;
  avatar_url: string;
  gravatar_id: string;
  url: string;
  html_url: string;
  followers_url: string;
  following_url: string;
  gists_url: string;
  starred_url: string;
  subscriptions_url: string;
  organizations_url: string;
  repos_url: string;
  events_url: string;
  received_events_url: string;
  type: string;
  site_admin: boolean;
  name: string;
  company: string;
  blog: string;
  location: string;
  email?: any;
  hireable?: any;
  bio: string;
  public_repos: number;
  public_gists: number;
  followers: number;
  following: number;
  created_at: string;
  updated_at: string;
}

export interface IGitHubService {
  getUserProfile(alias: string): Promise<IGitHubUserProfile>;
}

Now let's create the mock service!

Creating MockGitHubService

We'll use a mock service to return test data so that we don't have to worry about getting blocked by GitHub for calling the API too many times when we're testing the look and feel of the web part. The mock service will be interchangeable with the real service at a later time.

  1. In the src\services\GitHubServices folder, create a new file called MockGitHubService
  2. In the now empty file, paste the following code:
    import { IGitHubService, IGitHubUserProfile } from "./GitHubServices.types";
    export class MockGitHubService implements IGitHubService {
    public getUserProfile(alias: string): Promise<IGitHubUserProfile> {
    // This space for rent
    }
    }

The import { IGitHubService, IGitHubUserProfile } from "./GitHubServices.types"; line tells the TypeScript transpiler that those two interfaces are located in another file in the same folder.

The export class MockGitHubService implements IGitHubService says that this class (MockGitHubService) will do (or implements) everything the IGitHubService interface does, and that it should be available outside of this file by other files (or exported).

Let's add some code to return sample data after simulating some delays.

  1. In the code, replace the line that says // This space for rent with the following code:
    return new Promise<IGitHubUserProfile>((resolve) => {
      // pretend we're getting the data from the GitHub API by adding a delay
      setTimeout(() => {
        const fakeProfile: IGitHubUserProfile = {
          login: "skaggej",
          id: 1846656,
          node_id: "MDQ6VXNlcjE4NDY2NTY=",
          avatar_url: "https://avatars1.githubusercontent.com/u/1846656?v=4",
          gravatar_id: "",
          url: "https://api.github.com/users/skaggej",
          html_url: "https://github.com/skaggej",
          followers_url: "https://api.github.com/users/skaggej/followers",
          following_url: "https://api.github.com/users/skaggej/following{/other_user}",
          gists_url: "https://api.github.com/users/skaggej/gists{/gist_id}",
          starred_url: "https://api.github.com/users/skaggej/starred{/owner}{/repo}",
          subscriptions_url: "https://api.github.com/users/skaggej/subscriptions",
          organizations_url: "https://api.github.com/users/skaggej/orgs",
          repos_url: "https://api.github.com/users/skaggej/repos",
          events_url: "https://api.github.com/users/skaggej/events{/privacy}",
          received_events_url: "https://api.github.com/users/skaggej/received_events",
          type: "User",
          site_admin: false,
          name: "Eric Skaggs",
          company: "http://www.catapultsystems.com",
          blog: "http://www.ericskaggs.net",
          location: "Phoenix, AZ",
          email: null,
          hireable: null,
          bio: "Fuse Solution Architect at Catapult Systems",
          public_repos: 29,
          public_gists: 3,
          followers: 8,
          following: 33,
          created_at: "2012-06-13T14:01:52Z",
          updated_at: "2019-04-09T00:18:35Z"
        };
        resolve(fakeProfile);
      }, 500);
    });

The entire content of the MockGitHubService.ts file should be as follows:

import { IGitHubService, IGitHubUserProfile } from "./GitHubServices.types";

export class MockGitHubService implements IGitHubService {
  public getUserProfile(alias: string): Promise<IGitHubUserProfile> {

    return new Promise<IGitHubUserProfile>((resolve) => {
      // pretend we're getting the data from the GitHub API by adding a delay
      setTimeout(() => {
        const fakeProfile: IGitHubUserProfile = {
          login: "skaggej",
          id: 1846656,
          node_id: "MDQ6VXNlcjE4NDY2NTY=",
          avatar_url: "https://avatars1.githubusercontent.com/u/1846656?v=4",
          gravatar_id: "",
          url: "https://api.github.com/users/skaggej",
          html_url: "https://github.com/skaggej",
          followers_url: "https://api.github.com/users/skaggej/followers",
          following_url: "https://api.github.com/users/skaggej/following{/other_user}",
          gists_url: "https://api.github.com/users/skaggej/gists{/gist_id}",
          starred_url: "https://api.github.com/users/skaggej/starred{/owner}{/repo}",
          subscriptions_url: "https://api.github.com/users/skaggej/subscriptions",
          organizations_url: "https://api.github.com/users/skaggej/orgs",
          repos_url: "https://api.github.com/users/skaggej/repos",
          events_url: "https://api.github.com/users/skaggej/events{/privacy}",
          received_events_url: "https://api.github.com/users/skaggej/received_events",
          type: "User",
          site_admin: false,
          name: "Eric Skaggs",
          company: "http://www.catapultsystems.com",
          blog: "http://www.ericskaggs.net",
          location: "Phoenix, AZ",
          email: null,
          hireable: null,
          bio: "Fuse Solution Architect at Catapult Systems",
          public_repos: 29,
          public_gists: 3,
          followers: 8,
          following: 33,
          created_at: "2012-06-13T14:01:52Z",
          updated_at: "2019-04-09T00:18:35Z"
        };
        resolve(fakeProfile);
      }, 500);
    });
  }
}

Adding an index.ts file to GitHubServices

Now that we've defined some exports in our GitHubServices, we'll want to make it easy for the component to use them.

The problem is that if we want to import our IGitHubService, IGitHubUserProfile and MockGitHubService in our GitHubBadge component, we'll have to import each item from the files that contain them, like this:

import { IGitHubService, IGitHubUserProfile } from '../../../services/GitHubServices/GitHubServices.types';
import { MockGitHubService } from '../../../services/GitHubServices/MockGitHubService';

However, if we ever decide to move the various elements of the GitHubServices to different files, we'll have to update all the import statements in all the components that use the services.

But why should the components know about the internal structure of the GitHubServices? Wouldn't it be better to abstract all that stuff from the components?

Luckily, we can use index.ts to do just that!

  1. In the src\services\GitHubServices folder, add a new file called index.ts.
  2. In the index.ts file, export all the things that you want the components to have access to, as follows:
    export * from './GitHubServices.types';
    export * from './MockGitHubService';

Now we can just add the following line in our src\webparts\gitHubBadge\components\GitHubBadge.tsx file to import everything we need:

import { IGitHubService, IGitHubUserProfile, MockGitHubService } from '../../../services/GitHubServices';

Adding some state to GitHubBadge

State is a funny concept in React.

It allows us to temporarily capture the information we need to support the different "ways" we want our component can be in (I'm really trying hard not to use the word state here).

For example, our GitHubBadge component potentially has 5 states:

  • Not configured
  • Loading
  • Loaded with data
  • Error because the user was Not found
  • Error while calling GitHub API (network, throttling, etc.)

This is something that's represented as follows:

Title: GitHubBadge States
Not configured->Loading: When user sets web part properties
Loading->Loaded: Normal scenario
Loading-->Not found: Invalid user name
Loading-->Error: Exception with GitHub API

I promised Eric I'd keep this simple, so I'll ignore the Not configured and we'll combine the Not found and Error states for now. (I can't promise I won't come back to this in a later article though).

To represent these states in out GitHubBadge component, we'll use the following variables:

  • isLoading: a boolean that will be set to true when the web part loads.
  • userProfile: a IGitHubUserProfile variable that can be set to undefined (if there is no data to show)
  • errorMessage: a string containing an error message that can also be set to undefined if nothing went wrong.

If isLoading is false, it will mean that the service call is complete. If errorMessage contains a message, it means there was an error. Otherwise, if userProfile contains data, it means that we received our data and that we want to show it.

Let's start implementing this by creating an IGitHubBadgeState interface in our src\webparts\gitHubBadge\components\GitHubBadge.types.ts file:

  1. Open the GitHubBadge.types.ts file
  2. Add the following code below the IGitHubBadgeProps interface:
    export interface IGitHubBadgeState {
    isLoading: boolean;
    userProfile?: IGitHubUserProfile;
    errorMessage?: string;
    }
  3. Make sure to add the following line at the top of the file:
    import { IGitHubUserProfile } from "../../../services/GitHubServices";

The ? at the end of the variable names means that the variables can be nullable.

Now let's use the state in our component!

Adding state to the GitHubBadge component

Now we're finally getting somewhere!

Let's start by telling the GitHubBadge that is has a state:

  1. Open the src\webparts\gitHubBadge\components\GitHubBadge.tsx file

  2. At the top of the file, replace the following line:

    import { IGitHubBadgeProps } from './GitHubBadge.types';

    for this:

    import { IGitHubBadgeProps, IGitHubBadgeState } from './GitHubBadge.types';
  3. Replace the following line:

    export default class GitHubBadge extends React.Component<IGitHubBadgeProps, {}> {

    with this:

    export default class GitHubBadge extends React.Component<IGitHubBadgeProps, IGitHubBadgeState> {

    This tells the GitHubBadge class that it should use IGitHubBadgeProps for its properties, and IGitHubBadgeState for its state.

  4. Add a constructor to define a default state by adding the following code below the code you just changed, and above the public render() method:

    constructor(props:IGitHubBadgeProps) {
    super(props);
    
    this.state = {
      isLoading: true
    };
    }

    Note that the constructor is the only time you can change the state directly by using this.state = . Everywhere else, you'll only be able to use this.setState().

  5. For now, let's some conditional rendering logic in the render method so that when the web part is loading (i.e.: isLoading equals true), we'll write "Loading..." in the top of the web part. Replace the following line (just below <div className={ styles.column }>:

<div></div>

with this:

<div>{ this.state.isLoading && "Loading..." }</div>

If you try to use gulp serve now, you'll notice that the web part always displays "Loading..." because the isLoading state variable is set to true at in the constructor and we never change that.

Loading

But we'll fix that right now...

Loading and displaying mock data

React applications typically try to be responsive (as in "fast") by avoiding any delays in rendering the components.

It is better to render a "Loading..." web part and immediately change it to show the data that's you just retrieved than not rendering anything until the data has returned.

To achieve this, we'll call the getUserProfile method from the MockGitHubService after the GitHubBadge is mounted. Once the MockGitHubService returns data, we'll call this.setState() and set isLoading to false and populate the userProfile state variable with whatever data we received.

Calling this.setState will automatically trigger any elements that are bound to state variables on the component to re-render.

To the code!

  1. In src\webparts\gitHubBadge\components\GitHubBadge.tsx, add a method called componentDidMount above the render method, as follows:

    public componentDidMount(): void {
    
    }

    (It will still work if you put the code after the render method, I suggest where to put it in the code so that your code looks like mine once completed).

  2. Add the following code inside the componentDidMount function:

    // Create an instance of the GitHub service
    const service: IGitHubService = new MockGitHubService();
    
    // Call the GitHub service
    // In real-life, we would only call it when we're sure that there is a username
    service.getUserProfile(this.props.gitHubUserName).then((results: IGitHubUserProfile)=>{
      // Set the userProfile with the results we got and isLoading to false, because we're done
      // loading. It'll make things redraw magically.
      this.setState({
        userProfile: results,
        isLoading: false
      });
    });
  3. Replace the entire render function with the following code. Don't worry, I'll explain shortly:

    public render(): React.ReactElement<IGitHubBadgeProps> {
    const { userProfile, isLoading, errorMessage } = this.state;
    
    return (
      <div className={ styles.gitHubBadge }>
        <div className={ styles.container }>
          <div className={ styles.row }>
          { isLoading &&
            <div className={ styles.column }>
              <div>Loading...</div>
            </div>
          }
          { !isLoading && userProfile &&
            <div className={ styles.column }>
              <div><img src={userProfile.avatar_url} alt="GitHub User Profile Picture" /></div>
              <div className={ styles.title }>{this.props.gitHubUserName}</div>
              <div className={ styles.label }>{ userProfile.login}</div>
              <div className={ styles.label }>{userProfile.id}</div>
              <div className={ styles.label }>{userProfile.node_id}</div>
              <div className={ styles.label }>{userProfile.avatar_url}</div>
              <div className={ styles.label }>{userProfile.gravatar_id}</div>
              <div className={ styles.label }>{userProfile.url}</div>
              <div className={ styles.label }>{userProfile.html_url}</div>
              <div className={ styles.label }>{userProfile.followers_url}</div>
              <div className={ styles.label }>{userProfile.following_url}</div>
              <div className={ styles.label }>{userProfile.gists_url}</div>
              <div className={ styles.label }>{userProfile.starred_url}</div>
              <div className={ styles.label }>{userProfile.subscriptions_url}</div>
              <div className={ styles.label }>{userProfile.organizations_url}</div>
              <div className={ styles.label }>{userProfile.repos_url}</div>
              <div className={ styles.label }>{userProfile.events_url}</div>
              <div className={ styles.label }>{userProfile.received_events_url}</div>
              <div className={ styles.label }>{userProfile.type}</div>
              <div className={ styles.label }>{userProfile.site_admin}</div>
              <div className={ styles.label }>{userProfile.name}</div>
              <div className={ styles.label }>{userProfile.name}</div>
              <div className={ styles.label }>{userProfile.company}</div>
              <div className={ styles.label }>{userProfile.location}</div>
              <div className={ styles.label }>{userProfile.email}</div>
              <div className={ styles.label }>{userProfile.hireable}</div>
              <div className={ styles.label }>{userProfile.bio}</div>
              <div className={ styles.label }>{userProfile.public_repos}</div>
              <div className={ styles.label }>{userProfile.public_gists}</div>
              <div className={ styles.label }>{userProfile.followers}</div>
              <div className={ styles.label }>{userProfile.following}</div>
              <div className={ styles.label }>{userProfile.created_at}</div>
              <div className={ styles.label }>{userProfile.updated_at}</div>
            </div>
          }
          { !isLoading && errorMessage &&
            <div className={ styles.column }>
    <div className={styles.label}>WARNING - error when calling URL https://api.github.com/users/{this.props.gitHubUserName}. Error = {errorMessage}</div>
            </div>
          }
          </div>
        </div>
      </div>
    );
    }

Now save your code and treat yourself to a gulp serve. Refresh your web part and you should see the web part say Loading... for half a second, then load Eric's profile.

"But that was a lot of weird code you just introduced!", you'll say. I know! Let me walk you through it.

The first line in the render function:

const { userProfile, isLoading, errorMessage } = this.state;

defines a "shortcut" to the state variables isLoading, userProfile, and errorMessage. That way, in the rest of the code, we don't have to say this.state.userProfile, we can simply use userProfile.

The first section:

{ isLoading &&
            <div className={ styles.column }>
              <div>Loading...</div>
            </div>
          }

says: "if isLoading is true, render the HTML between { and }".

Similarly, this line:

{ !isLoading && userProfile &&

says "If isLoading is not true, and there is a userProfile render the HTML between the {}.

Guess what:

{ !isLoading && errorMessage &&

does? It only renders the HTML between the {} if the web part is done loading and there is an error message.

Everywhere else in that function uses {} to bind to a state or prop. For example:

<div><img src={userProfile.avatar_url} alt="GitHub User Profile Picture" /></div>

Renders an image that binds the src attribute to the avatar_url attribute of the userProfile state variable, while:

<div className={ styles.label }>{userProfile.login}</div>

Insert the value of the login attribute of the userProfile state variable inside the <div/> element.

Everywhere else in the render function works the same way.

We're almost done! We just need to retrieve the real data by passing the HTTP context and implementing the GitHubService to use it to call the real GitHub API.

Passing HTTP Context

In order to make HTTP requests, the component needs to use the HttpClient object exposed by the web part.

That means that the GitHubBadge component needs to add an HttpClient variable to its IGitHubBadgeProps interface.

Let's do this:

  1. Open src\webparts\gitHubBadge\components\GitHubBadge.types.ts
  2. At the top, import HttpClient from '@microsoft/sp-http', as follows:
    import { HttpClient } from '@microsoft/sp-http';
  3. Add a prop variable that will store the HttpClient to the IGitHubBadgeProps. The GitHubBadge.types.ts file will look as follows:
    
    import { IGitHubUserProfile } from "../../../services/GitHubServices";
    import { HttpClient } from '@microsoft/sp-http';

export interface IGitHubBadgeProps {
gitHubUserName: string;
httpClient: HttpClient;
}

export interface IGitHubBadgeState {
isLoading: boolean;
userProfile?: IGitHubUserProfile;
errorMessage?: string;
}

4. In the src\webparts\gitHubBadge\GitHubBadgeWebPart.ts web part, pass the new prop to the GitHubBadge component by changing the render as follows:
```TypeScript
public render(): void {
    const element: React.ReactElement<IGitHubBadgeProps> = React.createElement(
      GitHubBadge,
      {
        gitHubUserName: this.properties.gitHubUserName,
        httpClient: this.context.httpClient
      }
    );

    ReactDom.render(element, this.domElement);
  }</code></pre>
<blockquote>
<p>You may have seen some samples that pass the entire web part context to  the components (including some of my samples).  Waldek has a <a href="https://blog.mastykarz.nl/dont-pass-web-part-context-react-components/">great article</a> that explains why you shouldn't. In this example, we choose to pass the <code>HttpClient</code> object from the web part's <code>context</code> instead of passing the entire <code>context</code>.
As a general rule, always listen to Waldek :-)</p>
</blockquote>
<h2>Creating the GitHubService</h2>
<p>The moment of truth!</p>
<ol>
<li>Go to the <code>src\services\GitHubServices</code> folder and add a file called <code>GitHubService.ts</code></li>
<li>At the top of the file, add the following imports:
<pre><code class="language-TypeScript">import { IGitHubService, IGitHubUserProfile } from "./GitHubServices.types";
import { HttpClient, HttpClientResponse } from '@microsoft/sp-http';</code></pre></li>
<li>Create the <code>GitHubService</code> class that implements the <code>IGitHubService</code> interface:
<pre><code class="language-TypeScript">
export class GitHubService implements IGitHubService {</code></pre></li>
</ol>
<p>}</p>
<pre><code>4. Add a <code>private</code> variable of type <code>HttpClient</code> that will be used to store the HTTP client object passed into the GitHubService:
```TypeScript
export class GitHubService implements IGitHubService {
  private _httpClient: HttpClient;

}
  1. Add a constructor that receives the HttpClient object and stores it to the private variable:
    constructor(httpClient: HttpClient) {
    this._httpClient = httpClient;
    }
  2. Finally, implement the getUserProfile method that calls the GitHub API using the HttpClient

    public getUserProfile(alias: string): Promise<IGitHubUserProfile> {
    const gitHubUrl: string = "https://api.github.com/users/"+alias;
    
    // call the GitHub API
    return this._httpClient.get(gitHubUrl,
      HttpClient.configurations.v1, {}).then((response: HttpClientResponse) => response.json())
      .then((profile: IGitHubUserProfile) => {
        return profile;
      });
    }

    The code almost seems magical: it calls the API and converts the received JSON to an IGitHubUserProfile interface automatically.

The final GitHubService code looks like this:

import { IGitHubService, IGitHubUserProfile } from "./GitHubServices.types";
import { HttpClient, HttpClientResponse } from '@microsoft/sp-http';

export class GitHubService implements IGitHubService {
  private _httpClient: HttpClient;

  constructor(httpClient: HttpClient) {
    this._httpClient = httpClient;
  }

  public getUserProfile(alias: string): Promise<IGitHubUserProfile> {
    const gitHubUrl: string = "https://api.github.com/users/"+alias;

    // call the GitHub API
    return this._httpClient.get(gitHubUrl,
      HttpClient.configurations.v1, {}).then((response: HttpClientResponse) => response.json())
      .then((profile: IGitHubUserProfile) => {
        return profile;
      });
  }
}

To allow the GitHubBadge component to access the GitHubService, we need to add GitHubService to the src\services\GitHubServices\index.ts, making the entire index.ts as follows:

export * from './GitHubServices.types';
export * from './MockGitHubService';
export * from './GitHubService';

Note that unlike the MockGitHubService, the GitHubService needs the HttpClient to work. Because the getUserProfile function is defined in the IGitHubService interface, we can't change the function to pass the HttpClient when we need it.

However, we can change the constructor of the GitHubService to accept the HttpClient object we need.

Calling the GitHubService

To change the code in the GitHubBadge component to use the GitHubService, we simply need to change the componentDidMount by following these steps:

  1. Open the src\webparts\gitHubBadge\components\GitHubBadge.tsx file
  2. Change the import statement at the top to include GitHubService:
    import { IGitHubService, IGitHubUserProfile, MockGitHubService, GitHubService } from '../../../services/GitHubServices';
  3. In the componentDidMount function, comment out this line:
    const service: IGitHubService = new MockGitHubService();

    and add the following line just below:

    const service: IGitHubService = new GitHubService(this.props.httpClient);

Nothing else needs to change.

Run gulp serve and try the web part. The data will be really coming from GitHub.

However, if you try to change the user name property and click Apply, you won't see any changes unless you refresh the page.

We can fix that.

Responding to changing props

While React is happy to automatically redraw the components when their state changes, our component only changes the state once it receives the data from the GitHubService.

And the GitHubService is only called once after the component is mounted.

To call the GitHubService when the component is mounted and when the gitHubUserName prop changes, we need to move some code around.

To do so:

  1. In the src\webparts\gitHubBadge\components\GitHubBadge.tsx file, add a private function called getUserProfile that calls the web service:

    private getUserProfile()  {
    // Create an instance of the GitHub service
    //const service: IGitHubService = new MockGitHubService();
    const service: IGitHubService = new GitHubService(this.props.httpClient);
    
    // Call the GitHub service
    // In real-life, we would only call it when we're sure that there is a username
    service.getUserProfile(this.props.gitHubUserName).then((results: IGitHubUserProfile)=>{
      // Set the userProfile with the results we got and isLoading to false, because we're done
      // loading. It'll make things redraw magically.
      this.setState({
        userProfile: results,
        isLoading: false
      });
    });
    }
  2. Change the componentDidMount function to call the getUserProfile private function:
    public componentDidMount(): void {
    this.getUserProfile();
    }
  3. Add a componentDidUpdate that will compare if the previous props are different than the current props and will call the private getUserProfile function if it is different:
    public componentDidUpdate(prevProps: IGitHubBadgeProps, prevState: IGitHubBadgeState): void {
    if (prevProps.gitHubUserName !== this.props.gitHubUserName) {
      this.getUserProfile();
    }
    }

componentDidUpdate gets triggered any time the component's state or props change. In this case, we use it to compare gitHubUserName and react accordingly.

Try your changes now using gulp serve and see it all work when you update the GitHub user name.

The final result looks like this:
file

Conclusion

In this article, we took the GitHub Badge WebPart that Eric Skaggs wrote using only Javascript (no framework!) and converted it to a React web part that does the same thing.

We took a few detours on the way to convey some different concepts in React, but the result is mostly the same.

There are still a few things the sample web part should do:

  • Add error handler
  • Add a "Loading..." spinner
  • Add a placeholder when the web part isn't configured
  • Cache the results to avoid getting throttled by GitHub for making too many calls.
    ...but this article is already long enough.

You can find the entire solution on my GitHub repo.

Thanks again to Eric for writing such an awesome sample web part. I hope that I did your sample justice with the React version.

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?

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. 8.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

  • 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

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

I hate acronyms.

Should have thought of that before getting into IT for a living!

One of the most annoying acronyms, to me, is CORS. It is annoying because it shows up in an error message when you're trying to make an HTTP request to a URL external to SharePoint.

It may be hard to diagnose if you don't handle your HTTP request rejections, or if you don't have your developer tools enabled in your browser, but when you do, you'll get an error message that looks like this:

workbench.html:1 Access to fetch at &#039;https://somecoolexternalapi.com/api&#039; from origin 
&#039;https://localhost:4321&#039; has been blocked by CORS policy: Response to preflight request doesn&#039;t 
pass access control check: It does not have HTTP ok status.

This article will explain what CORS is, and how to avoid issues with CORS when making HTTP requests to an external resource.

What is CORS?

NOTE: I'm over-simplifying the explanation and definition of CORS. If you want the real definition, go look at Wikipedia. Just don't scream at me for being slightly inaccurate, ok? 🙂

CORS stands for Cross-origin resource sharing. It is a way to control how stuff from one web sites (like images, CSS, scripts, and even APIs) is shared with other web sites.

When it isn't busy ruining your day, CORS can be useful because it allows you to prevent people from pointing to your web site to steal resources from it (while causing extra traffic). Or worse.

It usually works by looking at the domain where the request originates from (e.g.: mytenant.sharepoint.com) and comparing against the domain where the resource sites (e.g.: mycoolapi.com). If the two domains aren't the same, it is a cross-domain request or -- in CORS terms -- a cross-origin request.

While you can do some CORS validation on the server-side (that's another blog), it is usually enforced by your browser. In fact, the CORS standards request that any requests that potentially change data (like an API call) should be pre-validated by your browser before even requesting the resource. That pre-verification is called preflight.

It goes a little something like this:

CLIENT-SIDE COMPONENT: "Hey browser, please call this API from https://somecoolapi.com
BROWSER: "Sure thing. Lemme ask.". "Hmm, somecoolapi.com is a different domain than mytenant.sharepoint.com, where we are now. I should check first"; calls somecoolapi.com.
WEBSITE: "New domain, who dis?"
BROWSER: "Hey, someone from origin: mytenant.sharepoint.com would like to get access to your API. You can find out all about it in my OPTIONS HTTP headers."
WEBSITE: "Sure, I don't see any reasons why you shouldn't be allowed. Here, let me give you some Access-Control-Allow-Origin headers to confirm I'm ok with it. Just make sure you only GET stuff, no POST or DELETEs, ok?".
WEBSITE: "Awesome!"; Turns to user, "Good news! somecoolapi.com said they'll do it!".
WEBSITE: Makes request. Gets results. Returns results to user.
They lived happily ever after.
The End.

Come to think of it, that's exactly how I handle phone calls; If call display is blocked, or it is a number I don't know, I let it go to voicemail. If it is my wife, I answer. She then asks me to buy more Nespresso coffee on the way home. I usually accept the request, because standing between my wife and coffee is like standing between a mother bear and her cub: dangerous.

So, CORS may be annoying, but it is useful.

The problem is that when you make requests to another domain in a SPFx web part using SPHttpClient, you're making a request from mytenant.sharepoint.com. It usually triggers a CORS error.

To make things worse, when you search for the error, you usually get tons of results on how to change the server settings to prevent the issue. Nothing on how to solve it in your client-side web part.

How to solve CORS issues with SPHttpClient

SPHttpClient, included in strong>@microsoft/sp-http</strong, make it easy to make HTTP requests using the current web part's context.

To access it from your component or service, you need to get the web part's WebPartContext -- I usually pass it into my component's props, like this:

import { WebPartContext } from &quot;@microsoft/sp-webpart-base&quot;;
export interface IMyCustomComponent {
   context: WebPartContext;
}

Once you have the WebPartContext you can make the Http request using SPHttpClient, usually something like this:

import { SPHttpClient, SPHttpClientResponse} from &#039;@microsoft/sp-http&#039;;

…
/* When ready to make request */
return this.props.context.spHttpClient.get(yourApiUrl, SPHttpClient.configuration.v1)
.then((apiResponse: SPHttpClientResponse) =&gt; apiResponse.json()
.then(…) /* Handle the results */

...which is usually when you get the CORS issue.

To avoid the CORS issue, you need to make sure that your request meets the following requirements:

  • No custom HTTP headers such as 'application/xml' or 'application/json'
  • Request method has to be GET, HEAD, or POST.
  • If method is POST, content type should be 'application/x-www-form-urlencoded', 'multipart/form-data', or 'text/plain'

However, SPHttpClient tries to be nice and sets a custom OPTIONS HTTP header for you by default.

In order to override the OPTIONS header in your SPHttpClient request, just pass a new/clean IHttpClientOptions parameter, as follows:

import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from &#039;@microsoft/sp-http&#039;;

…
/* When ready to make request */
const myOptions: ISPHttpClientOptions = {
      headers: new Headers(),
      method: &quot;GET&quot;,
      mode: &quot;cors&quot;
    };

return this.props.context.spHttpClient.get(yourApiUrl, SPHttpClient.configuration.v1, myOptions)
.then((apiResponse: SPHttpClientResponse) =&gt; apiResponse.json()
.then(…) /* Handle the results */

And that should be it.

Conclusion

CORS can be scary, it can be annoying, but it is a good thing.

You can avoid CORS issues when using SPHttpClient in your SPFx component by passing a ISPHttpClientOptions that doesn't set custom options.

I only covered how to make GET requests in the code above. You can use a similar approach for HEAD and POST requests.

This approach won't always work (for example, if the API you're calling requires custom HTTP headers), but it should solve most other CORS issues.

And if you have any more questions, post a comment, e-mail, or text. Don't call 🙂

I hope it helps?

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!

Introduction

A week ago, Microsoft officially released the SharePoint Framework Package v1.5, introducing new awesome features like the Developer Preview of Dynamic Data and the ability to create solutions with beta features by adding --plusbeta to the Yeoman command -- among other features.

While it isn't necessary to update your existing SPFx solutions, you may need to do so (let's say, because you have an existing solution that needs a feature only available in SPFx 1.5, for example).

Unfortunately, the solution upgrade process between versions of SPFx is often painful.

Thankfully, there is an easy way to do this now!

This article explain a (mostly) pain-free to upgrade your SPFx solution. Waldek explains this process in details, but this is a summary of how to do it.

Office 365 CLI

 

Office 365 CLI is a cross-platform command-line interface (at least, that's what I think CLI means... I hate acronyms) that allows you to do a lot of things with your Office 365 subscription, on pretty-much any operating system you want to do. (Find out more about Office 365 CLI).

The Office 365 CLI version 1.4.0-beta version introduced a new spfx project upgrade function; It can be used to upgrade an SPFx project.

If you don't have Office 365 CLI version 1.4.0-beta or above, you'll need to install it first.  To do so, run the following command:

npm i -g @pnp/office365-cli@next

Analyzing your project

The spfx project upgrade function does not change your project -- you'll need to do this yourself; it analyzes your project and gives you a report telling you exactly what you need to do.

Sample upgrade report
Sample upgrade report

To use it, follow these steps:

  • From your command-line, change your current directory to the root of your SPFx project.
  • Type the following command:
o365 spfx project upgrade --output md > report.md

Once analysis is completed, open the report.md file that was created in your SPFx project folder.

Upgrading your project

If you really want to all the required changes that the analysis found, you can read the report, but if you're in a hurry, follow these steps:

  • Back-up your project (do I really need to say this?)
  • Scroll to the (almost) end of the report.md file and look for the Summary section.
  • Copy the code block under the Execute script header and paste it into your console.

    The Summary Execute Script section
    The Summary Execute script section
  • Next, find every file in the Modify files section and make the highlighted changes. Pro tip: the report provides a hyperlink to each file that you need to change. Just use CTRL-Click to open the file.

    The Modify files section
    The Modify files section

Note that you may have multiple updates to make to the same file, but the report will list each update as a separate entry.  The report also pretends that there is nothing else in the file than what it shows in the report. So, for example, if your .yo-rc,json file looks like this before the upgrade:

{
    "@microsoft/generator-sharepoint": {
        "version": "1.4.1",
        "libraryName": "react-calendar-feed",
        "libraryId": "dd42aa00-b07d-48a2-8896-cc2f8c0d3fae",
        "environment": "spo"
     }
}

and the upgrade report tells you to update .yo-rc.json as follows:

{
    "@microsoft/generator-sharepoint": {
        "version": "1.5.0"
    }
}

You're really supposed to update the .yo-rc.json as follows (change highlighted in bold):

{
    "@microsoft/generator-sharepoint": {
        "version": "1.5.0",
        "libraryName": "react-calendar-feed",
        "libraryId": "dd42aa00-b07d-48a2-8896-cc2f8c0d3fae",
        "environment": "spo"
     }
}

But the next sections in the report will include more changes to the .yo-rc.json file, which -- when you've made all the changes -- will look like this:

{
    "@microsoft/generator-sharepoint": {
        "version": "1.5.0",
        "libraryName": "react-calendar-feed",
        "libraryId": "dd42aa00-b07d-48a2-8896-cc2f8c0d3fae",
        "environment": "spo",
        "isCreatingSolution": true,
        "packageManager": "npm",
        "componentType": "webpart"
     }
}

Once you've made all your changes, test your solution and (hopefully) it will work with SPFx 1.5!

Conclusion

You shouldn't need to upgrade your solution every single time Microsoft releases a new version of SPFx.

If you have to upgrade your solution, however, the Office 365 CLI spfx upgrade project command can save you a lot of time.

For more information

This article is mostly a note to myself on how to upgrade an SPFx project. For the real deal, I encourage you to read Waldek's detailed article, from where I learned about the spfx project upgrade command. (Thanks Waldek for being awesome!)

To learn more about Office 365 CLI, go to https://aka.ms/o365cli

To learn more about the cool new features available in SPFx 1.5, go to the Release Notes for SharePoint Framework Package 1.5.

The solution I used in this article is my React Calendar Feed sample web part, available on the SharePoint Framework Client-Side Web Part Samples & Tutorial Materials.

Introduction

Last week, I attended the SharePoint 2018 Conference in Las Vegas. There were a lot of cool announcements and demos. The SharePoint team rocks!

One of the cool things that I noticed which has nothing to do with SharePoint was that a lot of presenters who showed code had a really cool command prompt that showed the node module they were in, and their Git branch status in a pretty "boat chart".

Console showing node module version and git branching information

I had seen this many times before, but never realized how much easier it was to get a sense of what's going on until I was watching someone else code on a big screen.

Of course, I set out to find and configure this awesome command-line on my workstation.

This article will show you how you too can install and configure this command line interface.

Cmder

During Vesa's awesome session, I paid close attention to the title of his command line window. It said Cmder.

I had seen Cmder before; the article Set up your SPFx development environment mentions Cmder in the Optional Tools section.

But the version of Cmder I had installed didn't have the fancy "boat chart" at the top that got my attention.

As it turns out, you need to download another custom prompt for Cmder that adds the Powerline (that's the real name for the "boat chart") at the top.

Here is how to install and configure Cmder with the Powerline command prompt:

Installing Cmder

  1. Go to http://cmder.net/ and download either the Mini pack or the Full pack.
  2. Unzip the package. Cmder is designed to be portable and to require no administrative privileges to run, so their instructions tell you to not install it in the Program Files folder (where you'll need administrative privileges). I placed it in C:\Users\[myusername]\AppData\Local\cmder.
  3. Open a command prompt in Administrative mode from the folder where you copied the Cmder files
  4. From the command-prompt, type:
    cmder /REGISTER ALL
  5. If you get an Access Denied error, you probably forgot to run the command in Administrative mode. If you don't know how to do that, type cmd from your Start menu, and right-click on Command Prompt and select Run as administrator.
  6. Cmder should be installed. You can verify by opening a new File Explorer window and right-clicking on a folder. You should get a Cmder Here option.
    Cmder Here

Unfortunately, if you open Cmder with that command line, you don't get the fancy Powerline.

Let's fix that!

Installing Cmder Powerline custom prompt

The Cmder Powerline custom prompt changes the Cmder prompt to include the following modifications:

  • The folder portion of the prompt is displayed in blue. The user's home folder is also replaced with a tilde (~).
  • If the current folder is an npm package, the prompt will display the package package name and version number in teal.
  • If the current folder is a Git repository, the prompt will display the branch name with a green colour if the branch is unchanged, or yellow if changes are found.

To install the Cmder Powerline custom prompt:

  1. Download the AnonymousPro font. You can do so by clicking on each TTF file in GitHub and selecting View Raw. For your convenience, here are the links to the raw files:
    Anonymice Powerline Bold Italic.ttf
    Anonymice Powerline Bold.ttf
    Anonymice Powerline Italic.ttf
    Anonymice Powerline.ttf
  2. Once dowloaded each font, install them by double-clicking them and selecting Install on each one of them.
  3. Copy all the .lua files from the Cmder Powerline source and place them in the config folder under the Cmder install folder.
  4. If you haven't done so yet, launch a Cmder window by going to the folder where you installed in and double-clicking on Cmder.exe 
  5. From the Cmder window, open the Settings by hitting Windows-Alt-P.
  6. From the Main settings area, select Anonymice Powerline font from the Alternative font (pseudographics, CJK, etc.) drop-down.
  7. In the Unicode ranges combo box, type E0A0-E0B0 and select Apply.
  8. Select Save settings to save your settings and return to the command prompt in Cmder.

CmderSettings

That's all you need to do.

Cmder with Visual Studio Code

If you want Cmder to show up in Visual Studio Code, follow these steps:

  1. Launch Visual Studio Code.
  2. From the File menu, select Preferences | Settings or use Ctrl-, (Control and comma). This will open your settings editor.
  3. In the right-pane of the settings editor (the one that's actually editable), insert the following JSON, just before the last , making sure to replace the path to Cmder with the path where you installed it.
    "terminal.external.windowsExec": "C:\\Users\\[myusername]\\AppData\\Local\\cmder\\Cmder.exe",
    "terminal.integrated.shell.windows": "cmd.exe",
    "terminal.integrated.shellArgs.windows" : [
    "/K",
    "C:\\Users\\[myusername]\\AppData\\Local\\cmder\\vendor\\init.bat"
    ],

That's all!

Conclusion

I hope that you'll find Cmder and the custom Cmder Powerline command-prompt useful in your SPFx development endeavors.

I know I did!

For More Information

Cmder.net lists more information about Cmder, including the super-powerful shortcut keys.

Amr Eldib is the brilliant mind behind the Cmder Powerline command-prompt.

Sahil Malik has detailed instructions (and a video!) to to integrate with Cmder Visual Studio Code.

Update

In the previous revision of this article, I had forgotten to include the steps to copy the .lua files to the config folder. It works much better when you include all the steps, it turns out 🙂

 

Introduction

One of the premises of SPFx is that, with it, third-party developers have the same set of tools that the SharePoint team has. So, if you like the look of an out-of-the-box web part you can, in theory, reproduce the same look and feel yourself.

A friend of mine needed to display a list of upcoming events, but the events are coming from a WordPress site that uses the WP Fullcalendar widget. They also really liked the look of events in SharePoint.

So, I thought: why not try re-creating the out-of-the-box SharePoint events web part, but instead of reading events from a SharePoint list (or group calendar), it would read from WordPress?

Since I was taking the challenge, I decided to also try to do these extra features:

  • Read events from multiple event providers, including RSS, iCal, and WordPress.
  • Support additional event providers without having to re-design the entire web part
  • Make the web part responsive, just like the SharePoint events web part, with a narrow view and a wide view.
  • Support "Add to my calendar"
  • Make it possible to add more web parts, for example, the Event Search web part, reusing as many of the components as possible.

This article will explain the various components of this web part. Because I tend to ramble on and on, I'll then explain how to write every component of the web part in separate articles so that you can read as much (or as little) as you want.

And if you really don't want to read the articles, you can always get the code. I won't be offended if you do.

The Web Part

Configuration

If you download the web part and run

gulp serve

you'll see the web part in your web part catalog.

Adding the web part

Note: when I designed this web part, I created an SVG icon for it. At the time of this writing, there was an issue with using custom base64-encoded SVG icons. If your icon doesn't look like the one in the picture above, don't worry.

When you add the web part, you'll be prompted to configure it:

Configure event feed

Selecting the Configure button (or selecting Edit web part in the web part's "toolbox") will launch the web part's property pane.

The web part's property pane

In the property pane, the Feed type drop-down lists all the service providers that the web part can find.

feedtype

The idea is that if we add more feed types, they'll automatically show up here. Let me know in the comments if you have an idea for a feed type you think we should add, or if you'd like to add one yourself just submit a pull request.

If you're running the web part in a development environment, it'll offer you a Mock option, which will add bogus events for testing purposes. In production, this option will not appear.

The Feed URL input box will prompt you to enter a URL for the feed you wish to display. It validates the URL format (but doesn't yet check the URL for results).

FeedUrl

Because the WordPress feed URL that I was using supports a from and to date value in the URL, I added the ability to automatically insert today's date and an end date (see below). All you have to do is to add a {s} where you want the start date and {e} where you want the end date.

The Date range drop-down allows you to select anything from Next week to Next year.

DateRange

Unlike the out-of-the-box SharePoint events search, I didn't add a All events option because there was no way (that I know of) in React to find the maximum possible date. I could have passed a null value around, but I didn't want to do that. If there are enough requests for it, I'll figure out a way to do All events later.

The only event provider that I know of which actually supports specifying a start and end date is WordPress. When a provider doesn't support filtering at the source, I just filter them out once I have received the events.

In the Advanced section, you can specify the Maximum number of events per page for the narrow view (the normal view just fits in as many events as it can on every page).

MaxPageSize

The default is (that's what SharePoint events does), but you can put as many as you want on every page. You can also put 0 if you don't want pagination for the narrow view.

When I was testing this web part, I kept on getting all sorts of CORS issues on some of the feeds I was using. So I added a Use proxy option, which -- you guessed it -- routes your requests through a proxy.

UseProxy

Finally, the web part can use the user's local storage to cache events it retrieves so that the web part doesn't fetch every. single. time. you. resize. the. page.

CacheDuration

You can set the cache duration from 0 to 1440 minutes (1 day) in 15 minute increments. Be careful, though, because it'll always cache a user's results from the time they last retrieved the events. So, if you set it to cache for a day, it'll wait an entire day before reloading events again no matter the time of the day. You should probably set it to half-a-day, just to be safe.

If you don't want to cache, you can set the cache duration to 0 and it'll refresh from the source every time. If your feed is slow, the web part will take forever to load every time.

The Apply button is just to make sure that the web part won't try to load the feed as you type the URL.

Assuming you configured the web part (and that my code works well), you'll get to see your events in a pretty calendar view soon enough.

The narrow view

When you put the web part in a single-column, or when the web part is less than 480 pixels wide, the web part renders a list view of events.

NarrowView.png

The list will render all the events retrieved and paginate the results according to the page size option you configured.

The dates are rendered to look like a page-a-day calendar.

DateBox

If the event spans over multiple days, the date box will render differently:

MultiDayDateBox

The pagination component renders a Previous and Next button, and helps manage how many pages to render, which page to render, etc. Unfortunately, Office UI Fabric doesn't offer a pagination control so I had to write my own.

Of course, if I wasn't so lazy, I would have created a full pagination control with page numbers, and all, but the SharePoint events web part doesn't show the page numbers so I didn't do it. If there is enough demand for it, I'll make the component more generic and add the page numbers.

The Normal view (or carousel view)

When you view the web part on a full page (or when it is wider than 480 pixels), the web part switches to a carousel view.

Carousel View

The carousel view is responsive and renders between 1 and 4 events per page.

Like the SharePoint events web part, there is a next and previous arrow when you mouse over the calendar, with dots at the bottom to indicate what page you're on.

CarouselNav

Finally, the Add to my calendar button creates a dynamic ICS file, allowing you to import the event to most calendars on most devices.

Conclusion

In upcoming articles, I'll show how to build this, component by component.

I hope that you'll enjoy it.

As the World's Laziest Developer, I don't like to invent anything new if I can find something that already exists (and meets my needs).

This article is a great example of that mentality. I'm really standing on the shoulder of giants and combining a few links and re-using someone else's code (with credit, of course) to document what my approach to versioning SPFx packages is, with the hope that it helps someone else.

CHANGELOG.md: a standard way to communicate changes that doesn't suck

The problem with change logs

There are a few ways to communicate changes when working on a project: you can use your commit log diffs, GitHub Releases, use your own log, or any other standard out there.

The problem with commit log diffs is that, while comprehensive, they are an automated log of changes that include every-single-change. Log diffs are great for documenting code changes, but if you have a team of developers merging multiple commits every day between versions, they aren't great at summarizing the noteworthy differences.

GitHub Releases solves a part of this problem by making it easy to manually (or automatically) creating release notes with git tags. (f you haven't looked into GitHub Releases, it is awesome --  take a look!.

However, GitHub Releases is still not very user-friendly (or manager-friendly).

You can always write your own change log format, but why not adopt a format and structure that you can use consistently across projects & teams?

CHANGELOG.md

This is where CHANGELOGs come in. According to Olivier Lacan at KeepAChangeLog.com, a changelog is...

"a file which contains a curated, chronologically ordered list of notable changes for each version of a project."

Changelogs use the markdown syntax to make it easy to maintain. They follow a few principles (again, credit to KeepAChangeLog.com):

  • They are for humans not machines: they should be easy to read and quickly make sense of relevant changes.
  • There should be an entry on every single version:
    • Latest version comes first: List versions in reverse-chronological order, makes it easier to see what matters.
    • Release date of each version is displayed: use a consistent ISO standard date format (e.g.: 2018-04-16).
    • Versions should be linkable: becomes handy when you have a giant changelog. Just wrap your version number with [] (e.g.: [0.0.1]).
    • Changes should be grouped by type of change: group you changes into Added, Changed, Deprecated, Removed, Fixed, and Security. Only include the groups of change types you have (no need to have a Deprecated section if you don't have any deprecated-type changes).
  • Mention whether you follow Semantic Versioning: You should, by the way.

How to use CHANGELOG.md in your SPFx project

  1. Add a new file in your project -- wherever you put your README.md) and call it CHANGELOG.md.
    (Sure, you can name your changelog whatever you want, but the whole point of a changelog is to make it easy to find the changes on any projects, consistently. Just name it CHANGELOG.md. Trust me.)
  2. Paste this template in the new file you created:
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added

- (List new added features)

### Changed

- (List changes to existing functionality)

### Deprecated

- (List soon-to-be removed features)

### Removed

- (List features removed in this version)

### Fixed

- (List bugs fixed in this version)

### Security

- (List vulnerabilities that were fixed in this version)
  1. As you work, keep a log of your changes in the Unreleased section, making sure to put the changes under their respective change types. If you want, you can even link to commits, but I don't.
  2. When you change your solution version, create a new section version entry below the Unreleased section. For example, for version 0.0.1 created April 16, 2018, insert the following text below the unreleased version:

## [0.0.1] - 2018-04-16

Remember that not everyone is an American-born, native English speaker. Use the ISO Standard format for dates. The French-Canadian in me thanks you.

  1. Copy all the changes from Unreleased to your new version section, making sure to remove any empty change type sections. For example, if you don't have any deprecated changes, remove the ### Deprecated section.
  2. This is what the final version of your CHANGELOG.md would look like:
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.0.1] - 2018-04-16

### Added
- (List new added features)

### Changed
- (List changes to existing functionality)

### Removed
- (List features removed in this version)

### Fixed
- (List bugs fixed in this version)

### Security
- (List vulnerabilities that were fixed in this version)
  1. Copy back the section templates to the Unreleased section and continue steps 3-7 with every new version.

Semantic versioning

I have worked with Microsoft technologies as long as I can remember, so it is ingrained in me that every version number should consist of 4 parts: Major, Minor, Build, Revision. For example, 1.0.0.0.

When you package an SPFx solution, the solution version always starts with version 1.0.0.0, and you can't make it lower than that. (Well, you can, but SharePoint will ignore it and it will become version 1.0.0.0).

Imagine my horror when, one day, I was trying to change the version number of a solution and searched for 1.0.0 and found that the NodeJS package also has its own version, stored in a file called package.json. What's worse, it didn't even have 4 parts!

The heresy!

After my initial indignation, I decided to research this and found that the versioning schema is called Semantic Versioning (or sem-ver for short). It consists of three mandatory parts: Major, Minor, Patch, plus an optional label for pre-release and build metadata. For example, you could have a version 1.0.0-rc for a release candidate version.

Hmmm, makes it easier to keep track of versions. And it is more human-readable, which is always good.

To keep things even more confusing, each web part can have its own version number. While there are valid reasons why you would want to keep the package version, the solution version and the web part versions separate, it quickly becomes impossible to keep track of versions.

To keep things clean, it makes sense to keep version numbers in sync.

npm version

Luckily, makes it easy to update your package.json version by simply calling:

npm version <major|minor|patch>

Where you specify to increase either the major, minor, or patch version.

For example, if you start with a package.json version 0.0.3 and want to increase the major version, you'd call:

npm version major

Which would produce v1.0.0.

If only there was a way to make it this easy to synchronize the package.json version to the package-solution.json version.

If only someone way smarter than I had thought of this...

Sync npm version with package-solution.json

It turns out there is such a person: Stefan Bauer!

In his blog post, he shares a way to add a Gulp function that automatically syncs the package.json version with the package-solution.json.

(Thanks Stefan for being awesome!)

To add this Gulp function, do the following steps:

  1. In your SPFx project, open gulpfile.js
  2. Before build.initialize(gulp); add my slightly modified version of Stefan's code. If it works, credit goes to Stefan. If it fails, it was my changes.
    let syncVersionsSubtask = build.subTask('version-sync', function (gulp, buildOptions, done) {
      this.log('Synching versions');
    
      // import gulp utilits to write error messages
      const gutil = require('gulp-util');
    
      // import file system utilities form nodeJS
      const fs = require('fs');
    
      // read package.json
      var pkgConfig = require('./package.json');
    
      // read configuration of web part solution file
      var pkgSolution = require('./config/package-solution.json');
    
      // log old version
      this.log('package-solution.json version:\t' + pkgSolution.solution.version);
    
      // Generate new MS compliant version number
      var newVersionNumber = pkgConfig.version.split('-')[0] + '.0';
    
      if (pkgSolution.solution.version !== newVersionNumber) {
        // assign newly generated version number to web part version
        pkgSolution.solution.version = newVersionNumber;
    
        // log new version
        this.log('New package-solution.json version:\t' + pkgSolution.solution.version);
    
        // write changed package-solution file
        fs.writeFile('./config/package-solution.json', JSON.stringify(pkgSolution, null, 4));
      }
      else {
        this.log('package-solution.json version is up-to-date');
      }
      done();
    });
    
    let syncVersionTask = build.task('version-sync', syncVersionsSubtask);
    
    build.rig.addPreBuildTask(syncVersionTask);
  3. Save your file

The final gulpfile.js should look like this:

'use strict';

const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');

build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);

//BEGIN: Added code for version-sync
let syncVersionsSubtask = build.subTask('version-sync', function (gulp, buildOptions, done) {
  this.log('Synching versions');

  // import gulp utilits to write error messages
  const gutil = require('gulp-util');

  // import file system utilities form nodeJS
  const fs = require('fs');

  // read package.json
  var pkgConfig = require('./package.json');

  // read configuration of web part solution file
  var pkgSolution = require('./config/package-solution.json');

  // log old version
  this.log('package-solution.json version:\t' + pkgSolution.solution.version);

  // Generate new MS compliant version number
  var newVersionNumber = pkgConfig.version.split('-')[0] + '.0';

  if (pkgSolution.solution.version !== newVersionNumber) {
    // assign newly generated version number to web part version
    pkgSolution.solution.version = newVersionNumber;

    // log new version
    this.log('New package-solution.json version:\t' + pkgSolution.solution.version);

    // write changed package-solution file
    fs.writeFile('./config/package-solution.json', JSON.stringify(pkgSolution, null, 4));
  }
  else {
    this.log('package-solution.json version is up-to-date');
  }
  done();
});

let syncVersionTask = build.task('version-sync', syncVersionsSubtask);

build.rig.addPreBuildTask(syncVersionTask);
//END: Added code for version-sync

build.initialize(gulp);

Next time you build your package, the Gulp task version-sync will grab the package.json version (which you updated using npm version, right?) and will update package-solution.json, adding an extra zero at the end of the version number to Microsoftify the version.

When you get the version number, go update your CHANGELOG.md file by moving the changes from [unreleased] to a new section with the new version number you just created.

Sync package-solution.json version with webpart.manifest.json version

So far, we have done the following:

  • Created a CHANGELOG.md of unreleased changes
  • Maintained version number using npm version
  • Synchronized package.json versions with package-solution.json versions
  • Updated your CHANGELOG.md to describe the changes you made

But there is still a little annoying thing: the web part versions (stored in webpart.manifest.json,  where webpart is the name of your web part) can be different than the package.json and package-solution.json.

Turns out that it is pretty easy to fix:

  1. In your SPFx solution, open webpart.manifest.json where webpart is the name of your web part. For example, HelloWorldWebPart.manifest.json for HelloWorldWebPart.
  2. Find the "version" line and replace whatever version you have in there for "*", making it:
"version": "*",

Doing so will cause the version of the webpart.manifest.json to match the package-solution.json version.

(Turns out that the latest version of SPFx documents this by adding the following comment on the line above "version": "*".

// The "*" signifies that the version should be taken from the package.json
"version": "*",

How cool is that?!

Conclusion

By using CHANGELOG.md to keep track of changes between versions, and using semantic versioning for your versions, you can make it pretty easy to document your changes across versions.

By using npm version, you can easily maintain the semantic version of your package.json.

By using Stefan's cool version-sync Gulp command, you can easily sync your package.json version and your package-solution.json.

By using "version": "*", you can synchronize your package-solution.json and your webpart.manifest.json versions.

Finally, by not reinventing the wheel and by leveraging the hard-work of other people, you can do it all with very little effort!

I hope this helps you?!