DEV Community

Cover image for I Asked ChatGPT to Code Web Components and This is What Happened
Stephen Belovarich
Stephen Belovarich

Posted on • Updated on

I Asked ChatGPT to Code Web Components and This is What Happened

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 recently, so I know first-hand how the code should be formatted.

Getting Started

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

Image description

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

<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>
Enter fullscreen mode Exit fullscreen mode

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

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

Image description

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

Enter fullscreen mode Exit fullscreen mode

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

Image description

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

<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);

Enter fullscreen mode Exit fullscreen mode

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

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);

Enter fullscreen mode Exit fullscreen mode

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

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);
Enter fullscreen mode Exit fullscreen mode

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

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

Image description

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

Image description

Image description

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.

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);
Enter fullscreen mode Exit fullscreen mode

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.

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' });
Enter fullscreen mode Exit fullscreen mode

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).

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.

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);
Enter fullscreen mode Exit fullscreen mode

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

Image description

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);
Enter fullscreen mode Exit fullscreen mode

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

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

Purchase a copy of Fullstack Web Components today and begin your journey into the wonderful world of front-end development with Web Components.

Top comments (4)

Collapse
 
dannyengelman profile image
Danny Engelman

ChatGPT is trained on a dataset.

And many developers don't explore past the dataset either, copy pasting blogs from a long long past, writing as stupid code as ChatGPT:

constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `...`
Enter fullscreen mode Exit fullscreen mode

instead of:

constructor() {
    super()
      .attachShadow({ mode: 'open' })
      .innerHTML = `...`
Enter fullscreen mode Exit fullscreen mode

Even MDN documentation is plain wrong where it says use super() first

Collapse
 
dannyengelman profile image
Danny Engelman • Edited

One year down the road. It is January 2024
We can now create our own GPTs trained with an instruction set.

I spent the whole day writing instructions what code GPT should/not create.

It still is far from perfect.
But this is getting close what I expect my students to write.

OPENAI GPT: Create or Refactor (your) Web Component

Collapse
 
polterguy profile image
Thomas Hansen • Edited

You have to sanity check the code it produces, and look at it as a "brain extension". StackOverflow doesn't give you perfect code either. It also has to be modified. It's the same problem, only ChatGPT being a much better assistant, because it can give you code according to your variables, class names, and URLs, etc ...

If you don't like the generic version, create your own ^_^

Collapse
 
aarone4 profile image
Aaron Reese • Edited

So I have also played with ChatGPT to generate some code: the first was to ask it what I needed to do to start a new React Native project using sqlite generate the necessary database tables and a RN component to let me create a cricket match with two teams of 11 players it took about 3 hours but we eventually got a working component without me actually writing any code and I don't know enough RN to create it myself.
The second was proof of concept for a client that ChatGPT could create an XML file which acts as a mapping rule between a SQL dataset and an HTML mailmerge template with nested loops. Again it took quite a while but we got a perfect match.
I would think of it like pair programming with a junior developer. It will get a broadly correct answer and with a bit of prompting the resulting code will iteratively improve. A bit like googling for technical issues, better prompts will result in better code but you do need to have a good understanding of what the underlying tech should look like.
That said, I would be pretty confident that with a 3 hour crash course from a YouTube video, ChatGPT and CoPilot, I could generate working code in pretty much any language and stack.