Snippets in JavaScript: Converting PascalCase to kebab-case // Integrating with ShadowDOM

mattkenefick

Matt Kenefick

Posted on August 4, 2021

Snippets in JavaScript: Converting PascalCase to kebab-case // Integrating with ShadowDOM

Have you ever wanted to convert a string written in either PascalCase or camelCase to a hyphenated kebab-case? Of course you have; we all have. I get asked how to do this maybe 42 times per day, so here's how you do it.

A Regular Expression

In JavaScript:

"MyParagraphElement".replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()

// my-paragraph-element
Enter fullscreen mode Exit fullscreen mode

How about in PHP?

<?php

$input = 'MyParagraphElement';
$output = strtolower(preg_replace('/([a-z0-9])([A-Z])/', '$1-$2', $input));

echo $output;
Enter fullscreen mode Exit fullscreen mode

What about C#?

using System;
using System.Text.RegularExpressions;

public class Program
{
    public static void Main()
    {
        string input = "MyParagraphElement";
        string output = Regex.Replace(input, @"([a-z0-9])([A-Z])", "$1-$2").ToLower();

        Console.WriteLine(output);
    }
}
Enter fullscreen mode Exit fullscreen mode

How does it work?

The regular expression looks for a lowercase alphanumeric character [a-z0-9] followed by an uppercase alpha character [A-Z], both in a capture group (xyz). We use $1 and $2 because we want to retain the characters, but put something in between them.

If you were to replace it with $1.$2, you'd end up with a result like: my.paragraph.element

When would you use this?

This could be used for automatically inferring class names like converting MyParagraph to my-paragraph which is handy for autoloading ShadowDOM elements (or similar in PHP).

Example: https://jsfiddle.net/tbkhczd7/1/

Result

Let's look at two files: index.html and a main.js.

In the HTML below, you can see that we have two custom tags being utilized named my-paragraph and labeled-input.

They are defined using HTML's template tag. Read more on MDN to better understand their capabilities and how to use if you're unfamiliar.

These templates are one half of what defines our ShadowDOM elements. They provide the structure, allow customization, and utilize scoped <style> tags for visual representation.

<main>
    <my-paragraph>
        Lorem ispum sit amet dolor
    </my-paragraph>

    <hr />

    <labeled-input>
        This is the form label
    </labeled-input>
</main>

<!-- Template for the MyParagraphElement class -->
<template id="my-paragraph">
    <style>
        section {
            background-color: #fde7fc;
            padding: 5px;
        }
    </style>

    <section>
        <h3>Example Header</h3>
        <div>   
            <slot>Ambulance is on its way</slot>
        </div>
        <button>
            Click Me
        </button>
    </section>
</template>

<!-- Template for the LabeledInputElement class -->
<template id="labeled-input">
    <label>
        <div><slot></slot></div>
        <input type="text" />
    </label>
</template>
Enter fullscreen mode Exit fullscreen mode

The other half required is JavaScript to define and initialize the elements. There's a fair amount of code here, but the gist is:

  • Extend HTMLElement to abstract common functionality
  • Derive specific classes from the aforementioned
  • Associate our classes to our templates

Note that you could extend any element you want, not just HTMLElement; if you wanted to beef up a button, you could do something like this:

class MyButton extends HTMLButtonElement { ... }
Enter fullscreen mode Exit fullscreen mode

Below, you'll see in the static attach(...) method, we use our PascalCase converter mentioned earlier in this article.

Read through the code and we'll catch up down below.


/**
 * Base class for our shadow elements
 */
class CustomHtmlElement extends HTMLElement 
{   
    /**
     * Optional template element. If one is not supplied, we
     * will try to infer one based on the classname.
     *
     * @param HTMLElement template
     * @return void
     */
    static attach(template) {
        if (!template) {
            // Convert MyParagraphElement to my-paragraph
            const tagName = this.name
                .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
                .toLowerCase()
                .replace(/-?element/, '');

            template = document.querySelector(`#${tagName}`);
        }

        // Save template reference
        this.template = template;

        // Create shadow object
        customElements.define(this.template.id, this);
    }

    /**
     * @return void
     */
    constructor() {
        super();

        // Clone element from our template
        this.templateNode = this.constructor.template.content.cloneNode(true);

        // Make shadow
        this.attachShadow({ mode: 'open' }).appendChild(this.templateNode);

        // Attach events call
        this.attachEvents();
    }

    /**
     * @return void
     */
    attachEvents() {
        // Not implemented
    }

    /**
     * @return void
     */
    detachEvents() {
        // Not implemented
    }
}

/**
 * Custom element class extends our root class
 *
 * @extends CustomHtmlElement
 */
class MyParagraphElement extends CustomHtmlElement {
    /**
     * Attach events to the DOM
     *
     * @return void
     */
    attachEvents() {
        this.shadowRoot
            .querySelector('button')
            .addEventListener('click', this.Handle_OnClickButton);
    }

    /**
     * Respond to click events
     * 
     * @param MouseEvent e
     * @return void
     */
    Handle_OnClickButton(e) {
        alert('This button has been clicked');
    }
}

/**
 * Basic labeled input
 *
 * @extends CustomHtmlElement
 */
class LabeledInputElement extends CustomHtmlElement {
    // Not implemented
}


// -------------------------------------------------------------------------

// ⬇︎ We could explicitly pass in an element
// const element = document.querySelector('#my-paragraph');
// MyParagraphElement.attach(element);

// ⬇︎ Or we could derive it from the class name automatically
// MyParagraphElement.attach();

// ⬇︎ Or we can try to infer it inversely based on our templates
Array.from(document.getElementsByTagName('template')).forEach(element => {
    // Convert "my-paragraph" to "MyParagraphElement"
    const className = element.id
        .replace(/^([a-z])/, m => m.toUpperCase())
        .replace(/-([a-z])/g, m => m.toUpperCase())
        .replace('-', '')
        + 'Element';

    const reference = eval(className);

    reference.attach();
});
Enter fullscreen mode Exit fullscreen mode

The functionality provided within LabeledInputElement and MyParagraphElement are just demonstrative to illustrate how they have the ability to scope events/logic.

In our static attach(template) { ... } method, you can see there's a null check against template at which point it attempts to convert our class name into what the expected HTML tag would be. There's additional logic you could add here to ensure the element exists, but for the sake of example we're assuming that our coupled templates should exist.

At the bottom, the uncommented example iterates through all available <template> tags and reverses kebab-case to PascalCase in an attempt to find the defined class. Again, you should add logic here to ensure what you're looking for actually exists, but this is a demo.

By using our two string conversions, we're able to easily create and autoload custom ShadowDOM elements just by using basic definitions; two steps:

  • Create a <template> with a unique kebab-case identifier
  • Create a class with a similar PascalCase identifier

Now you can cleanly create classes + templates and autoload them without the hassle of maintaining coupled definitions.

💖 💪 🙅 🚩
mattkenefick
Matt Kenefick

Posted on August 4, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related