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.
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.
<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.
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.
<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.
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);
Interesting callouts from the above code.
-
LoginInput
sets the label using attributes, but doesn't utilizeobservedAttributes
orattributeChangedCallback
, 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. -
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.
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.
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.
ChatGPT chose to default to LitElement
again for this request despite the instruction to "not require a JavaScript library".
I asked again, this time calling out LitElement
and ChatGPT responded accordingly even though I had a slight typo.
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);
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' });
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);
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.
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.
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.
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)
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:
instead of:
Even MDN documentation is plain wrong where it says use super() first
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
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 ^_^
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.