Tag: from

Mondrian Art in CSS From 5 Code Artists

Mondrian is famous for paintings with big thick black lines forming a grid, where each cell is white, red, yellow, or blue. This aesthetic pairs well with the notoriously rectangular web, and that hasn’t gone unnoticed over the years with CSS developers. I saw some Mondrian Art in CSS going around the other day and figured I’d go looking for others I’ve seen over the years and round them up.

Vasilis van Gemert:
What if Mondrian used CSS instead of paint?

Many people have tried to recreate a work of art by Mondriaan with CSS. It seems like a nice and simple exercise: rectangles are easy with CSS, and now with grid, it is easy to recreate most of his works. I tried it as well, and it turned out to be a bit more complicated than I thought. And the results are, well, surprising.

Screenshot of a webpage with a large serif font in various sizes reading What if Mondrian Used CSS instead of Paint? above two paragraphs discussing Mondrian Art in CSS.

Jen Simmons Lab:
Mondrian Art in CSS Grid

I love how Jen went the extra mile with the texture. Like most of these examples, CSS grid is used heavily.

Mondrian Art in CSS Grid from Jen Simmons. Includes rough grungy texture across the entire piece.

Jen Schiffer:
var t;: Piet Mondrian

I started with Mondrian not because he is my favorite artist (he is not), or that his work is very recognizeable (it is), but because I thought it would be a fun (yes) and easy start (lol nope) to this project.

Mondrian Art in CSS randomized 12 times in a 4 by 3 grid of boxes. A bright yellow header is above the grid bearing the site title: var t.

Riley Wong:
Make Your Own Mondrian-Style Painting with Code

There is a 12-step tutorial on GitHub.

Adam Fuhrer:
CSS Mondrian

Generative Piet Mondrian style art using CSS grid.

Screenshot of a full page Mondrian art example. There is a refresh button centered at the bottom of the page.

John Broers:
CSS Mondriaan Grid

An example of Mondrian Art in CSS with a "Generate New" option. The example is a square box with plenty of padding around it on the white background page.

Mondrian Art in CSS From 5 Code Artists originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , ,

What Would it Take to Prevent CSS Tooltips From Overflowing?

Say you have an elements with CSS tooltips and you’re going to position those tooltips such that it opens up next to the element on hover (or probably better: when clicked/tapped). Next to it where? Above it? What if the element is already really close to the top of the screen? In that case, it should probably open below it. Or vice versa — and the same goes for the left and right edges of the screen. You definitely want it to be visible rather than overflowing the viewport.

Sometimes when you open new UI elements, they need to be edge-aware to prevent the content inside from triggering weird scrollbars, or worse, cutting off content.

A red button and an orange button, both with CSS tooltips, sitting above two large paragraphs of text. The orange button is hovered, revealing a tooltip to the right of it but it is cut off by the edge of the viewport, making the content illegible.
Very important what?!

This is an age-old problem on the web. I remember using jQuery UI tooltips on purpose because it had this special ability to be edge-aware. You can imagine the JavaScript behind it. You figure out where the element is going to be and use positioning math to figure out if it will be within the viewport. If it won’t be, try a different position that does fit.

As ever, everything old is new again. Check out Floating UI, designed just for this problem.

FloatingUI home screen showing a logo that looks like a CSS tooltip with a happy face.

Floating UI is a low-level toolkit to position floating elements while intelligently keeping them in view. Tooltips, popovers, dropdowns, menus, and more.

It looks super well done. I like the focus, the demos are super well done, and it’s a pretty tiny dependency.

But ya know what would be even cooler? If CSS could do this all by itself. That’s the vibe with CSS Anchored Positioning — for now just an “explainer” document:

When building interactive components or applications, authors frequently want to leverage UI elements that can render in a “top-layer”. Examples of such UI elements include content pickers, teaching UI, tooltips, and menus. “Enabling Popups” introduced a new popup element to make many of these top-layer elements easier to author.

Authors frequently wish to “pin” or “anchor” such top-layer UI to a point on another element, referred to here as an “anchor element”. How the top-layer UI is positioned with respect to its anchor element is further influenced or constrained by the edges of the layout viewport.

A four-by-four grid showing the same blue button positioned at different corners of each cell, and a tooltip that avoids the edge of the screen where the button sits.

I love it. The web platform at its best. Seeing what authors are needing to do and reaching for libraries to do, and trying to step in and do it natively (and hopefully better).


What Would it Take to Prevent CSS Tooltips From Overflowing? originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , , ,
[Top]

Learn From Remix

I’ve built websites that are used by millions of people all over the world. I’ve made more mistakes than I care to count and I’ve had to deal with the repercussions of those mistakes for years thereafter. Through all of this, my team and I have been trying to strike the best balance between user and developer experience. We have built custom solutions and used libraries/tools all in an effort to solve problems with our user experience and increase our own productivity. The struggle is real.

Then Remix came around and reduced that struggle for me a great deal. I rebuilt my website with Remix and was blown away. Honestly, I felt like I could create an amazing user experience and not be ashamed of the code it took to get me there. I love Remix so much that I eventually joined the team (so: disclaimer). In case you haven’t heard of Remix before, it’s a web framework for building stellar user experiences that “remixes” the web as it has worked since the 90s with the awesome technology we have today. Here are a few of the things I love most about it:

  • Seamless client-server code: I mean, there’s definitely a separation between what runs on the client and what runs on the server and it’s clear, but it’s so easy to move between the two within the same file that I feel like I can say “Yes” to more product feature ideas.
  • Progressive enhancement: Remix allows me to #useThePlatform better than anything else I’ve used. Their use of web APIs means that the better I get at Remix, the better I get at the web. And because apps I build with Remix work without JavaScript, I get real progressive enhancement for poor network conditions where the JavaScript takes a long time to load or fails to load entirely.
  • CSS — Bringing the cascade back: Because Remix allows me to easily control which of my CSS files is on the page at any given time, I don’t have all the problems that triggered the JavaScript community to invent workarounds like CSS-in-JS.
  • Nested routing: This allows Remix to optimize the data requests it makes as the user navigates around the page (which means it’s faster and cheaper for users who pay for limited internet). It also allows me to handle errors in the context of the part of the app that fail without taking down the entire page in the process.
  • Simple mutations: Instead of a complicated JavaScript library for managing mutations, Remix just uses the platform <form />. And Remix manages your client-cache so you don’t have to worry about invalidating your cache at all. In fact, with Remix you don’t think about this at all. It’s managed for you and you just get a nice declarative API.
  • Normalized platforms: We have lots of options for where we deploy our apps. Remix normalizes these (kinda like jQuery for platforms). So whether you want to deploy to serverless, cloudflare workers, or a regular node app, it doesn’t matter. Just write the same code and deploy wherever you like.

There’s a lot to love about Remix, but I’ll wrap it up here.

I realize that not everyone can migrate their site over to Remix, and that’s ok. The tagline of Remix is: Build better websites (sometimes with Remix). The one thing I think I want to encourage you to do to make your website better is to learn about and from Remix and apply some of the ideas to your website. And if you can migrate to Remix, all the better. 😆

Remember, we’re all just trying to make the world a little bit better, and my hope in writing this is that I’ve given you ideas of how you can make your own corner of the world better. Good luck!

CSS-Tricks

, ,
[Top]

Streaming Optimized Videos From AWS S3 in Minutes

(This is a sponsored post.)

Videos appeal to humans in a way no other form of the content does. A video includes motion, music, still images, text, speech, and a few other elements, all of which combine to deliver engagement like never before.

According to research, users spend 88% more time on a website with videos, and video content receives 1200% more shares than images or text. This is corroborated by the trend we see on key social media platforms, with businesses on these platforms now preferring video content over still images.

When streaming videos on one’s website or app, hosting a video on a platform like YouTube might not be a viable option. This is especially true if you want to present a native-looking user experience and control it, or you do not want to share revenues with a third party.

In such cases, cloud providers like AWS become the go-to choice for hosting and streaming videos.

Streaming optimized videos from S3 — current methods and drawbacks

A typical setup includes hosting the videos on AWS S3, the most popular cloud object storage around, and then directly streaming them on the users’ device.

For an even better video load time, you can use AWS CloudFront CDN with your S3 bucket.

Typical video streaming setup with AWS S3 and CloudFront CDN without any video optimizations

However, given that the original videos are often massive in size and that neither S3 nor CloudFront have built-in video optimization capabilities, this is far from ideal.

Therefore, the video streaming is slow and results in unnecessary waste of bandwidth, lower average video playback time, and lower user retention and revenues.

To solve this, we need to optimize videos for different devices, network speeds, and our website or app layout before streaming from the S3 storage.

While you could use a solution like AWS Elemental MediaConvert for all kinds of transcoding and optimizations, such solutions often have a steep learning curve and often become very complex to manage. For example:

  1. You need to understand how to set up these tools with your existing workflow and storage, understand job creation in these tools, and manage job queues and job completion notifications in your workflow.
  2. To create MediaConvert jobs, you would have to understand different streaming protocols, encoding, formats, resolution, codecs, etc., for video streaming and map them to your requirements to build the right jobs.
  3. AWS MediaConvert is not an on-the-fly video conversion tool in itself. You need to pair it with other AWS services to make it work in real-time, making the setup even more complex and expensive. If you do not opt for on-the-fly video conversion, you need to configure MediaConvert’s jobs correctly from the go. Any new requirement will require the job to be changed and re-run on all videos to generate a new encoding or format. With an ever-changing video streaming landscape coupled with new device sizes, this can be a severe limitation if you have a lot of videos coming in regularly.
< Description: Typical setup with on-the-fly video conversion with AWS MediaConvert involving multiple components. (Source: AWS)

With videos becoming the choice of media online, it should be easier to stream videos from your S3 storage so that you can focus on delivering a better user experience and the core business rather than understanding the intricacies of video streaming.

This is where ImageKit comes in.

ImageKit is a complete media management and delivery platform with a Video API that allows you to stream videos stored in AWS S3 and optimize and transform them in real-time with just a few minutes of setup.

That’s right. You will be able to deliver videos perfected for every device without learning about the complications of video streaming and without spending hours configuring a tool.

ImageKit + AWS S3 video streaming setup with on-the-fly video optimizations

Let’s look at how you can use ImageKit for streaming videos from your S3 bucket.

S3 video streaming with ImageKit

To be able to stream videos from in our S3 bucket with ImageKit, we would need to do two things:

  1. Allow ImageKit to read the objects in the S3 bucket
  2. Access the video in the S3 bucket via their ImageKit URLs

Let’s look at these two steps in detail.

1. Connecting your bucket with ImageKit for access

ImageKit can pull assets from all popular storage and servers, including private and public S3 buckets. Access to a private S3 bucket is done using a pair of read-only keys that you create in your AWS account. You can read more about adding the S3 bucket origin in detail here.

If your bucket or the video objects inside it are public, you can add it as a web folder type origin. To do this, you would need to use the S3 bucket’s domain name as the base URL for this origin.

Also, ensure that you map the S3 bucket to a URL endpoint in the ImageKit dashboard.

That’s it! You have now connected your S3 bucket with ImageKit.

Bonus: ImageKit also provides an integrated Media Library, file storage built on AWS S3, with easy-to-use UI and APIs for advanced media management and search. If you do not have an S3 bucket or want a better solution to organize your video content, you can host your videos in ImageKit’s Media Library as well.

2. Video Streaming from S3 using ImageKit URLs

With your S3 bucket now attached to ImageKit, you can now access any video file in your bucket via ImageKit.

For example, we have the video sample.mp4 in our test bucket at the key videos/woman-walking.mp4. We can access it with ImageKit using the following URL:

https://ik.imagekit.io/ikmedia/videos/woman-walking.mp4

When we open this URL in a browser tab, the browser will stream the video via ImageKit.

We can now use this URL in a <video> tag to stream videos on our web page.

<video src="https://ik.imagekit.io/ikmedia/videos/woman-walking.mp4" controls></video>

And because ImageKit comes integrated with AWS CloudFront CDN, your videos will get delivered in milliseconds to your users, thereby improving the overall streaming experience.

Using video optimizations and transformations with S3 streaming

We have seen how to stream videos from S3 using ImageKit on our apps. However, as mentioned earlier in the article, we need to optimize videos for different devices, website layout, and other conditions.

ImageKit helps us there as well. It can optimize and scale your videos or encode them to different formats in real-time. In addition, ImageKit makes most of the necessary optimizations automatically, using the best defaults, or exposes the functionalities as easy-to-use URL parameters that work in real-time.

So, unlike the other tools like AWS MediaConvert, there is minimal learning you need to do. And you definitely won’t have to learn the nuances of video streaming and encoding.

Let’s look at the video optimizations and transformations ImageKit is capable of, apart from direct streaming of videos from S3.

1. Automatic conversion to best format while streaming

ImageKit automatically identifies the video formats supported in browsers and combines them with the original video information to encode it to the best possible output format.

The video format optimization is done in real-time when you request your S3 video via ImageKit and results in a smaller video size sent to the user.

To enable this automatic format conversion, you have to enable the corresponding setting in your ImageKit dashboard. No complex setup, no configurations needed.

For example, after enabling the above setting, an MP4 video gets delivered in WebM format in Chrome and MP4 in Safari.

Same video delivered in WebM format in Chrome browser

2. Automatic compression of videos while streaming

Modern cameras capture videos that can easily run into a few 100MBs, if not GBs. It is not ideal to deliver such large video files to your users. It will take them ages to stream the video, results in a poor user experience, and impact your business.

Therefore, we need to compress the video to deliver it quickly to our users. However, we need to do so while maintaining the visual quality of the video.

That is again something ImageKit does for you automatically.

You can turn on the corresponding setting from the ImageKit dashboard and set the default compression level for your videos. By default, this value would be 50, which strikes a good balance between output size and visual quality.

Turning on automatic quality optimization in the dashboard

After enabling this setting, ImageKit will encode your video at the specified compression level before streaming it to the user’s device.

Because of the compression, though the following video has the same format as its original video, it is lighter by almost 15% at 12.6MB compared to the original video at 14.6MB.

Using real-time video compression for slow network users

As explained with examples later in this article, ImageKit allows you to transform videos in real-time. One of the real-time URL transformations possible in ImageKit is to modify the compression level.

This transformation allows us to override the default compression level selected in the dashboard.

The video below, for example, is compressed to a quality level of 30 and is almost 70% smaller than the video we encoded using the default quality level of 50 set in the dashboard.

This real-time compression-level transformation allows us to adapt our videos to users on slow networks.

For example, if someone is experiencing poor network conditions, you can stream a more compressed, lighter video to them.

3. Scaling S3 videos in real-time while streaming

Imagine you want to load a video on your page and have only a 200px width available for your content. The resolution of the original video you have, almost always, would be a lot higher than this size. Loading anything significantly larger than the size you require is a waste of bandwidth and offers no benefit to the user.

With ImageKit, you can scale the video to any size in real-time before streaming it on the device. Just like its real-time image transformation API, you can add a width or height parameter to the URL, and you will get a video with the required dimensions in real-time.

For example, we have scaled down our video to a width of 200px using the URL given below.

https://ik.imagekit.io/ikmedia/videos/woman-walking.mp4?tr=w-200

4. Adapting videos to different placeholders and creating vertical videos

We often shoot videos in landscape mode, i.e., the video’s width is greater than its height. However, you would often require a portrait or vertical video where the height is greater than the video’s width.

A very common use case would be converting your landscape video to a vertical one like an Instagram story.

ImageKit makes it super simple with its real-time video transformations. You can add the width and height parameters to the video URL and get the output video in the requested size.

https://ik.imagekit.io/ikmedia/videos/woman-walking.mp4?tr=w-300,h-500

ImageKit streams the video in the most suitable format and at the right compression level for all the transformations above. The video is delivered via its integrated AWS CloudFront CDN for a fast load time.

For the demo video used in this article, we started with a 14.6MB original video, but after all the optimizations and scaling it down, we were able to bring down the size to 1.8MB in the last example of the vertical video.

Signup with ImageKit for S3 video streaming for free

ImageKit offers a Forever Free plan that includes video delivery as well as processing. On this plan, you would be able to optimize and transform over 15 minutes of fresh video content every month and then deliver it to thousands of users without paying a single penny or even providing your credit card. This is perfect for a small business or your side project where you can connect your S3 bucket to ImageKit and start streaming optimized videos immediately.

Sign up now for ImageKit to start streaming optimized videos from S3 for free.

CSS-Tricks

, , , ,
[Top]

Animation Techniques for Adding and Removing Items From a Stack

Animating elements with CSS can either be quite easy or quite difficult depending on what you are trying to do. Changing the background color of a button when you hover over it? Easy. Animating the position and size of an element in a performant way that also affects the position of other elements? Tricky! That’s exactly what we’ll get into here in this article.

A common example is removing an item from a stack of items. The items stacked on top need to fall downwards to account for the space of an item removed from the bottom of the stack. That is how things behave in real life, and users may expect this kind of life-like motion on a website. When it doesn’t happen, it’s possible the user is confused or momentarily disorientated. You expect something to behave one way based on life experience and get something completely different, and users may need extra time to process the unrealistic movement.

Here is a demonstration of a UI for adding items (click the button) or removing items (click the item).

You could paper over the poor UI slightly by adding a “fade out” animation or something, but the result won’t be that great, as the list will will abruptly collapse and cause those same cognitive issues.

Applying CSS-only animations to a dynamic DOM event (adding brand new elements and fully removing elements) is extremely tricky work. We’re going to face this problem head-on and go over three very different types of animations that handle this, all accomplishing the same goal of helping users understand changes to a list of items. By the time we’re done, you’ll be armed to use these animations, or build your own based on the concepts.

We will also touch upon accessibility and how elaborate HTML layouts can still retain some compatibility with accessibility devices with the help of ARIA attributes.

The Slide-Down Opacity Animation

A very modern approach (and my personal favorite) is when newly-added elements fade-and-float into position vertically depending on where they are going to end up. This also means the list needs to “open up” a spot (also animated) to make room for it. If an element is leaving the list, the spot it took up needs to contract.

Because we have so many different things going on at the same time, we need to change our DOM structure to wrap each .list-item in a container class appropriately titled .list-container. This is absolutely essential in order to get our animation to work.

<ul class="list">   <li class="list-container">     <div class="list-item">List Item</div>   </li>   <li class="list-container">     <div class="list-item">List Item</div>   </li>   <li class="list-container">     <div class="list-item">List Item</div>   </li>   <li class="list-container">     <div class="list-item">List Item</div>   </li> </ul>  <button class="add-btn">Add New Item</button>

Now, the styling for this is unorthodox because, in order to get our animation effect to work later on, we need to style our list in a very specific way that gets the job done at the expense of sacrificing some customary CSS practices.

.list {   list-style: none; } .list-container {   cursor: pointer;   font-size: 3.5rem;   height: 0;   list-style: none;   position: relative;   text-align: center;   width: 300px; } .list-container:not(:first-child) {   margin-top: 10px; } .list-container .list-item {   background-color: #D3D3D3;   left: 0;   padding: 2rem 0;   position: absolute;   top: 0;   transition: all 0.6s ease-out;   width: 100%; } .add-btn {   background-color: transparent;   border: 1px solid black;   cursor: pointer;   font-size: 2.5rem;   margin-top: 10px;   padding: 2rem 0;   text-align: center;   width: 300px; }

How to handle spacing

First, we’re using margin-top to create vertical space between the elements in the stack. There’s no margin on the bottom so that the other list items can fill the gap created by removing a list item. That way, it still has margin on the bottom even though we have set the container height to zero. That extra space is created between the list item that used to be directly below the deleted list item. And that same list item should move up in reaction to the deleted list item’s container having zero height. And because this extra space expands the vertical gap between the list items further then we want it to. So that’s why we use margin-top — to prevent that from happening.

But we only do this if the item container in question isn’t the first one in the list. That’s we used :not(:first-child) — it targets all of the containers except the very first one (an enabling selector). We do this because we don’t want the very first list item to be pushed down from the top edge of the list. We only want this to happen to every subsequent item thereafter instead because they are positioned directly below another list item whereas the first one isn’t.

Now, this is unlikely to make complete sense because we are not setting any elements to zero height at the moment. But we will later on, and in order to get the vertical spacing between the list elements correct, we need to set the margin like we do.

A note about positioning

Something else that is worth pointing out is the fact that the .list-item elements nested inside of the parent .list-container elements are set to have a position of absolute, meaning that they are positioned outside of the DOM and in relation to their relatively-positioned .list-container elements. We do this so that we can get the .list-item element to float upwards when removed, and at the same time, get the other .list-item elements to move and fill the gap that removing this .list-item element has left. When this happens, the .list-container element, which isn’t positioned absolute and is therefore affected by the DOM, collapses its height allowing the other .list-container elements to fill its place, and the .list-item element — which is positioned with absolute — floats upwards, but doesn’t affect the structure of the list as it isn’t affected by the DOM.

Handling height

Unfortunately, we haven’t yet done enough to get a proper list where the individual list-items are stacked one by one on top of each other. Instead, all we will be able to see at the moment is just a single .list-item that represents all of the list items piled on top of each other in the exact same place. That’s because, although the .list-item elements may have some height via their padding property, their parent elements do not, but have a height of zero instead. This means that we don’t have anything in the DOM that is actually separating these elements out from each other because in order to do that, we would need our .list-item containers to have some height because, unlike their child element, they are affected by the DOM.

To get the height of our list containers to perfectly match the height of their child elements, we need to use JavaScript. So, we store all of our list items within a variable. Then, we create a function that is called immediately as soon as the script is loaded.

This becomes the function that handles the height of the list container elements:

const listItems = document.querySelectorAll('.list-item');  function calculateHeightOfListContainer(){ };  calculateHeightOfListContainer();

The first thing that we do is extract the very first .list-item element from the list. We can do this because they are all the same size, so it doesn’t matter which one we use. Once we have access to it, we store its height, in pixels, via the element’s clientHeight property. After this, we create a new <style> element that is prepended to the document’s body immediately after so that we can directly create a CSS class that incorporates the height value we just extracted. And with this <style> element safely in the DOM, we write a new .list-container class with styles that automatically have priority over the styles declared in the external stylesheet since these styles come from an actual <style> tag. That gives the .list-container classes the same height as their .list-item children.

const listItems = document.querySelectorAll('.list-item');  function calculateHeightOfListContainer() {   const firstListItem = listItems[0];   let heightOfListItem = firstListItem.clientHeight;   const styleTag = document.createElement('style');   document.body.prepend(styleTag);   styleTag.innerHTML = `.list-container{     height: $ {heightOfListItem}px;   }`; };  calculateHeightOfListContainer();

Showing and Hiding

Right now, our list looks a little drab — the same as the what we saw in the first example, just without any of the addition or removal logic, and styled in a completely different way to the list constructed from <ul> and <li> tags list that were used in that opening example.

Four light gray rectangular boxes with the words list item. The boxes are stacked vertically, one on top of the other. Below the bottom box is another box with a white background and thin black border that is a button with a label that says add new item.

We’re going to do something now that may seem inexplicable at the moment and modify our .list-container and .list-item classes. We’re also creating extra styling for both of these classes that will only be added to them if a new class, .show, is used in conjunction with both of these classes separately.

The purpose we’re doing this is to create two states for both the .list-container and the .list-item elements. One state is without the .show classes on both of these elements, and this state represents the elements as they are animated out from the list. The other state contains the .show class added to both of these elements. It represents the specified .list-item as firmly instantiated and visible in the list.

In just a bit, we will switch between these two states by adding/removing the .show class from both the parent and the container of a specific .list-item. We’ll combined that with a CSS transition between these two states.

Notice that combining the .list-item class with the .show class introduces some extra styles to things. Specifically, we’re introducing the animation that we are creating where the list item fades downwards and into visibility when it is added to the list — the opposite happens when it is removed. Since the most performant way to animate elements positions is with the transform property, that is what we will use here, applying opacity along the way to handle the visibility part. Because we already applied a transition property on both the .list-item and the .list-container elements, a transition automatically takes place whenever we add or remove the .show class to both of these elements due to the extra properties that the .show class brings, causing a transition whenever we either add or remove these new properties.

.list-container {   cursor: pointer;   font-size: 3.5rem;   height: 0;   list-style: none;   position: relative;   text-align: center;   width: 300px; } .list-container.show:not(:first-child) {   margin-top: 10px; } .list-container .list-item {   background-color: #D3D3D3;   left: 0;   opacity: 0;   padding: 2rem 0;   position: absolute;   top: 0;   transform: translateY(-300px);   transition: all 0.6s ease-out;   width: 100%; } .list-container .list-item.show {   opacity: 1;   transform: translateY(0); }

In response to the .show class, we are going back to our JavaScript file and changing our only function so that the .list-container element are only given a height property if the element in question also has a .show class on it as well, Plus, we are applying a transition property to our standard .list-container elements, and we will do it in a setTimeout function. If we didn’t, then our containers would animate on the initial page load when the script is loaded, and the heights are applied the first time, which isn’t something we want to happen.

const listItems = document.querySelectorAll('.list-item'); function calculateHeightOfListContainer(){   const firstListItem = listItems[0];   let heightOfListItem = firstListItem.clientHeight;   const styleTag = document.createElement('style');   document.body.prepend(styleTag);   styleTag.innerHTML = `.list-container.show {     height: $ {heightOfListItem}px;   }`;   setTimeout(function() {     styleTag.innerHTML += `.list-container {       transition: all 0.6s ease-out;     }`;   }, 0); }; calculateHeightOfListContainer();

Now, if we go back and view the markup in DevTools, then we should be able to see that the list has disappeared and all that is left is the button. The list hasn’t disappeared because these elements have been removed from the DOM; it has disappeared because of the .show class which is now a required class that must be added to both the .list-item and the .list-container elements in order for us to be able to view them.

The way to get the list back is very simple. We add the .show class to all of our .list-container elements as well as the .list-item elements contained inside. And once this is done we should be able to see our pre-created list items back in their usual place.

<ul class="list">   <li class="list-container show">     <div class="list-item show">List Item</div>   </li>   <li class="list-container show">     <div class="list-item show">List Item</div>   </li>   <li class="list-container show">     <div class="list-item show">List Item</div>   </li>   <li class="list-container show">     <div class="list-item show">List Item</div>   </li> </ul>  <button class="add-btn">Add New Item</button>

We won’t be able to interact with anything yet though because to do that — we need to add more to our JavaScript file.

The first thing that we will do after our initial function is declare references to both the button that we click to add a new list item, and the .list element itself, which is the element that wraps around every single .list-item and its container. Then we select every single .list-container element nested inside of the parent .list element and loop through them all with the forEach method. We assign a method in this callback, removeListItem, to the onclick event handler of each .list-container. By the end of the loop, every single .list-container instantiated to the DOM on a new page load calls this same method whenever they are clicked.

Once this is done, we assign a method to the onclick event handler for addBtn so that we can activate code when we click on it. But obviously, we won’t create that code just yet. For now, we are merely logging something to the console for testing.

const addBtn = document.querySelector('.add-btn'); const list = document.querySelector('.list'); function removeListItem(e){   console.log('Deleted!'); } // DOCUMENT LOAD document.querySelectorAll('.list .list-container').forEach(function(container) {   container.onclick = removeListItem; });  addBtn.onclick = function(e){   console.log('Add Btn'); } 

Starting work on the onclick event handler for addBtn, the first thing that we want to do is create two new elements: container and listItem. Both elements represent the .list-item element and their respective .list-container element, which is why we assign those exact classes to them as soon as we create the them.

Once these two elements are prepared, we use the append method on the container to insert the listItem inside of it as a child, the same as how these elements that are already in the list are formatted. With the listItem successfully appended as a child to the container, we can move the container element along with its child listItem element to the DOM with the insertBefore method. We do this because we want new items to appear at the bottom of the list but before the addBtn, which needs to stay at the very bottom of the list. So, by using the parentNode attribute of addBtn to target its parent, list, we are saying that we want to insert the element as a child of list, and the child that we are inserting (container) will be inserted before the child that is already on the DOM and that we have targeted with the second argument of the insertBefore method, addBtn.

Finally, with the .list-item and its container successfully added to the DOM, we can set the container’s onclick event handler to match the same method as every other .list-item already on the DOM by default.

addBtn.onclick = function(e){   const container = document.createElement('li');    container.classList.add('list-container');   const listItem = document.createElement('div');    listItem.classList.add('list-item');    listItem.innerHTML = 'List Item';   container.append(listItem);   addBtn.parentNode.insertBefore(container, addBtn);   container.onclick = removeListItem; }

If we try this out, then we won’t be able to see any changes to our list no matter how many times we click the addBtn. This isn’t an error with the click event handler. Things are working exactly how they should be. The .list-item elements (and their containers) are added to the list in the correct place, it is just that they are getting added without the .show class. As a result, they don’t have any height to them, which is why we can’t see them and is why it looks like nothing is happening to the list.

To get each newly added .list-item to animate into the list whenever we click on the addBtn, we need to apply the .show class to both the .list-item and its container, just as we had to do to view the list items already hard-coded into the DOM.

The problem is that we cannot just add the .show class to these elements instantly. If we did, the new .list-item statically pops into existence at the bottom of the list without any animation. We need to register a few styles before the animation additional styles that override those initial styles for an element to know what transition to make. Meaning, that if we just apply the .show class to are already in place — so no transition.

The solution is to apply the .show classes in a setTimeout callback, delaying the activation of the callback by 15 milliseconds, or 1.5/100th of a second. This imperceptible delay is long enough to create a transition from the proviso state to the new state that is created by adding the .show class. But that delay is also short enough that we will never know that there was a delay in the first place.

addBtn.onclick = function(e){   const container = document.createElement('li');    container.classList.add('list-container');   const listItem = document.createElement('div');    listItem.classList.add('list-item');    listItem.innerHTML = 'List Item';   container.append(listItem);   addBtn.parentNode.insertBefore(container, addBtn);   container.onclick = removeListItem;   setTimeout(function(){     container.classList.add('show');      listItem.classList.add('show');   }, 15); }

Success! It is now time to handle how we remove list items when they are clicked.

Removing list items shouldn’t be too hard now because we have already gone through the difficult task of adding them. First, we need to make sure that the element we are dealing with is the .list-container element instead of the .list-item element. Due to event propagation, it is likely that the target that triggered this click event was the .list-item element.

Since we want to deal with the associated .list-container element instead of the actual .list-item element that triggered the event, we’re using a while-loop to loop one ancestor upwards until the element held in container is the .list-container element. We know it works when container gets the .list-container class, which is something that we can discover by using the contains method on the classList property of the container element.

Once we have access to the container, we promptly remove the .show class from both the container and its .list-item once we have access to that as well.

function removeListItem(e) {   let container = e.target;   while (!container.classList.contains('list-container')) {     container = container.parentElement;   }   container.classList.remove('show');   const listItem = container.querySelector('.list-item');   listItem.classList.remove('show'); }

And here is the finished result:

Accessibility & Performance

Now you may be tempted to just leave the project here because both list additions and removals should now be working. But it is important to keep in mind that this functionality is only surface level and there are definitely some touch ups that need to be made in order to make this a complete package.

First of all, just because the removed elements have faded upwards and out of existence and the list has contracted to fill the gap that it has left behind does not mean that the removed element has been removed from the DOM. In fact, it hasn’t. Which is a performance liability because it means that we have elements in the DOM that serve no purpose other than to just accumulate in the background and slow down our application.

To solve this, we use the ontransitionend method on the container element to remove it from the DOM but only when the transition caused by us removing the .show class has finished so that its removal couldn’t possibly interrupt our transition.

function removeListItem(e) {   let container = e.target;   while (!container.classList.contains('list-container')) {     container = container.parentElement;   }   container.classList.remove('show');   const listItem = container.querySelector('.list-item');   listItem.classList.remove('show');   container.ontransitionend = function(){     container.remove();   } }

We shouldn’t be able to see any difference at this point because allwe did was improve the performance — no styling updates.

The other difference is also unnoticeable, but super important: compatibility. Because we have used the correct <ul> and <li> tags, devices should have no problem with correctly interpreting what we have created as an unordered list.

Other considerations for this technique

A problem that we do have however, is that devices may have a problem with the dynamic nature of our list, like how the list can change its size and the number of items that it holds. A new list item will be completely ignored and removed list items will be read as if they still exist.

So, in order to get devices to re-interpret our list whenever the size of it changes, we need to use ARIA attributes. They help get our nonstandard HTML list to be recognized as such by compatibility devices. That said, they are not a guaranteed solution here because they are never as good for compatibility as a native tag. Take the <ul> tag as an example — no need to worry about that because we were able to use the native unordered list element.

We can use the aria-live attribute to the .list element. Everything nested inside of a section of the DOM marked with aria-live becomes responsive. In other words, changes made to an element with aria-live is recognized, allowing them to issue an updated response. In our case, we want things highly reactive and we do that be setting the aria live attribute to assertive. That way, whenever a change is detected, it will do so, interrupting whatever task it was currently doing at the time to immediately comment on the change that was made.

<ul class="list" role="list" aria-live="assertive">

The Collapse Animation

This is a more subtle animation where, instead of list items floating either up or down while changing opacity, elements instead just collapse or expand outwards as they gradually fade in or out; meanwhile, the rest of the list repositions itself to the transition taking place.

The cool thing about the list (and perhaps some remission for the verbose DOM structure we created), would be the fact that we can change the animation very easily without interfering with the main effect.

So, to achieve this effect, we start of by hiding overflow on our .list-container. We do this so that when the .list-container collapses in on itself, it does so without the child .list-item flowing beyond the list container’s boundaries as it shrinks. Apart from that, the only other thing that we need to do is remove the transform property from the .list-item with the .show class since we don’t want the .list-item to float upwards anymore.

.list-container {   cursor: pointer;   font-size: 3.5rem;   height: 0;   overflow: hidden;   list-style: none;   position: relative;   text-align: center;   width: 300px; } .list-container.show:not(:first-child) {   margin-top: 10px; } .list-container .list-item {   background-color: #D3D3D3;   left: 0;   opacity: 0;   padding: 2rem 0;   position: absolute;   top: 0;   transition: all 0.6s ease-out;   width: 100%; } .list-container .list-item.show {   opacity: 1; }

The Side-Slide Animation

This last animation technique is strikingly different fromithe others in that the container animation and the .list-item animation are actually out of sync. The .list-item is sliding to the right when it is removed from the list, and sliding in from the right when it is added to the list. There needs to be enough vertical room in the list to make way for a new .list-item before it even begins animating into the list, and vice versa for the removal.

As for the styling, it’s very much like the Slide Down Opacity animation, only thing that the transition for the .list-item should be on the x-axis now instead of the y-axis.

.list-container {   cursor: pointer;   font-size: 3.5rem;   height: 0;   list-style: none;   position: relative;   text-align: center;   width: 300px; } .list-container.show:not(:first-child) {   margin-top: 10px; } .list-container .list-item {   background-color: #D3D3D3;   left: 0;   opacity: 0;   padding: 2rem 0;   position: absolute;   top: 0;   transform: translateX(300px);   transition: all 0.6s ease-out;   width: 100%; } .list-container .list-item.show {   opacity: 1;   transform: translateX(0); }

As for the onclick event handler of the addBtn in our JavaScript, we’re using a nested setTimeout method to delay the beginning of the listItem animation by 350 milliseconds after its container element has already started transitioning.

setTimeout(function(){   container.classList.add('show');    setTimeout(function(){     listItem.classList.add('show');   }, 350); }, 10);

In the removeListItem function, we remove the list item’s .show class first so it can begin transitioning immediately. The parent container element then loses its .show class, but only 350 milliseconds after the initial listItem transition has already started. Then, 600 milliseconds after the container element starts to transition (or 950 milliseconds after the listItem transition), we remove the container element from the DOM because, by this point, both the listItem and the container transitions should have come to an end.

function removeListItem(e){   let container = e.target;   while(!container.classList.contains('list-container')){     container = container.parentElement;   }   const listItem = container.querySelector('.list-item');   listItem.classList.remove('show');   setTimeout(function(){     container.classList.remove('show');     container.ontransitionend = function(){       container.remove();     }   }, 350); }

Here is the end result:

That’s a wrap!

There you have it, three different methods for animating items that are added and removed from a stack. I hope that with these examples you are now confident to work in a situation where the DOM structure settles into a new position in reaction to an element that has either been added or removed from the DOM.

As you can see, there’s a lot of moving parts and things to consider. We started with that we expect from this type of movement in the real world and considered what happens to a group of elements when one of them is updated. It took a little balancing to transition between the showing and hiding states and which elements get them at specific times, but we got there. We even went so far as to make sure our list is both performant and accessible, things that we’d definitely need to handle on a real project.

Anyway, I wish you all the best in your future projects. And that’s all from me. Over and out.


The post Animation Techniques for Adding and Removing Items From a Stack appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , ,
[Top]

From a Single Repo, to Multi-Repos, to Monorepo, to Multi-Monorepo

I’ve been working on the same project for several years. Its initial version was a huge monolithic app containing thousands of files. It was poorly architected and non-reusable, but was hosted in a single repo making it easy to work with. Later, I “fixed” the mess in the project by splitting the codebase into autonomous packages, hosting each of them on its own repo, and managing them with Composer. The codebase became properly architected and reusable, but being split across multiple repos made it a lot more difficult to work with.

As the code was reformatted time and again, its hosting in the repo also had to adapt, going from the initial single repo, to multiple repos, to a monorepo, to what may be called a “multi-monorepo.”

Let me take you on the journey of how this took place, explaining why and when I felt I had to switch to a new approach. The journey consists of four stages (so far!) so let’s break it down like that.

Stage 1: Single repo

The project is leoloso/PoP and it’s been through several hosting schemes, following how its code was re-architected at different times.

It was born as this WordPress site, comprising a theme and several plugins. All of the code was hosted together in the same repo.

Some time later, I needed another site with similar features so I went the quick and easy way: I duplicated the theme and added its own custom plugins, all in the same repo. I got the new site running in no time.

I did the same for another site, and then another one, and another one. Eventually the repo was hosting some 10 sites, comprising thousands of files.

A single repository hosting all our code.

Issues with the single repo

While this setup made it easy to spin up new sites, it didn’t scale well at all. The big thing is that a single change involved searching for the same string across all 10 sites. That was completely unmanageable. Let’s just say that copy/paste/search/replace became a routine thing for me.

So it was time to start coding PHP the right way.

Stage 2: Multirepo

Fast forward a couple of years. I completely split the application into PHP packages, managed via Composer and dependency injection.

Composer uses Packagist as its main PHP package repository. In order to publish a package, Packagist requires a composer.json file placed at the root of the package’s repo. That means we are unable to have multiple PHP packages, each of them with its own composer.json hosted on the same repo.

As a consequence, I had to switch from hosting all of the code in the single leoloso/PoP repo, to using multiple repos, with one repo per PHP package. To help manage them, I created the organization “PoP” in GitHub and hosted all repos there, including getpop/root, getpop/component-model, getpop/engine, and many others.

In the multirepo, each package is hosted on its own repo.

Issues with the multirepo

Handling a multirepo can be easy when you have a handful of PHP packages. But in my case, the codebase comprised over 200 PHP packages. Managing them was no fun.

The reason that the project was split into so many packages is because I also decoupled the code from WordPress (so that these could also be used with other CMSs), for which every package must be very granular, dealing with a single goal.

Now, 200 packages is not ordinary. But even if a project comprises only 10 packages, it can be difficult to manage across 10 repositories. That’s because every package must be versioned, and every version of a package depends on some version of another package. When creating pull requests, we need to configure the composer.json file on every package to use the corresponding development branch of its dependencies. It’s cumbersome and bureaucratic.

I ended up not using feature branches at all, at least in my case, and simply pointed every package to the dev-master version of its dependencies (i.e. I was not versioning packages). I wouldn’t be surprised to learn that this is a common practice more often than not.

There are tools to help manage multiple repos, like meta. It creates a project composed of multiple repos and doing git commit -m "some message" on the project executes a git commit -m "some message" command on every repo, allowing them to be in sync with each other.

However, meta will not help manage the versioning of each dependency on their composer.json file. Even though it helps alleviate the pain, it is not a definitive solution.

So, it was time to bring all packages to the same repo.

Stage 3: Monorepo

The monorepo is a single repo that hosts the code for multiple projects. Since it hosts different packages together, we can version control them together too. This way, all packages can be published with the same version, and linked across dependencies. This makes pull requests very simple.

The monorepo hosts multiple packages.

As I mentioned earlier, we are not able to publish PHP packages to Packagist if they are hosted on the same repo. But we can overcome this constraint by decoupling development and distribution of the code: we use the monorepo to host and edit the source code, and multiple repos (at one repo per package) to publish them to Packagist for distribution and consumption.

The monorepo hosts the source code, multiple repos distribute it.

Switching to the Monorepo

Switching to the monorepo approach involved the following steps:

First, I created the folder structure in leoloso/PoP to host the multiple projects. I decided to use a two-level hierarchy, first under layers/ to indicate the broader project, and then under packages/, plugins/, clients/ and whatnot to indicate the category.

Showing the HitHub repo for a project called PoP. The screen in is dark mode, so the background is near black and the text is off-white, except for blue links.
The monorepo layers indicate the broader project.

Then, I copied all source code from all repos (getpop/engine, getpop/component-model, etc.) to the corresponding location for that package in the monorepo (i.e. layers/Engine/packages/engine, layers/Engine/packages/component-model, etc).

I didn’t need to keep the Git history of the packages, so I just copied the files with Finder. Otherwise, we can use hraban/tomono or shopsys/monorepo-tools to port repos into the monorepo, while preserving their Git history and commit hashes.

Next, I updated the description of all downstream repos, to start with [READ ONLY], such as this one.

Showing the GitHub repo for the component-model project. The screen is in dark mode, so the background is near black and the text is off-white, except for blue links. There is a sidebar to the right of the screen that is next to the list of files in the repo. The sidebar has an About heading with a description that reads: Read only, component model for Pop, over which the component-based architecture is based." This is highlighted in red.
The downstream repo’s “READ ONLY” is located in the repo description.

I executed this task in bulk via GitHub’s GraphQL API. I first obtained all of the descriptions from all of the repos, with this query:

{   repositoryOwner(login: "getpop") {     repositories(first: 100) {       nodes {         id         name         description       }     }   } }

…which returned a list like this:

{   "data": {     "repositoryOwner": {       "repositories": {         "nodes": [           {             "id": "MDEwOlJlcG9zaXRvcnkxODQ2OTYyODc=",             "name": "hooks",             "description": "Contracts to implement hooks (filters and actions) for PoP"           },           {             "id": "MDEwOlJlcG9zaXRvcnkxODU1NTQ4MDE=",             "name": "root",             "description": "Declaration of dependencies shared by all PoP components"           },           {             "id": "MDEwOlJlcG9zaXRvcnkxODYyMjczNTk=",             "name": "engine",             "description": "Engine for PoP"           }         ]       }     }   } }

From there, I copied all descriptions, added [READ ONLY] to them, and for every repo generated a new query executing the updateRepository GraphQL mutation:

mutation {   updateRepository(     input: {       repositoryId: "MDEwOlJlcG9zaXRvcnkxODYyMjczNTk="       description: "[READ ONLY] Engine for PoP"     }   ) {     repository {       description     }   } }

Finally, I introduced tooling to help “split the monorepo.” Using a monorepo relies on synchronizing the code between the upstream monorepo and the downstream repos, triggered whenever a pull request is merged. This action is called “splitting the monorepo.” Splitting the monorepo can be achieved with a git subtree split command but, because I’m lazy, I’d rather use a tool.

I chose Monorepo builder, which is written in PHP. I like this tool because I can customize it with my own functionality. Other popular tools are the Git Subtree Splitter (written in Go) and Git Subsplit (bash script).

What I like about the Monorepo

I feel at home with the monorepo. The speed of development has improved because dealing with 200 packages feels pretty much like dealing with just one. The boost is most evident when refactoring the codebase, i.e. when executing updates across many packages.

The monorepo also allows me to release multiple WordPress plugins at once. All I need to do is provide a configuration to GitHub Actions via PHP code (when using the Monorepo builder) instead of hard-coding it in YAML.

To generate a WordPress plugin for distribution, I had created a generate_plugins.yml workflow that triggers when creating a release. With the monorepo, I have adapted it to generate not just one, but multiple plugins, configured via PHP through a custom command in plugin-config-entries-json, and invoked like this in GitHub Actions:

- id: output_data   run: |     echo "quot;::set-output name=plugin_config_entries::$ (vendor/bin/monorepo-builder plugin-config-entries-json)"

This way, I can generate my GraphQL API plugin and other plugins hosted in the monorepo all at once. The configuration defined via PHP is this one.

class PluginDataSource {   public function getPluginConfigEntries(): array   {     return [       // GraphQL API for WordPress       [         'path' => 'layers/GraphQLAPIForWP/plugins/graphql-api-for-wp',         'zip_file' => 'graphql-api.zip',         'main_file' => 'graphql-api.php',         'dist_repo_organization' => 'GraphQLAPI',         'dist_repo_name' => 'graphql-api-for-wp-dist',       ],       // GraphQL API - Extension Demo       [         'path' => 'layers/GraphQLAPIForWP/plugins/extension-demo',         'zip_file' => 'graphql-api-extension-demo.zip',         'main_file' =>; 'graphql-api-extension-demo.php',         'dist_repo_organization' => 'GraphQLAPI',         'dist_repo_name' => 'extension-demo-dist',       ],     ];   } }

When creating a release, the plugins are generated via GitHub Actions.

Dark mode screen in GitHub showing the actions for the project.
This figure shows plugins generated when a release is created.

If, in the future, I add the code for yet another plugin to the repo, it will also be generated without any trouble. Investing some time and energy producing this setup now will definitely save plenty of time and energy in the future.

Issues with the Monorepo

I believe the monorepo is particularly useful when all packages are coded in the same programming language, tightly coupled, and relying on the same tooling. If instead we have multiple projects based on different programming languages (such as JavaScript and PHP), composed of unrelated parts (such as the main website code and a subdomain that handles newsletter subscriptions), or tooling (such as PHPUnit and Jest), then I don’t believe the monorepo provides much of an advantage.

That said, there are downsides to the monorepo:

  • We must use the same license for all of the code hosted in the monorepo; otherwise, we’re unable to add a LICENSE.md file at the root of the monorepo and have GitHub pick it up automatically. Indeed, leoloso/PoP initially provided several libraries using MIT and the plugin using GPLv2. So, I decided to simplify it using the lowest common denominator between them, which is GPLv2.
  • There is a lot of code, a lot of documentation, and plenty of issues, all from different projects. As such, potential contributors that were attracted to a specific project can easily get confused.
  • When tagging the code, all packages are versioned independently with that tag whether their particular code was updated or not. This is an issue with the Monorepo builder and not necessarily with the monorepo approach (Symfony has solved this problem for its monorepo).
  • The issues board needs proper management. In particular, it requires labels to assign issues to the corresponding project, or risk it becoming chaotic.
Showing the list of reported issues for the project in GitHub in dark mode. The image shows just how crowded and messy the screen looks when there are a bunch of issues from different projects in the same list without a way to differentiate them.
The issues board can become chaotic without labels that are associated with projects.

All these issues are not roadblocks though. I can cope with them. However, there is an issue that the monorepo cannot help me with: hosting both public and private code together.

I’m planning to create a “PRO” version of my plugin which I plan to host in a private repo. However, the code in the repo is either public or private, so I’m unable to host my private code in the public leoloso/PoP repo. At the same time, I want to keep using my setup for the private repo too, particularly the generate_plugins.yml workflow (which already scopes the plugin and downgrades its code from PHP 8.0 to 7.1) and its possibility to configure it via PHP. And I want to keep it DRY, avoiding copy/pastes.

It was time to switch to the multi-monorepo.

Stage 4: Multi-monorepo

The multi-monorepo approach consists of different monorepos sharing their files with each other, linked via Git submodules. At its most basic, a multi-monorepo comprises two monorepos: an autonomous upstream monorepo, and a downstream monorepo that embeds the upstream repo as a Git submodule that’s able to access its files:

A giant red folder illustration is labeled as the downstream monorepo and it contains a smaller green folder showing the upstream monorepo.
The upstream monorepo is contained within the downstream monorepo.

This approach satisfies my requirements by:

  • having the public repo leoloso/PoP be the upstream monorepo, and
  • creating a private repo leoloso/GraphQLAPI-PRO that serves as the downstream monorepo.
The same illustration as before, but now the large folder is a bright pink and is labeled as with the project name, and the smaller folder is a purplish-blue and labeled with the name of the public downstream module,.
A private monorepo can access the files from a public monorepo.

leoloso/GraphQLAPI-PRO embeds leoloso/PoP under subfolder submodules/PoP (notice how GitHub links to the specific commit of the embedded repo):

This figure show how the public monorepo is embedded within the private monorepo in the GitHub project.

Now, leoloso/GraphQLAPI-PRO can access all the files from leoloso/PoP. For instance, script ci/downgrade/downgrade_code.sh from leoloso/PoP (which downgrades the code from PHP 8.0 to 7.1) can be accessed under submodules/PoP/ci/downgrade/downgrade_code.sh.

In addition, the downstream repo can load the PHP code from the upstream repo and even extend it. This way, the configuration to generate the public WordPress plugins can be overridden to produce the PRO plugin versions instead:

class PluginDataSource extends UpstreamPluginDataSource {   public function getPluginConfigEntries(): array   {     return [       // GraphQL API PRO       [         'path' => 'layers/GraphQLAPIForWP/plugins/graphql-api-pro',         'zip_file' => 'graphql-api-pro.zip',         'main_file' => 'graphql-api-pro.php',         'dist_repo_organization' => 'GraphQLAPI-PRO',         'dist_repo_name' => 'graphql-api-pro-dist',       ],       // GraphQL API Extensions       // Google Translate       [         'path' => 'layers/GraphQLAPIForWP/plugins/google-translate',         'zip_file' => 'graphql-api-google-translate.zip',         'main_file' => 'graphql-api-google-translate.php',         'dist_repo_organization' => 'GraphQLAPI-PRO',         'dist_repo_name' => 'graphql-api-google-translate-dist',       ],       // Events Manager       [         'path' => 'layers/GraphQLAPIForWP/plugins/events-manager',         'zip_file' => 'graphql-api-events-manager.zip',         'main_file' => 'graphql-api-events-manager.php',         'dist_repo_organization' => 'GraphQLAPI-PRO',         'dist_repo_name' => 'graphql-api-events-manager-dist',       ],     ];   } }

GitHub Actions will only load workflows from under .github/workflows, and the upstream workflows are under submodules/PoP/.github/workflows; hence we need to copy them. This is not ideal, though we can avoid editing the copied workflows and treat the upstream files as the single source of truth.

To copy the workflows over, a simple Composer script can do:

{   "scripts": {     "copy-workflows": [       "php -r "copy('submodules/PoP/.github/workflows/generate_plugins.yml', '.github/workflows/generate_plugins.yml');"",       "php -r "copy('submodules/PoP/.github/workflows/split_monorepo.yaml', '.github/workflows/split_monorepo.yaml');""     ]   } }

Then, each time I edit the workflows in the upstream monorepo, I also copy them to the downstream monorepo by executing the following command:

composer copy-workflows

Once this setup is in place, the private repo generates its own plugins by reusing the workflow from the public repo:

This figure shows the PRO plugins generated in GitHub Actions.

I am extremely satisfied with this approach. I feel it has removed all of the burden from my shoulders concerning the way projects are managed. I read about a WordPress plugin author complaining that managing the releases of his 10+ plugins was taking a considerable amount of time. That doesn’t happen here—after I merge my pull request, both public and private plugins are generated automatically, like magic.

Issues with the multi-monorepo

First off, it leaks. Ideally, leoloso/PoP should be completely autonomous and unaware that it is used as an upstream monorepo in a grander scheme—but that’s not the case.

When doing git checkout, the downstream monorepo must pass the --recurse-submodules option as to also checkout the submodules. In the GitHub Actions workflows for the private repo, the checkout must be done like this:

- uses: actions/checkout@v2   with:     submodules: recursive

As a result, we have to input submodules: recursive to the downstream workflow, but not to the upstream one even though they both use the same source file.

To solve this while maintaining the public monorepo as the single source of truth, the workflows in leoloso/PoP are injected the value for submodules via an environment variable CHECKOUT_SUBMODULES, like this:

env:   CHECKOUT_SUBMODULES: "";  jobs:   provide_data:     steps:       - uses: actions/checkout@v2         with:           submodules: $ {{ env.CHECKOUT_SUBMODULES }} 

The environment value is empty for the upstream monorepo, so doing submodules: "" works well. And then, when copying over the workflows from upstream to downstream, I replace the value of the environment variable to "recursive" so that it becomes:

env:   CHECKOUT_SUBMODULES: "recursive" 

(I have a PHP command to do the replacement, but we could also pipe sed in the copy-workflows composer script.)

This leakage reveals another issue with this setup: I must review all contributions to the public repo before they are merged, or they could break something downstream. The contributors would also completely unaware of those leakages (and they couldn’t be blamed for it). This situation is specific to the public/private-monorepo setup, where I am the only person who is aware of the full setup. While I share access to the public repo, I am the only one accessing the private one.

As an example of how things could go wrong, a contributor to leoloso/PoP might remove CHECKOUT_SUBMODULES: "" since it is superfluous. What the contributor doesn’t know is that, while that line is not needed, removing it will break the private repo.

I guess I need to add a warning!

env:   ### ☠️ Do not delete this line! Or bad things will happen! ☠️   CHECKOUT_SUBMODULES: "" 

Wrapping up

My repo has gone through quite a journey, being adapted to the new requirements of my code and application at different stages:

  • It started as a single repo, hosting a monolithic app.
  • It became a multirepo when splitting the app into packages.
  • It was switched to a monorepo to better manage all the packages.
  • It was upgraded to a multi-monorepo to share files with a private monorepo.

Context means everything, so there is no “best” approach here—only solutions that are more or less suitable to different scenarios.

Has my repo reached the end of its journey? Who knows? The multi-monorepo satisfies my current requirements, but it hosts all private plugins together. If I ever need to grant contractors access to a specific private plugin, while preventing them to access other code, then the monorepo may no longer be the ideal solution for me, and I’ll need to iterate again.

I hope you have enjoyed the journey. And, if you have any ideas or examples from your own experiences, I’d love to hear about them in the comments.


The post From a Single Repo, to Multi-Repos, to Monorepo, to Multi-Monorepo appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Learnings From a WebPageTest Session on CSS-Tricks

I got together with Tim Kadlec from over at WebPageTest the other day to use do a bit of performance testing on CSS-Tricks. Essentially use the tool, poke around, and identify performance pain points to work on. You can watch the video right here on the site, or over on their Twitch channel, which is worth a subscribe for more performance investigations like these.

Web performance work is twofold:

Step 1) Measure Things & Explore Problems
Step 2) Fix it

Tim and I, through the amazing tool that is WebPageTest, did a lot of Step 1. I took notes as we poked around. We found a number of problem areas, some fairly big! Of course, after all that, I couldn’t get them out of my head, so I had to spring into action and do the Step 2 stuff as soon as I could, and I’m happy to report I’ve done most of it and seen improvement. Let’s dig in!

Identified Problem #1) Poor LCP

Largest Contentful Paint (LCP) is one of the Core Web Vitals (CWV), which everyone is carefully watching right now with Google telling us it’s an SEO factor. My LCP was clocking in at 3.993s which isn’t great.

WebPageTest clearly tells you if there are problems with your CWV.

I also learned from time that it’s ideal if the First Contentful Paint (FCP) contains the LCP. We could see that wasn’t happening through WebPageTest.

Things to fix:

  • Make sure the LCP area, which was ultimately a big image, is properly optimized, has a responsive srcset, and is CDN hosted. All those things were failing on that particular image despire working elsewhere.
  • The LCP image had loading="lazy" on it, which we just learned isn’t a good place for that.

Fixing technique and learnings:

  • All the proper image handling stuff was in place, but for whatever reason, none of it works for .gif files, which is what that image was the day of the testing. We probably just shouldn’t use .gif files for that area anyway.
  • Turn off lazy loading of LCP image. This is a WordPress featured image, so I essentially had to do <?php the_post_thumbnail('', array('loading' => 'eager')); ?>. If it was an inline image, I’d do <img data-no-lazy="1" ... /> which tells WordPress what it needs to know.

Identified Problem #2) First Byte to Start Render gap

Tim saw this right away as a fairly obvious problem.

In the waterfall above (here’s a super detailed article on reading waterfalls from Matt Hobbs), you can see the HTML arrives in about 0.5 seconds, but the start of rendering (what people see, big green line), doesn’t start until about 2.9 seconds. That’s too dang long.

The chart also identifies the problem in a yellow line. I was linking out to a third-party CSS file, which then redirects to my own CSS files that contain custom fonts. That redirect costs time, and as we dug into, not just first-page-load time, but every single page load, even cached page loads.

Things to fix:

  • Eliminate the CSS file redirect.
  • Self-host fonts.

Fixing technique and learnings:

  • I’ve been eying up some new fonts anyway. I noted not long ago that I really love Mass-Driver’s licensing innovation (priced by # of employees), but I equally love MD Primer, so I bought that. For body type, I stuck with a comfortable serif with Blanco, which mercifully came with very nicely optimized RIBBI1 versions. Next time I swear I’m gonna find a variable font, but hey, you gotta follow your heart sometimes. I purchased these, and am now self-hosting the font-files.
  • Use @font-face right in my own CSS, with no redirects. Also using font-display: swap;, but gotta work a bit more on that loading technique. Can’t wait for size-adjust.

After re-testing with the change in place, you can see on a big article page the start render is a full 2 seconds faster on a 4G connection:

That’s a biiiiiig change. Especially as it affects cached page loads too.
See how the waterfall pulls back to the left without the CSS redirect.

Identified Problem #3) CLS on the Grid Guide is Bad

Tim had a neat trick up his sleeve for measuring Cumulative Layout Shift (CLS) on pages. You can instruct WebPageTest to scroll down the page for you. This is important for something like CLS, because layout shifting might happen on account of scrolling.

See this article about CLS and WebPageTest.

The trick is using an advanced setting to inject custom JavaScript into the page during the test:

At this point, we were testing not the homepage, but purposefully a very important page: our Complete Guide to Grid. With this in place, you can see the CWV are in much worse shape:

I don’t know what to think exactly about the LCP. That’s being triggered by what happens to be the largest image pretty far down the page.

I’m not terribly worried about the LCP with the scrolling in place. That’s just some image like any other on the page, lazily loaded.

The CLS is more concerning, to me, because any shifting layout is always obnoxious to users. See all these dotted orange lines? That is CLS happening:

The orange CLS lines correlate with images loading (as the page scrolls down and the lazy loaded images come in).

Things to fix:

  • CLS is bad because of lazy loaded images coming in and shifting the layout.

Fixing technique and learnings:

  • I don’t know! All those images are inline <img loading="lazy" ...> elements. I get that lazy loading could cause CLS, but these images have proper width and height attributes, which is supposed to reserve the exact space necessary for the image (even when fluid, thanks to aspect ratio) even before it loads. So… what gives? Is it because they are SVG?

If anyone does know, feel free to hit me up. Such is the nature of performance work, I find. It’s a mixture of easy wins from silly mistakes, little battles you can fight and win, bigger battles that sometimes involves outside influences that are harder to win, and mysterious unknowns that it takes time to heal. Fortunately we have tools like WebPageTest to tell us the real stories happening on our site and give us the insight we need to fight these performance battles.


  1. RIBBI, I just learned, means Regular, Italic, Bold, and Bold Italic. The classic combo that most body copy on the web needs.

The post Learnings From a WebPageTest Session on CSS-Tricks appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Migrating from Parcel to Snowpack

I find build tooling endlessly interesting, especially right now as we’re in a juicy next-gen transitional period with players like Vite, wmr, Snowpack, and esbuild. Hugh Haworth has a good run-down of the new players, and we’ve chatted on ShopTalk about them several times. I especially like it when people blog their personal journeys in moving build tools, like Ben Frain has done.

It’s not like buying a new car where the new one is faster, but they both have steering wheels and doors and brake pedals and stuff. They share a similarity in that they are trying to provide DX locally and UX (via performance) on production — otherwise their approach, what they provide, and what they expect are all quite different.

Those differences mean re-training your brain in how you expect things to work. Here’s Ben on Snowpack:

In Snowpack land, your index.html file needs to reference the transformed version of the files – even though they don’t exist on your file system.

Wait, what?

Let me say that again as it’s jolly important. You link to files that don’t exist.

That’s just weird, right?

But Ben was switching from Parcel to Snowpack, and Parcel was weird too. In the Gulp days, we were super explicit about what files we were selecting, running tasks on, and where the transformed code went. In webpack, there are very explicit entry and output destinations, and it’s very focused on JavaScript being the input. But Parcel really wanted an HTML file to be the entry point and it would explore itself from there.

I always thought Parcel would have struck a nerve with the WordPress crowd harder, since you could point it at the template file where you link up your assets in a WordPress template and have it do its thing. I guess WordPress is too funky with all the wp_enqueue_style stuff and it just didn’t work?

Ben gives Snowpack a tentative thumbs up.

If you are starting a greenfield project I’d have zero qualms opting for Snowpack. It doesn’t have the depth of support documentation or stack overflow questions if you find yourself in the weeds, but generally speaking, it is solid enough to pick up and run with.

Me, next time I get a chance to play with build tools, I think my bar is going to be incredible speed. I’ve spent too much time on projects in my life where the developer experience is slow. I want smokin’ fast updates for whatever I’m working on.

Direct Link to ArticlePermalink


The post Migrating from Parcel to Snowpack appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Going From Solid to Knockout Text on Scroll

Here’s a fun CSS trick to show your friends: a large title that switches from a solid color to knockout text as the background image behind it scrolls into place. And we can do it using plain ol’ HTML and CSS!

This effect is created by rendering two containers with fixed <h1> elements. The first container has a white background with knockout text. The second container has a background image with white text. Then, using some fancy clipping tricks, we hide the first container’s text when the user scrolls beyond its boundaries and vice-versa. This creates the illusion that the text background is changing.

Before we begin, please note that this won’t work on older versions of Internet Explorer. Also, fixed background images can be cumbersome on mobile WebKit browsers. Be sure to think about fallback behavior for these circumstances.

Setting up the HTML

Let’s start by creating our general HTML structure. Inside an outer wrapper, we create two identical containers, each with an <h1> element that is wrapped in a .title_wrapper.

<header>    <!-- First container -->   <div class="container container_solid">     <div class="title_wrapper">       <h1>The Great Outdoors</h1>     </div>   </div>    <!-- Second container -->   <div class="container container_image">     <div class="title_wrapper">       <h1>The Great Outdoors</h1>     </div>   </div>  </header>

Notice that each container has both a global .container class and its own identifier class — .container_solid and .container_image, respectively. That way, we can create common base styles and also target each container separately with CSS.

Initial styles

Now, let’s add some CSS to our containers. We want each container to be the full height of the screen. The first container needs a solid white background, which we can do on its .container_solid class. We also want to add a fixed background image to the second container, which we can do on its .container_image class.

.container {   height: 100vh; }  /* First container */ .container_solid {   background: white; }  /* Second container */ .container_image {   /* Grab a free image from unsplash */   background-image: url(/path/to/img.jpg);   background-size: 100vw auto;   background-position: center;   background-attachment: fixed; }

Next, we can style the <h1> elements a bit. The text inside .container_image can simply be white. However, to get knockout text for the <h1> element inside container_image, we need to apply a background image, then reach for the text-fill-color and background-clip CSS properties to apply the background to the text itself rather than the boundaries of the <h1> element. Notice that the <h1> background has the same sizing as that of our .container_image element. That’s important to make sure things line up.

.container_solid .title_wrapper h1 {   /* The text background */   background: url(https://images.unsplash.com/photo-1575058752200-a9d6c0f41945?ixlib=rb-1.2.1&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjE0NTg5fQ);   background-size: 100vw auto;   background-position: center;      /* Clip the text, if possible */   /* Including -webkit` prefix for bester browser support */   /* https://caniuse.com/text-stroke */   -webkit-text-fill-color: transparent;   text-fill-color: transparent;   -webkit-background-clip: text;   background-clip: text;      /* Fallback text color */   color: black; }  .container_image .title_wrapper h1 {   color: white; }

Now, we want the text fixed to the center of the layout. We’ll add fixed positioning to our global .title_wrapper class and tack it to the vertical center of the window. Then we use text-align to horizontally center our <h1> elements.

.header-text {   display: block;   position: fixed;    margin: auto;   width: 100%;   /* Center the text wrapper vertically */   top: 50%;   -webkit-transform: translateY(-50%);       -ms-transform: translateY(-50%);           transform: translateY(-50%); }  .header-text h1 {   text-align: center; }

At this point, the <h1> in each container should be positioned directly on top of one another and stay fixed to the center of the window as the user scrolls. Here’s the full, organized, code with some shadow added to better see the text positioning.

Clipping the text and containers

This is where things start to get really interesting. We only want a container’s <h1> to be visible when its current scroll position is within the boundaries of its parent container. Normally this can be solved using overflow: hidden; on the parent container. However, with both of our <h1> elements using fixed positioning, they are now positioned relative to the browser window, rather than the parent element. In this case using overflow: hidden; will have no effect.

For the parent containers to hide fixed overflow content, we can use the CSS clip property with absolute positioning. This tells our browser hide any content outside of an element’s boundaries. Let’s replace the styles for our .container class to make sure they don’t display any overflowing elements, even if those elements use fixed positioning.

.container {   /* Hide fixed overflow contents */   clip: rect(0, auto, auto, 0);    /* Does not work if overflow = visible */   overflow: hidden;    /* Only works with absolute positioning */   position: absolute;    /* Make sure containers are full-width and height */   height: 100vh;   left: 0;   width: 100%; }

Now that our containers use absolute positioning, they are removed from the normal flow of content. And, because of that, we need to manually position them relative to their respective parent element.

.container_solid {   /* ... */    /* Position this container at the top of its parent element */   top: 0; }  .container_image {   /* ... */  /* Position the second container below the first container */   top: 100vh; }

At this point, the effect should be taking shape. You can see that scrolling creates an illusion where the knockout text appears to change backgrounds. Really, it is just our clipping mask revealing a different <h1> element depending on which parent container overlaps the center of the screen.

Let’s make Safari happy

If you are using Safari, you may have noticed that its render engine is not refreshing the view properly when scrolling. Add the following code to the .container class to force it to refresh correctly.

.container {   /* ... */    /* Safari hack */   -webkit-mask-image: -webkit-linear-gradient(top, #ffffff 0%,#ffffff 100%); }

Here’s the complete code up to this point.

Time to clean house

Let’s make sure our HTML is following accessibility best practices. Users not using assistive tech can’t tell that there are two identical <h1> elements in our document, but those using a screen reader sure will because both headings are announced. Let’s add aria-hidden to our second container to let screen readers know it is purely decorative.

<!-- Second container --> <div class="container container_image" aria-hidden="true">   <div class="title_wrapper">     <h1>The Great Outdoors</h1>   </div> </div>

Now, the world is our oyster when it comes to styling. We are free to modify the fonts and font sizes to make the text just how we want. We could even take this further by adding a parallax effect or replacing the background image with a video. But, hey, at that point, just be sure to put a little additional work into the accessibility so those who prefer less motion get the right experience.

That wasn’t so hard, was it?


The post Going From Solid to Knockout Text on Scroll appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Dynamically Switching From One HTML Element to Another in Vue

A friend once contacted me asking if I had a way to dynamically change one HTML element into another within Vue’s template block. For instance, shifting a <div> element to a <span> element based on some criteria. The trick was to do this without relying on a series of v-if and v-else code.

I didn’t think much of it because I couldn’t see a strong reason to do such a thing; it just doesn’t come up that often. Later that same day, though, he reached out again and told me he learned how to change element types. He excitedly pointed out that Vue has a built-in component that can be used as a dynamic element in the very way that he needed.

This small feature can keep code in the template nice and tidy. It can reduce v-if and v-else glut down to a smaller amount of code that’s easier to understand and maintain. This allows us to use methods or computed methods to create nicely-coded, and yet more elaborate, conditions in the script block. That’s where such things belong: in the script, not the template block.

I had the idea for this article mainly because we use this feature in several places in the design system where I work. Granted it’s not a huge feature and it is barely mentioned in the documentation, at least as far as I can tell. Yet it has potential to help render specific HTML elements in components.

Vue’s built-in <component> element

There are several features available in Vue that allow for easy dynamic changes to the view. One such feature, the built-in <component> element, allows components to be dynamic and switched on demand. In both the Vue 2 and the Vue 3 documentation, there is a small note about using this element with HTML elements; that is the part we shall now explore.

The idea is to leverage this aspect of the <component> element to swap out common HTML elements that are somewhat similar in nature; yet with different functionality, semantics, or visuals. The following basic examples will show the potential of this element to help with keeping Vue components neat and tidy.

Buttons and links are often used interchangeably, but there are big differences in their functionality, semantics, and even visuals. Generally speaking, a button (<button>) is intended for an internal action in the current view tied to JavaScript code. A link (<a>), on the other hand, is intended to point to another resource, either on the host server or an external resource; most often web pages. Single page applications tend to rely more on the button than the link, but there is a need for both.

Links are often styled as buttons visually, much like Bootstrap’s .btn class that creates a button-like appearance. With that in mind, we can easily create a component that switches between the two elements based on a single prop. The component will be a button by default, but if an href prop is applied, it will render as a link.

Here is the <component> in the template:

<component   :is="element"   :href="href"   class="my-button" >   <slot /> </component>

This bound is attribute points to a computed method named element and the bound href attribute uses the aptly named href prop. This takes advantage of Vue’s normal behavior that the bound attribute does not appear in the rendered HTML element if the prop has no value. The slot provides the inner content regardless whether the final element is a button or a link.

The computed method is simple in nature:

element () {   return this.href ? 'a' : 'button'; }

If there’s an href prop,. then an <a> element is applied; otherwise we get a <button>.

<my-button>this is a button</my-button> <my-button href="https://www.css-tricks.com">this is a link</my-button>

The HTML renders as so:

<button class="my-button">this is a button</button> <a href="https://www.css-tricks.com" class="my-button">this is a link</a>

In this case, there could be an expectation that these two are similar visually, but for semantic and accessibility needs, they are clearly different. That said, there’s no reason the two outputted elements have to be styled the same. You could either use the element with the selector div.my-button in the style block, or create a dynamic class that will change based on the element.

The overall goal is to simplify things by allowing one component to potentially render as two different HTML elements as needed — without v-if or v-else!

Ordered or unordered list?

A similar idea as the button example above, we can have a component that outputs different list elements. Since an unordered list and an ordered list make use of the same list item (<li>) elements as children, then that’s easy enough; we just swap <ul> and <ol>. Even if we wanted to have an option to have a description list, <dl>, this is easily accomplished since the content is just a slot that can accept <li> elements or <dt>/<dd>combinations.

The template code is much the same as the button example:

<component   :is="element"   class="my-list" >   <slot>No list items!</slot> </component>

Note the default content inside the slot element, I’ll get to that in a moment.

There is a prop for the type of list to be used that defaults to <ul>:

props: {   listType: {     type: String,     default: 'ul'   } }

Again, there is a computed method named element:

element () {   if (this.$  slots.default) {     return this.listType;   } else {     return 'div';   } }

In this case, we are testing if the default slot exists, meaning it has content to render. If it does, then the the list type passed through the listType prop is used. Otherwise, the element becomes a <div> which would show the “No list items!” message inside the slot element. This way, if there are no list items, then the HTML won’t render as a list with one item that says there are no items. That last aspect is up to you, though it is nice to consider the semantics of a list with no apparent valid items. Another thing to consider is the potential confusion of accessibility tools suggesting this is a list with one item that just states there are no items.

Just like the button example above, you could also style each list differently. This could be based on selectors that target the element with the class name, ul.my-list. Another option is to dynamically change the class name based on the chosen element.

This example follows a BEM-like class naming structure:

<component   :is="element"   class="my-list"   :class="`my-list__$  {element}`" >   <slot>No list items!</slot> </component>

Usage is as simple as the previous button example:

<my-list>   <li>list item 1</li> </my-list>  <my-list list-type="ol">   <li>list item 1</li> </my-list>  <my-list list-type="dl">   <dt>Item 1</dt>   <dd>This is item one.</dd> </my-list>  <my-list></my-list>

Each instance renders the specified list element. The last one, though, results in a <div> stating no list items because, well, there’s no list to show!

One might wonder why create a component that switches between the different list types when it could just be simple HTML. While there could be benefits to keeping lists contained to a component for styling reasons and maintainability, other reasons could be considered. Take, for instance, if some forms of functionality were being tied to the different list types? Maybe consider a sorting of a <ul> list that switches to a <ol> to show sorting order and then switching back when done?

Now we’re controlling the elements

Even though these two examples are essentially changing the root element component, consider deeper into a component. For instance, a title that might need to change from an <h2> to an <h3> based on some criteria.

If you find yourself having to use ternary solutions to control things beyond a few attributes, I would suggest sticking with the v-if. Having to write more code to handle attributes, classes, and properties just complicates the code more than the v-if. In those cases, the v-if makes for simpler code in the long run and simpler code is easier to read and maintain.

When creating a component and there’s a simple v-if to switch between elements, consider this small aspect of a major Vue feature.

Expanding the idea, a flexible card system

Consider all that we’ve covered so far and put it to use in a flexible card component. This example of a card component allows for three different types of cards to be placed in specific parts of the layout of an article:

  • Hero card: This is expected to be used at the top of the page and draw more attention than other cards.
  • Call to action card: This is used as a line of user actions before or within the article.
  • Info card: This is intended for pull quotes.

Consider each of these as following a design system and the component controls the HTML for semantics and styling.

In the example above, you can see the hero card at the top, a line of call-to-action cards next, and then — scrolling down a bit — you’ll see the info card off to the right side.

Here is the template code for the card component:

<component :is="elements('root')" :class="'custom-card custom-card__' + type" @click="rootClickHandler">   <header class="custom-card__header" :style="bg">     <component :is="elements('header')" class="custom-card__header-content">       <slot name="header"></slot>     </component>   </header>   <div class="custom-card__content">     <slot name="content"></slot>   </div>   <footer class="custom-card__footer">     <component :is="elements('footer')" class="custom-card__footer-content" @click="footerClickHandler">       <slot name="footer"></slot>     </component>   </footer> </component>

There are three of the “component” elements in the card. Each represents a specific element inside the card, but will be changed based on what kind of card it is. Each component calls the elements() method with a parameter identifying which section of the card is making the call.

The elements() method is:

elements(which) {   const tags = {     hero: { root: 'section', header: 'h1', footer: 'date' },     cta: { root: 'section', header: 'h2', footer: 'div' },     info: { root: 'aside', header: 'h3', footer: 'small' }   }   return tags[this.type][which]; }

There are probably several ways of handing this, but you’ll have to go in the direction that works with your component’s requirements. In this case, there is an object that keeps track of HTML element tags for each section in each card type. Then the method returns the needed HTML element based on the current card type and the parameter passed in.

For the styles, I inserted a class on the root element of the card based on the type of card it is. That makes it easy enough to create the CSS for each type of card based on the requirements. You could also create the CSS based on the HTML elements themselves, but I tend to prefer classes. Future changes to the card component could change the HTML structure and less likely to make changes to the logic creating the class.

The card also supports a background image on the header for the hero card. This is done with a simple computed placed on the header element: bg. This is the computed:

bg() {   return this.background ? `background-image: url($  {this.background})` : null; }

If an image URL is provided in the background prop, then the computed returns a string for an inline style that applies the image as a background image. A rather simple solution that could easily be made more robust. For instance, it could have support for custom colors, gradients, or default colors in case of no image provided. There’s a large number of possibilities that his example doesn’t approach because each card type could potentially have their own optional props for developers to leverage.

Here’s the hero card from this demo:

<custom-card type="hero" background="https://picsum.photos/id/237/800/200">   <template v-slot:header>Article Title</template>   <template v-slot:content>Lorem ipsum...</template>   <template v-slot:footer>January 1, 2011</template> </custom-card>

You’ll see that each section of the card has its own slot for content. And, to keep things simple, text is the only thing expected in the slots. The card component handles the needed HTML element based solely on the card type. Having the component just expect text makes using the component rather simplistic in nature. It replaces the need for decisions over HTML structure to be made and in turn the card is simply implemented.

For comparison, here are the other two types being used in the demo:

<custom-card type="cta">   <template v-slot:header>CTA Title One</template>   <template v-slot:content>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</template>   <template v-slot:footer>footer</template> </custom-card>  <custom-card type="info">   <template v-slot:header>Here's a Quote</template>   <template v-slot:content>“Maecenas ... quis.”</template>   <template v-slot:footer>who said it</template> </custom-card>

Again, notice that each slot only expects text as each card type generates its own HTML elements as defined by the elements() method. If it’s deemed in the future that a different HTML element should be used, it’s a simple matter of updating the component. Building in features for accessibility is another potential future update. Even interaction features can be expanded, based on card types.

The power is in the component that’s in the component

The oddly named <component> element in Vue components was intended for one thing but, as often happens, it has a small side effect that makes it rather useful in other ways. The <component> element was intended to dynamically switch Vue components inside another component on demand. A basic idea of this could be a tab system to switch between components acting as pages; which is actually demonstrated in the Vue documentation. Yet it supports doing the same thing with HTML elements.

This is an example of a new technique shared by a friend that has become s surprisingly useful tool in the belt of Vue features that I’ve used. I hope that this article carries forward the ideas and information about this small feature for you to explore how you might leverage this in your own Vue projects.


The post Dynamically Switching From One HTML Element to Another in Vue appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]