Tag: Building

Building Interoperable Web Components That Even Work With React

Those of us who’ve been web developers more than a few years have probably written code using more than one JavaScript framework. With all the choices out there — React, Svelte, Vue, Angular, Solid — it’s all but inevitable. One of the more frustrating things we have to deal with when working across frameworks is re-creating all those low-level UI components: buttons, tabs, dropdowns, etc. What’s particularly frustrating is that we’ll typically have them defined in one framework, say React, but then need to rewrite them if we want to build something in Svelte. Or Vue. Or Solid. And so on.

Wouldn’t it be better if we could define these low-level UI components once, in a framework-agnostic way, and then re-use them between frameworks? Of course it would! And we can; web components are the way. This post will show you how.

As of now, the SSR story for web components is a bit lacking. Declarative shadow DOM (DSD) is how a web component is server-side rendered, but, as of this writing, it’s not integrated with your favorite application frameworks like Next, Remix or SvelteKit. If that’s a requirement for you, be sure to check the latest status of DSD. But otherwise, if SSR isn’t something you’re using, read on.

First, some context

Web Components are essentially HTML elements that you define yourself, like <yummy-pizza> or whatever, from the ground up. They’re covered all over here at CSS-Tricks (including an extensive series by Caleb Williams and one by John Rhea) but we’ll briefly walk through the process. Essentially, you define a JavaScript class, inherit it from HTMLElement, and then define whatever properties, attributes and styles the web component has and, of course, the markup it will ultimately render to your users.

Being able to define custom HTML elements that aren’t bound to any particular component is exciting. But this freedom is also a limitation. Existing independently of any JavaScript framework means you can’t really interact with those JavaScript frameworks. Think of a React component which fetches some data and then renders some other React component, passing along the data. This wouldn’t really work as a web component, since a web component doesn’t know how to render a React component.

Web components particularly excel as leaf components. Leaf components are the last thing to be rendered in a component tree. These are the components which receive some props, and render some UI. These are not the components sitting in the middle of your component tree, passing data along, setting context, etc. — just pure pieces of UI that will look the same, no matter which JavaScript framework is powering the rest of the app.

The web component we’re building

Rather than build something boring (and common), like a button, let’s build something a little bit different. In my last post we looked at using blurry image previews to prevent content reflow, and provide a decent UI for users while our images load. We looked at base64 encoding a blurry, degraded versions of our images, and showing that in our UI while the real image loaded. We also looked at generating incredibly compact, blurry previews using a tool called Blurhash.

That post showed you how to generate those previews and use them in a React project. This post will show you how to use those previews from a web component so they can be used by any JavaScript framework.

But we need to walk before we can run, so we’ll walk through something trivial and silly first to see exactly how web components work.

Everything in this post will build vanilla web components without any tooling. That means the code will have a bit of boilerplate, but should be relatively easy to follow. Tools like Lit or Stencil are designed for building web components and can be used to remove much of this boilerplate. I urge you to check them out! But for this post, I’ll prefer a little more boilerplate in exchange for not having to introduce and teach another dependency.

A simple counter component

Let’s build the classic “Hello World” of JavaScript components: a counter. We’ll render a value, and a button that increments that value. Simple and boring, but it’ll let us look at the simplest possible web component.

In order to build a web component, the first step is to make a JavaScript class, which inherits from HTMLElement:

class Counter extends HTMLElement {}

The last step is to register the web component, but only if we haven’t registered it already:

if (!customElements.get("counter-wc")) {   customElements.define("counter-wc", Counter); }

And, of course, render it:

<counter-wc></counter-wc>

And everything in between is us making the web component do whatever we want it to. One common lifecycle method is connectedCallback, which fires when our web component is added to the DOM. We could use that method to render whatever content we’d like. Remember, this is a JS class inheriting from HTMLElement, which means our this value is the web component element itself, with all the normal DOM manipulation methods you already know and love.

At it’s most simple, we could do this:

class Counter extends HTMLElement {   connectedCallback() {     this.innerHTML = "<div style='color: green'>Hey</div>";   } }  if (!customElements.get("counter-wc")) {   customElements.define("counter-wc", Counter); }

…which will work just fine.

The word "hey" in green.

Adding real content

Let’s add some useful, interactive content. We need a <span> to hold the current number value and a <button> to increment the counter. For now, we’ll create this content in our constructor and append it when the web component is actually in the DOM:

constructor() {   super();   const container = document.createElement('div');    this.valSpan = document.createElement('span');    const increment = document.createElement('button');   increment.innerText = 'Increment';   increment.addEventListener('click', () => {     this.#value = this.#currentValue + 1;   });    container.appendChild(this.valSpan);   container.appendChild(document.createElement('br'));   container.appendChild(increment);    this.container = container; }  connectedCallback() {   this.appendChild(this.container);   this.update(); }

If you’re really grossed out by the manual DOM creation, remember you can set innerHTML, or even create a template element once as a static property of your web component class, clone it, and insert the contents for new web component instances. There’s probably some other options I’m not thinking of, or you can always use a web component framework like Lit or Stencil. But for this post, we’ll continue to keep it simple.

Moving on, we need a settable JavaScript class property named value

#currentValue = 0;  set #value(val) {   this.#currentValue = val;   this.update(); }

It’s just a standard class property with a setter, along with a second property to hold the value. One fun twist is that I’m using the private JavaScript class property syntax for these values. That means nobody outside our web component can ever touch these values. This is standard JavaScript that’s supported in all modern browsers, so don’t be afraid to use it.

Or feel free to call it _value if you prefer. And, lastly, our update method:

update() {   this.valSpan.innerText = this.#currentValue; }

It works!

The counter web component.

Obviously this is not code you’d want to maintain at scale. Here’s a full working example if you’d like a closer look. As I’ve said, tools like Lit and Stencil are designed to make this simpler.

Adding some more functionality

This post is not a deep dive into web components. We won’t cover all the APIs and lifecycles; we won’t even cover shadow roots or slots. There’s endless content on those topics. My goal here is to provide a decent enough introduction to spark some interest, along with some useful guidance on actually using web components with the popular JavaScript frameworks you already know and love.

To that end, let’s enhance our counter web component a bit. Let’s have it accept a color attribute, to control the color of the value that’s displayed. And let’s also have it accept an increment property, so consumers of this web component can have it increment by 2, 3, 4 at a time. And to drive these state changes, let’s use our new counter in a Svelte sandbox — we’ll get to React in a bit.

We’ll start with the same web component as before and add a color attribute. To configure our web component to accept and respond to an attribute, we add a static observedAttributes property that returns the attributes that our web component listens for.

static observedAttributes = ["color"];

With that in place, we can add a attributeChangedCallback lifecycle method, which will run whenever any of the attributes listed in observedAttributes are set, or updated.

attributeChangedCallback(name, oldValue, newValue) {   if (name === "color") {     this.update();   } }

Now we update our update method to actually use it:

update() {   this.valSpan.innerText = this._currentValue;   this.valSpan.style.color = this.getAttribute("color") || "black"; }

Lastly, let’s add our increment property:

increment = 1;

Simple and humble.

Using the counter component in Svelte

Let’s use what we just made. We’ll go into our Svelte app component and add something like this:

<script>   let color = "red"; </script>  <style>   main {     text-align: center;   } </style>  <main>   <select bind:value={color}>     <option value="red">Red</option>     <option value="green">Green</option>     <option value="blue">Blue</option>   </select>    <counter-wc color={color}></counter-wc> </main>

And it works! Our counter renders, increments, and the dropdown updates the color. As you can see, we render the color attribute in our Svelte template and, when the value changes, Svelte handles the legwork of calling setAttribute on our underlying web component instance. There’s nothing special here: this is the same thing it already does for the attributes of any HTML element.

Things get a little bit interesting with the increment prop. This is not an attribute on our web component; it’s a prop on the web component’s class. That means it needs to be set on the web component’s instance. Bear with me, as things will wind up much simpler in a bit.

First, we’ll add some variables to our Svelte component:

let increment = 1; let wcInstance;

Our powerhouse of a counter component will let you increment by 1, or by 2:

<button on:click={() => increment = 1}>Increment 1</button> <button on:click={() => increment = 2}>Increment 2</button>

But, in theory, we need to get the actual instance of our web component. This is the same thing we always do anytime we add a ref with React. With Svelte, it’s a simple bind:this directive:

<counter-wc bind:this={wcInstance} color={color}></counter-wc>

Now, in our Svelte template, we listen for changes to our component’s increment variable and set the underlying web component property.

$ : {   if (wcInstance) {     wcInstance.increment = increment;   } }

You can test it out over at this live demo.

We obviously don’t want to do this for every web component or prop we need to manage. Wouldn’t it be nice if we could just set increment right on our web component, in markup, like we normally do for component props, and have it, you know, just work? In other words, it’d be nice if we could delete all usages of wcInstance and use this simpler code instead:

<counter-wc increment={increment} color={color}></counter-wc>

It turns out we can. This code works; Svelte handles all that legwork for us. Check it out in this demo. This is standard behavior for pretty much all JavaScript frameworks.

So why did I show you the manual way of setting the web component’s prop? Two reasons: it’s useful to understand how these things work and, a moment ago, I said this works for “pretty much” all JavaScript frameworks. But there’s one framework which, maddeningly, does not support web component prop setting like we just saw.

React is a different beast

React. The most popular JavaScript framework on the planet does not support basic interop with web components. This is a well-known problem that’s unique to React. Interestingly, this is actually fixed in React’s experimental branch, but for some reason wasn’t merged into version 18. That said, we can still track the progress of it. And you can try this yourself with a live demo.

The solution, of course, is to use a ref, grab the web component instance, and manually set increment when that value changes. It looks like this:

import React, { useState, useRef, useEffect } from 'react'; import './counter-wc';  export default function App() {   const [increment, setIncrement] = useState(1);   const [color, setColor] = useState('red');   const wcRef = useRef(null);    useEffect(() => {     wcRef.current.increment = increment;   }, [increment]);    return (     <div>       <div className="increment-container">         <button onClick={() => setIncrement(1)}>Increment by 1</button>         <button onClick={() => setIncrement(2)}>Increment by 2</button>       </div>        <select value={color} onChange={(e) => setColor(e.target.value)}>         <option value="red">Red</option>         <option value="green">Green</option>         <option value="blue">Blue</option>       </select>        <counter-wc ref={wcRef} increment={increment} color={color}></counter-wc>     </div>   ); }

As we discussed, coding this up manually for every web component property is simply not scalable. But all is not lost because we have a couple of options.

Option 1: Use attributes everywhere

We have attributes. If you clicked the React demo above, the increment prop wasn’t working, but the color correctly changed. Can’t we code everything with attributes? Sadly, no. Attribute values can only be strings. That’s good enough here, and we’d be able to get somewhat far with this approach. Numbers like increment can be converted to and from strings. We could even JSON stringify/parse objects. But eventually we’ll need to pass a function into a web component, and at that point we’d be out of options.

Option 2: Wrap it

There’s an old saying that you can solve any problem in computer science by adding a level of indirection (except the problem of too many levels of indirection). The code to set these props is pretty predictable and simple. What if we hide it in a library? The smart folks behind Lit have one solution. This library creates a new React component for you after you give it a web component, and list out the properties it needs. While clever, I’m not a fan of this approach.

Rather than have a one-to-one mapping of web components to manually-created React components, what I prefer is just one React component that we pass our web component tag name to (counter-wc in our case) — along with all the attributes and properties — and for this component to render our web component, add the ref, then figure out what is a prop and what is an attribute. That’s the ideal solution in my opinion. I don’t know of a library that does this, but it should be straightforward to create. Let’s give it a shot!

This is the usage we’re looking for:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

wcTag is the web component tag name; the rest are the properties and attributes we want passed along.

Here’s what my implementation looks like:

import React, { createElement, useRef, useLayoutEffect, memo } from 'react';  const _WcWrapper = (props) => {   const { wcTag, children, ...restProps } = props;   const wcRef = useRef(null);    useLayoutEffect(() => {     const wc = wcRef.current;      for (const [key, value] of Object.entries(restProps)) {       if (key in wc) {         if (wc[key] !== value) {           wc[key] = value;         }       } else {         if (wc.getAttribute(key) !== value) {           wc.setAttribute(key, value);         }       }     }   });    return createElement(wcTag, { ref: wcRef }); };  export const WcWrapper = memo(_WcWrapper);

The most interesting line is at the end:

return createElement(wcTag, { ref: wcRef });

This is how we create an element in React with a dynamic name. In fact, this is what React normally transpiles JSX into. All our divs are converted to createElement("div") calls. We don’t normally need to call this API directly but it’s there when we need it.

Beyond that, we want to run a layout effect and loop through every prop that we’ve passed to our component. We loop through all of them and check to see if it’s a property with an in check that checks the web component instance object as well as its prototype chain, which will catch any getters/setters that wind up on the class prototype. If no such property exists, it’s assumed to be an attribute. In either case, we only set it if the value has actually changed.

If you’re wondering why we use useLayoutEffect instead of useEffect, it’s because we want to immediately run these updates before our content is rendered. Also, note that we have no dependency array to our useLayoutEffect; this means we want to run this update on every render. This can be risky since React tends to re-render a lot. I ameliorate this by wrapping the whole thing in React.memo. This is essentially the modern version of React.PureComponent, which means the component will only re-render if any of its actual props have changed — and it checks whether that’s happened via a simple equality check.

The only risk here is that if you’re passing an object prop that you’re mutating directly without re-assigning, then you won’t see the updates. But this is highly discouraged, especially in the React community, so I wouldn’t worry about it.

Before moving on, I’d like to call out one last thing. You might not be happy with how the usage looks. Again, this component is used like this:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

Specifically, you might not like passing the web component tag name to the <WcWrapper> component and prefer instead the @lit-labs/react package above, which creates a new individual React component for each web component. That’s totally fair and I’d encourage you to use whatever you’re most comfortable with. But for me, one advantage with this approach is that it’s easy to delete. If by some miracle React merges proper web component handling from their experimental branch into main tomorrow, you’d be able to change the above code from this:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

…to this:

<counter-wc ref={wcRef} increment={increment} color={color} />

You could probably even write a single codemod to do that everywhere, and then delete <WcWrapper> altogether. Actually, scratch that: a global search and replace with a RegEx would probably work.

The implementation

I know, it seems like it took a journey to get here. If you recall, our original goal was to take the image preview code we looked at in my last post, and move it to a web component so it can be used in any JavaScript framework. React’s lack of proper interop added a lot of detail to the mix. But now that we have a decent handle on how to create a web component, and use it, the implementation will almost be anti-climactic.

I’ll drop the entire web component here and call out some of the interesting bits. If you’d like to see it in action, here’s a working demo. It’ll switch between my three favorite books on my three favorite programming languages. The URL for each book will be unique each time, so you can see the preview, though you’ll likely want to throttle things in your DevTools Network tab to really see things taking place.

View entire code
class BookCover extends HTMLElement {   static observedAttributes = ['url'];    attributeChangedCallback(name, oldValue, newValue) {     if (name === 'url') {       this.createMainImage(newValue);     }   }    set preview(val) {     this.previewEl = this.createPreview(val);     this.render();   }    createPreview(val) {     if (typeof val === 'string') {       return base64Preview(val);     } else {       return blurHashPreview(val);     }   }    createMainImage(url) {     this.loaded = false;     const img = document.createElement('img');     img.alt = 'Book cover';     img.addEventListener('load', () =&gt; {       if (img === this.imageEl) {         this.loaded = true;         this.render();       }     });     img.src = url;     this.imageEl = img;   }    connectedCallback() {     this.render();   }    render() {     const elementMaybe = this.loaded ? this.imageEl : this.previewEl;     syncSingleChild(this, elementMaybe);   } }

First, we register the attribute we’re interested in and react when it changes:

static observedAttributes = ['url'];  attributeChangedCallback(name, oldValue, newValue) {   if (name === 'url') {     this.createMainImage(newValue);   } }

This causes our image component to be created, which will show only when loaded:

createMainImage(url) {   this.loaded = false;   const img = document.createElement('img');   img.alt = 'Book cover';   img.addEventListener('load', () => {     if (img === this.imageEl) {       this.loaded = true;       this.render();     }   });   img.src = url;   this.imageEl = img; }

Next we have our preview property, which can either be our base64 preview string, or our blurhash packet:

set preview(val) {   this.previewEl = this.createPreview(val);   this.render(); }  createPreview(val) {   if (typeof val === 'string') {     return base64Preview(val);   } else {     return blurHashPreview(val);   } }

This defers to whichever helper function we need:

function base64Preview(val) {   const img = document.createElement('img');   img.src = val;   return img; }  function blurHashPreview(preview) {   const canvasEl = document.createElement('canvas');   const { w: width, h: height } = preview;    canvasEl.width = width;   canvasEl.height = height;    const pixels = decode(preview.blurhash, width, height);   const ctx = canvasEl.getContext('2d');   const imageData = ctx.createImageData(width, height);   imageData.data.set(pixels);   ctx.putImageData(imageData, 0, 0);    return canvasEl; }

And, lastly, our render method:

connectedCallback() {   this.render(); }  render() {   const elementMaybe = this.loaded ? this.imageEl : this.previewEl;   syncSingleChild(this, elementMaybe); }

And a few helpers methods to tie everything together:

export function syncSingleChild(container, child) {   const currentChild = container.firstElementChild;   if (currentChild !== child) {     clearContainer(container);     if (child) {       container.appendChild(child);     }   } }  export function clearContainer(el) {   let child;    while ((child = el.firstElementChild)) {     el.removeChild(child);   } }

It’s a little bit more boilerplate than we’d need if we build this in a framework, but the upside is that we can re-use this in any framework we’d like — although React will need a wrapper for now, as we discussed.

Odds and ends

I’ve already mentioned Lit’s React wrapper. But if you find yourself using Stencil, it actually supports a separate output pipeline just for React. And the good folks at Microsoft have also created something similar to Lit’s wrapper, attached to the Fast web component library.

As I mentioned, all frameworks not named React will handle setting web component properties for you. Just note that some have some special flavors of syntax. For example, with Solid.js, <your-wc value={12}> always assumes that value is a property, which you can override with an attr prefix, like <your-wc attr:value={12}>.

Wrapping up

Web components are an interesting, often underused part of the web development landscape. They can help reduce your dependence on any single JavaScript framework by managing your UI, or “leaf” components. While creating these as web components — as opposed to Svelte or React components — won’t be as ergonomic, the upside is that they’ll be widely reusable.


Building Interoperable Web Components That Even Work With React originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , , ,

Building a newbie-friendly codebase

Pedro Santos suggests:

  1. Using naming conventions such that you can learn them once and apply them everywhere
  2. Unidirectional data flows. Make it easy to follow the app flow.
  3. No magic numbers. I’d add they are even worse in CSS as it’s both the confusion they cause and how they are often tied to awkward or incorrect assumptions.
  4. Using data structures. Like state machines.
  5. Testing everything
  6. Good code > good comments
  7. Avoiding acronyms
  8. Refactoring opportunistically

To Shared LinkPermalink on CSS-Tricks


Building a newbie-friendly codebase originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, ,
[Top]

Building a Scrollable and Draggable Timeline with GSAP

Here’s a super classy demo from Michelle Barker over on Codrops that shows how to build a scrollable and draggable timeline with GSAP. It’s an interesting challenge to have two different interactions (vertical scrolling and horizontal dragging) be tied together and react to each other. I love seeing it all done with nice semantic markup, code that’s easy to follow, clear abstractions, and accessibility considered all the way through.

To Shared LinkPermalink on CSS-Tricks


Building a Scrollable and Draggable Timeline with GSAP originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , ,
[Top]

Does the Next Generation of Static Site Generators Make Building Sites Better?

Just ran across îles, a new static site generator mostly centered around Vue. The world has no particular shortage of static site generators, but it’s interesting to see what this “next generation” of SSGs seem to focus on or try to solve.

îles looks to take a heaping spoonful of inspiration from Astro. If we consider them together, along with other emerging and quickly-evolving SSGs, there is some similarities:

  • Ship zero JavaScript by default. Interactive bits are opt-in — that’s what the islands metaphor is all about. Astro and îles do it at the per-component level and SvelteKit prefers it at the page level.
  • Additional fanciness around controls for when hydration happens, like “when the browser is idle,” or “when the component is visible.”
  • Use a fast build tool, like Vite which is Go-based esbuild under the hood. Or Rust-based swc in the case of Next 12.
  • Support multiple JavaScript frameworks for componentry. Astro and îles do this out of the box, and another example is how Slinkity brings that to Eleventy.
  • File-system based routing.
  • Assumption that Markdown is used for content.

When you compare these to first-cohort SSGs, like Jekyll, I get a few feelings:

  1. These really aren’t that much different. The feature set is largely the same.
  2. The biggest change is probably that far more of them are JavaScript library based. Turns out JavaScript libraries are what really what people wanted out of HTML preprocessors, perhaps because of the strong focus on components.
  3. They are incrementally better. They are faster, the live reloading is better, the common needs have been ironed out.

The post Does the Next Generation of Static Site Generators Make Building Sites Better? appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , ,
[Top]

Building an Angular Data Grid With Filtering

(This is a sponsored post.)

Kendo UI makes it possible to go from a basic idea to a full-fledged app, thanks to a massive component library. We’re talking well over 100 components that are ready for you to drop into your app at will, whether it’s React, Angular or Vue you’re working in — they just work. That is because Kendo UI is actually a bundle of four JavaScript libraries, each built natively for their respective framework. But more than that, as we’ve covered before, the components are super themeable to the extent that you can make them whatever you want.

But here’s the real kicker with Kendo UI: it takes care of the heavy lifting. The styling is great and all, but what separates Kendo UI from other component frameworks is the functionality it provides right out of the box.

Case in point: data. Rather than spending all your time figuring out the best way to bind data to a component, that’s just a given which ultimately allows you to focus more of your time on theming and getting the UI just right.

Perhaps the best way to see how trivial Kendo UI makes working with data is to see it in action, so…

Let’s look at the Angular Grid component

This is the Kendo UI for Angular Data Grid component. Lots of data in there, right? We’re looking at a list of employees that displays a name, image, and other bits of information about each person.

Like all of Kendo UI’s components, it’s not like there’s one data grid component that they square-pegged to work in multiple frameworks. This data grid was built from scratch and designed specifically to work for Angular, just as their KendoReact Grid component is designed specifically for React.

Now, normally, a simple <table> element might do the trick, right? But Kendo UI for Angular’s data grid is chockfull of extras that make it a much better user experience. Notice right off the bat that it provides interactive functionality around things like exporting the data to Excel or PDF. And there are a bunch of other non-trivial features that would otherwise take a vast majority of the time and effort to make the component.

Filtering

Here’s one for you: filtering a grid of data. Say you’re looking at a list of employees like the data grid example above, but for a company that employees thousands of folks. It’d be hard to find a specific person without considering a slew of features, like search, sortable columns, and pagination — all of which Kendo UI’s data grid does.

Users can quickly parse the data bound to the Angular data grid. Filtering can be done through a dedicated filter row, or through a filter menu that pops up when clicking on a filter icon in the header of a column. 

One way to filter the data is to click on a column header, select the Filter option, and set the criteria.

Kendo UI’s documentation is great. Here’s how fast we can get this up and running.

First, import the component

No tricks here — import the data grid as you would any other component:

import { Component, OnInit, ViewChild } from '@angular/core'; import { DataBindingDirective } from '@progress/kendo-angular-grid'; import { process } from '@progress/kendo-data-query'; import { employees } from './employees'; import { images } from './images';

Next, call the component

@Component({   selector: 'my-app',   template: `     <kendo-grid>       // ...     </kendo-grid>   ` })

This is incomplete, of course, because next we have to…

Configure the component

The key feature we want to enable is filtering, but Kendo’s Angular Grid takes all kinds of feature parameters that can be enabled in one fell swoop, from sorting and grouping, to pagination and virtualization, to name a few.

Filtering? It’s a one-liner to bind it to the column headers.

@Component({   selector: 'my-app',   template: `     <kendo-grid       [kendoGridBinding]="gridView"       kendoGridSelectBy="id"       [selectedKeys]="mySelection"       [pageSize]="20"       [pageable]="true"       [sortable]="true"       [groupable]="true"       [reorderable]="true"       [resizable]="true"       [height]="500"       [columnMenu]="{ filter: true }"     >       // etc.     </kendo-grid>   ` })

Then, mark up the rest of the UI

We won’t go deep here. Kendo UI’s documentation has an excellent example of how that looks. This is a good time to work on the styling as well, which is done in a styles parameter. Again, theming a Kendo UI component is a cinch.

So far, we have a nice-looking data grid before we even plug any actual data into it!

And, finally, bind the data

You may have noticed right away when we imported the component that we imported “employee” data in the process. We need to bind that data to the component. Now, this is where someone like me would go run off in a corner and cry, but Kendo UI makes it a little too easy for that to happen.

// Active the component on init export class AppComponent implements OnInit {   // Bind the employee data to the component   @ViewChild(DataBindingDirective) dataBinding: DataBindingDirective;   // Set the grid's data source to the employee data file   public gridData: any[] = employees;   // Apply the data source to the Grid component view   public gridView: any[];    public mySelection: string[] = [];    public ngOnInit(): void {     this.gridView = this.gridData;   }   // Start processing the data   public onFilter(inputValue: string): void {     this.gridView = process(this.gridData, {       filter: {         // Set the type of logic (and/or)         logic: "or",         // Defining filters and their operators         filters: [           {             field: 'full_name',             operator: 'contains',             value: inputValue           },           {             field: 'job_title',             operator: 'contains',             value: inputValue           },           {             field: 'budget',             operator: 'contains',             value: inputValue           },           {             field: 'phone',             operator: 'contains',             value: inputValue           },           {             field: 'address',             operator: 'contains',             value: inputValue           }         ],       }     }).data;      this.dataBinding.skip = 0;   }    // ... }

Let’s see that demo again


That’s a heckuva lot of power with a minimal amount of effort. The Kendo UI APIs are extensive and turn even the most complex feature dead simple.

And we didn’t even get to all of the other wonderful goodies that we get with Kendo UI components. Take accessibility. Could you imagine all of the consideration that needs to go into making a component like this accessible? Like all of the other powerful features we get, Kendo UI tackles accessibility for us as well, taking on the heavy lifting that goes into making a keyboard-friendly UI that meets WCAG 2.0 Alice standards and is compliant with Section 508 and WAI-ARIA standards.


The post Building an Angular Data Grid With Filtering appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Building a Tennis Trivia App With Next.js and Netlify

Today we will be learning how to build a tennis trivia app using Next.js and Netlify. This technology stack has become my go-to on many projects. It allows for rapid development and easy deployment.

Without further ado let’s jump in!

What we’re using

  • Next.js
  • Netlify
  • TypeScript
  • Tailwind CSS

Why Next.js and Netlify

You may think that this is a simple app that might not require a React framework. The truth is that Next.js gives me a ton of features out of the box that allow me to just start coding the main part of my app. Things like webpack configuration, getServerSideProps, and Netlify’s automatic creation of serverless functions are a few examples.

Netlify also makes deploying a Next.js git repo super easy. More on the deployment a bit later on.

What we’re building

Basically, we are going to build a trivia game that randomly shows you the name of a tennis player and you have to guess what country they are from. It consists of five rounds and keeps a running score of how many you get correct.

The data we need for this application is a list of players along with their country. Initially, I was thinking of querying some live API, but on second thought, decided to just use a local JSON file. I took a snapshot from RapidAPI and have included it in the starter repo.

The final product looks something like this:

You can find the final deployed version on Netlify.

Starter repo tour

If you want to follow along you can clone this repository and then go to the start branch:

git clone git@github.com:brenelz/tennis-trivia.git cd tennis-trivia git checkout start

In this starter repo, I went ahead and wrote some boilerplate to get things going. I created a Next.js app using the command npx create-next-app tennis-trivia. I then proceeded to manually change a couple JavaScript files to .ts and .tsx. Surprisingly, Next.js automatically picked up that I wanted to use TypeScript. It was too easy! I also went ahead and configured Tailwind CSS using this article as a guide.

Enough talk, let’s code!

Initial setup

The first step is setting up environment variables. For local development, we do this with a .env.local file. You can copy the .env.sample from the starter repo.

cp .env.sample .env.local

Notice it currently has one value, which is the path of our application. We will use this on the front end of our app, so we must prefix it with NEXT_PUBLIC_.

Finally, let’s use the following commands to install the dependencies and start the dev server: 

npm install npm run dev

Now we access our application at http://localhost:3000. We should see a fairly empty page with just a headline:

Creating the UI markup

In pages/index.tsx, let’s add the following markup to the existing Home() function:

export default function Home() {   return (     <div className="bg-blue-500">     <div className="max-w-2xl mx-auto text-center py-16 px-4 sm:py-20 sm:px-6 lg:px-8">       <h2 className="text-3xl font-extrabold text-white sm:text-4xl">         <span className="block">Tennis Trivia - Next.js Netlify</span>       </h2>       <div>         <p className="mt-4 text-lg leading-6 text-blue-200">           What country is the following tennis player from?         </p>         <h2 className="text-lg font-extrabold text-white my-5">           Roger Federer         </h2>          <form>           <input             list="countries"             type="text"             className="p-2 outline-none"             placeholder="Choose Country"           />           <datalist id="countries">             <option>Switzerland</option>            </datalist>            <p>              <button                className="mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 sm:w-auto"                type="submit"              >                Guess             </button>           </p>         </form>          <p className="mt-4 text-lg leading-6 text-white">           <strong>Current score:</strong> 0         </p>       </div>     </div>     </div>   );

This forms the scaffold for our UI. As you can see, we are using lots of utility classes from Tailwind CSS to make things look a little prettier. We also have a simple autocomplete input and a submit button. This is where you will select the country you think the player is from and then hit the button. Lastly, at the bottom, there is a score that changes based on correct or incorrect answers.

Setting up our data

If you take a look at the data folder, there should be a tennisPlayers.json with all the data we will need for this application. Create a lib folder at the root and, inside of it, create a players.ts file. Remember, the .ts extension is required since is a TypeScript file. Let’s define a type that matches our JSON data..

export type Player = {   id: number,   first_name: string,   last_name: string,   full_name: string,   country: string,   ranking: number,   movement: string,   ranking_points: number, };

This is how we create a type in TypeScript. We have the name of the property on the left, and the type it is on the right. They can be basic types, or even other types themselves.

From here, let’s create specific variables that represent our data:

export const playerData: Player[] = require("../data/tennisPlayers.json"); export const top100Players = playerData.slice(0, 100);  const allCountries = playerData.map((player) => player.country).sort(); export const uniqueCountries = [...Array.from(new Set(allCountries))];

A couple things to note is that we are saying our playerData is an array of Player types. This is denoted by the colon followed by the type. In fact, if we hover over the playerData we can see its type:

In that last line we are getting a unique list of countries to use in our country dropdown. We pass our countries into a JavaScript Set, which gets rid of the duplicate values. We then create an array from it, and spread it into a new array. It may seem unnecessary but this was done to make TypeScript happy.

Believe it or not, that is really all the data we need for our application!

Let’s make our UI dynamic!

All our values are hardcoded currently, but let’s change that. The dynamic pieces are the tennis player’s name, the list of countries, and the score.

Back in pages/index.tsx, let’s modify our getServerSideProps function to create a list of five random players as well as pull in our uniqueCountries variable.

import { Player, uniqueCountries, top100Players } from "../lib/players"; ... export async function getServerSideProps() {   const randomizedPlayers = top100Players.sort((a, b) => 0.5 - Math.random());   const players = randomizedPlayers.slice(0, 5);    return {     props: {       players,       countries: uniqueCountries,     },   }; }

Whatever is in the props object we return will be passed to our React component. Let’s use them on our page:

type HomeProps = {   players: Player[];   countries: string[]; };  export default function Home({ players, countries }: HomeProps) {   const player = players[0];   ... }  

As you can see, we define another type for our page component. Then we add the HomeProps type to the Home() function. We have again specified that players is an array of the Player type.

Now we can use these props further down in our UI. Replace “Roger Federer” with {player.full_name} (he’s my favorite tennis player by the way). You should be getting nice autocompletion on the player variable as it lists all the property names we have access to because of the types that we defined.

Further down from this, let’s now update the list of countries to this:

<datalist id="countries">   {countries.map((country, i) => (     <option key={i}>{country}</option>   ))} </datalist>

Now that we have two of the three dynamic pieces in place, we need to tackle the score. Specifically, we need to create a piece of state for the current score.

export default function Home({ players, countries }: HomeProps) {   const [score, setScore] = useState(0);   ... }

Once this is done, replace the 0 with {score} in our UI.

You can now check our progress by going to http://localhost:3000. You can see that every time the page refreshes, we get a new name; and when typing in the input field, it lists all of the available unique countries.

Adding some interactivity

We’ve come a decent way but we need to add some interactivity.

Hooking up the guess button

For this we need to have some way of knowing what country was picked. We do this by adding some more state and attaching it to our input field.

export default function Home({ players, countries }: HomeProps) {   const [score, setScore] = useState(0);   const [pickedCountry, setPickedCountry] = useState("");   ...   return (     ...     <input       list="countries"       type="text"       value={pickedCountry}       onChange={(e) => setPickedCountry(e.target.value)}       className="p-2 outline-none"       placeholder="Choose Country"     />    ...   ); }

Next, let’s add a guessCountry function and attach it to the form submission:

const guessCountry = () => {   if (player.country.toLowerCase() === pickedCountry.toLowerCase()) {     setScore(score + 1);   } else {     alert(‘incorrect’);   } }; ... <form   onSubmit={(e) => {     e.preventDefault();     guessCountry();   }} >

All we do is basically compare the current player’s country to the guessed country. Now, when we go back to the app and guess the country right, the score increases as expected.

Adding a status indicator

To make this a bit nicer, we can render some UI depending whether the guess is correct or not.

So, let’s create another piece of state for status, and update the guess country method:

const [status, setStatus] = useState(null); ... const guessCountry = () => {   if (player.country.toLowerCase() === pickedCountry.toLowerCase()) {     setStatus({ status: "correct", country: player.country });     setScore(score + 1);   } else {     setStatus({ status: "incorrect", country: player.country });   } };

Then render this UI below the player name:

{status && (   <div className="mt-4 text-lg leading-6 text-white">     <p>             You are {status.status}. It is {status.country}     </p>     <p>       <button         autoFocus         className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 sm:w-auto"       >         Next Player       </button>     </p>   </div> )}

Lastly, we want to make sure our input field doesn’t show when we are in a correct or incorrect status. We achieve this by wrapping the form with the following:

{!status && (   <form>   ...   </form> )}

Now, if we go back to the app and guess the player’s country, we get a nice message with the result of the guess.

Progressing through players

Now probably comes the most challenging part: How do we go from one player to the next?

First thing we need to do is store the currentStep in state so that we can update it with a number from 0 to 4. Then, when it hits 5, we want to show a completed state since the trivia game is over.

Once again, let’s add the following state variables:

const [currentStep, setCurrentStep] = useState(0); const [playersData, setPlayersData] = useState(players);

…then replace our previous player variable with:

const player = playersData[currentStep];

Next, we create a nextStep function and hook it up to the UI:

const nextStep = () => {   setPickedCountry("");   setCurrentStep(currentStep + 1);   setStatus(null); }; ... <button   autoFocus   onClick={nextStep}   className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 sm:w-auto"  >     Next Player </button>

Now, when we make a guess and hit the next step button, we’re taken to a new tennis player. Guess again and we see the next, and so on. 

What happens when we hit next on the last player? Right now, we get an error. Let’s fix that by adding a conditional that represents that the game has been completed. This happens when the player variable is undefined.

{player ? (   <div>     <p className="mt-4 text-lg leading-6 text-blue-200">       What country is the following tennis player from?     </p>     ...     <p className="mt-4 text-lg leading-6 text-white">       <strong>Current score:</strong> {score}     </p>   </div> ) : (   <div>     <button       autoFocus       className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-600 bg-white hover:bg-indigo-50 sm:w-auto"       >       Play Again     </button>   </div> )}

Now we see a nice completed state at the end of the game.

Play again button

We are almost done! For our “Play Again” button we want to reset the state all of the game. We also want to get a new list of players from the server without needing a refresh. We do it like this:

const playAgain = async () => {   setPickedCountry("");   setPlayersData([]);   const response = await fetch(     process.env.NEXT_PUBLIC_API_URL + "/api/newGame"   );   const data = await response.json();   setPlayersData(data.players);   setCurrentStep(0);   setScore(0); };  <button   autoFocus   onClick={playAgain}   className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-600 bg-white hover:bg-indigo-50 sm:w-auto" >   Play Again </button>

Notice we are using the environment variable we set up before via the process.env object. We are also updating our playersData by overriding our server state with our client state that we just retrieved.

We haven’t filled out our newGame route yet, but this is easy with Next.js and Netlify serverless functions . We only need to edit the file in pages/api/newGame.ts.

import { NextApiRequest, NextApiResponse } from "next" import { top100Players } from "../../lib/players";  export default (req: NextApiRequest, res: NextApiResponse) => {   const randomizedPlayers = top100Players.sort((a, b) => 0.5 - Math.random());   const top5Players = randomizedPlayers.slice(0, 5);   res.status(200).json({players: top5Players}); }

This looks much the same as our getServerSideProps because we can reuse our nice helper variables.

If we go back to the app, notice the “Play Again” button works as expected.

Improving focus states

One last thing we can do to improve our user experience is set the focus on the country input field every time the step changes. That’s just a nice touch and convenient for the user. We do this using a ref and a useEffect:

const inputRef = useRef(null); ... useEffect(() => {   inputRef?.current?.focus(); }, [currentStep]);  <input   list="countries"   type="text"   value={pickedCountry}   onChange={(e) => setPickedCountry(e.target.value)}   ref={inputRef}   className="p-2 outline-none"   placeholder="Choose Country" />

Now we can navigate much easier just using the Enter key and typing a country.

Deploying to Netlify

You may be wondering how we deploy this thing. Well, using Netlify makes it so simple as it detects a Next.js application out of the box and automatically configures it.

All I did was set up a GitHub repo and connect my GitHub account to my Netlify account. From there, I simply pick a repo to deploy and use all the defaults.

The one thing to note is that you have to add the NEXT_PUBLIC_API_URL environment variable and redeploy for it to take effect.

You can find my final deployed version here.

Also note that you can just hit the “Deploy to Netlify” button on the GitHub repo.

Conclusion

Woohoo, you made it! That was a journey and I hope you learned something about React, Next.js, and Netlify along the way.

I have plans to expand this tennis trivia app to use Supabase in the near future so stay tuned!

If you have any questions/comments feel free to reach out to me on Twitter.


The post Building a Tennis Trivia App With Next.js and Netlify appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Developer Decisions For Building Flexible Components

Blog posts that get into the whole “how to think like a front-end developer” vibe are my favorite. Michelle Barker nails that in this post, and does it without sharing a line of code!

We simply can no longer design and develop only for “optimal” content or browsing conditions. Instead, we must embrace the inherent flexibility and unpredictability of the web, and build resilient components. Static mockups cannot cater to every scenario, so many design decisions fall to developers at build time. Like it or not, if you’re a UI developer, you are a designer — even if you don’t consider yourself one!

There are a lot of unknowns in front-end development. Much longer than my little list. Content of unknown size and length is certainly one of them. Then square the possibilities with every component variation while ensuring good accessibility and you’ve got, well, a heck of a job to do.

Direct Link to ArticlePermalink


The post Developer Decisions For Building Flexible Components appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Building a Form in PHP Using DOMDocument

Templating makes the web go round. The synthesis of data and structure into content. It’s our coolest superpower as developers — grab some data, then make it work for us, in whatever presentation we need. An array of objects can become a table, a list of cards, a chart, or whatever we think is most useful to the user. Whether the data is our own blog posts in Markdown files, or on-the-minute global exchange rates, the markup and resulting UX are up to us as front-end developers.

PHP is an amazing language for templating, providing many ways to merge data with markup. Let’s get into an example of using data to build out an HTML form in this post.

Want to get your hands dirty right away? Jump to the implementation.

In PHP, we can inline variables into string literals that use double quotes, so if we have a variable $ name = 'world', we can write echo "Hello, {$ name}", and it prints the expected Hello, world. For more complex templating, we can always concatenate strings, like: echo "Hello, " . $ name . ".".

For the old-schoolers, there’s printf("Hello, %s", $ name). For multiline strings, you can use Heredoc (the one that starts like <<<MYTEXT). And, last but certainly not least, we can sprinkle PHP variables inside HTML, like <p>Hello, <?= $ name ?></p>.

All of these options are great, but things can get messy when a lot of inline logic is required. If we need to build compound HTML strings, say a form or navigation, the complexity is potentially infinite, since HTML elements can nest inside each other.

What we’re trying to avoid

Before we go ahead and do the thing we want to do, it’s worth taking a minute to consider what we don’t want to do. Consider the following abridged passage from the scripture of WordPress Core, class-walker-nav-menu.php, verses 170-270:

<?php // class-walker-nav-menu.php // ... $  output .= $  indent . '<li' . $  id . $  class_names . '>'; // ... $  item_output  = $  args->before; $  item_output .= '<a' . $  attributes . '>'; $  item_output .= $  args->link_before . $  title . $  args->link_after; $  item_output .= '</a>'; $  item_output .= $  args->after; // ... $  output .= apply_filters( 'walker_nav_menu_start_el', $  item_output, $  item, $  depth, $  args ); // ... $  output .= "</li>{$  n}";

In order to build out a navigation <ul> in this function, we use a variable, $ output, which is a very long string to which we keep adding stuff. This type of code has a very specific and limited order of operations. If we wanted to add an attribute to the <a>, we must have access to $ attributes before this runs. And if we wanted to optionally nest a <span> or an <img> inside the <a>, we’d need to author a whole new block of code that would replace the middle of line 7 with about 4-10 new lines, depending on what exactly we want to add. Now imagine you need to optionally add the <span> , and then optionally add the <img>, either inside the <span> or after it. That alone is three if statements, making the code even less legible.

It’s very easy to end up with string spaghetti when concatenating like this, which is as fun to say as it is painful to maintain.

The essence of the problem is that when we try to reason about HTML elements, we’re not thinking about strings. It just so happens that strings are what the browser consumes and PHP outputs. But our mental model is more like the DOM — elements are arranged into a tree, and each node has many potential attributes, properties, and children.

Wouldn’t it be great if there were a structured, expressive way to build our tree?

Enter…

The DOMDocument class

PHP 5 added the DOM module to it’s roster of Not So Strictly Typed™ types. Its main entry point is the DOMDocument class, which is intentionally similar to the Web API’s JavaScript DOM. If you’ve ever used document.createElement or, for those of us of a certain age, jQuery’s $ ('<p>Hi there!</p>') syntax, this will probably feel quite familiar.

We start out by initializing a new DOMDocument:

$  dom = new DOMDocument();

Now we can add a DOMElement to it:

$  p = $  dom->createElement('p');

The string 'p' represents the type of element we want, so other valid strings would be 'div', 'img' , etc.

Once we have an element, we can set its attributes:

$  p->setAttribute('class', 'headline');

We can add children to it:

$  span = $  dom->createElement('span', 'This is a headline'); // The 2nd argument populates the element's textContent $  p->appendChild($  span);

And finally, get the complete HTML string in one go:

$  dom->appendChild($  p); $  htmlString = $  dom->saveHTML(); echo $  htmlString;

Notice how this style of coding keeps our code organized according to our mental model — a document has elements; elements can have any number of attributes; and elements nest inside one another without needing to know anything about each other. The whole “HTML is just a string” part comes in at the end, once our structure is in place.

The “document” here is a bit different from the actual DOM, in that it doesn’t need to represent an entire document, just a block of HTML. In fact, if you need to create two similar elements, you could save a HTML string using saveHTML(), modify the DOM “document” some more, and then save a new HTML string by calling saveHTML() again.

Getting data and setting the structure

Say we need to build a form on the server using data from a CRM provider and our own markup. The API response from the CRM looks like this:

{   "submit_button_label": "Submit now!",   "fields": [     {       "id": "first-name",       "type": "text",       "label": "First name",       "required": true,       "validation_message": "First name is required.",       "max_length": 30     },     {       "id": "category",       "type": "multiple_choice",       "label": "Choose all categories that apply",       "required": false,       "field_metadata": {         "multi_select": true,         "values": [           { "value": "travel", "label": "Travel" },           { "value": "marketing", "label": "Marketing" }         ]       }     }   ] }

This example doesn’t use the exact data structure of any specific CRM, but it’s rather representative.

And let’s suppose we want our markup to look like this:

<form>   <label class="field">     <input type="text" name="first-name" id="first-name" placeholder=" " required>     <span class="label">First name</span>     <em class="validation" hidden>First name is required.</em>   </label>   <label class="field checkbox-group">     <fieldset>       <div class="choice">         <input type="checkbox" value="travel" id="category-travel" name="category">         <label for="category-travel">Travel</label>       </div>       <div class="choice">         <input type="checkbox" value="marketing" id="category-marketing" name="category">         <label for="category-marketing">Marketing</label>       </div>     </fieldset>     <span class="label">Choose all categories that apply</span>   </label> </form>

What’s that placeholder=" "? It’s a small trick that allows us to track in CSS whether the field is empty, without needing JavaScript. As long as the input is empty, it matches input:placeholder-shown, but the user doesn’t see any visible placeholder text. Just the kind of thing you can do when we control the markup!

Now that we know what our desired result is, here’s the game plan:

  1. Get the field definitions and other content from the API
  2. Initialize a DOMDocument
  3. Iterate over the fields and build each one as required
  4. Get the HTML output

So let’s stub out our process and get some technicalities out of the way:

<?php function renderForm ($  endpoint) {   // Get the data from the API and convert it to a PHP object   $  formResult = file_get_contents($  endpoint);   $  formContent = json_decode($  formResult);   $  formFields = $  formContent->fields;    // Start building the DOM   $  dom = new DOMDocument();   $  form = $  dom->createElement('form');    // Iterate over the fields and build each one   foreach ($  formFields as $  field) {     // TODO: Do something with the field data   }    // Get the HTML output   $  dom->appendChild($  form);   $  htmlString = $  dom->saveHTML();   echo $  htmlString; }

So far, we’ve gotten the data and parsed it, initialized our DOMDocument and echoed its output. What do we want to do for each field? First off, let’s build the container element which, in our example, should be a <label>, and the labelling <span> which is common to all field types:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  label = null;    // Add a `<span>` for the label if it is set   if ($  field->label) {     $  label = $  dom->createElement('span', $  field->label);     $  label->setAttribute('class', 'label');   }    // Add the label to the `<label>`   if ($  label) $  element->appendChild($  label); }

Since we’re in a loop, and PHP doesn’t scope variables in loops, we reset the $ label element on each iteration. Then, if the field has a label, we build the element. At the end, we append it to the container element.

Notice that we set classes using the setAttribute method. Unlike the Web API, there unfortunately is no special handing of class lists. They’re just another attribute. If we had some really complex class logic, since It’s Just PHP™, we could create an array and then implode it:
$ label->setAttribute('class', implode($ labelClassList)).

Single inputs

Since we know that the API will only return specific field types, we can switch over the type and write specific code for each one:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     case 'text':     case 'email':     case 'telephone':       $  input = $  dom->createElement('input');       $  input->setAttribute('placeholder', ' ');       if ($  field->type === 'email') $  input->setAttribute('type', 'email');       if ($  field->type === 'telephone') $  input->setAttribute('type', 'tel');       break;   } }

Now let’s handle text areas, single checkboxes and hidden fields:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     //...       case 'text_area':       $  input = $  dom->createElement('textarea');       $  input->setAttribute('placeholder', ' ');       if ($  rows = $  field->field_metadata->rows) $  input->setAttribute('rows', $  rows);       break;      case 'checkbox':       $  element->setAttribute('class', 'field single-checkbox');       $  input = $  dom->createElement('input');       $  input->setAttribute('type', 'checkbox');       if ($  field->field_metadata->initially_checked === true) $  input->setAttribute('checked', 'checked');       break;      case 'hidden':       $  input = $  dom->createElement('input');       $  input->setAttribute('type', 'hidden');       $  input->setAttribute('value', $  field->field_metadata->value);       $  element->setAttribute('hidden', 'hidden');       $  element->setAttribute('style', 'display: none;');       $  label->textContent = '';       break;   } }

Notice something new we’re doing for the checkbox and hidden cases? We’re not just creating the <input> element; we’re making changes to the container <label> element! For a single checkbox field we want to modify the class of the container, so we can align the checkbox and label horizontally; a hidden <input>‘s container should also be completely hidden.

Now if we were merely concatenating strings, it would be impossible to change at this point. We would have to add a bunch of if statements regarding the type of element and its metadata in the top of the block. Or, maybe worse, we start the switch way earlier, then copy-paste a lot of common code between each branch.

And here is the real beauty of using a builder like DOMDocument — until we hit that saveHTML(), everything is still editable, and everything is still structured.

Nested looping elements

Let’s add the logic for <select> elements:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     //...       case 'select':       $  element->setAttribute('class', 'field select');       $  input = $  dom->createElement('select');       $  input->setAttribute('required', 'required');       if ($  field->field_metadata->multi_select === true)         $  input->setAttribute('multiple', 'multiple');            $  options = [];            // Track whether there's a pre-selected option       $  optionSelected = false;            foreach ($  field->field_metadata->values as $  value) {         $  option = $  dom->createElement('option', htmlspecialchars($  value->label));              // Bail if there's no value         if (!$  value->value) continue;              // Set pre-selected option         if ($  value->selected === true) {           $  option->setAttribute('selected', 'selected');           $  optionSelected = true;         }         $  option->setAttribute('value', $  value->value);         $  options[] = $  option;       }            // If there is no pre-selected option, build an empty placeholder option       if ($  optionSelected === false) {         $  emptyOption = $  dom->createElement('option');              // Set option to hidden, disabled, and selected         foreach (['hidden', 'disabled', 'selected'] as $  attribute)           $  emptyOption->setAttribute($  attribute, $  attribute);         $  input->appendChild($  emptyOption);       }            // Add options from array to `<select>`       foreach ($  options as $  option) {         $  input->appendChild($  option);       }   break;   } }

OK, so there’s a lot going on here, but the underlying logic is the same. After setting up the outer <select>, we make an array of <option>s to append inside it.

We’re also doing some <select>-specific trickery here: If there is no pre-selected option, we add an empty placeholder option that is already selected, but can’t be selected by the user. The goal is to place our <label class="label"> as a “placeholder” using CSS, but this technique can be useful for all kinds of designs. By appending it to the $ input before appending the other options, we make sure it is the first option in the markup.

Now let’s handle <fieldset>s of radio buttons and checkboxes:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     // ...       case 'multiple_choice':       $  choiceType = $  field->field_metadata->multi_select === true ? 'checkbox' : 'radio';       $  element->setAttribute('class', "field {$  choiceType}-group");       $  input = $  dom->createElement('fieldset');            // Build a choice `<input>` for each option in the fieldset       foreach ($  field->field_metadata->values as $  choiceValue) {         $  choiceField = $  dom->createElement('div');         $  choiceField->setAttribute('class', 'choice');              // Set a unique ID using the field ID + the choice ID         $  choiceID = "{$  field->id}-{$  choiceValue->value}";              // Build the `<input>` element         $  choice = $  dom->createElement('input');         $  choice->setAttribute('type', $  choiceType);         $  choice->setAttribute('value', $  choiceValue->value);         $  choice->setAttribute('id', $  choiceID);         $  choice->setAttribute('name', $  field->id);         $  choiceField->appendChild($  choice);              // Build the `<label>` element         $  choiceLabel = $  dom->createElement('label', $  choiceValue->label);         $  choiceLabel->setAttribute('for', $  choiceID);         $  choiceField->appendChild($  choiceLabel);              $  input->appendChild($  choiceField);       }   break;   } }

So, first we determine if the field set should be for checkboxes or radio button. Then we set the container class accordingly, and build the <fieldset>. After that, we iterate over the available choices and build a <div> for each one with an <input> and a <label>.

Notice we use regular PHP string interpolation to set the container class on line 21 and to create a unique ID for each choice on line 30.

Fragments

One last type we have to add is slightly more complex than it looks. Many forms include instruction fields, which aren’t inputs but just some HTML we need to print between other fields.

We’ll need to reach for another DOMDocument method, createDocumentFragment(). This allows us to add arbitrary HTML without using the DOM structuring:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     //...       case 'instruction':       $  element->setAttribute('class', 'field text');       $  fragment = $  dom->createDocumentFragment();       $  fragment->appendXML($  field->text);       $  input = $  dom->createElement('p');       $  input->appendChild($  fragment);       break;   } }

At this point you might be wondering how we found ourselves with an object called $ input, which actually represents a static <p> element. The goal is to use a common variable name for each iteration of the fields loop, so at the end we can always add it using $ element->appendChild($ input) regardless of the actual field type. So, yeah, naming things is hard.

Validation

The API we are consuming kindly provides an individual validation message for each required field. If there’s a submission error, we can show the errors inline together with the fields, rather than a generic “oops, your bad” message at the bottom.

Let’s add the validation text to each element:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;   $  validation = null;    // Add a `<span>` for the label if it is set   // ...    // Add a `<em>` for the validation message if it is set   if (isset($  field->validation_message)) {     $  validation = $  dom->createElement('em');     $  fragment = $  dom->createDocumentFragment();     $  fragment->appendXML($  field->validation_message);     $  validation->appendChild($  fragment);     $  validation->setAttribute('class', 'validation-message');     $  validation->setAttribute('hidden', 'hidden'); // Initially hidden, and will be unhidden with Javascript if there's an error on the field   }    // Build the input element   switch ($  field->type) {     // ...   } }

That’s all it takes! No need to fiddle with the field type logic — just conditionally build an element for each field.

Bringing it all together

So what happens after we build all the field elements? We need to add the $ input, $ label, and $ validation objects to the DOM tree we’re building. We can also use the opportunity to add common attributes, like required. Then we’ll add the submit button, which is separate from the fields in this API.

<?php function renderForm ($  endpoint) {   // Get the data from the API and convert it to a PHP object   // ...    // Start building the DOM   $  dom = new DOMDocument();   $  form = $  dom->createElement('form');    // Iterate over the fields and build each one   foreach ($  formFields as $  field) {     // Build the container `<label>`     $  element = $  dom->createElement('label');     $  element->setAttribute('class', 'field');        // Reset input values     $  input = null;     $  label = null;     $  validation = null;        // Add a `<span>` for the label if it is set     // ...        // Add a `<em>` for the validation message if it is set     // ...        // Build the input element     switch ($  field->type) {       // ...     }        // Add the input element     if ($  input) {       $  input->setAttribute('id', $  field->id);       if ($  field->required)         $  input->setAttribute('required', 'required');       if (isset($  field->max_length))         $  input->setAttribute('maxlength', $  field->max_length);       $  element->appendChild($  input);          if ($  label)         $  element->appendChild($  label);          if ($  validation)         $  element->appendChild($  validation);          $  form->appendChild($  element);     }   }      // Build the submit button   $  submitButtonLabel = $  formContent->submit_button_label;   $  submitButtonField = $  dom->createElement('div');   $  submitButtonField->setAttribute('class', 'field submit');   $  submitButton = $  dom->createElement('button', $  submitButtonLabel);   $  submitButtonField->appendChild($  submitButton);   $  form->appendChild($  submitButtonField);    // Get the HTML output   $  dom->appendChild($  form);   $  htmlString = $  dom->saveHTML();   echo $  htmlString; }

Why are we checking if $ input is truthy? Since we reset it to null at the top of the loop, and only build it if the type conforms to our expected switch cases, this ensures we don’t accidentally include unexpected elements our code can’t handle properly.

Hey presto, a custom HTML form!

Bonus points: rows and columns

As you may know, many form builders allow authors to set rows and columns for fields. For example, a row might contain both the first name and last name fields, each in a single 50% width column. So how would we go about implementing this, you ask? By exemplifying (once again) how loop-friendly DOMDocument is, of course!

Our API response includes the grid data like this:

{   "submit_button_label": "Submit now!",   "fields": [     {       "id": "first-name",       "type": "text",       "label": "First name",       "required": true,       "validation_message": "First name is required.",       "max_length": 30,       "row": 1,       "column": 1     },     {       "id": "category",       "type": "multiple_choice",       "label": "Choose all categories that apply",       "required": false,       "field_metadata": {         "multi_select": true,         "values": [           { "value": "travel", "label": "Travel" },           { "value": "marketing", "label": "Marketing" }         ]       },       "row": 2,       "column": 1     }   ] }

We’re assuming that adding a data-column attribute is enough for styling the width, but that each row needs to be it’s own element (i.e. no CSS grid).

Before we dive in, let’s think through what we need in order to add rows. The basic logic goes something like this:

  1. Track the latest row encountered.
  2. If the current row is larger, i.e. we’ve jumped to the next row, create a new row element and start adding to it instead of the previous one.

Now, how would we do this if we were concatenating strings? Probably by adding a string like '</div><div class="row">' whenever we reach a new row. This kind of “reversed HTML string” is always very confusing to me, so I can only imagine how my IDE feels. And the cherry on top is that thanks to the browser auto-closing open tags, a single typo will result in a gazillion nested <div>s. Just like fun, but the opposite.

So what’s the structured way to handle this? Thanks for asking. First let’s add row tracking before our loop and build an additional row container element. Then we’ll make sure to append each container $ element to its $ rowElement rather than directly to $ form.

<?php function renderForm ($  endpoint) {   // Get the data from the API and convert it to a PHP object   // ...    // Start building the DOM   $  dom = new DOMDocument();   $  form = $  dom->createElement('form');    // init tracking of rows   $  row = 0;   $  rowElement = $  dom->createElement('div');   $  rowElement->setAttribute('class', 'field-row');    // Iterate over the fields and build each one   foreach ($  formFields as $  field) {     // Build the container `<label>`     $  element = $  dom->createElement('label');     $  element->setAttribute('class', 'field');     $  element->setAttribute('data-row', $  field->row);     $  element->setAttribute('data-column', $  field->column);          // Add the input element to the row     if ($  input) {       // ...       $  rowElement->appendChild($  element);       $  form->appendChild($  rowElement);     }   }   // ... }

So far we’ve just added another <div> around the fields. Let’s build a new row element for each row inside the loop:

<?php // ... // Init tracking of rows $  row = 0; $  rowElement = $  dom->createElement('div'); $  rowElement->setAttribute('class', 'field-row');  // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // ...   // If we've reached a new row, create a new $  rowElement   if ($  field->row > $  row) {     $  row = $  field->row;     $  rowElement = $  dom->createElement('div');     $  rowElement->setAttribute('class', 'field-row');   }    // Build the input element   switch ($  field->type) {     // ...       // Add the input element to the row       if ($  input) {         // ...         $  rowElement->appendChild($  element);          // Automatically de-duped         $  form->appendChild($  rowElement);       }   } }

All we need to do is overwrite the $ rowElement object as a new DOM element, and PHP treats it as a new unique object. So, at the end of every loop, we just append whatever the current $ rowElement is — if it’s still the same one as in the previous iteration, then the form is updated; if it’s a new element, it is appended at the end.

Where do we go from here?

Forms are a great use case for object-oriented templating. And thinking about that snippet from WordPress Core, an argument might be made that nested menus are a good use case as well. Any task where the markup follows complex logic makes for a good candidate for this approach. DOMDocument can output any XML, so you could also use it to build an RSS feed from posts data.

Here’s the entire code snippet for our form. Feel free it adapt it to any form API you find yourself dealing with. Here’s the official documentation, which is good for getting a sense of the available API.

We didn’t even mention DOMDocument can parse existing HTML and XML. You can then look up elements using the XPath API, which is kinda similar to document.querySelector, or cheerio on Node.js. There’s a bit of a learning curve, but it’s a super powerful API for handling external content.

Fun(?) fact: Microsoft Office files that end with x (e.g. .xlsx) are XML files. Don’t tell the marketing department, but it’s possible to parse Word docs and output HTML on the server.

The most important thing is to remember that templating is a superpower. Being able to build the right markup for the right situation can be the key to a great UX.


The post Building a Form in PHP Using DOMDocument appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Building Your Own Subscription Newsletter

(This is a sponsored post.)

I did a sponsored video the other week explaining how to build a paid subscription newsletter using WordPress (we did it on WordPress.com but it could be hosted anywhere), MailPoet (a plugin to visually author the emails, as well as send them), and WooCommerce (to manage the payments and subscriptions).

I published the video here and there is a landing page for the whole concept here.

I spent a lot of time on it! I feel personally compelled by the idea because I’m pretty big on having a website you control be the home base for your business. If a paid newsletter is part of your business, awesome, might as well use your own website to do it.

If you’d like to be more slowly guided through the process, watch that video above. I literally do the entire thing from start to finish in that video. But I know some folks like a more rapid-fire explanation, so allow me to do that quick.


1. Have a WordPress site

Self-hosted works. You can also use WordPress.com so long as you’re on the Business or eCommerce plan, because you’ll need to install plugins.

2. Install WooCommerce & WooCommerce Subscriptions on it.

These are two separate plugins. WooCommerce is free. WooCommerce Subscriptions is $ 199/year.

3) Install MailPoet

MailPoet is a plugin. On WordPress.com, you might even be prompted to install it while installing the WooCommerce plugins. But if not, you can install it anytime.

4) Create a product that is a subscription

This is straightforward stuff in WooCommerce. Add a product and set the type to “Simple subscription.”

5. Craft an awesome newsletter in MailPoet

This is what MailPoet excels at. It’s a visual email builder right in your WordPress admin. It has excellent templates to start you started.

6. Send the newsletter only to people with an active subscription.

The trick is to make a “segment” of a list that is specifically created from WooCommerce customers that have an active subscription.


That’s it, really. You should know that MailPoet is an email sending service as well, and you’ll probably want to use that, since it’s free up to 1,000 subscribers anyway, is very easy to hook up, and will provide you with analytics data on the sends. But you can also wire it up to other email sending services as well.

Again, the beauty of all this is that it all happens from your website — meaning you could also be sending out a non-subscription newsletter with the same system. You could publish the newsletters on your website as an SEO play. You could sell other products and services since you have 100% of the infrastructure to do that now. You’re really in good hands here with a lot of power and flexibility.


The post Building Your Own Subscription Newsletter appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Building a Cool Front End Thing Generator

Whether you are just starting out on the front end, or you’ve been doing it for a long time, building a tool that can generate some cool front-end magic can help you learn something new, develop your skills and maybe even get you a little notoriety.

You might have run across some of these popular online generators:

I’ve had fun building a few of these myself over the years. Basically, any time you run across some cool front-end thing, there might be an opportunity to make an interactive generator for that thing.

In this case, we are going to make an Animated Background Gradient Generator.

Scaffolding the project in Next

A nice thing about these projects is that they’re all yours. Choose whatever stack you want and get going. I’m a big fan of Next.js, so for this project, I’m going to start as a basic Create Next App project.

npx create-next-app animated-gradient-background-generator 

This generates all the files we need to get started. We can edit pages/index.js to be the shell for our project.

import Head from "next/head" import Image from "next/image" export default function Home() {   return (     <>       <Head>         <title>Animated CSS Gradient Background Generator</title>         <meta name="description" content="A tool for creating animated background gradients in pure CSS." />         <link rel="icon" href="/favicon.ico" />       </Head>       <main>         <h1>           Animated CSS Gradient Background Generator         </h1>       </main>     </>   ) }

Animated gradients?

At the time I’m writing this article, if you do a search for animated CSS gradient background, the first result is this Pen by Manuel Pinto.

Let’s take a look at the CSS:

body {   background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);   background-size: 400% 400%;   animation: gradient 15s ease infinite; }  @keyframes gradient {   0% {     background-position: 0% 50%;   }   50% {     background-position: 100% 50%;   }   100% {     background-position: 0% 50%;   } } 

This is a great example that we can use as the foundation for the generated animation.

A React component to describe an animated gradient

We can break out a few possible configurable options for the generator:

  • An array of gradient colors
  • The angle of the gradient
  • The speed of the animation

To put in context, we want to provide these settings as data throughout our little app using a higher-order component, context/SettingsContext.js, along with some defaults.

import React, { useState, createContext } from "react"  const SettingsContext = createContext({ colorSelection: [] })  const SettingsProvider = ({ children }) => {   const [colorSelection, setColorSelection] = useState([     "deepskyblue",     "darkviolet",     "blue",   ])   const [angle, setAngle] = useState(300)   const [speed, setSpeed] = useState(5)      return (     <SettingsContext.Provider       value={{         colorSelection,         setColorSelection,         angle,         setAngle,         speed,         setSpeed,       }}     >       {children}     </SettingsContext.Provider>   ) }  export { SettingsContext, SettingsProvider }

For our generator’s components, we want to create:

  • a control components to adjust these settings,
  • a visual display component for generated animated gradient, and
  • a component for the CSS code output.

Let’s start with a Controls component that contains the various inputs we used to adjust the settings.

import Colors from "./Colors"  const Controls = (props) => (   <>     <Colors />   </> )  export default Controls

We can add our SettingsProvider and Controls components to pages/index.js:

import Head from "next/head" import Image from "next/image" import { SettingsProvider } from "../context/SettingsContext" import Controls from "../components/Controls" import Output from "../components/Output"  export default function Home() {   return (     <>       <Head>         ...       </Head>        <SettingsProvider>         <main style={{ textAlign: "center", padding: "64px" }}>           <h1>Animated CSS Gradient Background Generator</h1>           <Controls />           <Output />         </main>       </SettingsProvider>     </>   ) }

Our SettingsProvider begins with the three colors from our CodePen example as defaults. We can verify that we are getting the color settings via our SettingsContext in a new Colors component.

import React, { useContext } from "react" import { SettingsContext } from "../context/SettingsContext"  const Colors = () => {   const { colorSelection } = useContext(SettingsContext)   return (     <>       {colorSelection.map((color) => (         <div>{color}</div>       ))}     </>   ) }  export default Colors

Let’s use the Colors component to display individual color swatches with a small button to delete via our SettingsContext.

import React, { useContext } from "react" import { SettingsContext } from "../context/SettingsContext"  const Colors = () => {   const { colorSelection, setColorSelection } = useContext(SettingsContext)    const onDelete = (deleteColor) => {     setColorSelection(colorSelection.filter((color) => color !== deleteColor))   }    return (     <div>       {colorSelection.map((color) => (         <div           key={color}           style={{             background: color,             display: "inline-block",             padding: "32px",             margin: "16px",             position: "relative",             borderRadius: "4px",           }}         >           <button             onClick={() => onDelete(color)}             style={{               background: "crimson",               color: "white",               display: "inline-block",               borderRadius: "50%",               position: "absolute",               top: "-8px",               right: "-8px",               border: "none",               fontSize: "18px",               lineHeight: 1,               width: "24px",               height: "24px",               cursor: "pointer",               boxShadow: "0 0 1px #000",             }}           >             ×           </button>         </div>       ))}     </div>   ) }  export default Colors

You may notice that we have been using inline styles for CSS at this point. Who cares! We’re having fun here, so we can do whatever floats our boats.

Handling colors

Next, we create an AddColor component with a button that opens a color picker used to add more colors to the gradient.

For the color picker, we will install react-color and use the ChromePicker option.

npm install react-color

Once again, we will utilize SettingsContext to update the gradient color selection.

import React, { useState, useContext } from "react" import { ChromePicker } from "react-color" import { SettingsContext } from "../context/SettingsContext"  const AddColor = () => {   const [color, setColor] = useState("white")   const { colorSelection, setColorSelection } = useContext(SettingsContext)    return (     <>       <div style={{ display: "inline-block", paddingBottom: "32px" }}>         <ChromePicker           header="Pick Colors"           color={color}           onChange={(newColor) => {             setColor(newColor.hex)           }}         />       </div>       <div>         <button           onClick={() => {             setColorSelection([...colorSelection, color])           }}           style={{             background: "royalblue",             color: "white",             padding: "12px 16px",             borderRadius: "8px",             border: "none",             fontSize: "16px",             cursor: "pointer",             lineHeight: 1,           }}         >           + Add Color         </button>       </div>     </>   ) }  export default AddColor

Handling angle and speed

Now that our color controls are finished, let’s add some components with range inputs for setting the angle and animation speed.

Here’s the code for AngleRange, with SpeedRange being very similar.

import React, { useContext } from "react" import { SettingsContext } from "../context/SettingsContext"  const AngleRange = () => {   const { angle, setAngle } = useContext(SettingsContext)    return (     <div style={{ padding: "32px 0", fontSize: "18px" }}>       <label         style={{           display: "inline-block",           fontWeight: "bold",           width: "100px",           textAlign: "right",         }}         htmlFor="angle"       >         Angle       </label>       <input         type="range"         id="angle"         name="angle"         min="-180"         max="180"         value={angle}         onChange={(e) => {           setAngle(e.target.value)         }}         style={{           margin: "0 16px",           width: "180px",           position: "relative",           top: "2px",         }}       />       <span         style={{           fontSize: "14px",           padding: "0 8px",           position: "relative",           top: "-2px",           width: "120px",           display: "inline-block",         }}       >         {angle} degrees       </span>     </div>   ) }  export default AngleRange

Now for the fun part: rendering the animated background. Let’s apply this to the entire background of the page with an AnimatedBackground wrapper component.

import React, { useContext } from "react" import { SettingsContext } from "../context/SettingsContext" const AnimatedBackground = ({ children }) => {   const { colorSelection, speed, angle } = useContext(SettingsContext) const background =   "linear-gradient(" + angle + "deg, " + colorSelection.toString() + ")" const backgroundSize =   colorSelection.length * 60 + "%" + " " + colorSelection.length * 60 + "%" const animation =   "gradient-animation " +   colorSelection.length * Math.abs(speed - 11) +   "s ease infinite" return (   <div style={{ background, "background-size": backgroundSize, animation, color: "white" }}>     {children}   </div>   ) } export default AnimatedBackground

We’re calling the CSS animation for the gradient gradient-animation. We need to add that to styles/globals.css to trigger the animation:

@keyframes gradient-animation {   0% {     background-position: 0% 50%;   }   50% {     background-position: 100% 50%;   }   100% {     background-position: 0% 50%;   } } 

Making it useful to users

Next, let’s add some code output so people can copy and paste the generated CSS and use in their own projects.

import React, { useContext, useState } from "react" import { SettingsContext } from "../context/SettingsContext" const Output = () => {   const [copied, setCopied] = useState(false) const { colorSelection, speed, angle } = useContext(SettingsContext) const background =   "linear-gradient(" + angle + "deg," + colorSelection.toString() + ")" const backgroundSize =   colorSelection.length * 60 + "%" + " " + colorSelection.length * 60 + "%" const animation =   "gradient-animation " +   colorSelection.length * Math.abs(speed - 11) +   "s ease infinite" const code = `.gradient-background {   background: $  {background};   background-size: $  {backgroundSize};   animation: $  {animation}; } @keyframes gradient-animation {   0% {     background-position: 0% 50%;   }   50% {     background-position: 100% 50%;   }   100% {     background-position: 0% 50%;   } }` return (     <div       style={{ position: "relative", maxWidth: "640px", margin: "64px auto" }}     >       <pre         style={{           background: "#fff",           color: "#222",           padding: "32px",           width: "100%",           borderRadius: "4px",           textAlign: "left",           whiteSpace: "pre",           boxShadow: "0 2px 8px rgba(0,0,0,.33)",           overflowX: "scroll",         }}       >         <code>{code}</code>         <button           style={{             position: "absolute",             top: "8px",             right: "8px",             background: "royalblue",             color: "white",             padding: "8px 12px",             borderRadius: "8px",             border: "none",             fontSize: "16px",             cursor: "pointer",             lineHeight: 1,           }}           onClick={() => {             setCopied(true)             navigator.clipboard.writeText(code)           }}         >           {copied ? "copied" : "copy"}         </button>       </pre>     </div>   ) } export default Output

Making it fun

It is sometimes fun (and useful) to add a button that sets random values on a generator like this. That gives people a way to quickly experiment and see what kinds of results they can get out of the tool. It is also an opportunity to look up cool stuff like how to generate random hex colors.

import React, { useContext } from "react" import { SettingsContext } from "../context/SettingsContext"  const Random = () => {   const { setColorSelection, setAngle, setSpeed } = useContext(SettingsContext)    const goRandom = () => {     const numColors = 3 + Math.round(Math.random() * 3)     const colors = [...Array(numColors)].map(() => {       return "#" + Math.floor(Math.random() * 16777215).toString(16)     })     setColorSelection(colors)     setAngle(Math.floor(Math.random() * 361))     setSpeed(Math.floor(Math.random() * 10) + 1)   }    return (     <div style={{ padding: "48px 0 16px" }}>       <button         onClick={goRandom}         style={{           fontSize: "24px",           fontWeight: 200,           background: "rgba(255,255,255,.9)",           color: "blue",           padding: "24px 48px",           borderRadius: "8px",           cursor: "pointer",           boxShadow: "0 0 4px #000",           border: "none",         }}       >         RANDOM       </button>     </div>   ) }  export default Random

Wrapping up

There will be a few final things you’ll want to do to wrap up your project for initial release:

  • Update package.json with your project information.
  • Add some links to your personal site, the project’s repository and give credit where its due.
  • Update the README.md file that was generated with default content by Create Next App.

That’s it! We’re ready to release our new cool front end thing generator and reap the rewards of fame and fortune that await us!

You can see the code for this project on GitHub and the demo is hosted on Netlify.


The post Building a Cool Front End Thing Generator appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]