Tag

NoteToSelf

Browsing

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

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

Im my previous post, I explained how to use the SharePoint Get items action in Flow. As the name implies, it retrieves items from a SharePoint list.

Sometimes you need to know if your Get items action returned any items. For example, if you wanted to update an existing item or create a new item in none was found.

In this post, I'll show you how to count how many items were returned by SharePoint and how to test if any items were found.

And don't worry, this post won't be as long as the last one.

Counting results from SharePoint Get items

For the purpose of this example, we'll assume that you already created a flow with a SharePoint Get items action. If you haven't done so yet, take a look at my previous post.

Sample flow

When I have to use fancy formulas in many places within my flow, I like to define a variable. That way, I can just refer to the variable instead of re-entering the formula in many places.

You should always strive to make your flows easy to read so that if someone else has to maintain it (or if you have to come back to it later), it will be easy to understand what the flow does. Make sure to give your actions a descriptive name (not Get items like in my example, use something like Get existing responses from current user, for example). Using variables is another way to make your flows easier to use.

When using variables in flow, you use a different action to define a variable the first time (Initialize variable) than you would to set the variable or change its value (Set variable, Increment variable, and Decrement variable for example).

Since this is the first time we set the variable, we'll use Initalize variable using the following steps:

  1. In the flow editor, select +New step
  2. From the Choose an action box, type variable in the search box.
  3. From the list of suggested actions, select Initialize variable.
    Initialize variable
    4.An Initialize variable box will replace the Choose an action box. Give your variable a descriptive Name. For example: Number of existing items.
  4. In the Type field, select Integer -- because we'll be storing the number of items returned.
  5. We'll write the expression to calculate the number of items returned the Value field. If the dynamic content pane doesn't show, select Add dynamic content, the select Expression.
    Using an expression
  6. Look for the length(collection) function in the Collection category and select it to insert it in the expression box. The length function is specifically designed to calculate how long a collection of items is -- and that's what the Get items action returns: a collection of items.
    Length function
  7. Make sure your cursor is positioned between the two parantheses () in the length function. Select the Dynamic content tab and look for the value dynamic content for the Get items action (or whatever your SharePoint Get items action is called).
    Inserting body of get items
  8. Flow will automatically insert body('Get_items')?['value'] inside your length() function. The final expression should be:
    length(body('Get_items')?['value'])
  9. Select OK to insert the value.
    Formula inserted

Save and test your flow. Mine returned 1 item:
1 item returned

Testing if any items were returned

Now that you have a variable that contains the number of items, you can use it anywhere you want.

For example, if you wanted your flow to do something if any items were returned, and something else if nothing was returned you would follow these steps:

  1. In the flow editor, select +New step
  2. From the Choose an action box, select Control then Condition.
    Condition
  3. A Condition box will replace the Choose an action box. Give your condition a descriptive name. For example: Are there any existing items.
    Adding a condition
  4. If the Choose a value box, use Add dynamic content to select the variable you created earlier.
  5. In the next field, select is greater than
  6. In the next field (Choose a value) enter 0
    Condition for more than 0 items

Save and test your flow. If everything worked well, the Expression value from your condition should return true if SharePoint found items, and false if nothing was found.
We found items

Of course, you would want to add actions to your If yes and If no paths, but that's for another post.

Conclusion

The key to testing if the SharePoint Get items action returned items it to understand that Get items returns a collection of items. Using the length() function against the return value of your Get items action will tell you the length of your collection of items.

I could have avoided using a variable and just entered the length(body('Get_items')?['value']) formula directly in the condition, but I wouldn't be able to tell how many items were returned when I was testing the flow. This sample was an easy one, and I really didn't need to evaluate how many items were returned more than once -- so I really didn't need a variable -- but in more complicated flows, you'll find it a lot easier to define variables and use the variables throughout instead of copying the same formula every time.

I hope this helps?

Introduction

I think that custom SharePoint web parts should always look like they belong to SharePoint; I want my users to be unable to tell my custom web parts from the ones that come out-of-the-box (O.O.B.) with SharePoint. I think that it helps users by presenting a common interface that they are already familiar with.

Sometimes, I'll even go as far as examine the HTML, CSS and even analyze the network traffic an O.O.B. SharePoint web part produces to make sure mine behave consistently.

When testing a SharePoint web part on the SPFx workbench, analyzing network traffic using your web browser's developer tools can get pretty overwhelming; there are so many telemetry calls every few seconds that it becomes impossible to figure out what calls are real API calls, and which ones are telemetry calls.

For example, I added a test web part on my workbench page and refreshed the page, then took a screenshot of the number of telemetry calls the page made (non-telemetry calls are greyed out):

That's a lot of telemetry

I left the page running while I wrote this introduction, and took another screenshot of the network traffic the page logged; this is what it looks like now:

More telemetry calls

The page is filled with telemetry calls! Every little tick in the timeline pane above the list is a telemetry call.

Fortunately, you can temporarily disable telemetry while you're debugging your web parts on the SPFx workbench!

What is telemetry

SPFx uses telemetry to collect measurements (such as performance and usage) and automatically sends that data at regular intervals automatically. It normally does not affect performance and can be quite useful to the Engineering team to detect (and resolve) potential issues.

When using your browser's developer tools, on the Network tab, you can see a bunch of harmless Ajax calls to a link that looks like: https://spoprod-a.akamaihd.net/files/sp-client-prod_2019-06-21.008/3.vendors~sp-client-telemetry-aria_d335ca85ff1f5f8fcce5.js

DisableTelemetry=true

If you want to debug a web part on your SPFx workbench (https://localhost:5432/workbench or https://yourtenant.sharepoint.com/_layouts/15/workbench.aspx ), you can simply append ?disableTelemetry=true to the your query string to temporarily disable telemetry calls.

Notice how there are no more telemetry calls on my page's network traffic below? No more regular dots in the timeline means no more regular Ajax calls.

No more telemetry

That's all!

Conclusion

Telemetry can be super useful, but sometimes it can be annoying -- especially when you're trying to analyze a page's network traffic.

The disableTelemetry=true query string parameter only seems to work on SPFx workbench pages, but that's fine with me.

I hope it helps you?

Image Credit

Image by swooshed from Pixabay

Introduction

Sometimes, you need to create a view in a SharePoint list where the items are sorted using a custom sort order. For example, if you had a list of items that needs to be sorted by Rating where the possible choices are High, Medium, and Low, your list items would appear in the following order: High, Low, and Medium because SharePoint will want to sort your Rating values alphabetically.

Sorting a choice column alphabetically doesn't always work

Poor SharePoint, it doesn't know that you want a custom sort order!

First instinct is to change the possible choices so that the list gets sorted in the right order. For example, renaming Low to 1 - Low, Medium to 2 - Medium, and High to 3 - High. It will force SharePoint to sort by category in the right order.

But sometimes you don't want to change the values in your metadata just so that it sorts properly.

This post will explain a quick method I use to sort list items in a view using a custom sort order.

It is so easy, it is almost embarassing.

Solution

All you need to do is simply add a calculated column where you assign a numerical equivalent to the column you wish to sort on.

Here are the steps:

  1. From your list, select Add column
    Add column
  2. When prompted to choose a column type, select More...
    Select More...
  3. In the Create Column page, select a Column name that suits you. I usually call it [Choice column] Sort, where the [Choice column] is the name of the choice column you want to sort on. For example, to sort by Rating, I would call the column Rating Sort. Feel free to use whatever name you like.
  4. For The type of information in this column is:, select Calculated
  5. Let's skip the formula for a second, we'll get back to it. For The data type returned from this formula is:, select Number and select 0 for the Number of decimal places
    Select Number and Zero decimal places
  6. For the Formula, use a whatever logic you want to assign a numerical value to the item. For example, to sort Ranking, I would assign 1 to Low, 2 to Medium, and 3 to High by using the following formula:
    =IF(Rating="Low",1,If([Rating] = "Medium", 2, 3))
  7. Select OK to create your column.
  8. Test that your column is returning the right values:
    Does my formula work?
  9. If you're getting the right numerical values, change your view to sort by the column you just created by going to Settings | List settings and selecting the view you want to change under Views.
  10. In the Edit View page, make sure to de-select the column you created (we don't actually want to show it!)
    De-selecting your sorting column
  11. Scroll to the Sort section and select your new column as the sort order.
    Sort by Ranking Sort
  12. Select OK to save your view.
  13. Test your view. It should short your items sorted using your sort logic.
    Sorted!

Conclusion

I told you it was simple!

All you have to do is add a calculated column that calculates the sort value you want, and sort by that column!

I hope this helped?

More Information

The example in this post uses the IF function.

The IF syntax is as follows:

IF(logical_test,value_if_true,value_if_false)

So, if you want to return 1 if the [Rating] column is equal to Low, otherwise return 2 you would write:

IF([Rating] = "Low", 1, 2)

You can nest IF statements too. For example, if the value of [Rating] isn't Low, it could be Medium or High. To return the appropriate value, we nest a second IF in the first IF's value_if_false parameter, as follows:

IF([Rating] = "Low", 1, IF([Rating] = "Medium", 2, 3))

However, it'll suck if you have a lot of choices to pick from. For example, if you wanted to sort by month, you would have to do something ugly like this:

IF([Month] = "January",1 , IF([Month] = "February", 2, IF([Month] = "March", 3, IF([Month] = "April", 4, IF([Month] = "May", 5, IF([Month] = "June", 6, IF([Month] = "July", 7, IF([Month] = "August", 8, IF([Month] = "September", 9, IF([Month] = "October", 10, IF([Month] = 11, 12)))))))))))

But that's ugly!

You don't have to use IF to calculate your numerical equivalent. For example, I like to use FIND to find the value in a long string.

The FIND syntax is as follows:

FIND(find_text,within_text,start_num)

Where:

Find_text is the text you want to find.

Within_text is the text containing the text you want to find.

Start_num is the character at which to start the search. If you omit it, default is 1.

Since FIND returns the location in the string where it found the value you're searching for, you just need to make sure your possible values are in order (and unique). For example, this is how I would sort by month:

FIND([Month], "January February March April May June July August September October November December")

Which would return 1 for January, 9 for February, 18 for March, etc.

Sure, the numbers aren't sequential, but we don't care, as long as they get bigger with each value!

Examples of common formulas in SharePoint Lists
The IF function
The FIND function

Introduction

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!