Snippets in JavaScript: Converting PascalCase to kebab-case // Integrating with ShadowDOM
Tags: beginners, javascript, regex
Published: 2021-08-04 14:05:20

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:

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

// my-paragraph-element
```

How about in PHP?

```php
<?php

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

echo $output;
```

What about C#?

```csharp
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);
	}
}
```

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ekzkmlbmmn6gb5sklzus.png)
 
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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template) tag. *Read more on MDN to better understand their capabilities and how to use <slot> 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.

```html
<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>
```

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:

```javascript
class MyButton extends HTMLButtonElement { ... }
```

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.

```javascript

/**
 * 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();
});
```

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.