Tag: Create

Let’s Create an Image Pop-Out Effect With SVG Clip Path

Few weeks ago, I stumbled upon this cool pop-out effect by Mikael Ainalem. It showcases the clip-path: path() in CSS, which just got proper support in most modern browsers. I wanted to dig into it myself to get a better feel for how it works. But in the process, I found some issues with clip-path: path(); and wound up finding an alternative approach that I wanted to walk through with you in this article.

If you haven’t used clip-path or you are unfamiliar with it, it basically allows us to specify a display region for an element based on a clipping path and hide portions of the element that fall outside the clip path.

A rectangle with a pastel pattern, plus an unfilled star shape with a black border, equals a star shape with the pastel background pattern.
You can kind of think of it as though the star is a cookie cutter, the element is the cookie dough, and the result is a star-shaped cookie.

Possible values for clip-path include circle , ellipse and polygon which limit the use-case to just those specific shapes. This is where the new path value comes in — it allows us to use a more flexible SVG path to create various clipping paths that go beyond basic shapes.

Let’s take what we know about clip-path and start working on the hover effect. The basic idea of the is to make the foreground image of a person appear to pop-out from the colorful background and scale up in size when the element is hovered. An important detail is how the foreground image animation (scale up and move up) appears to be independent from the background image animation (scale up only).

This effect looks cool, but there are some issues with the path value. For starters, while we mentioned that support is generally good, it’s not great and hovers around 82% coverage at the time of writing. So, keep in mind that mobile support is currently limited to Chrome and Safari.

Besides support, the bigger and more bizarre issue with path is that it currently only works with pixel values, meaning that it is not responsive. For example, let’s say we zoom into the page. Right off the bat, the path shape starts to cut things off.

This severely limits the number of use cases for clip-path: path(), as it can only be used on fixed-sized elements. Responsive web design has been a widely-accepted standard for many years now, so it’s weird to see a new CSS property that doesn’t follow the principle and exclusively uses pixel units.

What we’re going to do is re-create this effect using standard, widely-supported CSS techniques so that it not only works, but is truly responsive as well.

The tricky part

We want anything that overflows the clip-path to be visible only on the top part of the image. We cannot use a standard CSS overflow property since it affects both the top and bottom.

Photo of a young woman against a pastel floral pattern cropped to the shape of a circle.
Using overflow-y: hidden, the bottom part looks good, but the image is cut-off at the top where the overflow should be visible.

So, what are our options besides overflow and clip-path? Well, let’s just use <clipPath> in the SVG itself. <clipPath> is an SVG property, which is different than the newly-released and non-responsive clip-path: path.

SVG <clipPath> element

SVG <clipPath> and <path> elements adapt to the coordinate system of the SVG element, so they are responsive out of the box. As the SVG element is being scaled, its coordinate system is also being scaled, and it maintains its proportions based on the various properties that cover a wide range of possible use cases. As an added benefit, using clip-path in CSS on SVG has 95% browser support, which is a 13% increase compared to clip-path: path.

Let’s start by setting up our SVG element. I’ve used Inkscape to create the basic SVG markup and clipping paths, just to make it easy for myself. Once I did that, I updated the markup by adding my own class attributes.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -10 100 120" class="image">   <defs>     <clipPath id="maskImage" clipPathUnits="userSpaceOnUse">       <path d="..." />     </clipPath>     <clipPath id="maskBackground" clipPathUnits="userSpaceOnUse">       <path d="..." />     </clipPath>   </defs>   <g clip-path="url(#maskImage)" transform="translate(0 -7)">     <!-- Background image -->     <image clip-path="url(#maskBackground)" width="120" height="120" x="70" y="38" href="..." transform="translate(-90 -31)" />     <!-- Foreground image -->     <image width="120" height="144" x="-15" y="0" fill="none" class="image__foreground" href="..." />   </g> </svg>
A bright green circle with a bright red shape coming out from the top of it, as if another shape is behind the green circle.
SVG <clipPath> elements created in Inkscape. The green element represents a clipping path that will be applied to the background image. The red is a clipping path that will be applied to both the background and foreground image.

This markup can be easily reused for other background and foreground images. We just need to replace the URL in the href attribute inside image elements.

Now we can work on the hover animation in CSS. We can get by with transforms and transitions, making sure the foreground is nicely centered, then scaling and moving things when the hover takes place.

.image {   transform: scale(0.9, 0.9);   transition: transform 0.2s ease-in; }  .image__foreground {   transform-origin: 50% 50%;   transform: translateY(4px) scale(1, 1);   transition: transform 0.2s ease-in; }  .image:hover {   transform: scale(1, 1); }  .image:hover .image__foreground {   transform: translateY(-7px) scale(1.05, 1.05); }

Here is the result of the above HTML and CSS code. Try resizing the screen and changing the dimensions of the SVG element to see how the effect scales with the screen size.

This looks great! However, we’re not done. We still need to address some issues that we get now that we’ve changed the markup from an HTML image element to an SVG element.

SEO and accessibility

Inline SVG elements won’t get indexed by search crawlers. If the SVG elements are an important part of the content, your page SEO might take a hit because those images probably won’t get picked up.

We’ll need additional markup that uses a regular <img> element that’s hidden with CSS. Images declared this way are automatically picked up by crawlers and we can provide links to those images in an image sitemap to make sure that the crawlers manage to find them. We’re using loading="lazy" which allows the browser to decide if loading the image should be deferred.

We’ll wrap both elements in a <figure> element so that we markup reflects the relationship between those two images and groups them together:

<figure>   <!-- SVG element -->   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -10 100 120" class="image">      <!-- ... -->   </svg>   <!-- Fallback image -->   <img src="..." alt="..." loading="lazy" class="fallback-image" /> </figure>

We also need to address some accessibility concerns for this effect. More specifically, we need to make improvements for users who prefer browsing the web without animations and users who browse the web using screen readers.

Making SVG elements accessible takes a lot of additional markup. Additionally, if we want to remove transitions, we would have to override quite a few CSS properties which can cause issues if our selector specificities aren’t consistent. Luckily, our newly added regular image has great accessibility features baked right in and can easily serve as a replacement for users who browse the web without animations.

<figure>   <!-- Animated SVG element -->   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -10 100 120" class="image" aria-hidden="true">     <!-- ... -->   </svg>    <!-- Fallback SEO & a11y image -->   <img src="..." alt="..." loading="lazy" class="fallback-image" /> </figure>

We need to hide the SVG element from assistive devices, by adding aria-hidden="true", and we need to update our CSS to include the prefers-reduced-motion media query. We are inclusively hiding the fallback image for users without the reduced motion preference meanwhile keeping it available for assistive devices like screen readers.

@media (prefers-reduced-motion: no-preference) { .fallback-image {   clip: rect(0 0 0 0);    clip-path: inset(50%);   height: 1px;   overflow: hidden;   position: absolute;   white-space: nowrap;    width: 1px;   }  }  @media (prefers-reduced-motion) {   .image {     display: none;   } }

Here is the result after the improvements:

Please note that these improvements won’t change how the effect looks and behaves for users who don’t have the prefers-reduced-motion preference set or who aren’t using screen readers.

That’s a wrap

Developers were excited about path option for clip-path CSS attribute and new styling possibilities, but many were displeased to find out that these values only support pixel values. Not only does that mean the feature is not responsive, but it severely limits the number of use cases where we’d want to use it.

We converted an interesting image pop-out hover effect that uses clip-path: path into an SVG element that utilizes the responsiveness of the <clipPath> SVG element to achieve the same thing. But in doing so, we introduced some SEO and accessibility issues, that we managed to work around with a bit of extra markup and a fallback image.

Thank you for taking the time to read this article! Let me know if this approach gave you an idea on how to implement your own effects and if you have any suggestions on how to approach this effect in a different way.


The post Let’s Create an Image Pop-Out Effect With SVG Clip Path appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , , ,

Let’s Create a Custom Audio Player

HTML has a built-in native audio player interface that we get simply using the <audio> element. Point it to a sound file and that’s all there is to it. We even get to specify multiple files for better browser support, as well as a little CSS flexibility to style things up, like giving the audio player a border, some rounded corners, and maybe a little padding and margin.

But even with all that… the rendered audio player itself can look a little, you know, plain.

Did you know it’s possible to create a custom audio player? Of course we can! While the default <audio> player is great in many cases, having a custom player might suit you better, like if you run a podcast and an audio player is the key element on a website for the podcast. Check out the sweet custom player Chris and Dave set up over at the ShopTalk Show website.

Showing a black audio player with muted orange controls, including options to jump or rewind 30 seconds on each side of a giant play button, which sits on top of a timeline showing the audio current time and duration at both ends of the timeline, and an option to set the audio speed in the middle.
The audio player fits in seamlessly with other elements on the page, sporting controls that complement the overall design.

We’re going to take stab at making our own player in this post. So, put on your headphones, crank up some music, and let’s get to work!

The elements of an audio player

First, let’s examine the default HTML audio players that some of the popular browsers provide.

Google Chrome, Opera, and Microsoft Edge
Blink

Mozilla Firefox
Firefox

Internet Explorer
Internet Explorer

If our goal is to match the functionality of these examples, then we need to make sure our player has:

  • a play/pause button,
  • a seek slider,
  • the current time indicator,
  • the duration of the sound file,
  • a way to mute the audio, and
  • a volume control slider.

Let’s say this is the design we’re aiming for:

We’re not going for anything too fancy here: just a proof of concept sorta thing that we can use to demonstrate how to make something different than what default HTML provides.

Basic markup, styling and scripts for each element

We should first go through the semantic HTML elements of the player before we start building features and styling things. We have plenty of elements to work with here based on the elements we just listed above.

Play/pause button

I think the HTML element appropriate for this button is the <button> element. It will contain the play icon, but the pause icon should also be in this button. That way, we’re toggling between the two rather than taking up space by displaying both at the same time.

Something like this in the markup:

<div id="audio-player-container">   <p>Audio Player</p>   <!-- swaps with pause icon -->   <button id="play-icon"></button> </div>

So, the question becomes: how do we swap between the two buttons, both visually and functionally? the pause icon will replace the play icon when the play action is triggered. The play button should display when the audio is paused and the pause button should display when the audio is playing.

Of course, a little animation could take place as the icon transitions from the play to pause. What would help us accomplish that is Lottie, a library that renders Adobe After Effects animations natively. We don’t have to create the animation on After Effects though. The animated icon we’re going to use is provided for free by Icons8.

New to Lottie? I wrote up a thorough overview that covers how it works.

In the meantime, permit me to describe the following Pen:

The HTML section contains the following:

  • a container for the player,
  • text that briefly describes the container, and
  • a <button> element for the play and pause actions.

The CSS section includes some light styling. The JavaScript is what we need to break down a bit because it’s doing several things:

// imports the Lottie library via Skypack import lottieWeb from 'https://cdn.skypack.dev/lottie-web';  // variable for the button that will contain both icons const playIconContainer = document.getElementById('play-icon'); // variable that will store the button’s current state (play or pause) let state = 'play';  // loads the animation that transitions the play icon into the pause icon into the referenced button, using Lottie’s loadAnimation() method const animation = lottieWeb.loadAnimation({   container: playIconContainer,   path: 'https://maxst.icons8.com/vue-static/landings/animated-icons/icons/pause/pause.json',   renderer: 'svg',   loop: false,   autoplay: false,   name: "Demo Animation", });  animation.goToAndStop(14, true);  // adds an event listener to the button so that when it is clicked, the the player toggles between play and pause playIconContainer.addEventListener('click', () => {   if(state === 'play') {     animation.playSegments([14, 27], true);     state = 'pause';   } else {     animation.playSegments([0, 14], true);     state = 'play';   } });

Here’s what the script is doing, minus the code:

  • It imports the Lottie library via Skypack.
  • It references the button that will contain both icons in a variable.
  • It defines a variable that will store the button’s current state (play or pause).
  • It loads the animation that transitions the play icon into the pause icon into the referenced button, using Lottie’s loadAnimation() method.
  • It displays the play icon on load since the audio is initially paused.
  • It adds an event listener to the button so that when it is clicked, the the player toggles between play and pause.

Current time and duration

The current time is like a progress indicate that shows you how much time has elapsed from the start of the audio file. The duration? That’s just how long the sound file is.

A <span> element is okay to display these. The <span> element for the current time, which is to be updated every second, has a default text content of 0:00. On the other side, the one for duration is the duration of the audio in mm:ss format.

<div id="audio-player-container">   <p>Audio Player</p>   <button id="play-icon"></button>   <span id="current-time" class="time">0:00</span>   <span id="duration" class="time">0:00</span> </div>

Seek slider and volume control slider

We need a way to move to any point in time in the sound file. So, if I want to skip ahead to the halfway point of the file, I can simply click and drag a slider to that spot in the timeline.

We also need a way to control the sound volume. That, too, can be some sort of click-and-drag slider thingy.

I would say <input type="range"> is the right HTML element for both of these features.

<div id="audio-player-container">   <p>Audio Player</p>   <button id="play-icon"></button>   <span id="current-time" class="time">0:00</span>   <input type="range" id="seek-slider" max="100" value="0">   <span id="duration" class="time">0:00</span>   <input type="range" id="volume-slider" max="100" value="100"> </div>

Styling range inputs with CSS is totally possible, but I’ll tell you what: it is difficult for me to wrap my head around. This article will help. Mad respect to you, Ana. Handling browser support with all of those vendor prefixes is a CSS trick in and of itself. Look at all the code needed on input[type="range"] to get a consistent experience:

input[type="range"] {   position: relative;   -webkit-appearance: none;   width: 48%;   margin: 0;   padding: 0;   height: 19px;   margin: 30px 2.5% 20px 2.5%;   float: left;   outline: none; } input[type="range"]::-webkit-slider-runnable-track {   width: 100%;   height: 3px;   cursor: pointer;   background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width)); } input[type="range"]::before {   position: absolute;   content: "";   top: 8px;   left: 0;   width: var(--seek-before-width);   height: 3px;   background-color: #007db5;   cursor: pointer; } input[type="range"]::-webkit-slider-thumb {   position: relative;   -webkit-appearance: none;   box-sizing: content-box;   border: 1px solid #007db5;   height: 15px;   width: 15px;   border-radius: 50%;   background-color: #fff;   cursor: pointer;   margin: -7px 0 0 0; } input[type="range"]:active::-webkit-slider-thumb {   transform: scale(1.2);   background: #007db5; } input[type="range"]::-moz-range-track {   width: 100%;   height: 3px;   cursor: pointer;   background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width)); } input[type="range"]::-moz-range-progress {   background-color: #007db5; } input[type="range"]::-moz-focus-outer {   border: 0; } input[type="range"]::-moz-range-thumb {   box-sizing: content-box;   border: 1px solid #007db5;   height: 15px;   width: 15px;   border-radius: 50%;   background-color: #fff;   cursor: pointer; } input[type="range"]:active::-moz-range-thumb {   transform: scale(1.2);   background: #007db5; } input[type="range"]::-ms-track {   width: 100%;   height: 3px;   cursor: pointer;   background: transparent;   border: solid transparent;   color: transparent; } input[type="range"]::-ms-fill-lower {   background-color: #007db5; } input[type="range"]::-ms-fill-upper {   background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width)); } input[type="range"]::-ms-thumb {   box-sizing: content-box;   border: 1px solid #007db5;   height: 15px;   width: 15px;   border-radius: 50%;   background-color: #fff;   cursor: pointer; } input[type="range"]:active::-ms-thumb {   transform: scale(1.2);   background: #007db5; }

Whoa! What does even mean, right?

Styling the progress section of range inputs is a tricky endeavor. Firefox provides the ::-moz-range-progress pseudo-element while Internet Explorer provides ::-ms-fill-lower. As WebKit browsers do not provide any similar pseudo-element, we have to use the ::before pseudo-element to improvise the progress. That explains why, if you noticed, I added event listeners in the JavaScript section to set custom CSS properties (e.g. --before-width) that update when the input event is fired on each of the sliders.

One of the native HTML <audio> examples we looked at earlier shows the buffered amount of the audio. The --buffered-width property specifies the amount of the audio, in percentage, that the user can play through to without having to wait for the browser to download. I imitated this feature with the linear-gradient() function on the track of the seek slider. I used the rgba() function in the linear-gradient() function for the color stops to show transparency. The buffered width has a much deeper color compared to the rest of the track. However, we would treat the actual implementation of this feature much later.

Volume percentage

This is to display the percentage volume. The text content of this element is updated as the user changes the volume through the slider. Since it is based on user input, I think this element should be the <output> element.

<div id="audio-player-container">   <p>Audio Player</p>   <button id="play-icon"></button>   <span id="current-time" class="time">0:00</span>   <input type="range" id="seek-slider" max="100" value="0">   <span id="duration" class="time">0:00</span>   <output id="volume-output">100</output>   <input type="range" id="volume-slider" max="100" value="100"> </div>

Mute button

Like for the play and pause actions, this should be in a <button> element. Luckily for us, Icons8 also has an animated mute icon. So we would use the Lottie library here just as we did for the play/pause button.

<div id="audio-player-container">   <p>Audio Player</p>   <button id="play-icon"></button>   <span id="current-time" class="time">0:00</span>   <input type="range" id="seek-slider" max="100" value="0">   <span id="duration" class="time">0:00</span>   <output id="volume-output">100</output>   <input type="range" id="volume-slider" max="100" value="100">   <button id="mute-icon"></button> </div>

That’s all of the basic markup, styling and scripting we need at the moment!

Working on the functionality

The HTML <audio> element has a preload attribute. This attribute gives the browser instructions for how to load the audio file. It accepts one of three values:

  • none – indicates that the browser should not load the audio at all (unless the user initiates the play action)
  • metadata – indicates that only the metadata (like length) of the audio should be loaded
  • auto – loads the complete audio file

An empty string is equivalent to the auto value. Note, however, that these values are merely hints to the browser. The browser does not have to agree to these values. For example, if a user is on a cellular network on iOS, Safari does not load any part of an audio, regardless of the preload attribute, except the user triggers the play action. For this player, we would use the metadata value since it doesn’t require much overhead and we want to display the length of the audio.

What would help us accomplish the features our audio player should have is the JavaScript HTMLMediaElement interface, which the HTMLAudioElement interface inherits. For our audio player code to be as self-explanatory as possible, I’d divide the JavaScript into two sections: presentation and functionality.

First off, we should create an <audio> element in the audio player that has the basic features we want:

<div id=”audio-player-container”>   <audio src=”my-favourite-song.mp3” preload=”metadata” loop>   <button id="play-icon"></button>   <!-- ... --> </div>

Display the audio duration

The first thing we want to display on the browser is the duration of the audio, when it is available. The HTMLAudioElement interface has a duration property, which returns the duration of the audio, returned in seconds units. If it is unavailable, it returns NaN.

We’ve set preload to metadata in this example, so the browser should provide us that information up front on load… assuming it respects preload. Since we’d be certain that the duration will be available when the browser has downloaded the metadata of the audio, we display it in our handler for the loadedmetadata event, which the interface also provides:

const audio = document.querySelector('audio');  audio.addEventListener('loadedmetadata', () => {   displayAudioDuration(audio.duration); });

That’s great but, again, we get the duration in second. We probably should convert that to a mm:ss format:

const calculateTime = (secs) => {   const minutes = Math.floor(secs / 60);   const seconds = Math.floor(secs % 60);   const returnedSeconds = seconds < 10 ? `0$ {seconds}` : `$ {seconds}`;   return `$ {minutes}:$ {returnedSeconds}`; }

We’re using Math.floor() because the seconds returned by the duration property typically come in decimals.

The third variable, returnedSeconds, is necessary for situations where the duration is something like 4 minutes and 8 seconds. We would want to return 4:08, not 4:8.

More often than not, the browser loads the audio faster than usual. When this happens, the loadedmetadata event is fired before its listener can be added to the <audio> element. Therefore, the audio duration is not displayed on the browser. Nevertheless, there’s a hack. The HTMLMediaElement has a property called readyState. It returns a number that, according to MDN Web Docs, indicates the readiness state of the media. The following describes the values:

  • 0 – no data about the media is available.
  • 1 – the metadata attributes of the media are available.
  • 2 – data is available, but not enough to play more than a frame.
  • 3 – data is available, but for a little amount of frames from the current playback position.
  • 4 – data is available, such that the media can be played through to the end without interruption.

We want to focus on the metadata. So our approach is to display the duration if the metadata of the audio is available. If it is not available, we add the event listener. That way, the duration is always displayed.

const audio = document.querySelector('audio'); const durationContainer = document.getElementById('duration');  const calculateTime = (secs) => {   const minutes = Math.floor(secs / 60);   const seconds = Math.floor(secs % 60);   const returnedSeconds = seconds < 10 ? `0$ {seconds}` : `$ {seconds}`;   return `$ {minutes}:$ {returnedSeconds}`; }  const displayDuration = () => {   durationContainer.textContent = calculateTime(audio.duration); }  if (audio.readyState > 0) {   displayDuration(); } else {   audio.addEventListener('loadedmetadata', () => {     displayDuration();   }); }

Seek slider

The default value of the range slider’s max property is 100. The general idea is that when the audio is playing, the thumb is supposed to be “sliding.” Also, it is supposed to move every second, such that it gets to the end of the slider when the audio ends.

Notwithstanding, if the audio duration is 150 seconds and the value of the slider’s max property is 100, the thumb will get to the end of the slider before the audio ends. This is why it is necessary to set the value of the slider’s max property to the audio duration in seconds. This way, the thumb gets to the end of the slider when the audio ends. Recall that this should be when the audio duration is available, when the browser has downloaded the audio metadata, as in the following:

const seekSlider = document.getElementById('seek-slider');  const setSliderMax = () => {   seekSlider.max = Math.floor(audio.duration); }  if (audio.readyState > 0) {   displayDuration();   setSliderMax(); } else {   audio.addEventListener('loadedmetadata', () => {     displayDuration();     setSliderMax();   }); }

Buffered amount

As the browser downloads the audio, it would be nice for the user to know how much of it they can seek to without delay. The HTMLMediaElement interface provides the buffered and seekable properties. The buffered property returns a TimeRanges object, which indicates the chunks of media that the browser has downloaded. According to MDN Web Docs, a TimeRanges object is a series of non-overlapping ranges of time, with start and stop times. The chunks are usually contiguous, unless the user seeks to another part in the media. The seekable property returns a TimeRanges object, which indicates “seekable” parts of the media, irrespective of whether they’ve been downloaded or not.

Recall that the preload="metadata" attribute is present in our <audio> element. If, for example the audio duration is 100 seconds, the buffered property returns a TimeRanges object similar to the following:

Displays a line from o to 100, with a marker at 20. A range from 0 to 20 is highlighted on the line.

When the audio has started playing, the seekable property would return a TimeRanges object similar to the following:

Another timeline, but with four different ranges highlighted on the line, 0 to 20, 30 to 40, 60 to 80, and 90 to 100.

It returns multiple chunks of media because, more often than not, byte-range requests are enabled on the server. What this means is that multiple parts of the media can be downloaded simultaneously. However, we want to display the buffered amount closest to the current playback position. That would be the first chunk (time range 0 to 20). That would be the first and last chunk from the first image. As the audio starts playing, the browser begins to download more chunks. We would want to display the one closest to the current playback position, which would be the current last chunk returned by the buffered property. The following snippet would store in the variable, bufferedAmount, i.e. the time for the end of the last range in the TimeRanges object returned by the buffered property.

const audio = document.querySelector('audio'); const bufferedAmount = audio.buffered.end(audio.buffered.length - 1);

This would be 20 from the 0 to 20 range in the first image. The following snippet stores in the variable, seekableAmount, the time for the end of the last range in the TimeRanges object returned by the seekable property.

const audio = document.querySelector('audio'); const seekableAmount = audio.seekable.end(audio.seekable.length - 1);

Nevertheless, this would be 100 from the 90 to 100 range in the second image, which is the entire audio duration. Note that there are some holes in the TimeRanges object as the browser only downloads some parts of the audio. What this means is that the entire duration would be displayed to the user as the buffered amount. Meanwhile, some parts in the audio are not available yet. Because this won’t provide the best user experience, the first snippet is what we should use.

As the browser downloads the audio, the user should expect that the buffered amount on the slider increases in width. The HTMLMediaElement provides an event, the progress event, which fires as the browser loads the media. Of course, I’m thinking what you’re thinking! The buffered amount should be incremented in the handler for the audio’s progress event.

Finally, we should actually display the buffered amount on the seek slider. We do that by setting the property we talked about earlier, --buffered-width, as a percentage of the value of the slider’s max property. Yes, in the handler for the progress event too. Also, because of the browser loading the audio faster than usual, we should update the property in the loadedmetadata event and its preceding conditional block that checks for the readiness state of the audio. The following Pen combines all that we’ve covered so far:

Current time

As the user slides the thumb along the range input, the range value should be reflected in the <span> element containing the current time of the audio. This tells the user the current playback position of the audio. We do this in the handler of the slider’s input event listener.

If you think the correct event to listen to should be the change event, I beg to differ. Say the user moved the thumb from value 0 to 20. The input event fires at values 1 through to 20. However, the change event will fire only at value 20. If we use the change event, it will not reflect the playback position from values 1 to 19. So, I think the input event is appropriate. Then, in the handler for the event, we pass the slider’s value to the calculateTime() function we defined earlier.

We created the function to take time in seconds and return it in a mm:ss format. If you’re thinking, Oh, but the slider’s value is not time in seconds, let me explain. Actually, it is. Recall that we set the value of the slider’s max property to the audio duration, when it is available. Let’s say the audio duration is 100 seconds. If the user slides the thumb to the middle of the slider, the slider’s value will be 50. We wouldn’t want 50 to appear in the current time box because it is not in accordance with the mm:ss format. When we pass 50 to the function, the function returns 0:50 and that would be a better representation of the playback position.

I added the snippet below to our JavaScript.

const currentTimeContainer = document.getElementById('current-time');  seekSlider.addEventListener('input', () => {   currentTimeContainer.textContent = calculateTime(seekSlider.value); }); 

To see it in action, you can move the seek slider’s thumb back and forth in the following Pen:

Play/pause

Now we’re going to set the audio to play or pause according to the respective action triggered by the user. If you recall, we created a variable, playState, to store the state of the button. That variable is what will help us know when to play or pause the audio. If its value is play and the button is clicked, our script is expected to perform the following actions:

  • play the audio
  • change the icon from play to pause
  • change the playState value to pause

We already implemented the second and third actions in the handler for the button’s click event. What we need to do is to add the statements to play and pause the audio in the event handler:

playIconContainer.addEventListener('click', () => {   if(playState === 'play') {     audio.play();     playAnimation.playSegments([14, 27], true);     playState = 'pause';   } else {     audio.pause();     playAnimation.playSegments([0, 14], true);     playState = 'play';   } });

It is possible that the user will want to seek to a specific part in the audio. In that case, we set the value of the audio’s currentTime property to the seek slider’s value. The slider’s change event will come in handy here. If we use the input event, various parts of the audio will play in a very short amount of time.

Recall our scenario of 1 to 20 values. Now imagine the user slides the thumb from 1 to 20 in, say, two seconds. That’s 20 seconds audio playing in two seconds. It’s like listening to Busta Rhymes on 3× speed. I’d suggest we use the change event. The audio will only play after the user is done seeking. This is what I’m talking about:

seekSlider.addEventListener('change', () => {   audio.currentTime = seekSlider.value; });

With that out of the way, something needs to be done while the audio is playing. That is to set the slider’s value to the current time of the audio. Or move the slider’s thumb by one tick every second. Since the audio duration and the slider’s max value are the same, the thumb gets to the end of the slider when the audio ends. Now the timeupdate event of the HTMLMediaElement interface should be the appropriate event for this. This event is fired as the value of the media’s currentTime property is updated, which is approximately four times in one second. So in the handler for this event, we could set the slider’s value to the audio’s current time. This should work just fine:

audio.addEventListener('timeupdate', () => {   seekSlider.value = Math.floor(audio.currentTime); });

However, there are some things to take note of here:

  1. As the audio is playing, and the seek slider’s value is being updated, a user is unable to interact with the slider. If the audio is paused, the slider won’t be able to receive input from the user because it is constantly being updated.
  2. In the handler, we update the value of the slider but its input event does not fire. This is because the event only fires when a user updates the slider’s value on the browser, and not when it is updated programmatically.

Let’s consider the first issue.

To be able to interact with the slider while the audio is playing, we would have to pause the process of updating it’s value when it receives input. Then, when the slider loses focus, we resume the process. But, we don’t have access to this process. My hack would be to use the requestAnimationFrame() global method for the process. But this time, we won’t be using the timeupdate event for this because it still won’t work. The animation would play forever until the audio is paused, and that’s not what we want. Therefore, we use the play/pause button’s click event.

To use the requestAnimationFrame() method for this feature, we have to accomplish these steps:

  1. Create a function to keep our “update slider value” statement.
  2. Initialize a variable in the function that was previously created to store the request ID returned by the function (that will be used to pause the update process).
  3. Add the statements in the play/pause button click event handler to start and pause the process in the respective blocks.

This is illustrated in the following snippet:

let rAF = null;  const whilePlaying = () => {   seekSlider.value = Math.floor(audio.currentTime);   rAF = requestAnimationFrame(whilePlaying); }  playIconContainer.addEventListener('click', () => {   if(playState === 'play') {     audio.play();     playAnimation.playSegments([14, 27], true);     requestAnimationFrame(whilePlaying);     playState = 'pause';   } else {     audio.pause();     playAnimation.playSegments([0, 14], true);     cancelAnimationFrame(rAF);     playState = 'play';   } });

But this doesn’t exactly solve our problem. The process is only paused when the audio is paused. We also need to pause the process, if it is in execution (i.e. if the audio is playing), when the user wants to interact with the slider. Then, after the slider loses focus, if the process was ongoing before (i.e. if the audio was playing), we start the process again. For this, we would use the slider’s input event handler to pause the process. To start the process again, we would use the change event because it is fired after the user is done sliding the thumb. Here is the implementation:

seekSlider.addEventListener('input', () => {   currentTimeContainer.textContent = calculateTime(seekSlider.value);   if(!audio.paused) {     cancelAnimationFrame(raf);   } });  seekSlider.addEventListener('change', () => {   audio.currentTime = seekSlider.value;   if(!audio.paused) {     requestAnimationFrame(whilePlaying);   } });

I was able to come up with something for the second issue. I added the statements in the seek slider’s input event handlers to the whilePlaying() function. Recall that there are two event listeners for the slider’s input event: one for the presentation, and the other for the functionality. After adding the two statements from the handlers, this is how our whilePlaying() function looks:

const whilePlaying = () => {   seekSlider.value = Math.floor(audio.currentTime);   currentTimeContainer.textContent = calculateTime(seekSlider.value);   audioPlayerContainer.style.setProperty('--seek-before-width', `$ {seekSlider.value / seekSlider.max * 100}%`);   raf = requestAnimationFrame(whilePlaying); }

Note that the statement on the fourth line is the seek slider’s appropriate statement from the showRangeProgress() function we created earlier in the presentation section.

Now we’re left with the volume-control functionality. Whew! But before we begin working on that, here’s a Pen covering all we’ve done so far:

Volume-control

For volume-control, we’re utilizing the second slider, #volume-slider. When the user interacts with the slider, the slider’s value is reflected in the volume of the audio and the <output> element we created earlier.

The slider’s max property has a default value of 100. This makes it easy to display its value in the <output> element when it is updated. We could implement this in the input event handler of the slider. However, to implement this in the volume of the audio, we’re going to have to do some math.

The HTMLMediaElement interface provides a volume property, which returns a value between 0 and 1, where 1 being is the loudest value. What this means is if the user sets the slider’s value to 50, we would have to set the volume property to 0.5. Since 0.5 is a hundredth of 50, we could set the volume to a hundredth of the slider’s value.

const volumeSlider = document.getElementById('volume-slider'); const outputContainer = document.getElementById('volume-output');  volumeSlider.addEventListener('input', (e) => {   const value = e.target.value;    outputContainer.textContent = value;   audio.volume = value / 100; });

Not bad, right?

Muting audio

Next up is the speaker icon, which is clicked to mute and unmute the audio. To mute the audio, we would use its muted property, which is also available via HTMLMediaElement as a boolean type. Its default value is false, which is unmuted. To mute the audio, we set the property to true. If you recall, we added a click event listener to the speaker icon for the presentation (the Lottie animation). To mute and unmute the audio, we should add the statements to the respective conditional blocks in that handler, as in the following:

const muteIconContainer = document.getElementById('mute-icon');  muteIconContainer.addEventListener('click', () => {   if(muteState === 'unmute') {     muteAnimation.playSegments([0, 15], true);     audio.muted = true;     muteState = 'mute';   } else {     muteAnimation.playSegments([15, 25], true);     audio.muted = false;     muteState = 'unmute';   } });

Full demo

Here’s the full demo of our custom audio player in all its glory!

But before we call it quits, I’d like to introduce something — something that will give our user access to the media playback outside of the browser tab where our custom audio player lives.

Permit me to introduce to you, drumroll, please…

The Media Session API

Basically, this API lets the user pause, play, and/or perform other media playback actions, but not with our audio player. Depending on the device or the browser, the user initiates these actions through the notification area, media hubs, or any other interface provided by their browser or OS. I have another article just on that for you to get more context on that.

The following Pen contains the implementation of the Media Session API:

If you view this Pen on your mobile, take a sneak peek at the notification area. If you’re on Chrome on your computer, check the media hub. If your smartwatch is paired, I’d suggest you look at it. You could also tell your voice assistant to perform some of the actions on the audio. Ten bucks says it’ll make you smile. 🤓

One more thing…

If you need an audio player on a webpage, there’s a high chance that the page contains other stuff. That’s why I think it’s smart to group the audio player and all the code needed for it into a web component. This way, the webpage possesses a form of separation of concerns. I transferred everything we’ve done into a web component and came up with the following:

Wrapping up, I’d say the possibilities of creating a media player are endless with the HTMLMediaElement interface. There’s so many various properties and methods for various functions. Then there’s the Media Session API for an enhanced experience.

What’s the saying? With great power comes great responsibility, right? Think of all the various controls, elements, and edge cases we had to consider for what ultimately amounts to a modest custom audio player. Just goes to show that audio players are more than hitting play and pause. Properly speccing out functional requirements will definitely help plan your code in advance and save you lots of time.


The post Let’s Create a Custom Audio Player appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[Top]

Use CSS Clamp to create a more flexible wrapper utility

I like Andy’s idea here:

.wrapper {   width: clamp(16rem, 90vw, 70rem);   margin-left: auto;   margin-right: auto;   padding-left: 1.5rem;   padding-right: 1.5rem; }

Normally I’d just set a max-width there, but as Andy says:

This becomes a slight issue in mid-sized viewports, such as tablets in portrait mode, in long-form content, such as this article because contextually, the line-lengths feel very long.

So, on super large screens, you’ll get capped at 70rem (or whatever you think a good maximum is), and on small screens you’ll get full width, which is fine. But it’s those in-betweens that aren’t so great. I made a little demo to get a feel for it. This video makes it clear I think:

Direct Link to ArticlePermalink


The post Use CSS Clamp to create a more flexible wrapper utility appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , ,
[Top]

How to Create a Shrinking Header on Scroll Without JavaScript

Imagine a header of a website that is nice and thick, with plenty of padding on top and bottom of the content. As you scroll down, it shrinks up on itself, reducing some of that padding, making more screen real estate for other content.

Normally you would have to use some JavaScript to add a shrinking effect like that, but there’s a way to do this using only CSS since the introduction of position: sticky.

Let me just get this out there: I’m generally not a fan of sticky headers. I think they take up too much of the screen’s real estate. Whether or not you should use sticky headers on your own site, however, is a different question. It really depends on your content and whether an ever-present navigation adds value to it. If you do use it, take extra care to avoid inadvertently covering or obscuring content or functionality with the sticky areas — that amounts to data loss.

Either way, here’s how to do it without JavaScript, starting with the markup. Nothing complicated here — a <header> with one descendant <div> which, intern, contains the logo and navigation.

<header class="header-outer">   <div class="header-inner">     <div class="header-logo">...</div>     <nav class="header-navigation">...</nav>   </div> </header>

As far as styling, we’ll declare a height for the parent <header> (120px) and set it up as a flexible container that aligns its descendant in the center. Then, we’ll make it sticky.

.header-outer {   display: flex;   align-items: center;   position: sticky;   height: 120px; }

The inner container contains all the header elements, such as the logo and the navigation. The inner container is in a way the actual header, while the only function of the parent <header> element is to make the header taller so there’s something to shrink from.

We’ll give that inner container, .header-inner, a height of 70px and make it sticky as well.

.header-inner {   height: 70px;   position: sticky;   top: 0;  }

That top: 0? It’s there to make sure that the container mounts itself at the very top when it becomes sticky.

Now for the trick! For the inner container to actually stick to the “ceiling” of the page we need to give the parent <header> a negative top value equal to the height difference between the two containers, making it stick “above” the viewport. That’s 70px minus 120px, leaving with with — drumroll, please — -50px. Let’s add that.

.header-outer {   display: flex;   align-items: center;   position: sticky;   top: -50px; /* Equal to the height difference between header-outer and header-inner */   height: 120px; }

Let’s bring it all together now. The <header> slides out of frame, while the inner container places itself neatly at the top of the viewport.

We can extend this to other elements! How about a persistent alert?

While it’s pretty awesome we can do this in CSS, it does have limitations. For example, the inner and outer containers use fixed heights. This makes them vulnerable to change, like if the navigation elements wrap because the number of menu items exceeds the amount of space.

Another limitation? The logo can’t shrink. This is perhaps the biggest drawback, since logos are often the biggest culprit of eating up space. Perhaps one day we’ll be able to apply styles based on the stickiness of an element…


The post How to Create a Shrinking Header on Scroll Without JavaScript appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , ,
[Top]

Create a Tag Cloud with some Simple CSS and even Simpler JavaScript

I’ve always liked tag clouds. I like the UX of seeing what tags are most popular on a website by seeing the relative font size of the tags, popular tags being bigger. They seem to have fallen out of fashion, though you do often see versions of them used in illustrations in tools like Wordle.

How difficult is it to make a tag cloud? Not very difficult at all. Let’s see!

Let’s start with the markup

For our HTML, we’re going to put each of our tags into a list, <ul class="tags"><ul>. We’ll be injecting into that with JavaScript.

If your tag cloud is already in HTML, and you are just looking to do the relative font-size thing, that’s good! Progressive enhancement! You should be able to adapt the JavaScript later on so it does just that part, but not necessarily building and injecting the tags themselves.

I have mocked out some JSON with a certain amount of articles tagged with each property. Let’s write some JavaScript to go grab that JSON feed and do three things.

First, it we’ll create an <li> from each entry for our list. Imagine the HTML, so far, is like this:

<ul class="tags">   <li>align-content</li>   <li>align-items</li>   <li>align-self</li>   <li>animation</li>   <li>...</li>   <li>z-index</li> </ul>

Second, we’ll put the number of articles each property has in parentheses beside inside each list item. So now, the markup is like this:

<ul class="tags">   <li>align-content (2)</li>   <li>align-items (2)</li>   <li>align-self (2)</li>   <li>animation (9)</li>   <li>...</li>   <li>z-index (4)</li> </ul>

Third, and last, we’ll create a link around each tag that goes to the correct place. This is where we can set the font-size property for each item depending on how many articles that property is tagged with, so animation that has 13 articles will be much bigger than background-color which only has one article.

<li class="tag">   <a     class="tag__link"     href="https://example.com/tags/animation"     style="font-size: 5em">     animation (9)   </a> </li>

The JavasScript part

Let’s have a look at the JavaScript to do this.

const dataURL =   "https://gist.githubusercontent.com/markconroy/536228ed416a551de8852b74615e55dd/raw/9b96c9049b10e7e18ee922b4caf9167acb4efdd6/tags.json"; const tags = document.querySelector(".tags"); const fragment = document.createDocumentFragment(); const maxFontSizeForTag = 6;  fetch(dataURL)   .then(function (res) {     return res.json();   })   .then(function (data) {     // 1. Create a new array from data     let orderedData = data.map((x) => x);     // 2. Order it by number of articles each tag has     orderedData.sort(function(a, b) {       return a.tagged_articles.length - b.tagged_articles.length;     });     orderedData = orderedData.reverse();     // 3. Get a value for the tag with the most articles     const highestValue = orderedData[0].tagged_articles.length;     // 4. Create a list item for each result from data.     data.forEach((result) => handleResult(result, highestValue));     // 5. Append the full list of tags to the tags element     tags.appendChild(tag);   });

The JavaScript above uses the Fetch API to fetch the URL where tags.json is hosted. Once it gets this data, it returns it as JSON. Here we seque into a new array called orderedData (so we don’t mutate the original array), find the tag with the most articles. We’ll use this value later on in a font-scale so all other tags will have a font-size relative to it. Then, forEach result in the response, we call a function I have named handleResult() and pass the result and the highestValue to this function as a parameter. It also creates:

  • a variable called tags which is what we will use to inject each list item that we create from the results,
  • a variable for a fragment to hold the result of each iteration of the loop, which we will later append to the tags, and
  • a variable for the max font size, which we’ll use in our font scale later.

Next up, the handleResult(result) function:

function handleResult(result, highestValue) {   const tag = document.createElement("li");   tag.classList.add("tag");   tag.innerHTML = `<a class="tag__link" href="$ {result.href}" style="font-size: $ {result.tagged_articles.length * 1.25}em">$ {result.title} ($ {result.tagged_articles.length})</a>`;    // Append each tag to the fragment   fragment.appendChild(tag); }

This is pretty simple function that creates a list element set to the variable named tag and then adds a .tag class to this list element. Once that’s created, it sets the innerHTML of the list item to be a link and populates the values of that link with values from the JSON feed, such as a result.href for the link to the tag. When each li is created, it’s then added as a string to the fragment, which we will later then append to the tags variable. The most important item here is the inline style tag that uses the number of articles—result.tagged_articles.length—to set a relative font size using em units for this list item. Later, we’ll change that value to a formula to use a basic font scale.

I find this JavaScript just a little bit ugly and hard on the eyes, so let’s create some variables and a simple font scale formula for each of our properties to tidy it up and make it easier to read.

function handleResult(result, highestValue) {   // Set our variables   const name = result.title;   const link = result.href;   const numberOfArticles = result.tagged_articles.length;   let fontSize = numberOfArticles / highestValue * maxFontSizeForTag;   fontSize = +fontSize.toFixed(2);   const fontSizeProperty = `$ {fontSize}em`;    // Create a list element for each tag and inline the font size   const tag = document.createElement("li");   tag.classList.add("tag");   tag.innerHTML = `<a class="tag__link" href="$ {link}" style="font-size: $ {fontSizeProperty}">$ {name} ($ {numberOfArticles})</a>`;      // Append each tag to the fragment   fragment.appendChild(tag); }

By setting some variables before we get into creating our HTML, the code is a lot easier to read. And it also makes our code a little bit more DRY, as we can use the numberOfArticles variable in more than one place.

Once each of the tags has been returned in this .forEach loop, they are collected together in the fragment. After that, we use appendChild() to add them to the tags element. This means the DOM is manipulated only once, instead of being manipulated each time the loop runs, which is a nice performance boost if we happen to have a large number of tags.

Font scaling

What we have now will work fine for us, and we could start writing our CSS. However, our formula for the fontSize variable means that the tag with the most articles (which is “flex” with 25) will be 6em (25 / 25 * 6 = 6), but the tags with only one article are going to be 1/25th the size of that (1 / 25 * 6 = 0.24), making the content unreadable. If we had a tag with 100 articles, the smaller tags would fare even worse (1 / 100 * 6 = 0.06).

To get around this, I have added a simple if statement that if the fontSize that is returned is less than 1, set the fontSize to 1. If not, keep it at its current size. Now, all the tags will be within a font scale of 1em to 6em, rounded off to two decimal places. To increase the size of the largest tag, just change the value of maxFontSizeForTag. You can decide what works best for you based on the amount of content you are dealing with.

function handleResult(result, highestValue) {   // Set our variables   const numberOfArticles = result.tagged_articles.length;   const name = result.title;   const link = result.href;   let fontSize = numberOfArticles / highestValue * maxFontSizeForTag;   fontSize = +fontSize.toFixed(2);      // Make sure our font size will be at least 1em   if (fontSize <= 1) {     fontSize = 1;   } else {     fontSize = fontSize;   }   const fontSizeProperty = `$ {fontSize}em`;      // Then, create a list element for each tag and inline the font size.   tag = document.createElement("li");   tag.classList.add("tag");   tag.innerHTML = `<a class="tag__link" href="$ {link}" style="font-size: $ {fontSizeProperty}">$ {name} ($ {numberOfArticles})</a>`;    // Append each tag to the fragment   fragment.appendChild(tag); }

Now the CSS!

We’re using flexbox for our layout since each of the tags can be of varying width. We then center-align them with justify-content: center, and remove the list bullets.

.tags {   display: flex;   flex-wrap: wrap;   justify-content: center;   max-width: 960px;   margin: auto;   padding: 2rem 0 1rem;   list-style: none;   border: 2px solid white;   border-radius: 5px; }

We’ll also use flexbox for the individual tags. This allows us to vertically align them with align-items: center since they will have varying heights based on their font sizes.

.tag {   display: flex;   align-items: center;   margin: 0.25rem 1rem; }

Each link in the tag cloud has a small bit of padding, just to allow it to be clickable slightly outside of its strict dimensions.

.tag__link {   padding: 5px 5px 0;   transition: 0.3s;   text-decoration: none; }

I find this is handy on small screens especially for people who might find it harder to tap on links. The initial text-decoration is removed as I think we can assume each item of text in the tag cloud is a link and so a special decoration is not needed for them.

I’ll just drop in some colors to style things up a bit more:

.tag:nth-of-type(4n+1) .tag__link {   color: #ffd560; } .tag:nth-of-type(4n+2) .tag__link {   color: #ee4266; } .tag:nth-of-type(4n+3) .tag__link {   color: #9e88f7; } .tag:nth-of-type(4n+4) .tag__link {   color: #54d0ff; }

The color scheme for this was stolen directly from Chris’ blogroll, where every fourth tag starting at tag one is yellow, every fourth tag starting at tag two is red, every fourth tag starting at tag three is purple. and every fourth tag starting at tag four is blue.

Screenshot of the blogroll on Chris Coyier's personal website, showing lots of brightly colored links with the names of blogs included in the blogroll.

We then set the focus and hover states for each link:

.tag:nth-of-type(4n+1) .tag__link:focus, .tag:nth-of-type(4n+1) .tag__link:hover {   box-shadow: inset 0 -1.3em 0 0 #ffd560; } .tag:nth-of-type(4n+2) .tag__link:focus, .tag:nth-of-type(4n+2) .tag__link:hover {   box-shadow: inset 0 -1.3em 0 0 #ee4266; } .tag:nth-of-type(4n+3) .tag__link:focus, .tag:nth-of-type(4n+3) .tag__link:hover {   box-shadow: inset 0 -1.3em 0 0 #9e88f7; } .tag:nth-of-type(4n+4) .tag__link:focus, .tag:nth-of-type(4n+4) .tag__link:hover {   box-shadow: inset 0 -1.3em 0 0 #54d0ff; }

I could probably have created a custom variable for the colors at this stage—like --yellow: #ffd560, etc.—but decided to go with the longhand approach for IE 11 support. I love the box-shadow hover effect. It’s a very small amount of code to achieve something much more visually-appealing than a standard underline or bottom-border. Using em units here means we have decent control over how large the shadow would be in relation to the text it needed to cover.

OK, let’s top this off by setting every tag link to be black on hover:

.tag:nth-of-type(4n+1) .tag__link:focus, .tag:nth-of-type(4n+1) .tag__link:hover, .tag:nth-of-type(4n+2) .tag__link:focus, .tag:nth-of-type(4n+2) .tag__link:hover, .tag:nth-of-type(4n+3) .tag__link:focus, .tag:nth-of-type(4n+3) .tag__link:hover, .tag:nth-of-type(4n+4) .tag__link:focus, .tag:nth-of-type(4n+4) .tag__link:hover {   color: black; }

And we’re done! Here’s the final result:


The post Create a Tag Cloud with some Simple CSS and even Simpler JavaScript appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , , ,
[Top]

Let’s Create a Lightweight Native Event Bus in JavaScript

An event bus is a design pattern (and while we’ll be talking about JavaScript here, it’s a design pattern in any language) that can be used to simplify communications between different components. It can also be thought of as publish/subscribe or pubsub.

The idea is that components can listen to the event bus to know when to do the things they do. For example, a “tab panel” component might listen for events telling it to change the active tab. Sure, that might happen from a click on one of the tabs, and thus handled entirely within that component. But with an event bus, some other elements could tell the tab to change. Imagine a form submission which causes an error that the user needs to be alerted to within a specific tab, so the form sends a message to the event bus telling the tabs component to change the active tab to the one with the error. That’s what it looks like aboard an event bus.

Pseudo-code for that situation would be like…

// Tab Component Tabs.changeTab = id =&gt; {   // DOM work to change the active tab. } MyEventBus.subscribe("change-tab", Tabs.changeTab(id));  // Some other component... // something happens, then: MyEventBus.publish(&quot;change-tab&quot;, 2);  

Do you need a JavaScript library to this? (Trick question: you never need a JavaScript library). Well, there are lots of options out there:

Also, check out Mitt which is a library that’s only 200 bytes gzipped. There is something about this simple pattern that inspires people to tackle it themselves in the most succincet way possible.

Let’s do that ourselves! We’ll use no third-party library at all and leverage an event listening system that is already built into JavaScript with the addEventListener we all know and love.

First, a little context

The addEventListener API in JavaScript is a member function of the EventTarget class. The reason we can bind a click event to a button is because the prototype interface of <button> (HTMLButtonElement) inherits from EventTarget indirectly.

Source: MDN Web Docs

Different from most other DOM interfaces, EventTarget can be created directly using the new keyword. It is supported in all modern browsers, but only fairly recently. As we can see in the screenshot above, Node inherits EventTarget, thus all DOM nodes have method addEventListener.

Here’s the trick

I’m suggesting an extremely lightweight Node type to act as our event-listening bus: an HTML comment (<!-- comment -->).

To a browser rendering engine, HTML comments are just notes in the code that have no functionality other than descriptive text for developers. But since comments are still written in HTML, they end up in the DOM as real nodes and have their own prototype interface—Comment—which inherits Node.

The Comment class can be created from new directly like EventTarget can:

const myEventBus = new Comment('my-event-bus');

We could also use the ancient, but widely-supported document.createComment API. It requires a data parameter, which is the content of the comment. It can even be an empty string:

const myEventBus = document.createComment('my-event-bus');

Now we can emit events using dispatchEvent, which accepts an Event Object. To pass user-defined event data, use CustomEvent, where the detail field can be used to contain any data.

myEventBus.dispatchEvent(   new CustomEvent('event-name', {      detail: 'event-data'   }) );

Internet Explorer 9-11 supports CustomEvent, but none of the versions support new CustomEvent. It’s complex to simulate it using document.createEvent, so if IE support is important to you, there’s a way to polyfill it.

Now we can bind event listeners:

myEventBus.addEventListener('event-name', ({ detail }) => {   console.log(detail); // => event-data });

If an event intends to be triggered only once, we may use { once: true } for one-time binding. Other options won’t fit here. To remove event listeners, we can use the native removeEventListener.

Debugging

The number of events bound to single event bus can be huge. There also can be memory leaks if you forget to remove them. What if we want to know how many events are bound to myEventBus?

myEventBus is a DOM node, so it can be inspected by DevTools in the browser. From there, we can find the events in the Elements → Event Listeners tab. Be sure to uncheck “Ancestors” to hide events bound on document and window.

An example

One drawback is that the syntax of EventTarget is slightly verbose. We can write a simple wrapper for it. Here is a demo in TypeScript below:

class EventBus<DetailType = any> {   private eventTarget: EventTarget;   constructor(description = '') { this.eventTarget = document.appendChild(document.createComment(description)); }   on(type: string, listener: (event: CustomEvent<DetailType>) => void) { this.eventTarget.addEventListener(type, listener); }   once(type: string, listener: (event: CustomEvent<DetailType>) => void) { this.eventTarget.addEventListener(type, listener, { once: true }); }   off(type: string, listener: (event: CustomEvent<DetailType>) => void) { this.eventTarget.removeEventListener(type, listener); }   emit(type: string, detail?: DetailType) { return this.eventTarget.dispatchEvent(new CustomEvent(type, { detail })); } }      // Usage const myEventBus = new EventBus<string>('my-event-bus'); myEventBus.on('event-name', ({ detail }) => {   console.log(detail); });  myEventBus.once('event-name', ({ detail }) => {   console.log(detail); });  myEventBus.emit('event-name', 'Hello'); // => HellonHello myEventBus.emit('event-name', 'World'); // => World

The following demo provides the compiled JavaScript.


And there we have it! We just created a dependency-free event-listening bus where one component can inform another component of changes to trigger an action. It doesn’t take a full library to do this sort of stuff, and the possibilities it opens up are pretty endless.


The post Let’s Create a Lightweight Native Event Bus in JavaScript appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , ,
[Top]

How to Create a Favicon That Changes Automatically

I found this Free Favicon Maker the other day. It’s a nice tool to make a favicon (true to its name), but unlike other favicon generators, this one lets you create one from scratch starting with a character or an emoji. Naturally, I was curious to look at the code to see how it works and, while I did, it got me thinking in different directions. I was reminded of something I read that says it is possible to dynamically change the favicon of a website. That’s actually what some websites use as a sort of notification for users: change the favicon to a red dot or some other indicator that communicates something is happening or has changed on the page.

I started browsing the emojis via emojipedia.org for inspiration and that’s when it struck: why not show the time with a clock emoji (🕛) and other related favicons? The idea is to check the time every minute and set the favicon with the corresponding clock emoji indicating the current time.

We’re going to do exactly that in this article, and it will work with plain JavaScript. Since I often use Gatsby for static sites though, we will also show how to do it in React. From there, the ideas should be usable regardless of what you want to do with your favicon and how.

Here’s a function that take an emoji as a parameter and returns a valid data URL that can be used as an image (or favicon!) source:

// Thanks to https://formito.com/tools/favicon const faviconHref = emoji =>   `data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22256%22 height=%22256%22 viewBox=%220 0 100 100%22><text x=%2250%%22 y=%2250%%22 dominant-baseline=%22central%22 text-anchor=%22middle%22 font-size=%2280%22>$ {emoji}</text></svg>`

And here’s a function that targets the favicon <link> in the <head> and changes it to that emoji:

const changeFavicon = emoji => {   // Ensure we have access to the document, i.e. we are in the browser.   if (typeof window === 'undefined') return    const link =     window.document.querySelector("link[rel*='icon']") ||     window.document.createElement("link")   link.type = "image/svg+xml"   link.rel = "shortcut icon"   link.href = faviconHref(emoji)    window.document.getElementsByTagName("head")[0].appendChild(link) }

(Shout out to this StackOverflow answer for that nice little trick on creating the link if it doesn’t exist.)

Feel free to give those a try! Open up the DevTools JavaScript console, copy and paste the two functions above, and call changeFavicon("💃"). You can do that right on this website and you’ll see the favicon change to that awesome dancing emoji.

Back to our clock/time project… If we want to show the emoji with the right emoji clock showing the correct time, we need to determine it from the current time. For example, if it’s 10:00 we want to show 🕙. If it’s 4.30 we want to show 🕟. There aren’t emojis for every single time, so we’ll show the best one we have. For example, between 9:45 to 10:14 we want to show the clock that shows 10:00; from 10:15 to 10:44 we want to show the clock that marks 10.30, etc.

We can do it with this function:

const currentEmoji = () => {   // Add 15 minutes and round down to closest half hour   const time = new Date(Date.now() + 15 * 60 * 1000)    const hours = time.getHours() % 12   const minutes = time.getMinutes() < 30 ? 0 : 30    return {     "0.0": "🕛",     "0.30": "🕧",     "1.0": "🕐",     "1.30": "🕜",     "2.0": "🕑",     "2.30": "🕝",     "3.0": "🕒",     "3.30": "🕞",     "4.0": "🕓",     "4.30": "🕟",     "5.0": "🕔",     "5.30": "🕠",     "6.0": "🕕",     "6.30": "🕡",     "7.0": "🕖",     "7.30": "🕢",     "8.0": "🕗",     "8.30": "🕣",     "9.0": "🕘",     "9.30": "🕤",     "10.0": "🕙",     "10.30": "🕥",     "11.0": "🕚",     "11.30": "🕦",   }[`$ {hours}.$ {minutes}`] }

Now we just have to call changeFavicon(currentEmoji()) every minute or so. If we had to do it with plain JavaScript, a simple setInterval would make the trick:

// One minute const delay = 60 * 1000  // Change the favicon when the page gets loaded... const emoji = currentEmoji() changeFavicon(emoji)  // ... and update it every minute setInterval(() => {   const emoji = currentEmoji()   changeFavicon(emoji) }, delay)

The React part

Since my blog is powered by Gatsby, I want to be able to use this code inside a React component, changing as little as possible. It is inherently imperative, as opposed to the declarative nature of React, and moreover has to be called every minute. How can we do that?

Enter Dan Abramov and his amazing blog post. Dan is a great writer who can explain complex things in a clear way, and I strongly suggest checking out this article, especially if you want a better understanding of React Hooks. You don’t necessarily need to understand everything in it — one of the strengths of hooks is that they can be used even without fully grasping the internal implementation. The important thing is to know how to use it. Here’s how that looks:

import { useEffect } from "react" import useInterval from "./useInterval"  const delay = 60 * 1000  const useTimeFavicon = () => {   // Change the favicon when the component gets mounted...   useEffect(() => {     const emoji = currentEmoji()     changeFavicon(emoji)   }, [])    // ... and update it every minute   useInterval(() => {     const emoji = currentEmoji()     changeFavicon(emoji)   }, delay) }

Finally, just call useTimeFavicon() in your root component, and you are good to go! Wanna see it work? Here’s a deployed CodePen Project where you can see it and here’s the project code itself.

Wrapping up

What we did here was cobble together three pieces of code together from three different sources to get the result we want. Ancient Romans would say Divide et Impera. (I’m Italian, so please indulge me a little bit of Latin!). That means “divide and conquer.” If you look at the task as a single entity, you may be a little anxious about it: “How can I show a favicon with the current time, always up to date, on my React website?” Getting all the details right is not trivial.

The good news is, you don’t always have to personally tackle all the details at the same moment: it is much more effective to divide the problem into sub-problems, and if any of these have already been solved by others, so much the better!

Sounds like web development, eh? There’s nothing wrong with using code written by others, as long as it’s done wisely. As they say, there is no need to reinvent the wheel and what we got here is a nice enhancement for any website — whether it’s displaying notifications, time updates, or whatever you can think of.


The post How to Create a Favicon That Changes Automatically appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]

How to create a client-serverless Jamstack app using Netlify, Gatsby and Fauna

The Jamstack is a modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup.

The key aspects of a Jamstack application are the following:

  • The entire app runs on a CDN (or ADN). CDN stands for Content Delivery Network and an ADN is an Application Delivery Network.
  • Everything lives in Git.
  • Automated builds run with a workflow when developers push the code.
  • There’s Automatic deployment of the prebuilt markup to the CDN/ADN.
  • Reusable APIs make hasslefree integrations with many of the services. To take a few examples, Stripe for the payment and checkout, Mailgun for email services, etc. We can also write custom APIs targeted to a specific use-case. We will see such examples of custom APIs in this article.
  • It’s practically Serverless. To put it more clearly, we do not maintain any servers, rather make use of already existing services (like email, media, database, search, and so on) or serverless functions.

In this article, we will learn how to build a Jamstack application that has:

  • A global data store with GraphQL support to store and fetch data with ease. We will use Fauna to accomplish this.
  • Serverless functions that also act as the APIs to fetch data from the Fauna data store. We will use Netlify serverless functions for this.
  • We will build the client side of the app using a Static Site Generator called Gatsbyjs.
  • Finally we will deploy the app on a CDN configured and managed by Netlify CDN.

So, what are we building today?

We all love shopping. How cool would it be to manage all of our shopping notes in a centralized place? So we’ll be building an app called ‘shopnote’ that allows us to manage shop notes. We can also add one or more items to a note, mark them as done, mark them as urgent, etc.

At the end of this article, our shopnote app will look like this,

TL;DR

We will learn things with a step-by-step approach in this article. If you want to jump into the source code or demonstration sooner, here are links to them.

Set up Fauna

Fauna is the data API for client-serverless applications. If you are familiar with any traditional RDBMS, a major difference with Fauna would be, it is a relational NOSQL system that gives all the capabilities of the legacy RDBMS. It is very flexible without compromising scalability and performance.

Fauna supports multiple APIs for data-access,

  • GraphQL: An open source data query and manipulation language. If you are new to the GraphQL, you can find more details from here, https://graphql.org/
  • Fauna Query Language (FQL): An API for querying Fauna. FQL has language specific drivers which makes it flexible to use with languages like JavaScript, Java, Go, etc. Find more details of FQL from here.

In this article we will explain the usages of GraphQL for the ShopNote application.

First thing first, sign up using this URL. Please select the free plan which is with a generous daily usage quota and more than enough for our usage.

Next, create a database by providing a database name of your choice. I have used shopnotes as the database name.

After creating the database, we will be defining the GraphQL schema and importing it into the database. A GraphQL schema defines the structure of the data. It defines the data types and the relationship between them. With schema we can also specify what kind of queries are allowed.

At this stage, let us create our project folder. Create a project folder somewhere on your hard drive with the name, shopnote. Create a file with the name, shopnotes.gql with the following content:

type ShopNote {   name: String!   description: String   updatedAt: Time   items: [Item!] @relation }   type Item {   name: String!   urgent: Boolean   checked: Boolean   note: ShopNote! }   type Query {   allShopNotes: [ShopNote!]! }

Here we have defined the schema for a shopnote list and item, where each ShopNote contains name, description, update time and a list of Items. Each Item type has properties like, name, urgent, checked and which shopnote it belongs to. 

Note the @relation directive here. You can annotate a field with the @relation directive to mark it for participating in a bi-directional relationship with the target type. In this case, ShopNote and Item are in a one-to-many relationship. It means, one ShopNote can have multiple Items, where each Item can be related to a maximum of one ShopNote.

You can read more about the @relation directive from here. More on the GraphQL relations can be found from here.

As a next step, upload the shopnotes.gql file from the Fauna dashboard using the IMPORT SCHEMA button,

Upon importing a GraphQL Schema, FaunaDB will automatically create, maintain, and update, the following resources:

  • Collections for each non-native GraphQL Type; in this case, ShopNote and Item.
  • Basic CRUD Queries/Mutations for each Collection created by the Schema, e.g. createShopNote allShopNotes; each of which are powered by FQL.
  • For specific GraphQL directives: custom Indexes or FQL for establishing relationships (i.e. @relation), uniqueness (@unique), and more!

Behind the scene, Fauna will also help to create the documents automatically. We will see that in a while.

Fauna supports a schema-free object relational data model. A database in Fauna may contain a group of collections. A collection may contain one or more documents. Each of the data records are inserted into the document. This forms a hierarchy which can be visualized as:

Here the data record can be arrays, objects, or of any other supported types. With the Fauna data model we can create indexes, enforce constraints. Fauna indexes can combine data from multiple collections and are capable of performing computations. 

At this stage, Fauna already created a couple of collections for us, ShopNote and Item. As we start inserting records, we will see the Documents are also getting created. We will be able view and query the records and utilize the power of indexes. You may see the data model structure appearing in your Fauna dashboard like this in a while,

Point to note here, each of the documents is identified by the unique ref attribute. There is also a ts field which returns the timestamp of the recent modification to the document. The data record is part of the data field. This understanding is really important when you interact with collections, documents, records using FQL built-in functions. However, in this article we will interact with them using GraphQL queries with Netlify Functions.

With all these understanding, let us start using our Shopenotes database that is created successfully and ready for use. 

Let us try some queries

Even though we have imported the schema and underlying things are in place, we do not have a document yet. Let us create one. To do that, copy the following GraphQL mutation query to the left panel of the GraphQL playground screen and execute.

mutation {   createShopNote(data: {     name: "My Shopping List"     description: "This is my today's list to buy from Tom's shop"     items: {       create: [         { name: "Butther - 1 pk", urgent: true }         { name: "Milk - 2 ltrs", urgent: false }         { name: "Meat - 1lb", urgent: false }       ]     }   }) {     _id     name     description     items {       data {         name,         urgent       }     }   } }

Note, as Fauna already created the GraphQL mutation classes in the background, we can directly use it like, createShopNote. Once successfully executed, you can see the response of a ShopNote creation at the right side of the editor.

The newly created ShopNote document has all the required details we have passed while creating it. We have seen ShopNote has a one-to-many relation with Item. You can see the shopnote response has the item data nested within it. In this case, one shopnote has three items. This is really powerful. Once the schema and relation are defined, the document will be created automatically keeping that relation in mind.

Now, let us try fetching all the shopnotes. Here is the GraphQL query:

query {     allShopNotes {     data {       _id       name       description       updatedAt       items {         data {           name,           checked,           urgent         }       }     }   } }

Let’s try the query in the playground as before:

Now we have a database with a schema and it is fully operational with creating and fetch functionality. Similarly, we can create queries for adding, updating, removing items to a shopnote and also updating and deleting a shopnote. These queries will be used at a later point in time when we create the serverless functions.

If you are interested to run other queries in the GraphQL editor, you can find them from here,

Create a Server Secret Key

Next, we need to create a secured server key to make sure the access to the database is authenticated and authorized.

Click on the SECURITY option available in the FaunaDB interface to create the key, like so,

On successful creation of the key, you will be able to view the key’s secret. Make sure to copy and save it somewhere safe.

We do not want anyone else to know about this key. It is not even a good idea to commit it to the source code repository. To maintain this secrecy, create an empty file called .env at the root level of your project folder.

Edit the .env file and add the following line to it (paste the generated server key in the place of, <YOUR_FAUNA_KEY_SECRET>).

FAUNA_SERVER_SECRET=<YOUR_FAUNA_KEY_SECRET>

Add a .gitignore file and write the following content to it. This is to make sure we do not commit the .env file to the source code repo accidentally. We are also ignoring node_modules as a best practice.

.env

We are done with all that had to do with Fauna’s setup. Let us move to the next phase to create serverless functions and APIs to access data from the Fauna data store. At this stage, the directory structure may look like this,

Set up Netlify Serverless Functions

Netlify is a great platform to create hassle-free serverless functions. These functions can interact with databases, file-system, and in-memory objects.

Netlify functions are powered by AWS Lambda. Setting up AWS Lambdas on our own can be a fairly complex job. With Netlify, we will simply set a folder and drop our functions. Writing simple functions automatically becomes APIs. 

First, create an account with Netlify. This is free and just like the FaunaDB free tier, Netlify is also very flexible.

Now we need to install a few dependencies using either npm or yarn. Make sure you have nodejs installed. Open a command prompt at the root of the project folder. Use the following command to initialize the project with node dependencies,

npm init -y

Install the netlify-cli utility so that we can run the serverless function locally.

npm install netlify-cli -g

Now we will install two important libraries, axios and dotenv. axios will be used for making the HTTP calls and dotenv will help to load the FAUNA_SERVER_SECRET environment variable from the .env file into process.env.

yarn add axios dotenv

Or:

npm i axios dotenv

Create serverless functions

Create a folder with the name, functions at the root of the project folder. We are going to keep all serverless functions under it.

Now create a subfolder called utils under the functions folder. Create a file called query.js under the utils folder. We will need some common code to query the data store for all the serverless functions. The common code will be in the query.js file.

First we import the axios library functionality and load the .env file. Next, we export an async function that takes the query and variables. Inside the async function, we make calls using axios with the secret key. Finally, we return the response.

// query.js   const axios = require("axios"); require("dotenv").config();   module.exports = async (query, variables) => {   const result = await axios({       url: "https://graphql.fauna.com/graphql",       method: "POST",       headers: {           Authorization: `Bearer $ {process.env.FAUNA_SERVER_SECRET}`       },       data: {         query,         variables       }  });    return result.data; };

Create a file with the name, get-shopnotes.js under the functions folder. We will perform a query to fetch all the shop notes.

// get-shopnotes.js   const query = require("./utils/query");   const GET_SHOPNOTES = `    query {        allShopNotes {        data {          _id          name          description          updatedAt          items {            data {              _id,              name,              checked,              urgent          }        }      }    }  }   `;   exports.handler = async () => {   const { data, errors } = await query(GET_SHOPNOTES);     if (errors) {     return {       statusCode: 500,       body: JSON.stringify(errors)     };   }     return {     statusCode: 200,     body: JSON.stringify({ shopnotes: data.allShopNotes.data })   }; };

Time to test the serverless function like an API. We need to do a one time setup here. Open a command prompt at the root of the project folder and type:

netlify login

This will open a browser tab and ask you to login and authorize access to your Netlify account. Please click on the Authorize button.

Next, create a file called, netlify.toml at the root of your project folder and add this content to it,

[build]     functions = "functions"   [[redirects]]    from = "/api/*"    to = "/.netlify/functions/:splat"    status = 200

This is to tell Netlify about the location of the functions we have written so that it is known at the build time.

Netlify automatically provides the APIs for the functions. The URL to access the API is in this form, /.netlify/functions/get-shopnotes which may not be very user-friendly. We have written a redirect to make it like, /api/get-shopnotes.

Ok, we are done. Now in command prompt type,

netlify dev

By default the app will run on localhost:8888 to access the serverless function as an API.

Open a browser tab and try this URL, http://localhost:8888/api/get-shopnotes:

Congratulations!!! You have got your first serverless function up and running.

Let us now write the next serverless function to create a ShopNote. This is going to be simple. Create a file named, create-shopnote.js under the functions folder. We need to write a mutation by passing the required parameters. 

//create-shopnote.js   const query = require("./utils/query");   const CREATE_SHOPNOTE = `   mutation($ name: String!, $ description: String!, $ updatedAt: Time!, $ items: ShopNoteItemsRelation!) {     createShopNote(data: {name: $ name, description: $ description, updatedAt: $ updatedAt, items: $ items}) {       _id       name       description       updatedAt       items {         data {           name,           checked,           urgent         }       }     }   } `;   exports.handler = async event => {      const { name, items } = JSON.parse(event.body);   const { data, errors } = await query(     CREATE_SHOPNOTE, { name, items });     if (errors) {     return {       statusCode: 500,       body: JSON.stringify(errors)     };   }     return {     statusCode: 200,     body: JSON.stringify({ shopnote: data.createShopNote })   }; };

Please give your attention to the parameter, ShopNotesItemRelation. As we had created a relation between the ShopNote and Item in our schema, we need to maintain that while writing the query as well.

We have de-structured the payload to get the required information from the payload. Once we got those, we just called the query method to create a ShopNote.

Alright, let’s test it out. You can use postman or any other tools of your choice to test it like an API. Here is the screenshot from postman.

Great, we can create a ShopNote with all the items we want to buy from a shopping mart. What if we want to add an item to an existing ShopNote? Let us create an API for it. With the knowledge we have so far, it is going to be really quick.

Remember, ShopNote and Item are related? So to create an item, we have to mandatorily tell which ShopNote it is going to be part of. Here is our next serverless function to add an item to an existing ShopNote.

//add-item.js   const query = require("./utils/query");   const ADD_ITEM = `   mutation($ name: String!, $ urgent: Boolean!, $ checked: Boolean!, $ note: ItemNoteRelation!) {     createItem(data: {name: $ name, urgent: $ urgent, checked: $ checked, note: $ note}) {       _id       name       urgent       checked       note {         name       }     }   } `;   exports.handler = async event => {      const { name, urgent, checked, note} = JSON.parse(event.body);   const { data, errors } = await query(     ADD_ITEM, { name, urgent, checked, note });     if (errors) {     return {       statusCode: 500,       body: JSON.stringify(errors)     };   }     return {     statusCode: 200,     body: JSON.stringify({ item: data.createItem })   }; };

We are passing the item properties like, name, if it is urgent, the check value and the note the items should be part of. Let’s see how this API can be called using postman,

As you see, we are passing the id of the note while creating an item for it.

We won’t bother writing the rest of the API capabilities in this article,  like updating, deleting a shop note, updating, deleting items, etc. In case, you are interested, you can look into those functions from the GitHub Repository.

However, after creating the rest of the API, you should have a directory structure like this,

We have successfully created a data store with Fauna, set it up for use, created an API backed by serverless functions, using Netlify Functions, and tested those functions/routes.

Congratulations, you did it. Next, let us build some user interfaces to show the shop notes and add items to it. To do that, we will use Gatsby.js (aka, Gatsby) which is a super cool, React-based static site generator.

The following section requires you to have basic knowledge of ReactJS. If you are new to it, you can learn it from here. If you are familiar with any other user interface technologies like, Angular, Vue, etc feel free to skip the next section and build your own using the APIs explained so far.

Set up the User Interfaces using Gatsby

We can set up a Gatsby project either using the starter projects or initialize it manually. We will build things from scratch to understand it better.

Install gatsby-cli globally. 

npm install -g gatsby-cli

Install gatsby, react and react-dom

yarn add gatsby react react-dom

Edit the scripts section of the package.json file to add a script for develop.

"scripts": {   "develop": "gatsby develop"  }

Gatsby projects need a special configuration file called, gatsby-config.js. Please create a file named, gatsby-config.js at the root of the project folder with the following content,

module.exports = {   // keep it empty     }

Let’s create our first page with Gatsby. Create a folder named, src at the root of the project folder. Create a subfolder named pages under src. Create a file named, index.js under src/pages with the following content:

import React, { useEffect, useState } from 'react';       export default () => {       const [loading, setLoading ] = useState(false);       const [shopnotes, setShopnotes] = useState(null);         return (     <>           <h1>Shopnotes to load here...</h1>     </>           )     } 

Let’s run it. We generally need to use the command gatsby develop to run the app locally. As we have to run the client side application with netlify functions, we will continue to use, netlify dev command.

netlify dev

That’s all. Try accessing the page at http://localhost:8888. You should see something like this,

Gatsby project build creates a couple of output folders which you may not want to push to the source code repository. Let us add a few entries to the .gitignore file so that we do not get unwanted noise.

Add .cache, node_modules and public to the .gitignore file. Here is the full content of the file:

.cache public node_modules *.env

At this stage, your project directory structure should match with the following:

Thinking of the UI components

We will create small logical components to achieve the ShopNote user interface. The components are:

  • Header: A header component consists of the Logo, heading and the create button to create a shopnote.
  • Shopenotes: This component will contain the list of the shop note (Note component).
  • Note: This is individual notes. Each of the notes will contain one or more items.
  • Item: Each of the items. It consists of the item name and actions to add, remove, edit an item.

You can see the sections marked in the picture below:

Install a few more dependencies

We will install a few more dependencies required for the user interfaces to be functional and look better. Open a command prompt at the root of the project folder and install these dependencies,

yarn add bootstrap lodash moment react-bootstrap react-feather shortid

Lets load all the Shop Notes

We will use the Reactjs useEffect hook to make the API call and update the shopnotes state variables. Here is the code to fetch all the shop notes. 

useEffect(() => {   axios("/api/get-shopnotes").then(result => {     if (result.status !== 200) {       console.error("Error loading shopnotes");       console.error(result);       return;     }     setShopnotes(result.data.shopnotes);     setLoading(true);   }); }, [loading]);

Finally, let us change the return section to use the shopnotes data. Here we are checking if the data is loaded. If so, render the Shopnotes component by passing the data we have received using the API.

return (   <div className="main">     <Header />     {       loading ? <Shopnotes data = { shopnotes } /> : <h1>Loading...</h1>     }   </div> );  

You can find the entire index.js file code from here The index.js file creates the initial route(/) for the user interface. It uses other components like, Shopnotes, Note and Item to make the UI fully operational. We will not go to a great length to understand each of these UI components. You can create a folder called components under the src folder and copy the component files from here.

Finally, the index.css file

Now we just need a css file to make things look better. Create a file called index.css under the pages folder. Copy the content from this CSS file to the index.css file.

import 'bootstrap/dist/css/bootstrap.min.css'; import './index.css'

That’s all. We are done. You should have the app up and running with all the shop notes created so far. We are not getting into the explanation of each of the actions on items and notes here not to make the article very lengthy. You can find all the code in the GitHub repo. At this stage, the directory structure may look like this,

A small exercise

I have not included the Create Note UI implementation in the GitHib repo. However, we have created the API already. How about you build the front end to add a shopnote? I suggest implementing a button in the header, which when clicked, creates a shopnote using the API we’ve already defined. Give it a try!

Let’s Deploy

All good so far. But there is one issue. We are running the app locally. While productive, it’s not ideal for the public to access. Let’s fix that with a few simple steps.

Make sure to commit all the code changes to the Git repository, say, shopnote. You have an account with Netlify already. Please login and click on the button, New site from Git.

Next, select the relevant Git services where your project source code is pushed. In my case, it is GitHub.

Browse the project and select it.

Provide the configuration details like the build command, publish directory as shown in the image below. Then click on the button to provide advanced configuration information. In this case, we will pass the FAUNA_SERVER_SECRET key value pair from the .env file. Please copy paste in the respective fields. Click on deploy.

You should see the build successful in a couple of minutes and the site will be live right after that.

In Summary

To summarize:

  • The Jamstack is a modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup.
  • 70% – 80% of the features that once required a custom back-end can now be done either on the front end or there are APIs, services to take advantage of.
  • Fauna provides the data API for the client-serverless applications. We can use GraphQL or Fauna’s FQL to talk to the store.
  • Netlify serverless functions can be easily integrated with Fauna using the GraphQL mutations and queries. This approach may be useful when you have the need of  custom authentication built with Netlify functions and a flexible solution like Auth0.
  • Gatsby and other static site generators are great contributors to the Jamstack to give a fast end user experience.

Thank you for reading this far! Let’s connect. You can @ me on Twitter (@tapasadhikary) with comments, or feel free to follow.


The post How to create a client-serverless Jamstack app using Netlify, Gatsby and Fauna appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , , ,
[Top]

How to Create a Timeline Task List Component Using SVG

I’m thoroughly convinced that SVG unlocks a whole entire world of building interfaces on the web. It might seem daunting to learn SVG at first, but you have a spec that was designed to create shapes and yet, still has elements, like text, links, and aria labels available to you. You can accomplish some of the same effects in CSS, but it’s a little more particular to get positioning just right, especially across viewports and for responsive development.

What’s special about SVG is that all the positioning is based on a coordinate system, a little like the game Battleship. That means deciding where everything goes and how it’s drawn, as well as how it’s relative to each other, can be really straightforward to reason about. CSS positioning is for layout, which is great because you have things that correspond to one another in terms of the flow of the document. This otherwise positive trait is harder to work with if you’re making a component that’s very particular, with overlapping and precisely placed elements.

Truly, once you learn SVG, you can draw anything, and have it scale on any device. Even this very site uses SVG for custom UI elements, such as my avatar, above (meta!).

That little half circle below the author image is just SVG markup.

We won’t cover everything about SVGs in this post (you can learn some of those fundamentals here, here, here and here), but in order to illustrate the possibilities that SVG opens up for UI component development, let’s talk through one particular use case and break down how we would think about building something custom.

The timeline task list component

Recently, I was working on a project with my team at Netlify. We wanted to show the viewer which video in a series of videos in a course they were currently watching. In other words, we wanted to make some sort of thing that’s like a todo list, but shows overall progress as items are completed. (We made a free space-themed learning platform and it’s hella cool. Yes, I said hella.)

Here’s how that looks:

So how would we go about this? I’ll show an example in both Vue and React so that you can see how it might work in both frameworks.

The Vue version

We decided to make the platform in Next.js for dogfooding purposes (i.e. trying out our own Next on Netlify build plugin), but I’m more fluent in Vue so I wrote the initial prototype in Vue and ported it over to React.

Here is the full CodePen demo:

Let’s walk through this code a bit. First off, this is a single file component (SFC), so the template HTML, reactive script, and scoped styles are all encapsulated in this one file.

We’ll store some dummy tasks in data, including whether each task is completed or not. We’ll also make a method we can call on a click directive so that we can toggle whether the state is done or not.

<script> export default {   data() {     return {       tasks: [         {           name: 'thing',           done: false         },         // ...       ]     };   },   methods: {     selectThis(index) {       this.tasks[index].done = !this.tasks[index].done     }   } }; </script> 

Now, what we want to do is create an SVG that has a flexible viewBox depending on the amount of elements. We also want to tell screen readers that this a presentational element and that we will provide a title with a unique id of timeline. (Get more information on creating accessible SVGs.)

<template>   <div id="app">     <div>       <svg :viewBox="`0 0 30 $ {tasks.length * 50}`"            xmlns="http://www.w3.org/2000/svg"             width="30"             stroke="currentColor"             fill="white"            aria-labelledby="timeline"            role="presentation">            <title id="timeline">timeline element</title>         <!-- ... -->       </svg>     </div>   </div> </template>

The stroke is set to currentColor to allow for some flexibility — if we want to reuse the component in multiple places, it will inherit whatever color is used on the encapsulating div.

Next, inside the SVG, we want to create a vertical line that’s the length of the task list. Lines are fairly straightforward. We have x1 and x2 values (where the line is plotted on the x-axis), and similarly, y1 and y2.

<line x1="10" x2="10" :y1="num2" :y2="tasks.length * num1 - num2" />

The x-axis stays consistently at 10 because we’re drawing a line downward rather than left-to-right. We’ll store two numbers in data: the amount we want our spacing to be, which will be num1, and the amount we want our margin to be, which will be num2.

data() {   return {     num1: 32,     num2: 15,     // ...   } }

The y-axis starts with num2, which is subtracted from the end, as well as the margin. The tasks.length is multiplied by the spacing, which is num1.

Now, we’ll need the circles that lie on the line. Each circle is an indicator for whether a task has been completed or not. We’ll need one circle for each task, so we’ll use v-for with a unique key, which is the index (and is safe to use here as they will never reorder). We’ll connect the click directive with our method and pass in the index as a param as well.

CIrcles in SVG are made up of three attributes. The middle of the circle is plotted at cx and cy, and then we draw a radius with r. Like the line, cx starts at 10. The radius is 4 because that’s what’s readable at this scale. cy will be spaced like the line: index times the spacing (num1), plus the margin (num2).

Finally, we’ll put use a ternary to set the fill. If the task is done, it will be filled with currentColor. If not, it will be filled with white (or whatever the background is). This could be filled with a prop that gets passed in the background, for instance, where you have light and dark circles.

<circle    @click="selectThis(i)"    v-for="(task, i) in tasks"   :key="task.name"   cx="10"   r="4"   :cy="i * num1 + num2"   :fill="task.done ? 'currentColor' : 'white'"   class="select"/>

Finally, we are using CSS grid to align a div with the names of tasks. This is laid out much in the same way, where we’re looping through the tasks, and are also tied to that same click event to toggle the done state.

<template>   <div>     <div        @click="selectThis(i)"       v-for="(task, i) in tasks"       :key="task.name"       class="select">       {{ task.name }}     </div>   </div> </template>

The React version

Here is where we ended up with the React version. We’re working towards open sourcing this so that you can see the full code and its history. Here are a few modifications:

  • We’re using CSS modules rather than the SCFs in Vue
  • We’re importing the Next.js link, so that rather than toggling a “done” state, we’re taking a user to a dynamic page in Next.js
  • The tasks we’re using are actually stages of the course —or “Mission” as we call them — which are passed in here rather than held by the component.

Most of the other functionality is the same 🙂

import styles from './MissionTracker.module.css'; import React, { useState } from 'react'; import Link from 'next/link';  function MissionTracker({ currentMission, currentStage, stages }) {  const [tasks, setTasks] = useState([...stages]);  const num1 = [32];  const num2 = [15];   const updateDoneTasks = (index) => () => {    let tasksCopy = [...tasks];    tasksCopy[index].done = !tasksCopy[index].done;    setTasks(tasksCopy);  };   const taskTextStyles = (task) => {    const baseStyles = `$ {styles['tracker-select']} $ {styles['task-label']}`;     if (currentStage === task.slug.current) {      return baseStyles + ` $ {styles['is-current-task']}`;    } else {      return baseStyles;    }  };   return (    <div className={styles.container}>      <section>        {tasks.map((task, index) => (          <div            key={`mt-$ {task.slug}-$ {index}`}            className={taskTextStyles(task)}          >            <Link href={`/learn/$ {currentMission}/$ {task.slug.current}`}>              {task.title}            </Link>          </div>        ))}      </section>       <section>        <svg          viewBox={`0 0 30 $ {tasks.length * 50}`}          className={styles['tracker-svg']}          xmlns="http://www.w3.org/2000/svg"          width="30"          stroke="currentColor"          fill="white"          aria-labelledby="timeline"          role="presentation"        >          <title id="timeline">timeline element</title>           <line x1="10" x2="10" y1={num2} y2={tasks.length * num1 - num2} />          {tasks.map((task, index) => (            <circle              key={`mt-circle-$ {task.name}-$ {index}`}              onClick={updateDoneTasks(index)}              cx="10"              r="4"              cy={index * +num1 + +num2}              fill={                task.slug.current === currentStage ? 'currentColor' : 'black'              }              className={styles['tracker-select']}            />          ))}        </svg>      </section>    </div>  ); }  export default MissionTracker;

Final version

You can see the final working version here:

This component is flexible enough to accommodate lists small and large, multiple browsers, and responsive sizing. It also allows the user to have better understanding of where they are in their progress in the course.

But this is just one component. You can make any number of UI elements: knobs, controls, progress indicators, loaders… the sky’s the limit. You can style them with CSS, or inline styles, you can have them update based on props, on context, on reactive data, the sky’s the limit! I hope this opens some doors on how you yourself can develop more engaging UI elements for the web.


The post How to Create a Timeline Task List Component Using SVG appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , ,
[Top]

How to Create a Commenting Engine with Next.js and Sanity

One of the arguments against the Jamstack approach for building websites is that developing features gets complex and often requires a number of other services. Take commenting, for example. To set up commenting for a Jamstack site, you often need a third-party solution such as Disqus, Facebook, or even just a separate database service. That third-party solution usually means your comments live disconnected from their content.

When we use third-party systems, we have to live with the trade-offs of using someone else’s code. We get a plug-and-play solution, but at what cost? Ads displayed to our users? Unnecessary JavaScript that we can’t optimize? The fact that the comments content is owned by someone else? These are definitely things worth considering.

Monolithic services, like WordPress, have solved this by having everything housed under the same application. What if we could house our comments in the same database and CMS as our content, query it in the same way we query our content, and display it with the same framework on the front end?

It would make this particular Jamstack application feel much more cohesive, both for our developers and our editors.

Let’s make our own commenting engine

In this article, we’ll use Next.js and Sanity.io to create a commenting engine that meets those needs. One unified platform for content, editors, commenters, and developers.

Why Next.js?

Next.js is a meta-framework for React, built by the team at Vercel. It has built-in functionality for serverless functions, static site generation, and server-side rendering.

For our work, we’ll mostly be using its built-in “API routes” for serverless functions and its static site generation capabilities. The API routes will simplify the project considerably, but if you’re deploying to something like Netlify, these can be converted to serverless functions or we can use Netlify’s next-on-netlify package.

It’s this intersection of static, server-rendered, and serverless functions that makes Next.js a great solution for a project like this.

Why Sanity?

Sanity.io is a flexible platform for structured content. At its core, it is a data store that encourages developers to think about content as structured data. It often comes paired with an open-source CMS solution called the Sanity Studio.

We’ll be using Sanity to keep the author’s content together with any user-generated content, like comments. In the end, Sanity is a content platform with a strong API and a configurable CMS that allows for the customization we need to tie these things together.

Setting up Sanity and Next.js

We’re not going to start from scratch on this project. We’ll begin by using the simple blog starter created by Vercel to get working with a Next.js and Sanity integration. Since the Vercel starter repository has the front end and Sanity Studio separate, I’ve created a simplified repository that includes both.

We’ll clone this repository, and use it to create our commenting base. Want to see the final code? This “Starter” will get you set up with the repository, Vercel project, and Sanity project all connected.

The starter repo comes in two parts: the front end powered by Next.js, and Sanity Studio. Before we go any further, we need to get these running locally.

To get started, we need to set up our content and our CMS for Next to consume the data. First, we need to install the dependencies required for running the Studio and connecting to the Sanity API.

# Install the Sanity CLI globally npm install -g @sanity/cli # Move into the Studio directory and install the Studio's dependencies cd studio npm install

Once these finish installing, from within the /studio directory, we can set up a new project with the CLI.

# If you're not logged into Sanity via the CLI already sanity login # Run init to set up a new project (or connect an existing project) sanity init

The init command asks us a few questions to set everything up. Because the Studio code already has some configuration values, the CLI will ask us if we want to reconfigure it. We do.

From there, it will ask us which project to connect to, or if we want to configure a new project.

We’ll configure a new project with a descriptive project name. It will ask us to name the “dataset” we’re creating. This defaults to “production” which is perfectly fine, but can be overridden with whatever name makes sense for your project.

The CLI will modify the file ~/studio/sanity.json with the project’s ID and dataset name. These values will be important later, so keep this file handy.

For now, we’re ready to run the Studio locally.

# From within /studio npm run start

After the Studio compiles, it can be opened in the browser at http://localhost:3333.

At this point, it makes sense to go into the admin and create some test content. To make the front end work properly, we’ll need at least one blog post and one author, but additional content is always nice to get a feel for things. Note that the content will be synced in real-time to the data store even when you’re working from the Studio on localhost. It will become instantly available to query. Don’t forget to push publish so that the content is publicly available.

Once we have some content, it’s time to get our Next.js project running.

Getting set up with Next.js

Most things needed for Next.js are already set up in the repository. The main thing we need to do is connect our Sanity project to Next.js. To do this, there’s an example set of environment variables set in /blog-frontent/.env.local.example. Remove .example from that file and then we’ll modify the environment variables with the proper values.

We need an API token from our Sanity project. To create this value, let’s head over to the Sanity dashboard. In the dashboard, locate the current project and navigate to the Settings → API area. From here, we can create new tokens to use in our project. In many projects, creating a read-only token is all we need. In our project, we’ll be posting data back to Sanity, so we’ll need to create a Read+Write token.

Showing a modal open in the Sanity dashboard with a Add New Token heading, a text field to set the token label with a value of Comment Engine, and three radio buttons that set if the token as read, write or deploy studio access where the write option is selected.
Adding a new read and write token in the Sanity dashboard

When clicking “Add New Token,” we receive a pop-up with the token value. Once it’s closed, we can’t retrieve the token again, so be sure to grab it!

This string goes in our .env.local file as the value for SANITY_API_TOKEN. Since we’re already logged into manage.sanity.io , we can also grab the project ID from the top of the project page and paste it as the value of NEXT_PUBLIC_SANITY_PROJECT_ID. The SANITY_PREVIEW_SECRET is important for when we want to run Next.js in “preview mode”, but for the purposes of this demo, we don’t need to fill that out.

We’re almost ready to run our Next front-end. While we still have our Sanity Dashboard open, we need to make one more change to our Settings → API view. We need to allow our Next.js localhost server to make requests.

In the CORS Origins, we’ll add a new origin and populate it with the current localhost port: http://localhost:3000. We don’t need to be able to send authenticated requests, so we can leave this off When this goes live, we’ll need to add an additional Origin with the production URL to allow the live site to make requests as well.

Our blog is now ready to run locally!

# From inside /blog-frontend npm run dev

After running the command above, we now have a blog up and running on our computer with data pulling from the Sanity API. We can visit http://localhost:3000 to view the site.

Creating the schema for comments

To add comments to our database with a view in our Studio, we need to set up our schema for the data.

To add our schema, we’ll add a new file in our /studio/schemas directory named comment.js. This JavaScript file will export an object that will contain the definition of the overall data structure. This will tell the Studio how to display the data, as well as structuring the data that we will return to our frontend.

In the case of a comment, we’ll want what might be considered the “defaults” of the commenting world. We’ll have a field for a user’s name, their email, and a text area for a comment string. Along with those basics, we’ll also need a way of attaching the comment to a specific post. In Sanity’s API, the field type is a “reference” to another type of data.

If we wanted our site to get spammed, we could end there, but it would probably be a good idea to add an approval process. We can do that by adding a boolean field to our comment that will control whether or not to display a comment on our site.

export default {   name: 'comment',   type: 'document',   title: 'Comment',   fields: [     {       name: 'name',       type: 'string',     },     {       title: 'Approved',       name: 'approved',       type: 'boolean',       description: "Comments won't show on the site without approval"     },        {       name: 'email',       type: 'string',     },     {       name: 'comment',       type: 'text',     },     {       name: 'post',       type: 'reference',       to: [         {type: 'post'}       ]     }   ], }

After we add this document, we also need to add it to our /studio/schemas/schema.js file to register it as a new document type.

import createSchema from 'part:@sanity/base/schema-creator' import schemaTypes from 'all:part:@sanity/base/schema-type' import blockContent from './blockContent' import category from './category' import post from './post' import author from './author' import comment from './comment' // <- Import our new Schema export default createSchema({   name: 'default',   types: schemaTypes.concat([     post,     author,     category,     comment, // <- Use our new Schema     blockContent   ]) }) 

Once these changes are made, when we look into our Studio again, we’ll see a comment section in our main content list. We can even go in and add our first comment for testing (since we haven’t built any UI for it in the front end yet).

An astute developer will notice that, after adding the comment, the preview our comments list view is not very helpful. Now that we have data, we can provide a custom preview for that list view.

Adding a CMS preview for comments in the list view

After the fields array, we can specify a preview object. The preview object will tell Sanity’s list views what data to display and in what configuration. We’ll add a property and a method to this object. The select property is an object that we can use to gather data from our schema. In this case, we’ll take the comment’s name, comment, and post.title values. We pass these new variables into our prepare() method and use that to return a title and subtitle for use in list views.

export default {   // ... Fields information   preview: {       select: {         name: 'name',         comment: 'comment',         post: 'post.title'       },       prepare({name, comment, post}) {         return {           title: `$ {name} on $ {post}`,           subtitle: comment         }       }     }   }  }

The title will display large and the subtitle will be smaller and more faded. In this preview, we’ll make the title a string that contains the comment author’s name and the comment’s post, with a subtitle of the comment body itself. You can configure the previews to match your needs.

The data now exists, and our CMS preview is ready, but it’s not yet pulling into our site. We need to modify our data fetch to pull our comments onto each post.

Displaying each post’s comments

In this repository, we have a file dedicated to functions we can use to interact with Sanity’s API. The /blog-frontend/lib/api.js file has specific exported functions for the use cases of various routes in our site. We need to update the getPostAndMorePosts function in this file, which pulls the data for each post. It returns the proper data for posts associated with the current page’s slug, as well as a selection of new posts to display alongside it.

In this function, there are two queries: one to grab the data for the current post and one for the additional posts. The request we need to modify is the first request.

Changing the returned data with a GROQ projection

The query is made in the open-source graph-based querying language GROQ, used by Sanity for pulling data out of the data store. The query comes in three parts:

  • The filter – what set of data to find and send back *[_type == "post" && slug.current == $ slug]
  • An optional pipeline component — a modification to the data returned by the component to its left | order(_updatedAt desc)
  • An optional projection — the specific data elements to return for the query. In our case, everything between the brackets ({}).

In this example, we have a variable list of fields that most of our queries need, as well as the body data for the blog post. Directly following the body, we want to pull all the comments associated with this post.

In order to do this, we create a named property on the object returned called 'comments' and then run a new query to return the comments that contain the reference to the current post context.

The entire filter looks like this:

*[_type == "comment" && post._ref == ^._id && approved == true]

The filter matches all documents that meet the interior criteria of the square brackets ([]). In this case, we’ll find all documents of _type == "comment". We’ll then test if the current post’s _ref matches the comment’s _id. Finally, we check to see if the comment is approved == true.

Once we have that data, we select the data we want to return using an optional projection. Without the projection, we’d get all the data for each comment. Not important in this example, but a good habit to be in.

curClient.fetch(     `*[_type == "post" && slug.current == $ slug] | order(_updatedAt desc) {         $ {postFields}         body,         'comments': *[_type == "comment" && post._ref == ^._id && approved == true]{             _id,              name,              email,              comment,              _createdAt         }     }`,  { slug }  )  .then((res) => res?.[0]),

Sanity returns an array of data in the response. This can be helpful in many cases but, for us, we just need the first item in the array, so we’ll limit the response to just the zero position in the index.

Adding a Comment component to our post

Our individual posts are rendered using code found in the /blog-frontend/pages/posts/[slug].js file. The components in this file are already receiving the updated data in our API file. The main Post() function returns our layout. This is where we’ll add our new component.

Comments typically appear after the post’s content, so let’s add this immediately following the closing </article> tag.

// ... The rest of the component </article> // The comments list component with comments being passed in <Comments comments={post?.comments} />

We now need to create our component file. The component files in this project live in the /blog-frontend/components directory. We’ll follow the standard pattern for the components. The main functionality of this component is to take the array passed to it and create an unordered list with proper markup.

Since we already have a <Date /> component, we can use that to format our date properly.

# /blog-frontend/components/comments.js  import Date from './date'  export default function Comments({ comments = [] }) {   return (     <>      <h2 className="mt-10 mb-4 text-4xl lg:text-6xl leading-tight">Comments:</h2>       <ul>         {comments?.map(({ _id, _createdAt, name, email, comment }) => (           <li key={_id} className="mb-5">             <hr className="mb-5" />             <h4 className="mb-2 leading-tight"><a href={`mailto:$ {email}`}>{name}</a> (<Date dateString={_createdAt}/>)</h4>             <p>{comment}</p>             <hr className="mt-5 mb-5" />          </li>         ))       </ul>     </>   ) }

Back in our /blog-frontend/pages/posts/[slug].js file, we need to import this component at the top, and then we have a comment section displayed for posts that have comments.

import Comments from '../../components/comments'

We now have our manually-entered comment listed. That’s great, but not very interactive. Let’s add a form to the page to allow users to submit a comment to our dataset.

Adding a comment form to a blog post

For our comment form, why reinvent the wheel? We’re already in the React ecosystem with Next.js, so we might as well take advantage of it. We’ll use the react-hook-form package, but any form or form component will do.

First, we need to install our package.

npm install react-hook-form

While that installs, we can go ahead and set up our Form component. In the Post component, we can add a <Form /> component right after our new <Comments /> component.

// ... Rest of the component <Comments comments={post.comments} /> <Form _id={post._id} />

Note that we’re passing the current post _id value into our new component. This is how we’ll tie our comment to our post.

As we did with our comment component, we need to create a file for this component at /blog-frontend/components/form.js.

export default function Form ({_id}) {    // Sets up basic data state   const [formData, setFormData] = useState()             // Sets up our form states    const [isSubmitting, setIsSubmitting] = useState(false)   const [hasSubmitted, setHasSubmitted] = useState(false)            // Prepares the functions from react-hook-form   const { register, handleSubmit, watch, errors } = useForm()    // Function for handling the form submission   const onSubmit = async data => {     // ... Submit handler   }    if (isSubmitting) {     // Returns a "Submitting comment" state if being processed     return <h3>Submitting comment…</h3>   }   if (hasSubmitted) {     // Returns the data that the user submitted for them to preview after submission     return (       <>         <h3>Thanks for your comment!</h3>         <ul>           <li>             Name: {formData.name} <br />             Email: {formData.email} <br />             Comment: {formData.comment}           </li>         </ul>       </>     )   }    return (     // Sets up the Form markup   ) }

This code is primarily boilerplate for handling the various states of the form. The form itself will be the markup that we return.

// Sets up the Form markup <form onSubmit={handleSubmit(onSubmit)} className="w-full max-w-lg" disabled>   <input ref={register} type="hidden" name="_id" value={_id} /> 									   <label className="block mb-5">     <span className="text-gray-700">Name</span>     <input name="name" ref={register({required: true})} className="form-input mt-1 block w-full" placeholder="John Appleseed"/>     </label> 																																																									   <label className="block mb-5">     <span className="text-gray-700">Email</span>     <input name="email" type="email" ref={register({required: true})} className="form-input mt-1 block w-full" placeholder="your@email.com"/>   </label>    <label className="block mb-5">     <span className="text-gray-700">Comment</span>     <textarea ref={register({required: true})} name="comment" className="form-textarea mt-1 block w-full" rows="8" placeholder="Enter some long form content."></textarea>   </label> 																																					   {/* errors will return when field validation fails  */}   {errors.exampleRequired && <span>This field is required</span>} 	   <input type="submit" className="shadow bg-purple-500 hover:bg-purple-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded" /> </form>

In this markup, we’ve got a couple of special cases. First, our <form> element has an onSubmit attribute that accepts the handleSubmit() hook. That hook provided by our package takes the name of the function to handle the submission of our form.

The very first input in our comment form is a hidden field that contains the _id of our post. Any required form field will use the ref attribute to register with react-hook-form’s validation. When our form is submitted we need to do something with the data submitted. That’s what our onSubmit() function is for.

// Function for handling the form submission const onSubmit = async data => {   setIsSubmitting(true)            setFormData(data)            try {     await fetch('/api/createComment', {       method: 'POST',      body: JSON.stringify(data),      type: 'application/json'     })       setIsSubmitting(false)     setHasSubmitted(true)   } catch (err) {     setFormData(err)   } }

This function has two primary goals:

  1. Set state for the form through the process of submitting with the state we created earlier
  2. Submit the data to a serverless function via a fetch() request. Next.js comes with fetch() built in, so we don’t need to install an extra package.

We can take the data submitted from the form — the data argument for our form handler — and submit that to a serverless function that we need to create.

We could post this directly to the Sanity API, but that requires an API key with write access and you should protect that with environment variables outside of your front-end. A serverless function lets you run this logic without exposing the secret token to your visitors.

Submitting the comment to Sanity with a Next.js API route

In order to protect our credentials, we’ll write our form handler as a serverless function. In Next.js, we can use “API routes” to create serverless function. These live alongside our page routes in the /blog-frontent/pages directory in the api directory. We can create a new file here called createComment.js.

To write to the Sanity API, we first need to set up a client that has write permissions. Earlier in this demo, we set up a read+write token and put it in /blog-frontent/.env.local. This environment variable is already in use in a client object from /blog-frontend/lib/sanity.js. There’s a read+write client set up with the name previewClient that uses the token to fetch unpublished changes for preview mode.

At the top of our createClient file, we can import that object for use in our serverless function. A Next.js API route needs to export its handler as a default function with request and response arguments. Inside our function, we’ll destructure our form data from the request object’s body and use that to create a new document.

Sanity’s JavaScript client has a create() method which accepts a data object. The data object should have a _type that matches the type of document we wish to create along with any data we wish to store. In our example, we’ll pass it the name, email, and comment.

We need to do a little extra work to turn our post’s _id into a reference to the post in Sanity. We’ll define the post property as a reference and give the_id as the _ref property on this object. After we submit it to the API, we can return either a success status or an error status depending on our response from Sanity.

// This Next.js template already is configured to write with this Sanity Client import {previewClient} from '../../lib/sanity'  export default async function createComment(req, res) {   // Destructure the pieces of our request   const { _id, name, email, comment} = JSON.parse(req.body)   try {     // Use our Client to create a new document in Sanity with an object       await previewClient.create({       _type: 'comment',       post: {         _type: 'reference',         _ref: _id,       },      name,      email,      comment     })   } catch (err) {     console.error(err)     return res.status(500).json({message: `Couldn't submit comment`, err})   }        return res.status(200).json({ message: 'Comment submitted' }) }

Once this serverless function is in place, we can navigate to our blog post and submit a comment via the form. Since we have an approval process in place, after we submit a comment, we can view it in the Sanity Studio and choose to approve it, deny it, or leave it as pending.

Take the commenting engine further

This gets us the basic functionality of a commenting system and it lives directly with our content. There is a lot of potential when you control both sides of this flow. Here are a few ideas for taking this commenting engine further.


The post How to Create a Commenting Engine with Next.js and Sanity appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[Top]