 Hello everyone and welcome. I hope you're enjoying EmberCom so far and thank you for joining me today. We're going to be talking about how you can build a Pinterest-like masonry layout using technologies such as modifiers and CSS Grid. But first, a quick introduction. My name is Jordan Hawker. I'm the staff front-end engineer at LinkedIn and tech lead for developer experience within my organization. This is my dog Cooper. He's an Australian cattle dog. He's five years old and I love him dearly and he's going to be helping us out with our talk today. So keep an eye out. He might be playing a little peek-a-boo today. Now I work for the LinkedIn Talent Solutions UI infrastructure team and we enable our teams to ship applications faster with enterprise grade quality by providing them with the tools they need to work more efficiently and productively. We support three Ember applications and nearly 60 internal add-ons and we're focused on a number of areas including application platform, developer experience, testing infrastructure, and operational excellence. Now let's talk a little bit about masonry layouts which you may know as the Pinterest layout. Now a masonry layout is similar to a strict grid layout where one access does have that very strict grid setup but the other access, the masonry access, eliminates any space between items so instead of sticking to that strict grid, usually this is vertical space so that you have that nice layout where no matter how tall each item is, the next item below it is going to come up right underneath it without any major gaps. Now why did I care about solving this particular problem? Well last year I was working on a side project where I wanted to build a sort of vintage newspaper layout so I wanted a multi-column layout where these articles would finish at any unknown length and then the next article would be begin below it right where the article finished so in any given column as articles had different links I wouldn't necessarily know where that article would end and where the next one began so I started exploring ways to build this sort of masonry layout to eliminate the gaps between articles and have this nice newspaper look to it. So let's think about how we might implement a masonry layout. Many of you may know about the masonry javascript library found on github at desandro slash masonry. It was first released in 2010 and it's one of the most popular javascript solutions for masonry layouts today. In fact it has over 400,000 downloads a month on npm and part of its popularity came from a strong integration with jQuery over the last 10 years however the library itself is 2,500 lines of code unminified whereas the solution I'm going to show you today takes less than 150 lines of code to implement in ember. Additionally the masonry js library was last released in 2018 so it hasn't received any modern updates to its implementation Now let's look at a website called christianhammerstad.com which uses the masonry js library to implement a grid layout. Now as you can see from this recording as you resize the page the content in the grid does get shifted around as the number of columns change and the library recalculates where things need to be positioned. However you can also see that this recalculation takes some time and so content gets squished together or you get a lot of blank space between the items until the library catches up and shifts the content into its new locations. So how could we build something a little bit more lightweight? Now you may have seen a lot of demos on the internet of trying to accomplish a masonry layout using only css and html and a lot of these demos look great and they get you really close to the masonry solution that we're looking for. However there's a number of assumptions that get built into these approaches often a fixed number of columns or the utilization of media queries to adjust the number of columns based on specific known break points in the container size. Additionally you can use flexbox to implement this sort of solution and the way you would do that is to set the flex direction to be vertical along the columns rather than along the rows. The problem here is that that makes the ordering of items difficult because the items themselves would actually flow down the column instead of flowing horizontally the way you would expect them to so your content may not be in the order that you would expect with this sort of solution. So what is it that we're actually trying to accomplish with a perfect masonry layout? Well first of all we want to be able to account for a dynamic number of columns so no matter what size the container of our grid is we want to be able to fill that space with the correct number of columns based on the width of our content in each column. We also want to make sure that we're eliminating any unused space between items along the masonry axis and we want this solution to be fully responsive with a performant and efficient reflow of content as the size of the container is changed. So let's look at Pinterest itself. Now I'm sure many of you are familiar with this product and it's probably the most popular and well-known instance of a masonry layout on the internet today. Now if you look at this recording you do see that the content is getting shifted around and the relative order is more or less staying the same as it shifts things from more columns to less columns and moves them down. However you can also tell that you get into situations where there's some horizontal scrolling involved as you make the page smaller and as you make the page larger there's a lot of white space around it until they recalculate where everything needs to go and then re-render the page. So let's look under the hood and understand a little bit more how they're accomplishing this. Now Pinterest does their grid layout via explicit CSS transforms so they look at the height and width of each item in their grid and then they look at the overall height and width of the container for the entire grid and they use JavaScript to calculate the exact position that each item in the grid needs to be and you can see here that they've set explicit transforms to translate each item by a number of pixels along the x and y axis so that everything gets laid out nicely on the grid. Now where this comes up short is that they have noticeable delays in the way that the content gets reflowed because they have to recalculate the position of every element in the grid every time the page resizes in order to update those transforms. Now this is a computationally heavy approach because JavaScript is slow compared to native HTML and CSS features. However in the case of Pinterest as a product it's possible that resize performance is not a priority for them. If this isn't something that their users do often resizing the home page they may not have had a reason to prioritize that performance in their solution. However if we're thinking about a more generic approach to a masonry layout that could be used across any sort of product situations we should care about the performance of the reflow because we can't predict whether the users of somebody trying to implement this solution are going to be the kind of users that are going to resize the page if it's a product that's going to be resized often that sort of thing. So CSS grid comes onto the scene and really helps us get very close to solving this problem. First of all it provides us with fully responsive content reflow that is highly configurable so CSS grid as a browser feature allows us to set the width of columns it allows us to set the height of rows it allows us to set the gap between the columns and rows and you can even set individual pieces of content to span multiple columns or multiple rows which is going to be very important to our solution today. Because it's a native browser feature it has great performance and there's no expensive calculations that need to happen in javascript however CSS grid doesn't yet support masonry it expects consistent content sizes that align to the set heights of each row and the width of each column so we're not quite there with being able to just do masonry with CSS grid however there is a property of CSS grid called masonry that has been worked on it's part of the CSS grid three specification which is currently in editor's draft however you can see from this grid here that the feature is still experimental and it's not fully implemented in any of the major browsers so you wouldn't want to use it in production but can we still try to build some sort of performance masonry layout today the answer is what i've been working on so i have a project that i've been working on since last year called ember photo gallery and you can actually check out this demo live today at ember dash photo dash gallery dot jhawk.co and as you can see from this recording what i've been able to build here will actually efficiently reflow the content in the grid as you resize the page it adjusts the number of columns and it is able to shift the content around without any noticeable delays in terms of deciding where everything needs to be positioned so let's dive in a little bit to understand how i thought about approaching this problem and what some of this implementation looks like so the first thing i wanted to do was design a masonry grid component that provided a generic api for exposing this feature to any sort of consumers and i did that utilizing css grid and ember modifiers so here's what the architecture of my ember photo gallery demo looks like at the top level we have a component called masonry grid and that component has a modifier called masonry columns that it wraps around the user content that's getting passed in now it then yields out a component called masonry item which has a its own modifier masonry rows that wraps around the individual pieces of content passed in by the consumer and that's where my specific demo comes into play because i've built a component called unsplashed photo which just lays out the content for each photo in my grid and it hooks up to an api service and then it has its own modifier internally called onload that handles the asynchronous nature of loading images in the browser so that we're able to calculate the correct layout at the right time once the images have fully loaded so let's look at what the external api of our masonry grid component looks like now first of all we have arguments that allow us to essentially configure the css grid and then we also yield out this grid item which is the masonry item component that the consumer can then use to wrap any sort of arbitrary content that's going into the grid so masonry grid allows you to configure the number of columns the width between the width of the columns the space between the columns also known as the column gap and the row gap as well so the distance between each row in our grid it also yields out the masonry item component which the consumer will use to wrap its content and the main goal that i wanted to have here was giving the consumer the full flexibility of configuring this masonry grid so i wanted to expose a number of the features of css grid to be dynamically set by the consumer and i wanted to make sure that i yielded this out in a block content api that gave the consumer the ability to define how their content would look in a way that felt ergonomic and followed best practices for ember so internally within our masonry grid template we have the masonry columns modifier which gets past a number of our arguments it gets past the number of columns or the string auto if we want it to auto adjust as the container resizes it gets past the width of the column and it gets past the gap between columns and then we yield out a hash that contains our masonry item component and then we pass the row gap argument to the masonry item component right away so before we dive more into the implementation i want to take a step back and just briefly talk about the concept of modifiers so for this demo i'm using ember modifier which is an add-on that provides an ergonomic api for dom interactions in ember apps modifiers can be applied directly to elements in the template and you can use them to mutate dom nodes or generate state from the dom so our very first modifier here is masonry columns and masonry columns is going to dynamically adjust the number of columns in our grid it's also going to account for the width of columns and the size of the gap between columns when it calculates how many columns are available and it's going to update the number of columns automatically using resize observer which is a more accurate approach than static media queries which have preset breakpoints in the size of your app let's look a little bit closer at the implementation of our modifier now as you can see this is a class-based modifier it has a number of getters that set the grid width and the number of columns and you can see i've just quickly circled the uh calculation that takes place to determine the number of columns all we're doing here is looking at the width of the container itself and we're dividing it by the width of the column plus the gap between columns to figure out how much of that how many times that space can be repeated within the container before we uh exceed the size of the container itself then in the constructor you can see that we're setting up a resize observer which is going to go ahead and run the calculations that we need when the container gets resized one thing i do want to note here is that if you want a performant resize observer you can actually create one centralized within your application and reuse it for all resize behavior that you need because any number of elements can be observed by a single resize observer uh however for the purposes of simplicity within this demo i've just gone ahead and made a new resize observer within the modifier itself and then at the bottom here in our organized grid columns method which gets called by the resize observer you can see that we're setting two styles on the element the first one is the most important because that's our grid template columns style which tells us how many columns that we have and what the width of each column is and then also our grid column gap which configures the gap between columns so these are properties of css grid that we're setting within the modifier which allows us to interact with the DOM so let's look at our masonry item component which is really just a simple wrapper around the user's content it applies a modifier to that wrapper and it also handles some async behavior which we'll come back to so the template again is very simple it applies this masonry rows modifier on our wrapper div uh we pass the row gap in and then we pass it a promise which handles the async behavior and then we're going to yield out an action to the consumer that also will get used for the async behavior that we'll cover later let's look a little bit closer at masonry rows because this is really where the magic of our masonry implementation takes place it's going to do a number of things first it's going to calculate the number of rows that each content element spans accounting for the gap between the elements as well and what we're going to do here is we're going to set the row size to one pixel so that we get a pixel perfect grid layout let me show you what i mean by that in our css we set grid auto rows to one pixel and what this does is it tells css grid that the height of each row is exactly one pixel so every single row in the grid is one pixel tall and that may seem counterintuitive but that's what allows us to make this masonry layout work because as you can see in the modifier implementation itself what we're going to do is calculate the height of the element in pixels add the row gap to it and then set the number of rows equal to the height plus gap in pixels so for example if you had a piece of content that was 200 pixels tall and your row gap was set to say five pixels then we would set the grid row and style to span 205 rows and since we have set a row to be one pixel tall then we know that we can span 205 rows so let's add some data to our masonry grid on the consumer side i've used the unsplash api which allows me to retrieve up to 30 images at once and for this demo i went ahead and queried on the keyword ember because why not i'm also going to load the images asynchronously so even though the api itself is of course asynchronous we're also going to have to account for the fact that once we set the url on the image elements there is asynchronous loading of the image itself in the browser so there's not a lot you need to care about in this data service this is pretty much just interfacing with the unsplash api i built a method that retrieves random photos and then uh returns the response from the api to our content uh that we're having set up here so the masonry item template i want to come back to this again we touched on it briefly but there's two things that i want to note here one is that the masonry rows modifier is being passed this dot on load which is a promise we are going to define in the masonry item component we have also passed an action and yielded it out to the consumer and this action allows us to as a consumer to pass a promise back to our wrapper component now if you look closer at the component class we have this dot on load being set in the constructor and this is just a standard promise where we're capturing the resolve part of the promise and when we get the promise from the consumer via the action we're going to chain the resolution of that promise to the resolution of our internal promise and what this does is when the consumer tells us that their async behavior is concluded and resolves their promise then we're going to resolve our promise which we passed to the masonry rows modifier and the masonry rows modifier is going to wait for that promise to calculate the height of the content so that you ensure that the calculation happens at the right time and doesn't happen too early if you were to not handle the async behavior there then there's a possibility that your grid would end up squished vertically because you wouldn't have the correct calculations of the height if there was any sort of asynchronous delay in the loading of the content that would change the height of the actual elements so let's look a little closer at unsplash photo which is our content component this is just a very simple container for the photo and its metadata but it also handles the async image loading that we were just talking about so on the left here we have our class which just handles the action that was passed in and this action gets passed to an onload modifier which as you can see is being used on the image element itself so the onload modifier is what handles our load events for images and as you can see in here all it does is create a promise that then gets passed back out to the unsplash photo component and that promise resolves when the load event on the element gets fired so we're going to add an event listener onto our image element and we're waiting for the load event which tells us that the image is fully loaded in the browser when that happens we're resolving this promise which then allows the entire promise chain to resolve and allows us to correctly calculate the height of our content and that's it we've done everything we needed to build masonry grid and to consume it for the demo so a quick review of the way our architecture is laid out we have the masonry grid component which exposes the main api for configuring the grid itself we have masonry columns as a modifier which configures the columns of our css grid and this gets wrapped around the entire content and then we have our masonry item component which gets yielded out to the consumer and it applies a masonry rose modifier around each item in the grid and the masonry rose modifier calculates the height of the content in order to make that masonry layout work now we have the unsplash photo component which is the consumer side content and it hooks up to a data service for api and then we have the onload modifier which just handles the async loading of the image in the browser and kicks off the resolution of that promise chain which goes up to the masonry item component and then back down to the masonry rose modifier and lets the masonry rose modifier know that it can now safely calculate the height of the content in order to set up that layout so let's take another look at the final product here which again you can check out for yourself at emberphotogallery.jhawk.co i'm not going to show the resizing behavior here but what i want to really show you is that as you refresh the page and load in different image content the grid is quickly laying out in whatever order it needs to in order to fill this full masonry layout quickly so no matter how tall each of the individual images are this is able to performantly lay out the content because we're mostly leveraging the native css grid feature in order to keep it performant the only calculations we do on the javascript side are a one-time calculation of the height of the content when the content first loads and we set that as the css property telling us how many rows are spanned by each individual piece of content everything else is handled by css grid itself which keeps the solution performant and allows us to ensure that all of the content reflow is very efficient no matter how often we're resizing the content or how often we're changing the content in the grid itself let's talk a little bit about what your takeaways should be from today now there's a number of technologies that we've used that you can take back to your product development at home and at work and utilize in your everyday development process first of all css grid is a great feature for modern grid layouts in the browser it allows you to configure any number of aspects of the grid including the size of the columns and rows the gaps between each column or each row as well as whether any individual element spans one or more columns or one or more rows which as you saw was important for solving the masonry layout today now css grid 3 is coming soon which is great because once that lands it's going to include a specification for native masonry property on css grid and that will actually eliminate a lot of the work that we had to do today to make our masonry layout work because now masonry layout will work natively in the browser but until that lands and becomes a feature that is available in all the major browsers we can still implement that today using modifiers now modifiers are great because you can attach them to any element in a template and easily make DOM modifications or generate state from the DOM one of the things I love about modifiers is that they give you a nice barrier between the two-way nature of browser event bindings and the one-way nature of embers state flow so that's an important concept to use modifiers to make sure that the logic of your ember application stays very clear and that the state flow is very easy to understand while still enabling you to interface with native browser features now if you'd like to check out the code from today's talk you can go to elwayman o2 slash ember-photo-gallery and you can also check out the demo of that code at ember-photo-gallery.jhoc.co and that's everything I have for you today I want to thank you all for coming to my talk if you'd like to connect with me online you can find me on twitter at axle health fire or on github at elwayman zero two and if you'd like to check out the slides from this talk you can go to jhoc.co slash ember conf 21 thank you all I hope you enjoyed the talk and I hope you enjoy the rest of the conference