I Asked ChatGPT to Code Web Components and This is What Happened
Tags: webcomponents, javascript, chatgpt, webdev
Published: 2023-01-18 08:00:00


The past couple months and I've heard of web engineers tinkering with ChatGPT while another engineer told me they relied on the output for the basis of every coding decision at work. I found myself in the first camp, poking at ChatGPT to generate anything from code examples to blog outlines, ideas for novels, or generative artwork. I was genuinely curious. A.I. will write the code for me? You mean I don't have to think? Sign me up.

I was primarily interested if ChatGPT could be used to generate anything close to production-ready code. I had talked with an engineer using ChatGPT as a basis for coding anything at their job. I wanted to understand what that experience was like. Granted, I'm a ChatGPT novice, so this article won't get overly technical about using ChatGPT by any means. I highly doubt I unlocked the tool's full potential. 

I'm a front-end web engineer that codes Web Components and have coded applications with several JavaScript frameworks. I asked ChatGPT to generate some Web Components, because that's what I know extremely well. I wanted to see how precise ChatGPT could produce a simple view comprised of Web Components. I wrote [a book about Web Components](https://www.newline.co/courses/fullstack-web-components) recently, so I know first-hand how the code should be formatted. 

## Getting Started

I logged into [https://chat.openai.com](https://chat.openai.com) and prompted ChatGPT with a simple request to generate an application login form with Web Components.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g39437ydct7uxza4ernt.png)

ChatGPT responded with the following code snippet that used the popular Web Components library Lit. I didn't specifically ask for ChatGPT to use a JavaScript library, but here we are. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l3z6az11klh8cx1tdcg7.png)

```html
<template>
  <form>
    <label for="username">Username:</label>
    <input type="text" id="username" @input="${this._onUsernameInput}">
    <br>
    <label for="password">Password:</label>
    <input type="password" id="password" @input="${this._onPasswordInput}">
    <br>
    <button @click="${this._onLoginButtonClick}">Login</button>
  </form>
</template>

<script>
  import { LitElement, html } from 'lit-element';

  class LoginForm extends LitElement {
    static get properties() {
      return {
        username: { type: String },
        password: { type: String }
      };
    }

    constructor() {
      super();
      this.username = '';
      this.password = '';
    }

    _onUsernameInput(event) {
      this.username = event.target.value;
    }

    _onPasswordInput(event) {
      this.password = event.target.value;
    }

    _onLoginButtonClick() {
      // Send a login request with the username and password
      console.log(`Attempting to login with username: ${this.username} and password: ${this.password}`);
    }

    render() {
      return html`
        <form>
          <label for="username">Username:</label>
          <input type="text" id="username" @input="${this._onUsernameInput}">
          <br>
          <label for="password">Password:</label>
          <input type="password" id="password" @input="${this._onPasswordInput}">
          <br>
          <button @click="${this._onLoginButtonClick}">Login</button>
        </form>
      `;
    }
  }

  customElements.define('login-form', LoginForm);
</script>
```

The duplicate templates surprised me and seem like an error. One declared as a HTML template, the other declared in the `render` method. The HTML template seems superfluous. 

ChatGPT followed the code snippet with an explanation and example of how to implement the `LoginForm` custom element defined in the `<script>`. Most of that explanation describing how LitElement behaves is omitted here for sake of brevity. The last bit of the explanation was interesting. A callout the form needed further measures to address security, to keep the transmission of the password secure.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pnu4y8in7dx829s96ayz.png)

ChatGPT generated a boilerplate example with Lit. It's interesting that was the first choice, given I hadn't prompted ChatGPT to generate any code with a library, just Web Components which are a set of browser specifications. 

I wasn't satisfied with the example using LitElement. Nothing against Lit. Lit is actually the first library I would adopt for Web Components that need to be developed at scale. I merely don't want any dependencies in a login view, which should be highly performant. Zero dependencies is a benefit of coding Web Components, especially for a view application like a login form which could be code split from the main bundle.

I prompted ChatGPT to produce the same example but without a JavaScript library.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x8flt9pvn6twkzc5c2yr.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5uxgav0ov5d4kmbtgch5.png)

```html
<template id="login-form-template">
  <form>
    <label for="username">Username:</label>
    <input type="text" id="username">
    <br>
    <label for="password">Password:</label>
    <input type="password" id="password">
    <br>
    <button id="login-button">Login</button>
  </form>
</template>

<script>
  class LoginForm extends HTMLElement {
    constructor() {
      super();

      // Attach the template to the element
      const template = document.getElementById('login-form-template');
      const templateContent = template.content;
      this.appendChild(templateContent.cloneNode(true));

      // Get references to the form elements
      this._usernameInput = this.querySelector('#username');
      this._passwordInput = this.querySelector('#password');
      this._loginButton = this.querySelector('#login-button');

      // Add event listeners
      this._usernameInput.addEventListener('input', this._onUsernameInput.bind(this));
      this._passwordInput.addEventListener('input', this._onPasswordInput.bind(this));
      this._loginButton.addEventListener('click', this._onLoginButtonClick.bind(this));
    }

    _onUsernameInput(event) {
      this._username = event.target.value;
    }

    _onPasswordInput(event) {
      this._password = event.target.value;
    }

    _onLoginButtonClick() {
      // Send a login request with the username and password
      console.log(`Attempting to login with username: ${this._username} and password: ${this._password}`);
    }
  }

  customElements.define('login-form', LoginForm);
</script>

```

The usage of HTML templates in this example make a lot more sense. `LoginForm` references each HTML template to append the username and password inputs and the button to display a login form. Event listeners are attached to methods on the class that store the username and password as private properties, denoted by the underscore. A call to `customElements.define` registers the component class with the browser.
 
ChatGPT followed the code example with a similar explanation (omitted here).

The above example is alright, although I wanted to follow best practices of making the input and button separate components from the login form. This promotes reusability. The input and button could be exported from a UI library. I asked ChatGPT to generate the same code but this time allowing for a separation of concerns.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4laz41jx786va0sn3kog.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i3wjpmcno4ybgjveb2q4.png)

ChatGPT produced three separate HTML templates and classes for the form, input, and button.

```html
<template id="login-form-template">
  <form>
    <login-input label="Username"></login-input>
    <login-input label="Password" type="password"></login-input>
    <login-button>Login</login-button>
  </form>
</template>

<template id="login-input-template">
  <label for="input"></label>
  <input id="input">
</template>

<template id="login-button-template">
  <button></button>
</template>

<script>
  class LoginForm extends HTMLElement {
    constructor() {
      super();

      // Attach the template to the element
      const template = document.getElementById('login-form-template');
      const templateContent = template.content;
      this.appendChild(templateContent.cloneNode(true));

      // Get references to the form elements
      this._usernameInput = this.querySelector('login-input[label="Username"]');
      this._passwordInput = this.querySelector('login-input[label="Password"]');
      this._loginButton = this.querySelector('login-button');

      // Add event listeners
      this._usernameInput.addEventListener('input', this._onUsernameInput.bind(this));
      this._passwordInput.addEventListener('input', this._onPasswordInput.bind(this));
      this._loginButton.addEventListener('click', this._onLoginButtonClick.bind(this));
    }

    _onUsernameInput(event) {
      this._username = event.target.value;
    }

    _onPasswordInput(event) {
      this._password = event.target.value;
    }

    _onLoginButtonClick() {
      // Send a login request with the username and password
      console.log(`Attempting to login with username: ${this._username} and password: ${this._password}`);
    }
  }

  customElements.define('login-form', LoginForm);

  class LoginInput extends HTMLElement {
    constructor() {
      super();

      // Attach the template to the element
      const template = document.getElementById('login-input-template');
      const templateContent = template.content;
      this.appendChild(templateContent.cloneNode(true));

      // Get references to the input element
      this._input = this.querySelector('#input');
      this._label = this.querySelector('label');

      // Set attributes
      this._input.name = this.getAttribute('label');
      this._input.type = this.getAttribute('type') || 'text';
      this._label.innerText = this.getAttribute('label');
    }
  }
  customElements.define('login-input', LoginInput);

  class LoginButton extends HTMLElement {
    constructor() {
      super();

      // Attach the template to the element
      const template = document.getElementById('login-button-template');
      const templateContent = template.content;
      this.appendChild(templateContent.cloneNode(true));

      this.innerText = this.textContent;
    }
  }
  customElements.define('login-button', LoginButton);

```

Interesting callouts from the above code. 

1. `LoginInput` sets the label using attributes, but doesn't utilize `observedAttributes` or `attributeChangedCallback`, two methods available to every custom element that enable engineers to effectively respond to changes in attribute values. Using those callbacks are the only reliable means to track changes in attribute values.
2. `LoginButton` sets in the label of the button using a strange line: `this.innerText = this.textContent;` I would have preferred a method that set the content with a `<slot>` via the HTML template or an attribute. 

Some engineers may be put off by the usage of HTML templates here, but I appreciate how ChatGPT used specifications in this case to provide a stable template for each component. Instead you would declare the templates in the constructor of each component `class`, a lot like many examples out there.

## Adding TypeScript

Next, I asked ChatGPT to produce the same code, but this time with TypeScript. This would give me an idea of how to make the components type safe. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o8kgv59xcke4dlv0tvhi.png)

```javascript
class LoginForm extends HTMLElement {
  private _usernameInput: HTMLInputElement;
  private _passwordInput: HTMLInputElement;
  private _loginButton: HTMLButtonElement;
  private _username: string;
  private _password: string;

  constructor() {
    super();

    // Attach the template to the element
    const template = document.getElementById('login-form-template') as HTMLTemplateElement;
    const templateContent = template.content;
    this.appendChild(templateContent.cloneNode(true));

    // Get references to the form elements
    this._usernameInput = this.querySelector('#username') as HTMLInputElement;
    this._passwordInput = this.querySelector('#password') as HTMLInputElement;
    this._loginButton = this.querySelector('#login-button') as HTMLButtonElement;

    // Add event listeners
    this._usernameInput.addEventListener('input', this._onUsernameInput.bind(this));
    this._passwordInput.addEventListener('input', this._onPasswordInput.bind(this));
    this._loginButton.addEventListener('click', this._onLoginButtonClick.bind(this));
  }

  private _onUsernameInput(event: Event) {
    this._username = (event.target as HTMLInputElement).value;
  }

  private _onPasswordInput(event: Event) {
    this._password = (event.target as HTMLInputElement).value;
  }

  private _onLoginButtonClick() {
    // Send a login request with the username and password
    console.log(`Attempting to login with username: ${this._username} and password: ${this._password}`);
  }
}

customElements.define('login-form', LoginForm);

```

ChatGPT only output the `LoginForm` class and failed to generate the other components in Typescript, so I asked again. There were a few other instances where I had to prompt ChatGPT a second, or even third time, to get an acceptable answer. ChatGPT struggled with large code examples. Finally ChatGPT delivered a full TypeScript implementation.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h4869lj7z6c6rxr7h6fr.png)


```typescript
class LoginInput extends HTMLElement {
  private _input: HTMLInputElement;
  private _label: HTMLLabelElement;

  constructor() {
    super();

    // Attach the template to the element
    const template = document.getElementById('login-input-template') as HTMLTemplateElement;
    const templateContent = template.content;
    this.appendChild(templateContent.cloneNode(true));

    // Get references to the input element
    this._input = this.querySelector('#input') as HTMLInputElement;
    this._label = this.querySelector('label') as HTMLLabelElement;

    // Set attributes
    this._input.name = this.getAttribute('label');
    this._input.type = this.getAttribute('type') || 'text';
    this._label.innerText = this.getAttribute('label');
  }
}
customElements.define('login-input', LoginInput);

class LoginButton extends HTMLElement {
  constructor() {
    super();

    // Attach the template to the element
    const template = document.getElementById('login-button-template') as HTMLTemplateElement;
    const templateContent = template.content;
    this.appendChild(templateContent.cloneNode(true));

    this.innerText = this.textContent;
  }
}
customElements.define('login-button', LoginButton);

class LoginForm extends HTMLElement {
  private _usernameInput: HTMLInputElement;
  private _passwordInput: HTMLInputElement;
  private _loginButton: HTMLButtonElement;
  private _username: string;
  private _password: string;

  constructor() {
    super();

    // Attach the template to the element
    const template = document.getElementById('login-form-template') as HTMLTemplateElement;
    const templateContent = template.content;
    this.appendChild(templateContent.cloneNode(true));

    // Get references to the form elements
    this._usernameInput = this.querySelector('login-input[label="Username"]') as HTMLInputElement;
    this._passwordInput = this.querySelector('login-input[label="Password"]') as HTMLInputElement;
    this._loginButton = this.querySelector('login-button') as HTMLButtonElement;

    // Add event listeners
    this._usernameInput.addEventListener('input', this._onUsernameInput.bind(this));
    this._passwordInput.addEventListener('input', this._onPasswordInput.bind(this));
    this._loginButton.addEventListener('click', this._onLoginButtonClick.bind(this));
  }

  private _onUsernameInput(event: Event) {
    this._username = (event.target as HTMLInputElement).value;
  }

  private _onPasswordInput(event: Event) {
    this._password = (event.target as HTMLInputElement).value;
  }

  private _onLoginButtonClick() {
    // Send a login request with the username and password
    console.log(`Attempting to login with username: ${this._username} and password: ${this._password}`);
  }
}

customElements.define('login-form', LoginForm);
```

The TypeScript output was spot on. I was particularly glad to see strict typing of the `event.target` in a style that was familiar. Pertinent properties on each class are labeled `private` and typed appropriately.

## Styling The Components

I wanted to add styling to the input and button so I prompted ChatGPT to add CSS to the components, although it kept setting styling through `this.style` and not by injecting CSS through the `<style>` tag which I am accustomed. This is due to the assumption the code examples relied on the previous generated HTML templates. The styling could be provided in the HTML templates, although ChatGPT never chose to place the styles there. I generally prefer declaring CSS in the `constructor` of each `class`. 

I started a new prompt, this time leading with the styling and ChatGPT generated a completely different solution that aligned to my expectations. To achieve this I was more descriptive.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/767t9sojkc668qnnfmvq.png)

ChatGPT chose to default to `LitElement` again for this request despite the instruction to "not require a JavaScript library". 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z0lgex5zfbh1km6fptcc.png)

I asked again, this time calling out `LitElement` and ChatGPT responded accordingly even though I had a slight typo.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oo849heu97wk4gz4spu7.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/emf40s3txwjxbvfh75s8.png)

ChatGPT output three separate code samples, one for each component this time around. This made the output way more reliable than when ChatGPT generated one long code snippet with everything.

```javascript
export class MyInput extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                :host {
                    display: block;
                    background-color: #333;
                    color: #fff;
                }
            </style>
            <input type="text" />
        `;
    }
}
customElements.define('my-input', MyInput);
```

Each component declares the specified styles in the constructor, setting the `innerHTML` of each `shadowRoot` to include a `<style>` tag. I greatly prefer this format over setting `this.style`. The following `MyButton class` even sets the `cursor: pointer` to enhance accessibility. 

```javascript
export class MyButton extends HTMLButtonElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                :host {
                    background-color: #868e96;
                    color: #000;
                    padding: 0.5em 1em;
                    border: none;
                    border-radius: 0.25em;
                    cursor: pointer;
                }
            </style>
            <slot></slot>
        `;
    }
}
customElements.define('my-button', MyButton, { extends: 'button' });
```

ChatGPT chose to extend `HTMLButtonElement`, making the above component a customized built-in element. This allows any instance of `MyButton` to take on the characteristics of `HTMLButtonElement`, meaning you don't have to declare event listeners for accessibility because they are baked in. The styling declared in the host that selects :host won’t work because ShadowDOM isn’t available to customized built-ins that extend from `HTMLButtonElement`. ChatGPT failed to provide an example of how to use `MyButton` in HTML, which differs from any custom element extended from `HTMLElement` because of the `is` attribute. ChatGPT didn't also call out this code would be inoperable in Safari due to WebKit's stance that blocks customized built-ins from working in Safari ([which is currently being reconsidered for inclusion, although WebKit has not signaled support](https://github.com/WebKit/standards-positions/issues/97)).

Finally, ChatGPT declared a custom element for the form itself, setting the `shadowRoot` with boilerplate styling for the labels, although omitting the `is` attribute from the instance of `<button>`, which is required of customized built-in elements.

```javascript
export class MyFormLayout extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                :host {
                    display: block;
                }
                label {
                    display: block;
                    font-size: 0.8em;
                    margin-bottom: 0.5em;
                }
            </style>
            <form>
                <label>Username</label>
                <my-input></my-input>
                <label>Password</label>
                <my-input type="password"></my-input>
                <button>
                    <slot name="submit"></slot>
                </button>
            </form>
        `;
    }
}
customElements.define('my-form-layout', MyFormLayout);
```

The above examples were derived from a request to ChatGPT to generate a separation of concerns between the components and style them. One problem is found on the last line of the `MyFormLayout` example. Element tag names can only include one dash according to spec, so `my-form-layout` should be `my-formlayout` to be parsable by the browser. Otherwise, the `HTMLButtonElement` doesn't employ the `is` attribute which is necessary due to `MyButton` being declared as a customized built-in element.  

The logic for authentication is missing from the prior examples. I ask ChatGPT to add the functionality to `MyFormLayout` and was surprised the output was different than before.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q65ljfv2py1as6ctbnse.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oibuludvna3qxtvpakag.png)

```javascript
export class MyFormLayout extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                :host {
                    display: block;
                }
                label {
                    display: block;
                    font-size: 0.8em;
                    margin-bottom: 0.5em;
                }
            </style>
            <form>
                <label>Username</label>
                <my-input id="username"></my-input>
                <label>Password</label>
                <my-input type="password" id="password"></my-input>
                <button id="submit-button">
                    <slot name="submit"></slot>
                </button>
            </form>
        `;
    }
    connectedCallback() {
        this.shadowRoot.getElementById("submit-button").addEventListener("click", (e) => this.handleSubmit(e));
    }
    handleSubmit(e: Event) {
        e.preventDefault();
        let username = this.shadowRoot.getElementById("username") as HTMLInputElement;
        let password = this.shadowRoot.getElementById("password") as HTMLInputElement;
        
        // logic to check the entered credentials against a user database or API
        // if the credentials are valid, redirect the user to the home page
        // else display an error message
        if (username.value === "user" && password.value === "password") {
            window.location.href = "home.html";
        } else {
            alert("Invalid username or password");
        }
    }
}
customElements.define('my-form-layout', MyFormLayout);
```

ChatGPT added a single event listener for the submit button, then read the value of the username and password from each input, and implemented some boilerplate validation that checks the value of username and password fields, along with inline documentation to explain the changes. Additional documentation followed the code snippet. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g7vcu28pg89gijp4vytb.png)

It's nice there is a warning the example "is not recommended for production" considering the boilerplate code doesn't secure the password with any sort of hashing algorithm. The description of the code is suitable to gain a basic understanding of how everything works.

The pseudo code validating the username and password doesn't make much sense. There would most likely be much more robust validation logic here. You could coax ChatGPT into producing an algorithm close to your intended business logic, but it's unlikely to get it just right. 

## Conclusion

When I heard an engineer was using ChatGPT to generate code everyday for his job, I was interested to see what that was like. Tools like this are no substitute for the knowledge gained by coding something from scratch. While it's entirely possible to generate boilerplate code with ChatGPT, I can't recommend it. Like other responses for "factual" content, ChatGPT will often answer with either completely wrong information or base part of an answer on incorrect assumptions. The same is true of code snippets. As we observed, ChatGPT omitted valuable information about the generated code and generated incorrectly formatted code. Overall, ChatGPT did an alright job at generating Web Components. With some alterations the generated code could be used as the basis for coding a login view. For code generation ChatGPT possibly doesn't replace tools like plop or Nx, which give you consistent control over boilerplate.

There's possibly more advanced queries I could have made to generate the login view code. I'm just a novice. 

It's fairly interesting that ChatGPT defaulted to generating Web Components with LitElement. This is a testament to how synonymous the library is becoming with Web Components.

In another query (not featured here), I prompted ChatGPT to make the input and button form-associated custom elements which the service promptly ignored. In a separate query from that, ChatGPT struggled to generate a single example that implemented `formAssociated` or `ElementInternals`. 

ChatGPT could get better over time, but for now I feel quite secure in my job as a front-end web developer. If I were completely new to Web Components, the incorrect and omitted code would be awfully confusing. Due to the inaccuracy of the code samples, In the future, maybe our robot overloads will take our jobs, but I don't think we will be replaced by A.I. anytime soon.

OK, now what I really think. We are doomed, at least in the immediate future. Bloggers will use the ChatGPT to generate posts. The rest of us won't get the content we so desperately deserve. We'll be forced to sift through cursory explanations of code that are incorrect or malformed. I suppose that was already happening before ChatGPT, but I'm predicting it's only going to be worse. Luckily there's entire books devoted to the topic of Web Components.


## Fullstack Web Components Book


Want to learn how to code UI Libraries and Web Applications with Web Components?

[Fullstack Web Components](https://www.newline.co/courses/fullstack-web-components) is the complete guide to building UI Libraries with Web Components. Through ten chapters of hands-on exercises, you'll learn every aspect to the set of specifications in the context of developing a UI Library and Web Application. 

![Front cover of Fullstack Web Components Book](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/svpbg90fy2sq7lfjatmx.png)


[Purchase a copy of Fullstack Web Components](https://www.newline.co/courses/fullstack-web-components) today and begin your journey into the wonderful world of front-end development with Web Components.