Tag: Components

Creating UI Components in 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 Creating UI Components in SVG appeared first on CSS-Tricks.

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

CSS-Tricks

,

Using Your Own Design System with KendoReact Components

Maybe you’ve already heard of (or even worked with!) KendoReact. It’s popped up in some of my day-to-day conversations, especially those about working with design systems and React. You could think of it as a component library like Bootstrap or Material Design, except the components in KendoReact are far more robust. These are interactive, state-driven components ready to start building full-blown UI’s right out of the gate (not to mention, if you want to use Bootstrap as the theme, you absolutely can).

Whenever you’re thinking about using a UI library, you need to think about the styling capabilities. Are you able to really express your brand with these? Were they meant to be styled? What is the styling experience going to be like?

Fortunately, KendoReact really makes styling a citizen of the entire UI library.

KendoReact is a collection of UI components for building sites. It’s a pretty massive one. Over 80 by my count, and that doesn’t include the child components of heavy lifters like the <Grid /> family.

Here’s one, the <DropDownList />, and just using the default theme (even that is optional):

If I want to style this, I don’t need any special proprietary skills, I can just use CSS. Here’s me forcing a whole new look onto it with different colors and fonts, with just some simple CSS:

But hey, maybe you want to do something a bit more systematized than cowboying some random override CSS. I don’t blame you. Good news: KendoReact themes are Sass-powered. So you can control a lot of the colorization and styling just by changing a few Sass variables.

They have a whole theme builder you can use right on their site that spits out exactly what you need. Say you want to start from their base theme and go from there, select the Default theme:

Then you can play with all the colors in the UI to your liking. Here’s me poking at a theme with some CSS-Tricks colors.

I can download that from the site which will give me the variables as a SCSS file that I can apply before the default theme in my build (there is a great tutorial covering how to do that over on the Telerik blog). Plus, it gives me the whole dang CSS file of the theme if I want to use it that way, which is simple and quick. Here’s me using their conversational chat widget with that theme:

Again, I can start with Bootstrap, I can start with Material, I can start with their default theme, or I can start from scratch. Styling is totally up to me. Each theme has its perks and, as you might expect, are super flexible as far as configuring colors, fonts, and other design elements.

If you really get into this, of course you’ll be consulting their docs and finding your way around there (it’s nice to know they have really comprehensive docs). It’s all pretty straightforward though, you’ll do great! If you need to get going building out a state-driven interactive interface quickly without sacrificing any customizability or power, you’ll find KendoReact is your friend.


The post Using Your Own Design System with KendoReact Components appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[Top]

Our Best Posts on Web Components

A bit of backstory on why this page exists…

I made a fancy new Gutenberg block to insert posts into other posts, so this page is an example of using that (heavily) make a meta blog post about other blog posts. For the most part, topical pages like this are best served by tag pages. For example, our tag page for Web Components has more posts and is sortable in ways this is not. This is just a way to make a curated and hand-sorted grouping of posts, which is something we’ve done for a while with “Guide Collections”. But, we’re spinning down Guide Collections and redirecting them to tag pages and pages like this because they are easier to maintain.


The post Our Best Posts on Web Components appeared first on CSS-Tricks.

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

CSS-Tricks

, ,
[Top]

The failed promise of Web Components

Lea has some words:

Perusing the components on webcomponents.org fills me with anxiety, and I’m perfectly comfortable writing JS — I write JS for a living! What hope do those who can’t write JS have? Using a custom element from the directory often needs to be preceded by a ritual of npm flugelhorn, import clownshoes, build quux, all completely unapologetically because “here is my truckload of dependencies, yeah, what”. Many steps are even omitted, likely because they are “obvious”.

When I wrote A Bit on Web Component Libraries, I was told the main thing I got wrong is that:

The idea was to make primitives that libraries could build on top of so they could ship less code. It was always the intention that you would use a library with them.

It was many years ago that HTML imports died. It was Dave’s pet peeve about Web Components for a long time. So I guess after that, it was a all-JavaScript-or-bust approach for Web Components. And I hate to say it, but it feels like it’s a lot closer to a bust than a boon.

I’m still optimistic though. Web Components can do some very cool stuff that only Web Components can do. The Shadow DOM being a big part of that. For example, I remember years ago Twitter experimented with making embedded Tweets into Web Components (instead of iframes) because it was way faster (in every way). That never manifested (🤷‍♂️), but it seemed like a damn fine idea to me.

I think the styling story is a big deal. I bet I’d reach for them at least slightly more if styling them wasn’t so weird. I saw Scott was asking about it just today and 75% of people wish there was a way to just reach into that Shadow DOM and style it from regular CSS. I get why that needs to be protected (that’s a huge point of the Shadow DOM in the first place), but having to very explicitly reach in seems like enough protection to me.

Direct Link to ArticlePermalink


The post The failed promise of Web Components appeared first on CSS-Tricks.

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

CSS-Tricks

, ,
[Top]

Grid for layout, flexbox for components

When should we reach for CSS grid and when should we use flexbox? Rachel Andrew wrote about this very conundrum way back in 2016:

Flexbox is essentially for laying out items in a single dimension – in a row OR a column. Grid is for layout of items in two dimensions – rows AND columns.

Ahmad Shadeed wrote a post where he gives the same advice, but from a different angle. He argues we should use grid for layout and flexbox for components:

Remember that old layout method might be perfect for the job. Overusing flexbox or grid can increase the complexity of your CSS by time. I don’t mean they are complex, but using them correctly and in the right context as explained from the examples in this article is much better.

Speaking of which, there’s so many great layout examples in this post, too.

Direct Link to ArticlePermalink

The post Grid for layout, flexbox for components appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

React Single File Components Are Here

Shawn Wang is talking about RedwoodJS here:

…  it is the first time React components are being expressed in a single file format with explicit conventions.

Which is the RedwoodJS idea of Cells. To me, it feels like a slightly cleaner version of how Apollo wants you to do it with useQuery. Shawn makes that same connection and I know RedwoodJS uses Apollo, so I’m thinking it’s some nice semantic sugar.

There is a lot of cool stuff going on in RedwoodJS. “A highly opinionated stack” if its helpful to think of it that way, but Tom made clear in our last episode of ShopTalk that it’s not like Rails. Not that Rails is bad (it isn’t), but that this new world can do things in new and better ways that make for long-term healthy software.

The post React Single File Components Are Here appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Dealing With Stale Props and States in React’s Functional Components

There’s one aspect of JavaScript that always has me pulling my hair: closures. I work with React a lot, and the overlap there is that they can sometimes be the cause of stale props and state. We’ll get into exactly what that means, but the trouble is that the data we use to build our UI can be totally wrong in unexpected ways, which is, you know, bad.

Stale props and states

Long story short: it’s when code that is executed asynchronously has a reference to a prop or state that is no longer fresh, and thus, the value it returns is not the latest one.

To be even more clear, let’s play around with the same stale reference example React has in its documentation.

function Counter() {   const [count, setCount] = useState(0);    function handleAlertClick() {     setTimeout(() => {       alert("You clicked on: " + count);     }, 3000);   }    return (     <div>       <p>You clicked {count} times</p>       <button onClick={() => setCount(count + 1)}>Click me</button>       <button onClick={handleAlertClick}>Show alert</button>     </div>   ); }

(Live demo)

Nothing fancy here. We have a functional component named Counter. It keeps track of how many times the user has clicked one button and shows an alert that displays how many times that button was clicked when clicking another button. Try this:

  1. Click the “Click me” button. You are going to see the click counter go up.
  2. Now click the “Show alert”button. Three seconds should go by and then trigger an alert telling you how many times you clicked the “Click me” button.
  3. Now, click the “Show alert” button again and quickly click the “Click me” button before it triggers the alert in three seconds.

See what happens? The count shown on the page and the count shown in the alert do not match. The number in the alert is not just some random number, though. That number is the value the count variable had in the moment the asynchronous function inside the setTimeout was defined, which is the moment the “Show alert” button is clicked.

That’s just how closures work. We’re not going to get into the specifics of them in this post, but here are some docs that cover them in greater detail.

Let’s focus on how we can avoid these stale references with our states and props.

React offers a tip on how to deal with stale dates and props in the same documentation where the example was pulled.

If you intentionally want to read the latest state from some asynchronous callback, you could keep it in a ref, mutate it, and read from it.

By keeping the value  asynchronously in a ref, we can bypass stale references. If you need to know more about ref in functional components, React’s documentation has a lot more information.

So, that begs the question: How can we keep our props or state in a ref?

Let’s do it the dirty way first.

The dirty way to store props and state in a ref

We can easily create a ref using useRef() and use count as its initial value. Then, wherever the state is being updated, we set the ref.current property to the new value. Lastly, use ref.current instead of count in the asynchronous part of our code.

function Counter() {   const [count, setCount] = useState(0);   const ref = useRef(count); // Make a ref and give it the count    function handleAlertClick() {     setTimeout(() => {       alert("You clicked on: " + ref.current); // Use ref instead of count     }, 3000);   }    return (     <div>       <p>You clicked {count} times</p>       <button         onClick={() => {           setCount(count + 1);           ref.current = count + 1; // Update ref whenever the count changes         }}       >         Click me       </button>       <button         onClick={() => {           handleAlertClick();         }}       >         Show alert       </button>     </div>   ); }

(Live demo)

Go ahead and do the same as last time. Click “Show alert” and then click “Click me” before the alert is triggered in three seconds.

Now we have the latest value!

Here’s why it works. When the asynchronous callback function is defined inside setTimeout, it saves a reference to the variables it uses, which is count in this case. This way, when the state updates, React not only changes the value but the variable reference in memory is completely different as well.

This means that — even if the state’s value is non-primitive — the variable you are working with in your asynchronous callback is not the same in memory. An object that would typically keep its reference throughout different functions now has a different value.

How does using a ref solve this? If we take a quick look at React’s docs again, we find an interesting, but easy-to-miss, bit of information:

[…] useRef will give you the same ref object on every render.

It doesn’t matter what we do. Throughout the lifetime of your component, React will give us the exact same ref object in memory. Any callback, no matter when it’s defined or executed, is working with the same object. No more stale reference.

The cleaner way to store props and state in a ref

Let’s be honest… using a ref like that is an ugly fix. What happens if your state is being updated in a thousand different places? Now you have to change your code and manually update the ref in all those places. That’s a no-no.

We are going to make this more scalable by giving ref the value of the state automatically when the state changes.

Let’s start by getting rid of the manual change to the ref in the “Click me”button.

Next, we make a function called updateState that is called whenever we need to change the state. This function takes the new state as an argument and it sets the ref.current property to the new state and updates the state as well with that same value.

Finally, let’s substitute the original setCount function React gives us with the new updateState function where the state is being updated.

function Counter() {   const [count, setCount] = useState(0);   const ref = useRef(count);    // Keeps the state and ref equal   function updateState(newState) {     ref.current = newState;     setCount(newState);   }    function handleAlertClick() { ... }    return (     <div>       <p>You clicked {count} times</p>       <button         onClick={() => {           // Use the created function instead of the manual update           updateState(count + 1);         }}       >         Click me       </button>       <button onClick={handleAlertClick}>Show alert</button>     </div>   ); }

(Live demo)

Using a custom hook

The cleaner solution works just fine. It gets the job done just like the dirty solution, but only calls a single function to update the state and ref.

But guess what? We can do better. What if we need to add more states? What if we want to do this in other components too? Let’s take the state, ref and updateState function and make them truly portable. Custom hooks to the rescue!

Outside the Counter component, we are going to define a new function. Let’s name it useAsyncReference. (It can be named anything, really, but note that it’s common practice to name custom hooks with “use” as a prefix.) Our new hook will have a single parameter for now. We’ll call it value.

Our previous solution had the same information stored twice: once in the state and once in the ref. We are going to optimize that by keeping the value just in ref this time. In other words, we will create a ref and give it the value parameter as its initial value.

Right after the ref, we will make an updateState function that takes the new state and sets it to the ref.current property.

Lastly, we return an array with ref and the updateState function, very similar to what React does with useState.

function useAsyncReference(value) {   const ref = useRef(value);    function updateState(newState) {     ref.current = newState;   }    return [ref, updateState]; }  function Counter() { ... }

We are forgetting something! If we check the useRef documentation, we learn that updating a ref does not trigger a re-render. So, while ref has the updated value, we wouldn’t see the changes on screen. We need to force a re-render every time ref gets updated.

What we need is a fake state. The value doesn’t matter. It’s only going to be there to provoke the re-render. We can even ignore the state and only keep its update function. We are calling that update function forceRender and giving it an initial value of false.

Now, inside updateState, we force the re-render by calling forceRender and passing it a state different to the current one after setting ref.current to newState.

function useAsyncReference(value) {   const ref = useRef(value);   const [, forceRender] = useState(false);    function updateState(newState) {     ref.current = newState;     forceRender(s => !s);   }    return [ref, updateState]; }  function Counter() { ... }

Take whatever value it has and return the opposite. The state doesn’t really matter. We are merely changing it so React detects a change in state and re-renders the component.

Next, we can clean the Count component and remove the previously used useState, ref and updateState function, then implement the new hook. The first value of the returned array is the state in the form of a ref. We’ll keep calling it count, where the second value is the function to update the state/ref. We’ll continue calling it setCount.

We also have to change the references to the count since now that they all must be count.current. And we must call setCount instead of calling updateState.

function useAsyncReference(value) { ... }  function Counter() {   const [count, setCount] = useAsyncReference(0);    function handleAlertClick() {     setTimeout(() => {       alert("You clicked on: " + count.current);     }, 3000);   }    return (     <div>       <p>You clicked {count.current} times</p>       <button         onClick={() => {           setCount(count.current + 1);         }}       >         Click me       </button>       <button onClick={handleAlertClick}>Show alert</button>     </div>   ); }

Making this work with props

We have a truly portable solution for our problem. But guess what… there’s still a little more to do. Specifically, we need to make the solution compatible with props.

Let’s take the “Show alert” button and handleAlertClick function to a new component outside the Counter component. We are gonna call it Alert and it’s going to take a single prop called count. This new component is going to show the count prop value we are passing it in an alert after a three second delay.

function useAsyncReference(value) { ... }  function Alert({ count }) {   function handleAlertClick() {     setTimeout(() => {       alert("You clicked on: " + count);     }, 3000);   }    return <button onClick={handleAlertClick}>Show alert</button>; }  function Counter() { ... }

In Counter, we’re swapping the “Show alert” button for the Alert component. We’ll pass count.current to the count prop.

function useAsyncReference(value) { ... }  function Alert({ count }) { ... }  function Counter() {   const [count, setCount] = useAsyncReference(0);    return (     <div>       <p>You clicked {count.current} times</p>       <button         onClick={() => {           setCount(count.current + 1);         }}       >         Click me       </button>       <Alert count={count.current} />     </div>   ); }

(Live demo)

Alright, time to run through the testing steps again. See? Even though we are using a safe reference to the count in Counter, the reference to the count prop in the Alert component is not asynchronously safe and our custom hook is not suitable to use with props… yet.

Lucky for us, the solution is fairly simple.

All we have to do is add a second parameter to our useAsyncReference hook named isProp, with false as the initial value. Just before we return the array with ref and updateState, we set up a condition. If isProp is true, we set the ref.current property to value and only return ref.

function useAsyncReference(value, isProp = false) {   const ref = useRef(value);   const [, forceRender] = useState(false);    function updateState(newState) {     ref.current = newState;     forceRender(s => !s);   }    if (isProp) {     ref.current = value;     return ref;   }    return [ref, updateState]; }  function Alert({ count }) { ... }  function Counter() { ... }

Now let’s update Alert so that is uses the hook. Remember to pass true as a second argument to useAsyncReference since we are passing a prop and not a state.

function useAsyncReference(value) { ... }  function Alert({ count }) {   const asyncCount = useAsyncReference(count, true);    function handleAlertClick() {     setTimeout(() => {       alert("You clicked on: " + asyncCount.current);     }, 3000);   }    return <button onClick={handleAlertClick}>Show alert</button>; }  function Counter() { ... }

(Live demo)

Give it another try. Now it works perfectly whether you use states or props.

One last thing…

There’s one last change I’d like to make. React’s useState docs tell us that React will bail out of a re-render if the new state is identical to the previous one. Our solution doesn’t do that. If we pass the current state again to the hook’s updateState function, we will force a re-render no matter what. Let’s change that.

Let’s put the body of updateState inside an if statement and execute it when ref.current is different than the new state. The comparison must be done with Object.is(), just like React does.

function useAsyncReference(value, isProp = false) {   const ref = useRef(value);   const [, forceRender] = useState(false);    function updateState(newState) {     if (!Object.is(ref.current, newState)) {       ref.current = newState;       forceRender(s => !s);     }   }    if (isProp) {     ref.current = value;     return ref;   }    return [ref, updateState]; }  function Alert({ count }) { ... }  function Counter() { ... }

Now we are finally done!


React can sometimes seem like a black box that is full of little quirks. Those quirks might be daunting to deal with, like the one we just tackled. But if you are patient and enjoy being challenged, you’ll soon realize it’s an awesome framework and a pleasure to work with.

The post Dealing With Stale Props and States in React’s Functional Components appeared first on CSS-Tricks.

CSS-Tricks

, , , , , ,
[Top]

Creating a Modal Image Gallery With Bootstrap Components

Have you ever clicked on an image on a webpage that opens up a larger version of the image with navigation to view other photos?

Some folks call it a pop-up. Others call it a lightbox. Bootstrap calls it a modal. I mention Bootstrap because I want to use it to make the same sort of thing. So, let’s call it a modal from here on out.

Why Bootstrap? you might ask. Well, a few reasons:

  • I’m already using Bootstrap on the site where I want this effect, so there’s no additional overhead in terms of loading resources.
  • I want something where I have complete and easy control over aesthetics. Bootstrap is a clean slate compared to most modal plugins I’ve come across.
  • The functionality I need is fairly simple. There isn’t much to be gained by coding everything from scratch. I consider the time I save using the Bootstrap framework to be more beneficial than any potential drawbacks.

Here’s where we’ll end up:

Let’s go through that, bit by bit.

Step 1: Create the image gallery grid

Let’s start with the markup for a grid layout of images. We can use Bootstrap’s grid system for that.

<div class="row" id="gallery">   <div class="col-12 col-sm-6 col-lg-3">     <img class="w-100" src="/image-1">   </div>   <div class="col-12 col-sm-6 col-lg-3">     <img class="w-100" src="/image-2">   </div>   <div class="col-12 col-sm-6 col-lg-3">     <img class="w-100" src="/image-3">   </div>   <div class="col-12 col-sm-6 col-lg-3">     <img class="w-100" src="/image-4">   </div> </div>

Now we need data attributes to make those images interactive. Bootstrap looks at data attributes to figure out which elements should be interactive and what they should do. In this case, we’ll be creating interactions that open the modal component and allow scrolling through the images using the carousel component.

About those data attributes:

  1. We’ll add data-toggle="modal"  and data-target="#exampleModal" to the parent element (#gallery). This makes it so clicking anything in the gallery opens the modal. We should also add the data-target value (#exampleModal) as the ID of the modal itself, but we’ll do that once we get to the modal markup.
  2. Let’s add data-target="#carouselExample"  and a data-slide-to attribute to each image. We could add those to the image wrappers instead, but we’ll go with the images in this post. Later on, we’ll want to use the data-target value (#carouselExample) as the ID for the carousel, so note that for when we get there. The values for data-slide-to are based on the order of the images.

Here’s what we get when we put that together:

<div class="row" id="gallery" data-toggle="modal" data-target="#exampleModal">   <div class="col-12 col-sm-6 col-lg-3">     <img class="w-100" src="/image-1.jpg" data-target="#carouselExample" data-slide-to="0">   </div>   <div class="col-12 col-sm-6 col-lg-3">     <img class="w-100" src="/image-2.jpg" data-target="#carouselExample" data-slide-to="1">   </div>   <div class="col-12 col-sm-6 col-lg-3">     <img class="w-100" src="/image-3.jpg" data-target="#carouselExample" data-slide-to="2">   </div>   <div class="col-12 col-sm-6 col-lg-3">     <img class="w-100" src="/image-4.jpg" data-target="#carouselExample" data-slide-to="3">   </div> </div>

Interested in knowing more about data attributes? Check out the CSS-Tricks guide to them.

Step 2: Make the modal work

This is a carousel inside a modal, both of which are standard Bootstrap components. We’re just nesting one inside the other here. Pretty much a straight copy-and-paste job from the Bootstrap documentation.

Here’s some important parts to watch for though:

  1. The modal ID should match the data-target of the gallery element.
  2. The carousel ID should match the data-target of the images in the gallery.
  3. The carousel slides should match the gallery images and must be in the same order.

Here’s the markup for the modal with our attributes in place:

<!-- Modal markup: https://getbootstrap.com/docs/4.4/components/modal/ --> <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-hidden="true">   <div class="modal-dialog" role="document">     <div class="modal-content">       <div class="modal-header">         <button type="button" class="close" data-dismiss="modal" aria-label="Close">           <span aria-hidden="true">×</span>         </button>       </div>       <div class="modal-body">                <!-- Carousel markup goes here --> 
       <div class="modal-footer">         <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>       </div>     </div>   </div> </div>

We can drop the carousel markup right in there, Voltron style!

<!-- Modal markup: https://getbootstrap.com/docs/4.4/components/modal/ --> <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-hidden="true">   <div class="modal-dialog" role="document">     <div class="modal-content">       <div class="modal-header">         <button type="button" class="close" data-dismiss="modal" aria-label="Close">           <span aria-hidden="true">×</span>         </button>       </div>       <div class="modal-body">                <!-- Carousel markup: https://getbootstrap.com/docs/4.4/components/carousel/ -->       <div id="carouselExample" class="carousel slide" data-ride="carousel">           <div class="carousel-inner">             <div class="carousel-item active">               <img class="d-block w-100" src="/image-1.jpg">             </div>             <div class="carousel-item">               <img class="d-block w-100" src="/image-2.jpg">             </div>             <div class="carousel-item">               <img class="d-block w-100" src="/image-3.jpg">             </div>             <div class="carousel-item">               <img class="d-block w-100" src="/image-4.jpg">             </div>           </div>           <a class="carousel-control-prev" href="#carouselExample" role="button" data-slide="prev">             <span class="carousel-control-prev-icon" aria-hidden="true"></span>             <span class="sr-only">Previous</span>           </a>           <a class="carousel-control-next" href="#carouselExample" role="button" data-slide="next">             <span class="carousel-control-next-icon" aria-hidden="true"></span>             <span class="sr-only">Next</span>           </a>         </div>       </div>        <div class="modal-footer">         <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>       </div>     </div>   </div> </div>

Looks like a lot of code, right? Again, it’s basically straight from the Bootstrap docs, only with our attributes and images.

Step 3: Deal with image sizes

This isn’t necessary, but if the images in the carousel have different dimensions, we can crop them with CSS to keep things consistent. Note that we’re using Sass here.

// Use Bootstrap breakpoints for consistency. $  bootstrap-sm: 576px; $  bootstrap-md: 768px; $  bootstrap-lg: 992px; $  bootstrap-xl: 1200px; 
 // Crop thumbnail images. #gallery {   img {     height: 75vw;     object-fit: cover;          @media (min-width: $  bootstrap-sm) {       height: 35vw;     }          @media (min-width: $  bootstrap-lg) {       height: 18vw;     }   } } 
 // Crop images in the coursel .carousel-item {   img {     height: 60vw;     object-fit: cover;          @media (min-width: $  bootstrap-sm) {       height: 350px;     }   } }

Step 4: Optimize the images

You may have noticed that the markup uses the same image files in the gallery as we do in the modal. That doesn’t need to be the case. In fact, it’s a better idea to use smaller, more performant versions of the images for the gallery. We’re going to be blowing up the images to their full size version anyway in the modal, so there’s no need to have the best quality up front.

The good thing about Bootstrap’s approach here is that we can use different images in the gallery than we do in the modal. They’re not mutually exclusive where they have to point to the same file.

So, for that, I’d suggest updating the gallery markup with lower-quality images:

<div class="row" id="gallery" data-toggle="modal" data-target="#exampleModal">   <div class="col-12 col-sm-6 col-lg-3">     <img class="w-100" src="/image-1-small.jpg" data-target="#carouselExample" data-slide-to="0">      <!-- and so on... --> </div>

That’s it!

The site where I’m using this has already themed Bootstrap. That means everything is already styled to spec. That said, even if you haven’t themed Bootstrap you can still easily add custom styles! With this approach (Bootstrap vs. plugins), customization is painless because you have complete control over the markup and Bootstrap styling is relatively sparse.

Here’s the final demo:

The post Creating a Modal Image Gallery With Bootstrap Components appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

How to Build Vue Components in a WordPress Theme

Intrigued by the title and just wanna see some code? Skip ahead.

A few months ago, I was building a WordPress website that required a form with a bunch of fancy conditional fields. Different options and info were required for different choices you could make on the form, and our client needed complete control over all fields 1. In addition, the form needed to appear in multiple places in each page, with slightly different configs.

And the header instance of the form needed to be mutually exclusive with the hamburger menu, so that opening one closes the other.

And the form had text content that was relevant to SEO.

And we wanted the server response to present some cute animated feedback.

(Phew.)

The whole thing felt complex enough that I didn’t want to handle all that state manually. I remembered reading Sarah Drasner’s article “Replacing jQuery With Vue.js: No Build Step Necessary” which shows how to replace classic jQuery patterns with simple Vue micro-apps. That seemed like a good place to start, but I quickly realized that things would get messy on the PHP side of WordPress.

What I really needed were reusable components

PHP → JavaScript

I love the static-first approach of Jamstack tools, like Nuxt, and was looking to do something similar here — send the full content from the server, and progressively enhance on the client side.

But PHP doesn’t have a built-in way to work with components. It does, however, support require-ing files inside other files 2. WordPress has an abstraction of require called get_template_part, that runs relative to the theme folder and is easier to work with. Dividing code into template parts is about the closest thing to components that WordPress provides 3.

Vue, on the other hand, is all about components — but it can only do its thing after the page has loaded and JavaScript is running.

The secret to this marriage of paradigms turns out to be the lesser-known Vue directive inline-template. Its great and wonderful powers allow us to define a Vue component using the markup we already have. It’s the perfect middle ground between getting static HTML from the server, and mounting dynamic DOM elements in the client.

First, the browser gets the HTML, then Vue makes it do stuff. Since the markup is built by WordPress, rather than by Vue in the browser, components can easily use any information that site administrators can edit. And, as opposed to .vue files (which are great for building more app-y things), we can keep the same separation of concerns we use for the whole site — structure and content in PHP, style in CSS, and functionality in JavaScript.

To show how this all fits together, we’re going to build a few features for a recipe blog. First, we’ll add a way for users to rate recipes. Then we’ll build a feedback form based on that rating. Finally, we’ll allow users to filter recipes, based on tags and rating.

We’ll build a few components that share state and live on the same page. To get them to play nicely together — and to make it easy to add additional components in the future — we’ll make the whole page our Vue app, and register components inside it.

Each component will live in its own PHP file and be included in the theme using get_template_part.

Laying the groundwork

There are a few special considerations to take into account when applying Vue to existing pages. The first is that Vue doesn’t want you loading scripts inside it — it will send ominous errors to the console if you do. The easiest way to avoid this is to add a wrapper element around the content for every page, then load scripts outside of it (which is already a common pattern for all kinds of reasons). Something like this:

<?php /* header.php */ ?>  <body <?php body_class(); ?>> <div id="site-wrapper">
<?php /* footer.php */ ?>   </div> <!-- #site-wrapper --> <?php wp_footer(); ?>

The second consideration is that Vue has to be called at the end of body element so that it will load after the rest of the DOM is available to parse. We’ll pass true as the fifth argument  (in_footer) for the wp_enqueue_script  function. Also, to make sure Vue is loaded first, we’ll register it as a dependency of the main script.

<?php // functions.php  add_action( 'wp_enqueue_scripts', function() {   wp_enqueue_script('vue', get_template_directory_uri() . '/assets/js/lib/vue.js', null, null, true); // change to vue.min.js for production   wp_enqueue_script('main', get_template_directory_uri() . '/assets/js/main.js', 'vue', null, true);

Finally, in the main script, we’ll initialize Vue on the site-wrapper element.

// main.js  new Vue({   el: document.getElementById('site-wrapper') })

The star rating component

Our single post template currently looks like this:

<?php /* single-post.php */ ?>  <article class="recipe">   <?php /* ... post content */ ?>    <!-- star rating component goes here --> </article>

We’ll register the star rating component and add some logic to manage it:

// main.js  Vue.component('star-rating', {   data () {     return {       rating: 0     }   },   methods: {     rate (i) { this.rating = i }   },   watch: {     rating (val) {       // prevent rating from going out of bounds by checking it to on every change       if (val < 0)          this.rating = 0       else if (val > 5)          this.rating = 5        // ... some logic to save to localStorage or somewhere else     }   } })  // make sure to initialize Vue after registering all components new Vue({   el: document.getElementById('site-wrapper') })

We’ll write the component template in a separate PHP file. The component will comprise six buttons (one for unrated, 5 with stars). Each button will contain an SVG with either a black or transparent fill.

<?php /* components/star-rating.php */ ?>  <star-rating inline-template>   <div class="star-rating">     <p>Rate recipe:</p>     <button @click="rate(0)">       <svg><path d="..." :fill="rating === 0 ? 'black' : 'transparent'"></svg>     </button>     <button v-for="(i in 5) @click="rate(i)">       <svg><path d="..." :fill="rating >= i ? 'black' : 'transparent'"></svg>     </button>   </div> </star-rating>

As a rule of thumb, I like to give a component’s top element a class name that is identical to that of the component itself. This makes it easy to reason between markup and CSS (e.g. <star-rating> can be thought of as .star-rating).

And now we’ll include it in our page template.

<?php /* single-post.php */ ?>  <article class="recipe">   <?php /* post content */ ?>    <?php get_template_part('components/star-rating'); ?> </article>

All the HTML inside the template is valid and understood by the browser, except for <star-rating>. We can go the extra mile to fix that by using Vue’s is directive:

<div is="star-rating" inline-template>...</div>

Now let’s say that the maximum rating isn’t necessarily 5, but is controllable by the website’s editor using Advanced Custom Fields, a popular WordPress plugin that adds custom fields for pages, posts and other WordPress content. All we need to do is inject that value as a prop of the component that we’ll call maxRating:

<?php // components/star-rating.php  // max_rating is the name of the ACF field $  max_rating = get_field('max_rating'); ?> <div is="star-rating" inline-template :max-rating="<?= $  max_rating ?>">   <div class="star-rating">     <p>Rate recipe:</p>     <button @click="rate(0)">       <svg><path d="..." :fill="rating === 0 ? 'black' : 'transparent'"></svg>     </button>     <button v-for="(i in maxRating) @click="rate(i)">       <svg><path d="..." :fill="rating >= i ? 'black' : 'transparent'"></svg>     </button>   </div> </div>

And in our script, let’s register the prop and replace the magic number 5:

// main.js  Vue.component('star-rating', {   props: {     maxRating: {       type: Number,       default: 5 // highlight     }   },   data () {     return {       rating: 0     }   },   methods: {     rate (i) { this.rating = i }   },   watch: {     rating (val) {       // prevent rating from going out of bounds by checking it to on every change       if (val < 0)          this.rating = 0       else if (val > maxRating)          this.rating = maxRating        // ... some logic to save to localStorage or somewhere else     }   } })

In order to save the rating of the specific recipe, we’ll need to pass in the ID of the post. Again, same idea:

<?php // components/star-rating.php  $  max_rating = get_field('max_rating'); $  recipe_id = get_the_ID(); ?> <div is="star-rating" inline-template :max-rating="<?= $  max_rating ?>" recipe-id="<?= $  recipe_id ?>">   <div class="star-rating">     <p>Rate recipe:</p>     <button @click="rate(0)">       <svg><path d="..." :fill="rating === 0 ? 'black' : 'transparent'"></svg>     </button>     <button v-for="(i in maxRating) @click="rate(i)">       <svg><path d="..." :fill="rating >= i ? 'black' : 'transparent'"></svg>     </button>   </div> </div>
// main.js  Vue.component('star-rating', {   props: {     maxRating: {        // Same as before     },     recipeId: {       type: String,       required: true     }   },   // ...   watch: {     rating (val) {       // Same as before        // on every change, save to some storage       // e.g. localStorage or posting to a WP comments endpoint       someKindOfStorageDefinedElsewhere.save(this.recipeId, this.rating)     }   },   mounted () {     this.rating = someKindOfStorageDefinedElsewhere.load(this.recipeId)       } })

Now we can include the same component file in the archive page (a loop of posts), without any additional setup:

<?php // archive.php  if (have_posts()): while ( have_posts()): the_post(); ?> <article class="recipe">   <?php // Excerpt, featured image, etc. then:   get_template_part('components/star-rating'); ?> </article> <?php endwhile; endif; ?>

The feedback form

The moment a user rates a recipe is a great opportunity to ask for more feedback, so let’s add a little form that appears right after the rating is set.

// main.js  Vue.component('feedback-form', {   props: {     recipeId: {       type: String,       required: true     },     show: { type: Boolean, default: false }   },   data () {     return {       name: '',       subject: ''       // ... other form fields     }   } })
<?php // components/feedback-form.php  $  recipe_id = get_the_ID(); ?> <div is="feedback-form" inline-template recipe-id="<?= $  recipe_id ?>" v-if="showForm(recipe-id)">   <form class="recipe-feedback-form" id="feedback-form-<?= $  recipe_id ?>">     <input type="text" :id="first-name-<?= $  recipe_id ?>" v-model="name">     <label for="first-name-<?= $  recipe_id ?>">Your name</label>     <?php /* ... */ ?>   </form> </div>

Notice that we’re appending a unique string (in this case, recipe-id) to each form element’s ID. This is to make sure they all have unique IDs, even if there are multiple copies of the form on the page.

So, where do we want this form to live? It needs to know the recipe’s rating so it knows it needs to open. We’re just building good ol’ components, so let’s use composition to place the form inside the <star-rating>:

<?php // components/star-rating.php  $  max_rating = get_field('max_rating'); $  recipe_id = get_the_ID(); ?> <div is="star-rating" inline-template :max-rating="<?= $  max_rating ?>" recipe-id="<?= $  recipe_id ?>">   <div class="star-rating">     <p>Rate recipe:</p>     <button @click="rate(0)">       <svg><path d="..." :fill="rating === 0 ? 'black' : 'transparent'"></svg>     </button>     <button v-for="(i in maxRating) @click="rate(i)">       <svg><path d="..." :fill="rating >= i ? 'black' : 'transparent'"></svg>     </button>     <?php get_template_part('components/feedback-form'); ?>   </div> </div>

If at this point you’re thinking, “We really should be composing both components into a single parent component that handles the rating state,” then please give yourself 10 points and wait patiently.

A small progressive enhancement we can add to make the form usable without JavaScript, is to give it the traditional PHP action and then override it in Vue. We’ll use @submit.prevent to prevent the original action, then run a submit method to send the form data in JavaScript.

<?php // components/feedback-form.php  $  recipe_id = get_the_ID(); ?> <div is="feedback-form" inline-template recipe-id="<?= $  recipe_id ?>">   <form action="path/to/feedback-form-handler.php"        @submit.prevent="submit"       class="recipe-feedback-form"        id="feedback-form-<?= $  recipe_id ?>">     <input type="text" :id="first-name-<?= $  recipe_id ?>" v-model="name">     <label for="first-name-<?= $  recipe_id ?>">Your name</label>    <!-- ... -->   </form> </div>

Then, assuming we want to use fetch, our submit method can be something like this:

// main.js  Vue.component('feedback-form', {   // Same as before    methods: {     submit () {       const form = this.$  el.querySelector('form')       const URL = form.action       const formData = new FormData(form)       fetch(URL, {method: 'POST', body: formData})         .then(result => { ... })         .catch(error => { ... })     }   } })

OK, so what do we want to do in .then and .catch? Let’s add a component that will show real-time feedback for the form’s submit status. First let’s add the state to track sending, success, and failure, and a computed property telling us if we’re pending results.

// main.js  Vue.component('feedback-form', {   // Same as before    data () {     return {       name: '',       subject: ''       // ... other form fields       sent: false,       success: false, ​​      error: null     }   },   methods: {     submit () {       const form = this.$  el.querySelector('form')       const URL = form.action       const formData = new FormData(form)       fetch(URL, {method: 'POST', body: formData})         .then(result => {            this.success = true          })         .catch(error => {            this.error = error          })       this.sent = true     }   } })

To add the markup for each message type (success, failure, pending), we could make another component like the others we’ve built so far. But since these messages are meaningless when the server renders the page, we’re better off rendering them only when necessary. To do this we’re going to place our markup in a native HTML <template> tag, which doesn’t render anything in the browser. Then we’ll reference it by id as our component’s template.

<?php /* components/form-status.php */ ?>  <template id="form-status-component" v-if="false">   <div class="form-message-wrapper">     <div class="pending-message" v-if="pending">       <img src="<?= get_template_directory_uri() ?>/spinner.gif">       <p>Patience, young one.</p>     </div>     <div class="success-message" v-else-if="success">       <img src="<?= get_template_directory_uri() ?>/beer.gif">       <p>Huzzah!</p>     </div>     <div class="success-message" v-else-if="error">       <img src="<?= get_template_directory_uri() ?>/broken.gif">       <p>Ooh, boy. It would appear that: {{ error.text }}</p>     </div>   </div </template>

Why add v-if="false" at the top, you ask? It’s a tricky little thing. Once Vue picks up the HTML <template>, it will immediately think of it as a Vue <template> and render it. Unless, you guessed it, we tell Vue not to render it. A bit of a hack, but there you have it.

Since we only need this markup once on the page, we’ll include the PHP component in the footer.

<?php /* footer.php */ ?>  </div> <!-- #site-wrapper --> <?php get_template_part('components/form-status'); ?> <?php wp_footer(); ?>

Now we’ll register the component with Vue…

// main.js  Vue.component('form-status', {   template: '#form-status-component'   props: {     pending: { type: Boolean, required: true },     success: { type: Boolean, required: true },     error: { type: [Object, null], required: true },   } })

…and call it inside our form component:

<?php // components/feedback-form.php  $  recipe_id = get_the_ID(); ?> <div is="feedback-form" inline-template recipe-id="<?= $  recipe_id ?>">   <form action="path/to/feedback-form-handler.php"          @submit.prevent="submit"         class="recipe-feedback-form"          id="feedback-form-<?= $  recipe_id ?>">     <input type="text" :id="first-name-<?= $  recipe_id ?>" v-model="name">     <label for="first-name-<?= $  recipe_id ?>">Your name</label>     <?php // ... ?>   </form>   <form-status v-if="sent" :pending="pending" :success="success" :error="error" /> </div>

Since we registered <form-status> using Vue.component, it’s available globally, without specifically including it in the parent’s components: { }.

Filtering recipes

Now that users can personalize some bits of their experience on our blog, we can add all kinds of useful functionality. Specifically, let’s allow users to set a minimum rating they want to see, using an input at the top of the page.
The first thing we need is some global state to track the minimum rating set by the user. Since we started off by initializing a Vue app on the whole page, global state will just be data on the Vue instance:

// main.js // Same as before  new Vue({   el: document.getElementById('site-wrapper'),   data: {     minimumRating: 0   } })

And where can we put the controls to change this? Since the whole page is the app, the answer is almost anywhere. For instance, at the top of the archive page:

<?php /* archive.php */ ?>  <label for="minimum-rating-input">Only show me recipes I've rated at or above:</label> <input type="number" id="minimum-rating-input" v-model="minimumRating">  <?php if (have_posts()): while ( have_posts()): the_post(); ?> <article class="recipe">   <?php /* Post excerpt, featured image, etc. */ ?>    <?php get_template_part('components/star-rating'); ?> </article> <?php endwhile; endif; ?>

As long as it’s inside our site-wrapper and not inside another component, it’ll just work. If we want, we could also build a filtering component that would change the global state. And if we wanted to get all fancy, we could even add Vuex to the mix (since Vuex can’t persist state between pages by default, we could add something like vuex-persist to use localStorage).

So, now we need to hide or show a recipe based on the filter. To do this, we’ll need to wrap the recipe content in its own component, with a v-show directive. It’s probably best to use the same component for both the single page and the archive page. Unfortunately, neither require nor get_template_part can pass parameters into the called file — but we can use global variables:

<?php /* archive.php */ ?>  <label for="minimum-rating-input">Only show me recipes I've rated at or above:</label> <input type="number" id="minimum-rating-input" v-model="minimumRating">  <?php  $  is_archive_item = true; if (have_posts()): while ( have_posts()): the_post();   get_template_part('components/recipe-content'); endwhile; endif; ?>

We can then use $ is_archive_item as a global variable inside the PHP component file to check if it is set and true. Since we won’t need to hide the content on the single post page, we’ll conditionally add the v-show directive.

<?php  // components/recipe-content.php  global $  is_archive_item; ?> <div is="recipe-content">   <article class="recipe"      <?php if ($  is_archive_item): ?>        v-show="show"     <?php endif; ?>   >     <?php     if ($  is_archive_item):       the_excerpt();     else       the_content();     endif;          get_template_part('components/star-rating');     ?>   </article> </div>

In this specific example, we could have also tested with  is_archive() inside the component, but in most cases we’ll need to set explicit props.

We’ll need to move the rating state and logic up into the <recipe-content> component so it can know if it needs to hide itself. Inside <star-rating>, we’ll make a custom v-model by replacing rating with value, and this.rating = i with  $ emit('input', i) as well . So our component registration will now look like this:

// main.js  Vue.component('recipe-content', {   data () {     rating: 0   },   watch: {     rating (val) {       // ...     }   },   mounted () {     this.rating = someKindOfStorageDefinedElsewhere.load(this.recipeId)       } })  Vue.component('star-rating', {   props: {     maxRating: { /* ... */ },     recipeId: { /* ... */ },     value: { type: Number, required: true }   },   methods: {     rate (i) { this.$  emit('input', i) }   }, })

We’ll add v-model in star-rating.php and change rating to value. In addition, we can now move the <feedback-form> up into <recipe-content>:

<?php // components/star-rating.php  $  max_rating = get_field('max_rating'); $  recipe_id = get_the_ID(); ?> <div is="star-rating"    inline-template    :max-rating="<?= $   max_rating ?>"    recipe-id="<?= $  recipe_id ?>"    v-model="value" >   <div class="star-rating">     <p>Rate recipe:</p>     <button @click="rate(0)">       <svg><path d="..." :fill="value === 0 ? 'black' : 'transparent'"></svg>     </button>     <button v-for="(i in maxRating) @click="rate(i)">       <svg><path d="..." :fill="value >= i ? 'black' : 'transparent'"></svg>     </button>   </div> </div>
<?php // components/recipe-content.php  global $  is_archive_item; ?> <div is="recipe-content">   <article class="recipe"      <?php if ($  is_archive_item): ?>        v-show="show"     <?php endif; ?>   >          <?php     if ($  is_archive_item):       the_excerpt();     else       the_content();     endif;          get_template_part('components/star-rating');     get_template_part('components/feedback-form');     ?>   </article> </div>

Now everything is set up so the initial render shows all recipes, and then the user can filter them based on their rating. Moving forward, we could add all kinds of parameters to filter content. And it doesn’t have to be based on user input — we can allow filtering based on the content itself (e.g. number of ingredients or cooking time) by passing the data from PHP to Vue.

Conclusion

Well, that was a bit of a long ride, but look at what we’ve built: independent, composable, maintainable, interactive, progressively enhanced components in our WordPress theme. We brought together the best of all worlds!

I’ve been using this approach in production for a while now, and I love the way it allows me to reason about the different parts of my themes. I hope I’ve inspired you to try it out too.


  1. Of course, two days before launch, the client’s legal department decided they don’t want to collect all that info. Currently the live form is but a shadow of its development self.
  2. Fun fact: Rasmus Lerdorf said that his original intent was for PHP to be templating only, with all business logic handled in C. Let that sink in for a moment. Then clear an hour from your schedule and watch the whole talk.
  3. There are third-party WordPress templating engines that can compile down to optimized PHP. Twig, for example, comes to mind. We’re trying to go the reverse route and send vanilla PHP to be handled by JavaScript.

The post How to Build Vue Components in a WordPress Theme appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

Third-Party Components at Their Best

I’m a fan of the componentization of the web. I think it’s a very nice way to build a website at just about any scale (except, perhaps, the absolute most basic). There are no shortage of opinions about what makes a good component, but say we scope that to third-party for a moment. That is, components that you just use, rather than components that you build yourself as part of your site’s unique setup.

What makes a third-party component good? My favorite attribute of a third-party component is when it takes something hard and makes it easy. Particularly things that recognize and properly handle nuances, or things that you might not even know enough about to get right.

Perhaps you use some component that does pop-up contextual menus for you. It might perform browser edge detection, such as ensuring the menu never appears cut off or off-screen. That’s a tricky little bit of programming that you might not get right if you did it yourself — or even forget to do.

I think of the <Link /> component that React Router has or what’s used on Gatsby sites. It automatically injects aria-current="page" for you on the links when you’re on that page. You can and probably should use that for a styling hook! And you probably would have forgotten to program that if you were handling your own links.

In that same vein, Reach UI Tabs have rigorous accessibility baked into them that you probably wouldn’t get right if you hand-rolled them. This React image component does all sorts of stuff that is relatively difficult to pull off with images, like the complex responsive images syntax, lazy loading, placeholders, etc. This is, in a sense, handing you best practices for “free.”

Here’s a table library that doesn’t even touch UI for you, and instead focuses on other needs you’re likely to have with tables, which is another fascinating approach.

Anyway! Here’s what y’all said when I was asking about this. What makes a third-party component awesome? What do the best of them do? (besides the obvious, like good docs and good accessibility)? Some of these might be at-odds. I’m just listing what people said they like.

  • Plug-and-play. It should “just work” with minimal config.
  • Lots of editable demos
  • Highly configurable
  • “White label” styling. Don’t bring too strong of design choices.
  • Styled via regular CSS so you can BYO own styling tools
  • Fast
  • Small
  • Is installable via a package manager
  • Can be manually instantiated
  • Can be given a DOM node where it can go
  • Follows a useful versioning scheme
  • Is manintained, particularly for security
  • Has a public roadmap
  • Is framework-agnostic
  • Doesn’t have other dependencies
  • Uses intuitive naming conventions
  • Supports internationalization
  • Has lots of tests

Anything you’d add to that list?

The post Third-Party Components at Their Best appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]