Introduction
Boards is one of the many sample apps that are available for users to download and use in Teams. Per the official Microsoft document (which can be found here), it "provides a simple way to connect and share with people in your organization with similar interests". One of the key features of this app is that it allows users to create "pins" of different types - links to Teams teams, channels, chats, websites, etc. These "pins", which are called board items in the app, can be categorized under different boards, which can, in turn, belong to different categories. Selecting a board displays a gallery of its related board items or pins.
To learn how to install it, click here.
Pinterest-like gallery
The biggest challenge with this app was to create the gallery of pins within a board. This is how it looks like:
Does this look familiar? I bet it does! Doesn't it look a lot like this?
Challenges with the design - gallery wrap count
If you look at the Pinterest gallery, the first thing you would notice is that to achieve a UI like that in Power Apps, we will need a flexible height gallery. Additionally, we would either need multiple flexible height galleries side-by-side OR a flexible height gallery with a variable wrap count. For those of you who don't know what wrap count is, it represents the number of columns in a vertical gallery and the number of rows in a horizontal gallery. Having multiple flexible height galleries wouldn't be ideal as you cannot add more galleries if the number of galleries you want is more than the number of controls you have added on the screen. That leaves us with only one option - to use a flexible height gallery with a variable wrap count. BUT, flexible height galleries in Power Apps do not have a wrap count property!!! Bummer right? That's what I thought but I didn't want to give up. Because I honestly think that you can achieve anything with Power Apps.
How I made a flexible height gallery work
Initial high level approach
- Since we know that in either case we will need a flexible height gallery, the first logical step was to use a flexible height gallery.
- I knew that to achieve the equivalent of having a wrap count property, I had to come up with a workaround.
- Blank vertical height galleries do have a wrap count. So I needed a way to take advantage of it.
- A light bulb went off - I then decided to embed a flexible height gallery inside a blank vertical height one.
- First thing I had to do then was to control the wrap count of the blank vertical gallery based on the screen width as this app had to be responsive.
- Keeping the width of the flexible height gallery fixed to 350, I set the wrap count of the blank vertical gallery to
RoundDown( 'Board Items Screen'.Width / 350, 0 )
This simply divides the width of the screen by 350 and then rounds it down to the nearest whole number.
- Next challenge was to figure out how to display the list of pins/board items. In other words, how do I share the list across multiple flexible height galleries.
- I then had to figure out the data source of the parent blank vertical gallery. It had to be equal to the wrap count as otherwise the gallery would have had more than one row (a vertical gallery with a wrap count of 4 which has 6 records to display, will show 4 in the 1st row and then 2 in the next row), which would have broken the UI I was trying to achieve. So I the Items property of the blank vertical gallery to:
Sequence( RoundDown( 'Board Items Screen'.Width / 350, 0 ) )
Splitting board items across galleries
- The next challenge was to figure out how to split the pins across the flexible height galleries. In other words, I had to figure out the Items property for the flexible height gallery.
- I then did something I had not done in a long time. I took out a notepad and pen, and started to write down how the records should be split for different combinations of total number of records and wrap counts.
- I was basically desperately trying to figure out a mathematical representation of how the records should be split based on wrap count and the total number of records.
- For example, with a total count of 13 and a wrap count of 4, I needed the split up to be 4, 3, 3, 3. And with a total count of 13 and a wrap count of 5, the split up had to be 3, 3, 3, 2, 2. In other words, I had to first divide the total count equally across the wrap count, then assign the remaining records 1 per each column. So, in the first example, 13/4 rounded down is 3. With 4 columns, that's 12, which leaves us with 1, so we assign that 1 to the first column. Giving us 4, 3, 3, 3. Similarly, 13/5 rounded down is 2. With 5 columns, that's 10, which leaves us with 3, so we assign 1 to the 1st 3 columns. Giving us 3, 3, 3, 2, 2. I could generalize this with:
RoundDown(Count / Wrap) + If( Count - RoundDown(Count / Wrap)*Wrap >= 1 && Count - RoundDown(Count / Wrap)*Wrap >= ColumnNumber, 1, 0 )
Some more fun with galleries and math!
- So taking the example of total count of 13 and wrap count of 5, I needed to create 5 sub-tables with the 1st 3, 6, 9, 11, and 13 records. Then I had to take the last 3, 3, 3, 2, and 2 records from each of these sub-tables. The above formula translated into this:
LastN( FirstN( colSelectedBoardItems, RoundDown( Count/WrapCount, 0 ) * ColumnNumber+ If( Count - (RoundDown( Count / WrapCount, 0 ) * WrapCount) >= 1, Min( ColumnNumber, Count - (RoundDown( Count / WrapCount, 0 ) * WrapCOunt) ), 0 ) ), RoundDown( Count / WrapCount, 0 ) + If( Count - (RoundDown( Count / WrapCount, 0 ) * WrapCount) >= ColumnNumber, 1, 0 ) )
- Assuming colSelectedBoardItems is the collection of board items that we want to display, replacing Count, WrapCount and ColumnNumber with their actual expressions, I got the following formula:
LastN( FirstN( colSelectedBoardItems, RoundDown( CountRows( colSelectedBoardItems ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) * lblNestedGalleryCardHeight.Text + If( CountRows( colSelectedBoardItems ) - (RoundDown( CountRows( colSelectedBoardItems ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) * RoundDown( 'Board Items Screen'.Width / 350, 0 )) >= 1, Min( Value(lblNestedGalleryCardHeight.Text), CountRows( colSelectedBoardItems ) - (RoundDown( CountRows( colSelectedBoardItems ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) * RoundDown( 'Board Items Screen'.Width / 350, 0 )) ), 0 ) ), RoundDown( CountRows( colSelectedBoardItems ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) + If( CountRows( colSelectedBoardItems ) - (RoundDown( CountRows( colSelectedBoardItems ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) * RoundDown( 'Board Items Screen'.Width / 350, 0 )) >= Value(lblNestedGalleryCardHeight.Text), 1, 0 ) )
Note: lblNestedGalleryCardHeight (probably not the best control name) is actually the column number. Since the Items property of the parent blank vertical gallery is the Sequence function, the Text property of lblNestedGalleryCardHeight is set to ThisItem.Value, thus representing the column number.
The final Items property of the flexible height gallery
- I then had to add filter and search capabilities. Which at this point of time was simple, I just had to replace colSelectedBoardItems with the Search/Filter expression that I normally would have used for a list of board items. This resulted in the following formula:
LastN( FirstN( Search( Filter( colSelectedBoardItems, locBoardItemCategoryFilter = "All" && true || (locBoardItemCategoryFilter = "Websites" && Category = 'Board Item Categories'.Website) || (locBoardItemCategoryFilter = "Teams" && Category = 'Board Item Categories'.'Teams team') || (locBoardItemCategoryFilter = "Channels" && Category = 'Board Item Categories'.'Teams channel') || (locBoardItemCategoryFilter = "Tabs" && Category = 'Board Item Categories'.'Channel tab') || (locBoardItemCategoryFilter = "Conversations" && Category = 'Board Item Categories'.Conversation) || (locBoardItemCategoryFilter = "Apps" && Category = 'Board Item Categories'.App) || (locBoardItemCategoryFilter = "Files" && Category = 'Board Item Categories'.File) || (locBoardItemCategoryFilter = "Yammer" && Category = 'Board Item Categories'.Yammer) ), txtFindBoardItems.Text, "msft_name", "msft_description" ), RoundDown( CountRows( Search( Filter( colSelectedBoardItems, locBoardItemCategoryFilter = "All" && true || (locBoardItemCategoryFilter = "Websites" && Category = 'Board Item Categories'.Website) || (locBoardItemCategoryFilter = "Teams" && Category = 'Board Item Categories'.'Teams team') || (locBoardItemCategoryFilter = "Channels" && Category = 'Board Item Categories'.'Teams channel') || (locBoardItemCategoryFilter = "Tabs" && Category = 'Board Item Categories'.'Channel tab') || (locBoardItemCategoryFilter = "Conversations" && Category = 'Board Item Categories'.Conversation) || (locBoardItemCategoryFilter = "Apps" && Category = 'Board Item Categories'.App) || (locBoardItemCategoryFilter = "Files" && Category = 'Board Item Categories'.File) || (locBoardItemCategoryFilter = "Yammer" && Category = 'Board Item Categories'.Yammer) ), txtFindBoardItems.Text, "msft_name", "msft_description" ) ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) * lblNestedGalleryCardHeight.Text + If( CountRows( Search( Filter( colSelectedBoardItems, locBoardItemCategoryFilter = "All" && true || (locBoardItemCategoryFilter = "Websites" && Category = 'Board Item Categories'.Website) || (locBoardItemCategoryFilter = "Teams" && Category = 'Board Item Categories'.'Teams team') || (locBoardItemCategoryFilter = "Channels" && Category = 'Board Item Categories'.'Teams channel') || (locBoardItemCategoryFilter = "Tabs" && Category = 'Board Item Categories'.'Channel tab') || (locBoardItemCategoryFilter = "Conversations" && Category = 'Board Item Categories'.Conversation) || (locBoardItemCategoryFilter = "Apps" && Category = 'Board Item Categories'.App) || (locBoardItemCategoryFilter = "Files" && Category = 'Board Item Categories'.File) || (locBoardItemCategoryFilter = "Yammer" && Category = 'Board Item Categories'.Yammer) ), txtFindBoardItems.Text, "msft_name", "msft_description" ) ) - (RoundDown( CountRows( Search( Filter( colSelectedBoardItems, locBoardItemCategoryFilter = "All" && true || (locBoardItemCategoryFilter = "Websites" && Category = 'Board Item Categories'.Website) || (locBoardItemCategoryFilter = "Teams" && Category = 'Board Item Categories'.'Teams team') || (locBoardItemCategoryFilter = "Channels" && Category = 'Board Item Categories'.'Teams channel') || (locBoardItemCategoryFilter = "Tabs" && Category = 'Board Item Categories'.'Channel tab') || (locBoardItemCategoryFilter = "Conversations" && Category = 'Board Item Categories'.Conversation) || (locBoardItemCategoryFilter = "Apps" && Category = 'Board Item Categories'.App) || (locBoardItemCategoryFilter = "Files" && Category = 'Board Item Categories'.File) || (locBoardItemCategoryFilter = "Yammer" && Category = 'Board Item Categories'.Yammer) ), txtFindBoardItems.Text, "msft_name", "msft_description" ) ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) * RoundDown( 'Board Items Screen'.Width / 350, 0 )) >= 1, Min( Value(lblNestedGalleryCardHeight.Text), CountRows( Search( Filter( colSelectedBoardItems, locBoardItemCategoryFilter = "All" && true || (locBoardItemCategoryFilter = "Websites" && Category = 'Board Item Categories'.Website) || (locBoardItemCategoryFilter = "Teams" && Category = 'Board Item Categories'.'Teams team') || (locBoardItemCategoryFilter = "Channels" && Category = 'Board Item Categories'.'Teams channel') || (locBoardItemCategoryFilter = "Tabs" && Category = 'Board Item Categories'.'Channel tab') || (locBoardItemCategoryFilter = "Conversations" && Category = 'Board Item Categories'.Conversation) || (locBoardItemCategoryFilter = "Apps" && Category = 'Board Item Categories'.App) || (locBoardItemCategoryFilter = "Files" && Category = 'Board Item Categories'.File) || (locBoardItemCategoryFilter = "Yammer" && Category = 'Board Item Categories'.Yammer) ), txtFindBoardItems.Text, "msft_name", "msft_description" ) ) - (RoundDown( CountRows( Search( Filter( colSelectedBoardItems, locBoardItemCategoryFilter = "All" && true || (locBoardItemCategoryFilter = "Websites" && Category = 'Board Item Categories'.Website) || (locBoardItemCategoryFilter = "Teams" && Category = 'Board Item Categories'.'Teams team') || (locBoardItemCategoryFilter = "Channels" && Category = 'Board Item Categories'.'Teams channel') || (locBoardItemCategoryFilter = "Tabs" && Category = 'Board Item Categories'.'Channel tab') || (locBoardItemCategoryFilter = "Conversations" && Category = 'Board Item Categories'.Conversation) || (locBoardItemCategoryFilter = "Apps" && Category = 'Board Item Categories'.App) || (locBoardItemCategoryFilter = "Files" && Category = 'Board Item Categories'.File) || (locBoardItemCategoryFilter = "Yammer" && Category = 'Board Item Categories'.Yammer) ), txtFindBoardItems.Text, "msft_name", "msft_description" ) ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) * RoundDown( 'Board Items Screen'.Width / 350, 0 )) ), 0 ) ), RoundDown( CountRows( Search( Filter( colSelectedBoardItems, locBoardItemCategoryFilter = "All" && true || (locBoardItemCategoryFilter = "Websites" && Category = 'Board Item Categories'.Website) || (locBoardItemCategoryFilter = "Teams" && Category = 'Board Item Categories'.'Teams team') || (locBoardItemCategoryFilter = "Channels" && Category = 'Board Item Categories'.'Teams channel') || (locBoardItemCategoryFilter = "Tabs" && Category = 'Board Item Categories'.'Channel tab') || (locBoardItemCategoryFilter = "Conversations" && Category = 'Board Item Categories'.Conversation) || (locBoardItemCategoryFilter = "Apps" && Category = 'Board Item Categories'.App) || (locBoardItemCategoryFilter = "Files" && Category = 'Board Item Categories'.File) || (locBoardItemCategoryFilter = "Yammer" && Category = 'Board Item Categories'.Yammer) ), txtFindBoardItems.Text, "msft_name", "msft_description" ) ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) + If( CountRows( Search( Filter( colSelectedBoardItems, locBoardItemCategoryFilter = "All" && true || (locBoardItemCategoryFilter = "Websites" && Category = 'Board Item Categories'.Website) || (locBoardItemCategoryFilter = "Teams" && Category = 'Board Item Categories'.'Teams team') || (locBoardItemCategoryFilter = "Channels" && Category = 'Board Item Categories'.'Teams channel') || (locBoardItemCategoryFilter = "Tabs" && Category = 'Board Item Categories'.'Channel tab') || (locBoardItemCategoryFilter = "Conversations" && Category = 'Board Item Categories'.Conversation) || (locBoardItemCategoryFilter = "Apps" && Category = 'Board Item Categories'.App) || (locBoardItemCategoryFilter = "Files" && Category = 'Board Item Categories'.File) || (locBoardItemCategoryFilter = "Yammer" && Category = 'Board Item Categories'.Yammer) ), txtFindBoardItems.Text, "msft_name", "msft_description" ) ) - (RoundDown( CountRows( Search( Filter( colSelectedBoardItems, locBoardItemCategoryFilter = "All" && true || (locBoardItemCategoryFilter = "Websites" && Category = 'Board Item Categories'.Website) || (locBoardItemCategoryFilter = "Teams" && Category = 'Board Item Categories'.'Teams team') || (locBoardItemCategoryFilter = "Channels" && Category = 'Board Item Categories'.'Teams channel') || (locBoardItemCategoryFilter = "Tabs" && Category = 'Board Item Categories'.'Channel tab') || (locBoardItemCategoryFilter = "Conversations" && Category = 'Board Item Categories'.Conversation) || (locBoardItemCategoryFilter = "Apps" && Category = 'Board Item Categories'.App) || (locBoardItemCategoryFilter = "Files" && Category = 'Board Item Categories'.File) || (locBoardItemCategoryFilter = "Yammer" && Category = 'Board Item Categories'.Yammer) ), txtFindBoardItems.Text, "msft_name", "msft_description" ) ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) * RoundDown( 'Board Items Screen'.Width / 350, 0 )) >= Value(lblNestedGalleryCardHeight.Text), 1, 0 ) )
WOW! That's a 145 line Items property for a gallery - in a low code platform! Pretty impressive huh? Or may be not?
Optimized version of the Items property of the flexible height gallery
- I could have simplified the above expression using the With function in the following way:
With({varBoardItems: Search( Filter( colSelectedBoardItems, locBoardItemCategoryFilter = "All" && true || (locBoardItemCategoryFilter = "Websites" && Category = 'Board Item Categories'.Website) || (locBoardItemCategoryFilter = "Teams" && Category = 'Board Item Categories'.'Teams team') || (locBoardItemCategoryFilter = "Channels" && Category = 'Board Item Categories'.'Teams channel') || (locBoardItemCategoryFilter = "Tabs" && Category = 'Board Item Categories'.'Channel tab') || (locBoardItemCategoryFilter = "Conversations" && Category = 'Board Item Categories'.Conversation) || (locBoardItemCategoryFilter = "Apps" && Category = 'Board Item Categories'.App) || (locBoardItemCategoryFilter = "Files" && Category = 'Board Item Categories'.File) || (locBoardItemCategoryFilter = "Yammer" && Category = 'Board Item Categories'.Yammer) ), txtFindBoardItems.Text, "msft_name", "msft_description" )}, LastN( FirstN( varBoardItems, RoundDown( CountRows( varBoardItems ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) * lblNestedGalleryCardHeight.Text + If( CountRows( varBoardItems ) - (RoundDown( CountRows( varBoardItems ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) * RoundDown( 'Board Items Screen'.Width / 350, 0 )) >= 1, Min( Value(lblNestedGalleryCardHeight.Text), CountRows( varBoardItems ) - (RoundDown( CountRows( varBoardItems ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) * RoundDown( 'Board Items Screen'.Width / 350, 0 )) ), 0 ) ), RoundDown( CountRows( varBoardItems ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) + If( CountRows( varBoardItems ) - (RoundDown( CountRows( varBoardItems ) / RoundDown( 'Board Items Screen'.Width / 350, 0 ), 0 ) * RoundDown( 'Board Items Screen'.Width / 350, 0 )) >= Value(lblNestedGalleryCardHeight.Text), 1, 0 ) ))
That's still a 82 line Items property! This highlights how awesome the With function is, it brought the number of lines to almost half!
Polishing the galleries
- The only thing after this was to adjust the heights of the galleries so there is no scroll bar for both of them.
- Based on an article I wrote on how to calculate the height of flexible height galleries (link below under recent articles), I set the height of the flexible height gallery to:
Sum( Self.AllItems, lblCardHeight_BoardItem.Text ) + 60
The text property of lblCardHeight_BoardItem is set to
lblFlexHeight_BoardItem.Y + lblFlexHeight_BoardItem.Height
where lblFlexHeight_BoardItem is the lowest control in the flexible height gallery.
- And finally, although the parent blank vertical gallery isn't flexible height, it contains a series of embedded flexible height galleries. So to avoid it from having a scrollbar, all I needed to do was to figure out the height of the longest/tallest flexible height gallery. For this, I set the height of the parent gallery to:
Max( Self.AllItems, lblNestedGalleryHeight.Text )
The text property of lblNestedGalleryHeight is set to:
Sum( galBoardItems_Nested.AllItems, Value(lblCardHeight_BoardItem.Text) ) + 60
Summary
Let me clarify that this wasn't easy. It took me almost 10 hours to figure it out. And the fact that I took out my pen and notepad should tell you something! So why did I share this experience with you all? Would you ever want to reuse this pattern? May be not (although I hope you will).
I had two main goals:
- To show that nothing is impossible with Power Apps. Ok there might be some things that aren't, but more often than not, we give in too easily without trying.
- To highlight that most makers (I am guilty too) do not pay enough attention to UI/UX and if you do, you can do wonders.
- And last, once you decide to overcome a difficult challenge, how to go about solving the problem. Power Apps being a low code platform, it allows you to make changes and test the changes right away (what I call instant gratification)! Usage of labels to debug is something I do multiple times with every app that I make and I used that technique a lot with this app too.
So here's hoping we pay more attention to UI/UX, and dig deep to overcome design challenges with Power Apps!
That’s super awesome @Hardit. Always amazed at how you explain things in detail. Really appreciate your time and effort to share with us.
Thank you Jeeten for your kind words! I am glad you find the information useful!
This is great! Great way to change up presenting information. 🙂
Thank you Susan, am glad you liked it!
Pretty amazing stuff Hardin. Everytime I think something is not possible it gets done by amazing makers like you.
Thanks a lot Matt! This one was definitely an interesting challenge, one that I also thought wasn’t possible when I set out to do.