In this post I’ll demonstrate some new tweaks the team has made that lets you customize the flow panel itself using list formatting!
Specifically, you can now provide custom text for the panel header and/or the run flow button:
This goes a long way to making the flow panel less scary. The words “Run flow” don’t mean a whole lot to users. Even if you’ve named your flow well, it can still be confusing. Now you can make things even easier by providing context and meaning directly in the panel. You could even customize these values based on values of the list item!
The part we’re interested in is line 6, the actionParams. The actionParams property is currently only used for the executeFlow action. It is an escaped JSON string (the double quotes have a slash in front of them). And thus far, it’s been used to specify the ID of the flow.
Now you can specify the headerText and the runFlowButtonText properties inside of actionParams as well! Here’s what that looks like using the above format:
The ID is always required but you can specify either or both of the headerText and runFlowButtonText to provide that customization:
Escaped Property
What it does
ID
The ID of the flow to run. This is required.
headerText
Replaces the big text at the top of the panel with whatever you specify.
runFlowButtonText
Sets the text of the primary button with whatever you specify.
Some things to keep in mind:
headerText will wrap if you get especially wordy:
But just because you can, doesn’t mean you should. It’s best to keep the header as short and concise as possible. (for anyone who wants to ignore me, I’ve found I had no problem displaying 5000+ characters)
runFlowButtonText will also allow you to put a large amount of text in the button, but it looks terrible because the cancel button wraps below it somewhere around 14-16 characters:
Unlike the headerText, runFlowButtonText will eventually just run off the screen as it never wraps to a new line.
Changing these properties does nothing to obscure connection details or the name of the Flow (yay!)
The headerText is shown even the first time a flow is executed (when the user is asked about the connections used), but as you might expect, the runFlowbuttonText is not:
This is a fantastic addition by the team! Making a flow button that simplifies launching a flow for an item is a great way to increase adoption, decrease confusion, and impress your boss! Special thanks to Cyrus Balsara (Microsoft) for letting me know about these awesome changes!
A few weeks back I demonstrated how to work with multi-select person or choice fields using indexOf to perform startsWith or contains checks to make some pretty cool formats.
While these are still valid techniques, they have some limitations that the new forEach property and the related operator, loopIndex, can solve. Specifically, applying formatted elements for each value of a multi-select field!
The Problem
I previously created a sample, multi-person-currentuser, that allows you to highlight a multi-person field when one of the users is the current user. The results end up looking something like this:
This sample takes advantage of the contains logic previously discussed by looking for the indexOf the @me(current user’s email address) within a flat string generated using the join operator:
Works pretty well and if that’s all you need, go grab that sample!
But what if we could take it further than just displaying the fields as a string? What if we could apply elements for each item? Well… good news, that’s exactly what the new forEach property allows us to do!
The forEach Property
The forEach property is not yet part of the schema (so don’t be surprised if it gets highlighted as invalid in something like VS Code). You can use it within column formatting or inside of your rowFormatter for view formatting.
The forEach property allows you to create virtual fields that you can access within an element. The element where you add the property (along with all of it children) are rendered once for each item within the array (array refers to the collection of selected people or choices).
Because the element is rendered multiple times, you must have a containing element. This is why if you attempt to use the forEach property in the root element, you’ll get an error.
The forEach property’s value is a simple sentence in the form virtualFieldName in ArrayField. Let’s look at an example.
For this first example we’ll use a simple multi-select choice field. In this case we’ve just made the choices some letters. Here it is with no formatting applied:
Now let’s apply a format using the forEach property:
So what did we do? As mentioned above, you can’t apply forEach to the root element. So we created a div and then gave it a single child. However, by using forEach within the child we’re using this element as a template that will be repeated within the root container once per selected choice.
The forEach value requires you to provide the virtual field name followed by the word in and concluding with the name of the array to loop over. In this case, as seen on line 6, we are using choiceIterator as the virtual field name and our array is the @currentField(this could have just as easily been another array field in your view using the [$FieldName] syntax).
Note that the virtual field name should be unique. It is possible to clobber your other fields if you use the same name as one of the internal names of your fields! This means that if you use Title then you’ll no longer have access to the actual Title field’s value! This will be true even after the loop completes. So choose carefully. I find it best practice to use either the field name or type followed by the word Iterator. This has the added benefit of making it obvious that you are retrieving a loop value within your element – but that’s up to you.
Now that we’ve added the forEach property, we can access the virtual field anywhere within our template object (and its children) just like it was any other field! You can see this in the txtContent property on line 8 and we even use it in an expression within the title property to create a nice tooltip on line 11.
Taking it further with loopIndex
Back to our person example from above, wouldn’t it be great to do more than simply show their names? There’s another sample called person-roundimage-format that applies the standard circle image for users. Using the techniques above we can quickly convert it to support multi-select person fields (add a forEach and change our field accessors):
Now the images are showing up and we even get multiple when more than one person is selected! But what happens when we have lots of selections? The results aren’t great and give us all a sad:
You can see that up to 3 people looks just fine, but 4+ starts to have some weird squishing (and nobody likes weird squishing). So we need some way of knowing how many people we have for an item and which one we are on in the template.
Fortunately, you can use the length and loopIndex operators to accomplish this!
The length operator will provide the total number of items in an array (it does NOT provide string length). We can use this value to determine when we shouldn’t show an element (to remove face 4, 5, 6, etc.).
The loopIndex operator provides us with the zero-based index of where we are in the forEach loop. To use it, simply provide it the virtual property name you want to get the index of (since you can nest multiple forEach loops) as a string. So, in our case we can use "=loopIndex('choiceIterator')".
We’re going to base our solution on the UI Fabric Facepile with descriptive overflow. In order to do that, we want to accomplish the following:
Show 1 to 3 faces without change (that seems to work great)
Never show more than 3 circles
Replace the third circle with a descriptive overflow circle when there are 4 or more people selected
The first case we’ve got handled. The second can be done by using "display":"none" as mentioned in my last post. The third requires an alternate element that only shows when there are 4 or more people and we are on the 3rd person.
On line 14, we added a display property to our template element to set the value to none if the loopIndex is greater than or equal to 3 (keep in mind that the index starts at zero so we’re basically saying never show items 4 and up)
On line 31, we added a similar display property to our img element to set the value to none if the number of items is greater than 3 and the loopIndex >= 2. This allows us to show it as normal if there are 3 or less people selected but when there are 4 or more, we don’t want to show that 3rd person.
On lines 34-54 we add our descriptive overflow element. This is a gray circle that says how many more people were selected than are shown.
On line 46, we once again take advantage of the display property to ensure that the overflow element is only shown when there are more than 3 people selected and we are on the 3rd element (loopIndex = 2).
We use the join operator to create a tooltip with everybody’s name in it on line 37.
We determine the number of additional people by simply subtracting 2 (since we know how many we are showing) from the length of the array. Notice that the 2 is wrapped in parenthesis. This is to combat an issue with the subtraction operator.
While that certainly isn’t the simplest sample in the world, it demonstrates common list formatting patterns such as conditional display, element loops, and customization based on loop position.
This opens up even more possibilities in the already awesome List Formatting world! Whoo!!
Creating a button with Column Formatting to launch a flow is a fantastic way to make actions obvious (not hidden in the Flow menu). If you have a list item flow, I strongly recommend doing this (just copy, paste, and tweak the sample). But what if you want to go further? What if you have multiple flows for list items and you want to help guide your users through this more complex (but relatively common) scenario?
Good news! List formatting expressions make it possible to only show the flow launch button(s) that make sense based on the values of the item! Here’s what we’re building:
In the list above, we have 3 separate flows (Develop, Deploy, and Destroy). We want to only prompt the user to launch one of these based on the value of the Status column in the list item. You could argue that it might make more sense to launch a single flow that handles the conditional logic directly. Sure, but customizing the text, icon, and color to make it obvious to the user what action they are taking is still an awesome thing to do.
Conditional Logic Across Properties
In our sample, we want to conditionally change the color, icon, text, visibility, as well as the flow launched. This brings us to a very common scenario in List Formatting: applying the same logic to multiple properties.
It would be awesome to apply your logic to entire elements, but in List Formatting it is only possible to conditionally apply the value of a property. This means you can’t conditionally include/exclude a property or element. These properties/elements have to be included with their individual values conditionally set.
So, if you want to set a style property based on a condition, like the text color, you have to include the property and set its value regardless. Generally, you handle this by setting up an expression like this: “color”:”=if(@currentField>2,’red’,”)”
You cannot, however, apply that same logic to the inclusion of the color property itself.
Conditional Logic for Elements
Although you must include all the elements and properties whose values you want to set (even if only in some conditions), you can use the style display attribute to remove entire elements by simply setting the value to none. This is how we remove the entire button when the status is ‘Destroyed’ in our sample (line 15 below). We don’t want to prompt the user to launch any flow, so we simply remove the button altogether.
You can extend this pattern further by creating a placeholder top element of a div with children. The individual child elements can be turned on or off by conditionally setting the display property. That’s not what we’re doing here, but it’s something to keep in mind.
You can see that we are applying each of our elements conditionally by comparing the value of the Stage column (also present in the view). For the flow, we build the actionParams conditionally by building the escaped JSON value and swapping the ID value in and out based on the stage (line 6).
You can customize this sample by adding additional conditions, changing the comparison column (use the internal name), and the ID(s) of the flows themselves.
Getting a Flow’s ID
To use the code, you must substitute the ID of the Flow(s) you want to run. The IDs are contained within the expression inside the customRowAction attribute inside the button element.
To obtain a Flow’s ID:
Click Flow > See your flows in the SharePoint list where the Flow is configured
Click on the Flow you want to run
Copy the ID from the end of the URL (between flows/ and /details)
Update
See this demoed on the PnP Call (Live from MVP Summit):
Also, come get the full picture in my sessions about List Formatting at the SharePoint Conference in Las Vegas in May, or the European Collaboration Summit in Germany in May:
An easy way to make your list views far more readable is to provide alternating styles between rows. This is especially helpful for wide lists with lots of columns.
Importantly, the alternating colors need to alternate regardless of the content of the list items. This means that even with changing sorts and filters applied, the alternating styles should remain consistent. Up until a few days ago, this wasn’t possible with List Formatting. However, thanks to a new operation and magic string, applying alternating styles is super easy!
Although you can use this same basic concept for advanced visualizations within column formatting or row format style view formatting, the most common usage is to apply a color across the whole row for every other row. That’s what I’ll demonstrate here using a row class style view format.
@rowIndex
A new magic string has been added that will provide you the index (relative to the view) for a given row. The index starts at 0. So, when a view is rendered, the row at the top has a @rowIndex of 0, the next is 1, the next is 2 and so on. If the sort changes, whatever row just became the new top row has a @rowIndex of 0.
We can see this in action with a simple column format:
The @rowIndex value remains consistent based on render position!
Awesome! But just showing the index isn’t very helpful. Fortunately, a new operation has been added that makes using this value in an expression super easy.
Modulus (Remainder)
The modulus operator is the % sign. This operator returns the remainder left over when one operand is divided by a second operand. Here are some examples:
Expression
Result
“=13 % 5”
3
“=0 % 2”
0
“=1 % 2”
1
“=2 % 2”
0
“=3 % 2”
1
It’s those last 4 examples that are particularly relevant for us. By using the remainder operator with the second operand of 2, we can consistently get results for every other value!
Alternating Row Class
View formats provide an additionalRowClass property that allows us to apply a class(es) to whole rows. We can even do this conditionally! So, using what we learned above we can use this simple format to apply a class to every other row:
We are checking to see if the remainder of dividing the @rowIndex by 2 is 0 (even numbers will be 0 and odd numbers will be 1). When it’s 0, we use the UI Fabric theme classes to apply a theme color across the whole row. We also add an additional class to add a hover effect as well. Here’s the result:
I had the honor of attending and speaking at SPTechCon West this week. It was great! I attended several really awesome sessions, attendees were super engaged, and I even got some sweet swag. Even better? I crushed Vlad Catrinescu at Mario Kart on the same big screen we both presented on the next day.
Getting the Most out of SharePoint Patterns and Practices (PnP)
Photo via David Warner II
On Monday I presented one of my favorite sessions. This session is like a sampler platter of the awesome stuff the PnP team and community has made available for everyone to use and learn from. There were lots of people who hadn’t heard of PnP, which means their jobs are now going to be so much easier. Whoo!
There was a lot of great feedback and participation. People were very excited about Page Transformation for Modernization and, as always, PnP PowerShell. The samples and contribution opportunities were also of great interest.
Earlier today I got to present on my other favorite topic: List Formatting. Unfortunately, I got over ambitious and tried to fill my session with tons of information AND demos. I ran out of time. Sadness.
It was a big crowd and lots of people were very excited about the amazing things you can do with List Formatting. I had lots of people asking questions afterwards and even helped setup some formats for attendees right in the room. That’s the power of List Formatting, we can apply them with no installations, deployments, or admin privileges!
Thank you so much to all the attendees, speakers, sponsors, and organizers. SPTechCon has lots of user targeted information, but so many of the sessions went into a lot of depth that I left with lots of great tips and I’m sure everyone else did too!
Multi-select values (choice or person fields where you can pick more than one) have traditionally been tough to work with in List Formatting. You can use the values directly but right away you’ll see formatting differences from the standard/default formatting:
“txtContent”: “@currentField.title”
“txtContent”: “@currentField”
Person fields get separated by a semi-colon and a space, choice values are separated by a comma and lose the space. Aw, come on!
Fortunately, you can have some control over how these are separated using the join function. The join function takes in an array as the first parameter and the separator text to use between items. Here are some samples:
Expression
Field Value
Result
“=join(@currentField, ‘, ‘)”
[Water,Wine,Beer]
Water, Wine, Beer
“=join(@currentField, ‘|’)”
[Water,Wine,Beer]
Water|Wine|Beer
“=join(@currentField, ‘ & ‘)”
[Water,Wine,Beer]
Water & Wine & Beer
That’s pretty slick!
If you read my last post about using the indexOf function to see if your text contains a given value you might think you could use this to see if the selected values contain one of the choices/person fields. Not quite…
Selection Contains a Value
The indexOf function works on strings not arrays. Multi-select fields are arrays internally and the indexOf function will always return -1 when applied directly to the array value.
Fortunately, we can either use the join function shown above or the toString function on our array value first, then we can easily apply our contains logic using the indexOf function!
I recommend using the join function so that you know exactly what you’re going to get since the toString will vary it’s separator depending on the field type (choice or person).
Here are some examples of combining these functions to see if a choice is selected:
Now you can easily work with multi-select fields and apply dynamic formats that can take these fields from strings of text to meaningful visualizations.
Also, come get the full picture in my sessions about List Formatting at the SharePoint Conference in Las Vegas in May, or the European Collaboration Summit in Germany in May:
In my last post, I demonstrated applying conditional formats when text starts with a given value. But what if you just want to know if your text contains that value (beginning, middle, end, wherever)?
We can use that same indexOf function provided by List Formatting. This function tells you the first index (starting character position) of some text within your text. The index starts at 0 and if the text isn’t found then the result is -1. Here are some sample inputs and results:
Expression
Result
“=indexOf(‘Unreliable Peanut’, ‘U’)”
0
“=indexOf(‘Unreliable Peanut’, ‘e’)”
3
“=indexOf(‘Unreliable Peanut’, ‘reliable’)”
2
“=indexOf(‘Unreliable Peanut’, ‘p’)”
-1
“=indexOf(‘Unreliable Peanut’, ‘tasty’)”
-1
As you can see, you can pass in a single character or a whole word/phrase. You can also see that this function is case-sensitive.
How does knowing the index help us? The key is that the result is always -1 when the value is not contained within the text. So we can reverse that logic to know when our text contains a value.
But what about that last entry? Remember the indexOf function is case-sensitive which may be exactly what you want sometimes. But in this case, that capital D is really messing us up.
Case-Insensitive Contains
Fortunately, we can combine our indexOf function with another function provided by List Formatting, toLowerCase to negate the casing issue.
toLowerCase takes one text parameter and returns that value in all lowercase. So now we can wrap our field value in toLowerCase and always search using a lower case value:
Also, come get the full picture in my sessions about List Formatting at the SharePoint Conference in Las Vegas in May, or the European Collaboration Summit in Germany in May:
Applying conditional formats based on the value of your field is pretty straightforward in view and column formatting. But what if you only care if your field’s value starts with a given value?
Most formats rely on knowing all possible values and providing conditions and formats for them. This can quickly get out of hand and in many cases just isn’t feasible.
For instance, what if we wanted to show the flag of a country based on the international calling code portion of a phone number? There are millions of phone numbers in the world and trying to create conditions to match all of them is a terrible idea. But we don’t care about the whole phone number, just the calling code part at the beginning. That’s that +1 for US numbers or +39 for Italian numbers, etc.
List Formatting provides a function called indexOf. This function tells you the first index (starting character position) of some text within your text. The index starts at 0 and if the text isn’t found then the result is -1. Here are some sample inputs and results:
Expression
Result
“=indexOf(‘Old Lady Wigs’, ‘O’)”
0
“=indexOf(‘Old Lady wigs’, ‘d’)”
2
“=indexOf(‘Old Lady wigs’, ‘Lady’)”
4
“=indexOf(‘Old Lady wigs’, ‘o’)”
-1
“=indexOf(‘Old Lady wigs’, ‘rock’)”
-1
As you can see, you can pass in a single character or a whole word/phrase. You can also see that this function is case-sensitive.
So, how do we use this to apply a format based on our text starting with a given value? The key is to look for that 0 index. This means that our value is at the very beginning of the text.
Using this same logic, we can check a given phone number for the presence of the Italian international calling code doing something like this:
This opens up all sorts of possibilities for making some really smart formats. Stick around for my next post where we’ll take this concept even further!
Also, come get the full picture in my sessions about List Formatting at the SharePoint Conference in Las Vegas in May, or the European Collaboration Summit in Germany in May:
During my day job I create PowerApps fairly regularly for both clients and internal use. The platform has come a long way in a very short amount of time and has gone from interesting idea to amazing enterprise accelerator. So what did I decide to do with all that enterprise capability? Create a game of course!
PowerPush: Dungeon Explorer 3000!
PowerPush is a Sokoban clone made entirely in PowerApps. It has 6 levels, 4 playable characters, music, sound effects, supports right and left handed players, and has only 4 screens.
Fair warning, there’s a lot of sweet techno loops
PowerPush is open source (in as much as PowerApps can be) and you can import it into your tenant to give it a whirl and take a peek under the hood. There are no data connections at all, so it really is as simple as importing and playing.
To push myself and learn some stuff I wouldn’t necessarily learn by creating another form wrapper app. Believe me, I learned a lot! Number one thing I learned was that PowerApps really isn’t a great platform for making games. However, I also learned several really cool techniques (“wonky hacks” might be a better term for several of them). I’ll talk more about some of these below but I’ll also be following up with some individual tips and tricks in the next few weeks.
How it works
There are a lot of pieces to this app, and the best way to understand it is to just download it and take a look. But here are some details about each of the screens:
Screen 1: Main Menu
When the app starts, several global variables and collections are initialized. The most important of which is the levels global variable. This is an array of level objects that define the tiles to be used and details about where the crates start and where they should end up:
The levels object is later processed into a set of context variables and collections to manage current state for the active level and provide details about which image to use for each tile and whether those tiles are solid, etc.
The main menu has some animation based on a timer, but otherwise there are simple controls to set global variables used on other screens (like the character).
Screen 2: Level Start
The level start screen simply shows the current level details along with a randomly selected “tip”. This screen uses the fade navigation to create a nice transition into the level. You can also click anywhere on the screen to skip the automatic navigation based on a short timer.
One of the key things it does, however, is to process the level object into some key collections:
More details will later be pulled out of the activeLevel object to set up initial state for the player, but the info above determines how many crates are visible and where they are along with key information about the tiles. By using an image map collection we can use the strings setup in the level (first 2 characters are the tile image and the last indicates if the tile should be “solid”).
This step greatly simplifies level design by making it a simple array of strings so that you can almost visualize the level while you design it. By processing it here, we get easily usable values in a collection that tiles can reference details from by index.
This is how we can have one screen for the main game with any number of levels! This lets us simply bind to a data model and write logic (movement) to update that model. This is the area that PowerApps really shines.
Additionally, we could easily provide multiple image maps to create “themes” for levels to allow the same logic to apply a completely different tile set. The tile objects could also be extended to provide an additional layer of tiles for foreground elements if we wanted using nearly the same logic.
Screen 3: Main Game
This is where all the levels are shown. The tile system is based on 140 images laid out in 10 rows of 14 columns. Each of these tile image controls hijack their own BorderThickness property to store their “index” value. This works because the BorderStyle is set to None(so that value isn’t used).
This “index” value is used to position each of the individual tiles (the number of tiles, size, rows, columns, etc. can all be changed through some simple global variables). The “index” is also used to pull what the image should be by referencing the tiles object’s background array (shown above).
While not easy, you can reference an item within a collection by index by using this non-obvious formula:
The actual movement logic is handled in a single spot. There aren’t really global functions in PowerApps, but I’ve got something pretty close in that I’ve added a timer with a Duration value of 0. Then I set the AutoStart to a context variable. In the OnTimerEnd action I update any variables as needed and finally set that same AutoStart context variable to false.
So now to call that “global function” I simply need to call an UpdateContext function. In that call I update any “parameter” variables the global function will reference and set the AutoStart variable to true. For instance, here is the OnSelect for the Left button of the virtual D-Pad:
Both of these calls are nearly identical with the only exception being the “parameter” variable requestedMove.
The key part, however, is that evaluateMove:true. This will cause the timer to start and because it has a Duration of 0 it immediately performs it’s OnTimerEnd actio. This was really important because the move evaluation logic is pretty complicated and I really didn’t want to cut and paste it with some minor tweaks between 4 different buttons. That would be pretty hard to maintain.
The actual move evaluation uses a series of If statements and looks at the tiles collection to determine the current positioning of objects to determine if the requested position of the player is open and what should happen (push a crate, play a grunt since he hit a wall, etc.). It’s pretty convoluted, but you can see that some pretty complex logic can be performed to update our positioning elements (which specific controls like the player or the crates are bound too). Because of the power of PowerApps, all of the controls redraw themselves automatically just by our editing the model!
"Handle movement";
"If no wall in this direction";
If(!Last(FirstN(tiles.background,(playerY+requestedMove.y)*14+playerX+requestedMove.x+1)).solid,
"If no crate in this direction";
If(CountIf(crates,x=playerX+requestedMove.x And y=playerY+requestedMove.y) = 0,
"No crate, just move the player";
UpdateContext({playerX:playerX+requestedMove.x, playerY:playerY+requestedMove.y, playerRot:requestedMove.rot, playerPushing:false,playSndWalk:true});
Reset(audWalk),
"There is a crate, see if it is up against a wall";
If(!Last(FirstN(tiles.background,(playerY+requestedMove.y*2)*14+playerX+(requestedMove.x*2)+1)).solid,
"Not against a wall, check for another crate";
If(CountIf(crates,x=playerX+(requestedMove.x*2) And y=playerY+(requestedMove.y*2)) = 0,
"Push that crate!";
UpdateIf(crates, x=playerX+requestedMove.x And y=playerY+requestedMove.y, {x:playerX+(requestedMove.x*2),y:playerY+(requestedMove.y*2)});
UpdateContext({playerX:playerX+requestedMove.x, playerY:playerY+requestedMove.y, playerRot:requestedMove.rot, playerPushing:true,playSndWalk:true,playSndPush:true});
Reset(audWalk);
Reset(audPush),
"Can't move the crate or player (crate against a crate), but update player look";
UpdateContext({playerRot:requestedMove.rot, playerPushing:true, playSndGrunt:true});
Reset(audGrunt)
),
"Can't move the crate or player (crate against a wall), but update player look";
UpdateContext({playerRot:requestedMove.rot, playerPushing:true, playSndGrunt:true});
Reset(audGrunt)
)
),
"Can't move the player (wall), but update player look";
UpdateContext({playerRot:requestedMove.rot, playerPushing:false});
If(Rand()<.5,
UpdateContext({playSndHuh1:true});
Reset(audHuh1),
UpdateContext({playSndHuh2:true});
Reset(audHuh2)
)
);
"Check crate positions";
ClearCollect(prevTorches, torches);
ClearCollect(torches, ForAll(crates, {on:CountIf(activeLevel.start.targets, x=Value.x And y=Value.y)>0}));
If(CountRows(Filter(torches,on=true)) CountRows(Filter(prevTorches,on=true)),
UpdateContext({playSndIgnite:true});
Reset(audIgnite)
);
"Check for Win";
If(CountRows(Filter(torches,on=true)) = CountRows(activeLevel.start.targets),
UpdateContext({finalDescent:true});
Reset(audMusicWin)
);
UpdateContext({evaluateMove:false})
Screen 4: Credits
This is a really simple screen that has a gallery bound to a custom credits collection. Then the Y value of the gallery is set based on a timer value to provide the scrolling animation. The timer is on a loop, so the scrolling is too.
This screen really exists to thank the tile artists (used with permission, but still greatly appreciated) and to make my kids happy (they’re the testers listed above).
Lessons Learned
I’m going to post a few different techniques I used as separate articles over the next few weeks, but there are several pain points I identified that I’d love to see solved in the future. Here’s a few:
Need for a this operator
You refer to a control by it’s name in your formulas. This works great. Even better, if you copy one or more controls they’ll get renamed and all the formulas updated. This is super powerful. But… if you suddenly need to update that formula on multiple controls (say 140 tiles), you can’t just select them all and update their formula because all of those formulas are now unique because they refer to specific controls. Simply introducing a this keyword that would refer to the current control in it’s own functions would be awesome.
Currently, if you loop and play your audio file it will continue to play even in the editor. This is weird behavior since, for instance, the timer stops running in the editor. This means if you do something like add really annoying techno music (see video above), you’ll never escape it.
Right now you can flip an image control horizontally or vertically and you can rotate in 90 degree increments. It would be far more powerful if you could specify an exact degree of rotation. This would make animations far better.
To simplify things, I shoved an index variable for the tiles, crates, torches, etc. into their border width property. This works because I wasn’t using this property (BorderStyle: None). This made creating multiple controls that knew what variables to pull from far simpler. However, it would be even better if we could just associate custom properties of a given type directly on controls instead of hijacking an unused property (since this could have unintended consequences).
When you assign an image to an image control you do so by the Media variable. This is true for other controls as well. This is fine, but it can make dynamic assignments difficult since the control will need to be able to reference (know about) the actual media variables directly. I solved this using an image map collection so that I could build strings and then find the referenced image that way. This works pretty well, but it would be even easier if there were just a Media(“MediaName”) function.
PowerPush was designed for touch controls on a phone using a simulated d-pad. This works, but is clunky for the web. It would be far nicer to move with the arrow keys. Unfortunately, there isn’t a key down event to do this.
Multi-line text columns don’t provide the standard “Format this column” option under Column settings in the modern list view column menu. They used to, but now they don’t. Fortunately, there is still a way to apply column formatting to these fields!
There are 2 ways in within the interface to apply column formatting for a column (you can also do it programatically). The easiest and most common way is to use the “Format this column” option mentioned above, but it’s not the only way! The advanced settings for a column provide an additional spot where you can paste your formats. Aw yeah!
The Format
I’m using the text-wrap-format sample from PnP created by Aaron Miao. This is a great format for when you really want to see your full text (instead of the cut-off fade provided by default). I’ve modified the sample slightly to apply the primary theme color for the text to make it even more obvious. Here’s the full format:
Here’s what our list view looks like before applying column formatting to the multi-line text field, “Synergy”:
To apply column formatting to a multi-line text column:
Navigate to the List Settings (Site Actions > List Settings):
Choose the multi-line column from the column settings
Scroll to the bottom of the multi-line column settings and paste your format in the Column Formatting section:
This same option is also available for site columns!
Click OK, then return to your list view and refresh to see the format applied:
The blue is the theme color and was added to the sample just to make it more obvious a format was applied. The key thing to notice is that the full text is now shown.
Weep at the beauty of thy column!
NOTE – List Formatting encodes values prior to rendering which makes the use of enhanced (rich text) multi-line fields basically unusable in your formats. These values come back as HTML and that HTML will be encoded and then displayed inline with your values. It is NOT recommended to use Rich text fields in your formats.
Update
See this demoed on the PnP Call (Live from MVP Summit):
Also, come get the full picture in my sessions about List Formatting at the SharePoint Conference in Las Vegas in May, or the European Collaboration Summit in Germany in May: