Developing User Interfaces with GitHub Copilot, Part 1

by John Miller | First published: February 24, 2025 - Last Updated: February 28, 2025

In the next few posts I will explore techniques for creating and maintaining user interfaces leveraging GitHub Copilot. Using real-world examples, I'll show how AI handles making UI modifications. Including:

  • Adding new controls to an existing page
  • Creating components that can be shared with multiple pages
  • Creating data visualizations
  • Adding client-side sorting to data tables.

In this post I will take on adding a new control to an existing page. While my demo application will show a Vue.js Typescript implementation, you can apply the techniques I'll demonstrate to many UI frameworks.

The app I'll modify in this series is a lead management app written in Vue.js. In this post, I'll show you the changes Copilot suggests that we make to the Active Leads by Owner vue to show the number of active leads for individual owners. The vue currently reports the total for all owners and it needs to report the total for individual owners. Our first modification is to add a new control to a page and wire the control to parameterize the request for data from the back end. Before we begin to make modifications let's review the existing app.

What is Vue.js and Why

The app is implemented using the Vue.js UI framework. Vue.js written in typescript as is the front-end that leverages the framework. The fron-end technology is not particularlly relevant and the ideas presented here should be applicable to most UI frameworks and implementation. This app happens to be written in Vue.js, which has some advantages that may simplify the implementatio, such as:

Feature Description
Reactive Data Binding: A reactive data binding system that automatically updates the DOM when the underlying data changes, making it easier to manage and synchronize the UI with the application state.
Component-Based Architecture: A component-based architecture, allowing developers to build reusable, encapsulated components. This modular approach improves code maintainability and reusability.
Single-File Components: Single-file components, which encapsulate the HTML, CSS and JavaScript of a component in a single file. This approach promotes better organization and easier maintenance of code.
TypeScript Support: Robust support for TypeScript, allowing developers to leverage static typing and other TypeScript features to improve code quality and maintainability.

In Vue.js, a “vue” typically refers to a Vue component. A Vue component is a reusable, self-contained unit of code that encapsulates the HTML, CSS, and JavaScript needed to render a part of the user interface. Components are the building blocks of a Vue.js application and allow developers to create modular and maintainable code.

Overall, Vue.js provides a powerful and flexible framework for interactive web applications.

Adding a Dropdown Component to a Vue.js View

I'll review the current implementation and then walk through the steps to add the dropdown to the page and integrate it with the existing code.

Active Leads by Owner

Figure 1 shows the Active Leads by Owner page before any modifications. The objective is to add a dropdown component to the page that when selected will return the count of leads for that owner.

The Active Leads report before adding the dropdown control.

Figure 1: The Active Leads report before adding the dropdown control.

Listing 1: The vActiveLeadsByOwner vue before modification

<template>
 <div>
   <h1>Active Leads By Owner</h1>
   <div v-if="loading">Loading...</div>
   <div v-else-if="error">{{ error }}</div>
   <div v-else>
     <div v-if="activeleads.success">
       <p>Total Count: {{ activeleads.totalCount }}</p>
     </div>
     <div v-else>
       <p>Error: {{ activeleads ? activeleads.failureInformation : 'No data available' }}</p>
     </div>
   </div>
 </div>
</template>

<script>

import { ref, onMounted, watch } from "vue";
import { getActiveLeadsByOwner } from "@/services/leadService";

export default {
 props: {
   ownerId: {
     type: String,
     required: true,
   },
 },
 name: "ActiveLeadsByOwner",
 setup(props) {
   const activeleads = ref(null);
   const loading = ref(true);
   const error = ref(null);

   const fetchActiveLeadsByOwner = async () => {
     try {
       const response = await getActiveLeadsByOwner(props.ownerId);
       if (response.success) {
         activeleads.value = response;
       } else {
         error.value = response.failureInformation || "Failed to fetch active leads by owner";
       }
     } catch (err) {
       error.value = "Failed to fetch active leads by owner";
     } finally {
       loading.value = false;
     }
   };

   onMounted(() => {
     fetchActiveLeadsByOwner(props.ownerId);
   });

   watch(() => props.ownerId, (newOwnerId) => {
     fetchActiveLeadsByOwner(newOwnerId);
   });

   return {
     activeleads,
     loading,
     error,
   };
 },
};
</script>
<style scoped>
h1 {
 font-size: 24px;
 margin-bottom: 20px;
}
</style>

Key points in this code:

  1. The page displays whatever is stored in activeleads.totalCount as the total count.
  2. The code calls getActiveLeadsByOwner which returns the active leads.
  3. The function getActiveLeadsByOwner is passed the ownerId.
  4. There is a function, getAllLeadOwners that returns all of the lead owners.

To address the new requiredment to dispay the lead count for individual owners, The new component needs to display the list of lead owners, pass the selected ownerId to the getActiveLeadsByOwner function, and update the page with the new activeleads.totalCount.

ChatGPT

Let's write some code! First let's prompt Copilot to add the control to the vue and wire it up to the rest of the app. Then we'll review the changes the Copilot suggest that we make to me the requirements for displaying total leads for individual owners.

Prompt: @workspace Add a dropdown component for ownerId. Populate the dropdown with the lead owners from the getAllLeadOwners function. When the user selects a lead owner, fetch the active leads for that owner.

Response

To add a dropdown component for ownerId and populate it with the lead owners from the getAllLeadOwners function, you can modify the vActiveLeadsByOwner.vue file as follows:

  1. Add a dropdown component in the template.
  2. Fetch the lead owners and populate the dropdown.
  3. Update the ownerId when a user selects a lead owner from the dropdown.

Here is the code with the changes:

<!-- Add the dropdown component to the page -->
    <div>
      <label for="owner-select">Select Owner:</label>
      <select id="owner-select" v-model="selectedOwnerId" @change="fetchActiveLeadsByOwner">
        <option v-for="owner in leadOwners" :key="owner.nameId" :value="owner.nameId">
          {{ owner.name }}
        </option>
      </select>
    </div>

Add the getLeadOwners function that retrieves the lead owners to the import which makes the function available to call:

import { getActiveLeadsByOwner, getAllLeadOwners } from "@/services/leadService";

Drop the OwnerId custom property as it no longer needs to be passed into the vue.

  props: {
    ownerId: {
      type: String,
      required: true,
    },
  },

Remove the custom property from the setup call.

  setup() {

Define an array to contain the fetched list of lead owners.

  const leadOwners = ref([]);
  const selectedOwnerId = ref(props.ownerId);

If there is no selected owner, don't attempt to retrieve active leads for the owner.

    if (!selectedOwnerId.value) return;

Setting the loading value to true activateS the spinning loading image.

      loading.value = true;

Change the call to the getActiveLeadsByOwner function passing the selected owner instead of the ownerId.

        const response = await getActiveLeadsByOwner(selectedOwnerId.value);

Fetch the lead owners.

    const fetchLeadOwners = async () => {
      try {
        const response = await getAllLeadOwners();
        if (response.success) {
          leadOwners.value = response.people;
          if (leadOwners.value.length > 0) {
            selectedOwnerId.value = leadOwners.value[0].nameId;
            fetchActiveLeadsByOwner();
          }
        } else {
          error.value = response.failureInformation || "Failed to fetch lead owners";
        }
      } catch (err) {
        error.value = "Failed to fetch lead owners";
      }
    };

When loading the page call fetchLeadOwners rather than fetchActiveLeadsbyOwner.

    fetchLeadOwners();

When an owner is selected from the dropdown, call the fetchActiveLeadsByOwner function passing in the new owner id to fetch the active leads.

    watch(selectedOwnerId, (newOwnerId) => {
      fetchActiveLeadsByOwner(newOwnerId);
    });

Make leadOwners, selectedOwnerId, and fetchActiveLeadsByOwner available for use in the template.

      leadOwners,
      selectedOwnerId,
      fetchActiveLeadsByOwner,

Listing 2: Below is the modified vActiveLeadsByOwner vue with all of the Copilot modifications

<template>
  <div>
    <h1>Active Leads By Owner</h1>
    <div>
      <label for="owner-select">Select Owner:</label>
      <select id="owner-select" v-model="selectedOwnerId" @change="fetchActiveLeadsByOwner">
        <option v-for="owner in leadOwners" :key="owner.nameId" :value="owner.nameId">
          {{ owner.name }}
        </option>
      </select>
    </div>
    <div v-if="loading">Loading...</div>
    <div v-else-if="error">{{ error }}</div>
    <div v-else>
      <div v-if="activeleads.success">
        <p>Total Count: {{ activeleads.totalCount }}</p>
      </div>
      <div v-else>
        <p>Error: {{ activeleads ? activeleads.failureInformation : "No data available" }}</p>
      </div>
    </div>
  </div>
</template>

<script>
import { ref, onMounted, watch } from "vue";
import { getActiveLeadsByOwner, getAllLeadOwners } from "@/services/leadService";

export default {
  name: "ActiveLeadsByOwner",
  setup() {
    const activeleads = ref(null);
    const loading = ref(true);
    const error = ref(null);
    const leadOwners = ref([]);
    const selectedOwnerId = ref("");

    const fetchActiveLeadsByOwner = async () => {
      if (!selectedOwnerId.value) return;
      try {
        loading.value = true;
        const response = await getActiveLeadsByOwner(selectedOwnerId.value);
        if (response.success) {
          activeleads.value = response;
        } else {
          error.value = response.failureInformation || "Failed to fetch active leads by owner";
        }
      } catch (err) {
        error.value = "Failed to fetch active leads by owner";
      } finally {
        loading.value = false;
      }
    };

    const fetchLeadOwners = async () => {
      try {
        const response = await getAllLeadOwners();
        if (response.success) {
          leadOwners.value = response.people;
          if (leadOwners.value.length > 0) {
            selectedOwnerId.value = leadOwners.value[0].nameId;
            fetchActiveLeadsByOwner();
          }
        } else {
          error.value = response.failureInformation || "Failed to fetch lead owners";
        }
      } catch (err) {
        error.value = "Failed to fetch lead owners";
      }
    };

    onMounted(() => {
      fetchLeadOwners();
    });

    watch(selectedOwnerId, (newOwnerId) => {
      fetchActiveLeadsByOwner(newOwnerId);
    });

    return {
      activeleads,
      loading,
      error,
      leadOwners,
      selectedOwnerId,
      fetchActiveLeadsByOwner,
    };
  },
};
</script>

<style scoped>
h1 {
  font-size: 24px;
  margin-bottom: 20px;
}
</style>

Figure 2 shows the rendered page.

The Active Leads report after the Copilot modifications.

Figure 2: The Active Leads report after the Copilot modifications.

The app has been correctly updated to include a new component and all of the code necessary to wire up the component into the existing app is implemented and working.

Revisiting the Prompt

Let's take another look at the prompt that caused Copilot to correctly implement the new feature. Here is the prompt again:

@workspace Add a dropdown component for ownerId. Populate the dropdown with the lead owners from the getAllLeadOwners function. When the user selects a lead owner, fetch the active leads for that owner.

It's key to provide Copilot with the information it needs to complete a task successfully. Here is a breakdown of the prompt:

Prompt Requirement
Add a dropdown component for ownerId. Bind the dropdown to the OwnerId value.
Populate the dropdown with the lead owners from the getAllLeadOwners function. Without the getAllLeadOwners specification, Copilot will try to infer the data source, often incorrectly.
When the user selects a lead owner, fetch the active leads for that owner. Defines the expected behavior when an owner is selected.

This is where we developers add value. We provide direction, context and review.

  • By providing clear direction, we communicate our expectations for what should be produced.
  • When providing context we include specific details to point Copilot in the right direction. In this case, providing the name of the function that provides the data allows Copilot to correctly bind the control to the data.
  • We always need to review the generated code for completeness and accuracy. Leveraging test automation can help here.

When prompts are not specific enough, we can find ourselves in debugging cycles that may, or may not, result in a working fix. When Copilot is producing code that doesn't run, or run properly, we have two options:

  1. Revise the prompt to provide better direction and more context.
  2. Provide feedback to Copilot (as additional prompts) that describes the errant behavior and give Copilot a chance to address the issue.

I've seen both work acceptably. With the second option, if Copilot can't determine the root cause it will add debugging code to expose the internals of the code. This information can be given back to Copilot which can help in determining what changes are needed. This information is additional context including error messages, logs and any other output pertinent to the problem at hand.

Conclusion

As you can see, Copilot made a number of non-trivial changes to the app in order to implement the new feature. We provided Copilot clear direction on what we expected and the context needed for Copilot to understand the current implementation and Copilot determined what changes needed to be made and the changes it made worked without further modification.

In the next installment in this series, we'll use Copilot to turn this new control into a reuseable component, update this page to use the new component and add the component to another page that could take advantage of this functionality.

Feedback Loop

Feedback is always welcome. Please direct it to john.miller@codemag.com

Disclaimer

AI contributed to the writing to this blog post, but humans reviewed it, refined it, enhanced it, and gave it soul.

Prompts:

  • What are the pros of using Vue.js for front-end development?
  • In vue.js, what is a vue?