Day 19 – Github Card project Part 2 – Component Composition
Table of Contents
- Create a GithubProfileCard component
- Create a GithubProfileList component
- Github Repositories
- Resources
The demo displays Github users in a card layout. There is a GithubProfileList
that iterates a list of user names and displays each user in a GithubProfileCard
.
Create a GithubProfileCard component
Vue 3 application
// components/GithubProfileCard.vue
<script setup lang="ts">
import { useGithubProfile } from '@/composables/useGithubProfile.ts'
type Prop = {
username: string
}
const { username } = defineProps<Prop>()
</script>
The GithubProfileCard
expects a username
prop that can assign to the username
ref of the useGithubProfile
composable. The project is written in TypeScript, so defineProps
macro can accept a prop type.
<script setup lang="ts">
const { username: name, profile, error } = useGithubProfile()
name.value = username
</script>
Destructure username
, profile
, and error
refs from the useGithubProfile
composable. username
is aliased to name
to not conflicted with the username
prop.
When the name
ref is updated, the composable retrieves the Github profile and the value is available in the the profile
ref.
<template>
<div v-if="profile">
<p>Username: {{ profile.login }}</p>
<p>Name: {{ profile.name }}</p>
<p>Bio: {{ profile.bio || 'N/A' }}</p>
</div>
<div v-else-if="error">
Error: {{ error }}
</div>
</template>
The v-if
directive checks the value of the profile
ref. The template displays the login, name and bio when the profile is defined. The v-else-if
directive examines the value of error
and displays the error message When itis not blank.
SvelteKit application
// github-profile.type.ts
export type GithubProfileItem = {
key: number;
profile?: GithubProfile;
error?: string
}
The GithubProfileItem
type consists of profile
or error
property.
// github-profile-card.svelte
<script lang="ts">
import type { GithubProfileItem } from './github-profile-item.type';
type Props = {
profile: GithubProfileItem
};
const { profile: result }: Props = $props();
const { profile, error } = result;
</script>
In this component, $props()
is casted to the Props
type. The prop
is destructured to profile
and error
.
{#if profile}
<div>
<p>Username: {profile.login}</p>
<p>Name: {profile.name}</p>
<p>Bio: {profile.bio || 'N/A'}</p>
</div>
{:else if error}
<div>
<p>Error: {error}</p>
</div>
{/if}
In the template, the if-else-if control-flow syntax examines both profile
and error
. The if
branch displays the Github profile when it is defined. The else-if
branch displays the error message when it is not blank.
Angular 20 application
// github-profile-card.component.ts
import { httpResource } from '@angular/common/http';
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { GithubProfile } from '../types/github-profile.type';
@Component({
selector: 'app-github-profile-card',
templateUrl: './github-profile-card.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GithubProfileCardComponent {
username = input.required<string>();
profileResource = httpResource<GithubProfile>(() => this.username() ? {
url: `https://api.github.com/users/${this.username()}`,
method: 'GET',
headers: {
Authorization: `Bearer ${GITHUB_TOKEN}`
}
}: undefined, {
equal: (a, b) => a?.login === b?.login,
});
profile = computed(() => this.profileResource.hasValue() ? this.profileResource.value() : undefined);
error = computed(() => this.profileResource.error()?.message || '');
}
The component has a required signal input, username
.
When the username
input changes, the profileResource
reactively creates a HttpResourceRequest
{
url: `https://api.github.com/users/${this.username()}`,
method: 'GET',
headers: {
Authorization: `Bearer ${GITHUB_TOKEN}`
}
}
The request has the Github URL, HTTP Method and an HTTP headers.
When the profileResource
has a new value, the profile
and error
computed signals derive the profile and the error message respectively.
// github-profile-card.component.html
@let status = profileResource.status();
@if (status === 'loading') {
<p>Loading profile...</p>
} @else if (status === 'error') {
<p>Error loading profile: {{ error() }}</p>
} @else {
@if (profile(); as profile) {
<div>
<p>Username: {{ profile.login }}</p>
<p>Name: {{ profile.name }}</p>
<p>Bio: {{ profile.bio || 'N/A' }}</p>
</div>
}
}
The HTML template conditionally displays Loading profile...
when the status is loading. When the status is error
, the template displays the error message. Finally, the else branch displays the profile’s login, name and bio.
Create a GithubProfileList component
Next, we create a GithubProfileList
component that passes a username to GithubProfileCard
to render. The GithubProfileList
is reusable because it also accepts any usernames list.
Vue 3 application
// GithubProfileList.vue
<script setup lang="ts">
import GithubProfileCard from './GithubProfileCard.vue'
const { usernames } = defineProps<{ usernames: string[] }>()
</script>
Destructure defineProps
to extract the usernames
list.
<template>
<div class="header">
<h1>Github Profile List (Vue 3 Ver.)</h1>
</div>
<GithubProfileCard v-for="username in usernames" :key="username" :username="username" />
</template>
The v-for
directive iterates the username
prop and binds username
to both key
and username
attributes.
// App.vue
<script setup lang="ts">
import GithubProfileList from './components/GithubProfileList.vue'
const usernames = ['johnsoncodehk', 'antfu', 'railsstudent', 'danielkellyio', 'hootlex', 'MooseSaeed']
</script>
<template>
<GithubProfileList :usernames="usernames" />
</template>
The App
component passes the usernames
list to the GithubProfileList
.
SvelteKit application
// github-profile-list.svelte
<script lang="ts">
import GithubProfileCard from "./github-profile-card.svelte";
import type { GithubProfileItem } from './github-profile-item.type';
type Props = {
profiles: GithubProfileItem[]
};
const { profiles }: Props = $props();
</script>
The GithubProfileList
destructures props from $props()
.
<div class="header">
<h1>Github Profile List (Svelte ver.)</h1>
</div>
{#each profiles as profile (profile.key)}
<GithubProfileCard {profile} />
{/each}
In the template, #each
iterates profiles and bind the profile
to the props of GuthubProfileCard
.
// +page.svelte
<script lang="ts">
import type { PageProps } from './$types';
import GithubProfileList from '$lib/github-profile-list.svelte';
const { data: results }: PageProps = $props();
const { data: profiles } = results;
</script>
{#if !profiles || profiles.length === 0}
<p>No profiles found.</p>
{:else}
<GithubProfileList {profiles} />
{/if}
The loader function loads the profiles
and it is passed to the GithubProfileList
as props.
Angular 20 application
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { GithubProfileCardComponent } from './github-profile-card.coponent';
@Component({
selector: 'app-github-profile-list',
imports: [GithubProfileCardComponent],
template: `
<div class="header">
<h1>Github Profile List (Angular Ver.)</h1>
</div>
@for (username of usernames(); track username) {
<app-github-profile-card [username]="username"/>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GithubProfileListComponent {
usernames = input.required<string[]>();
}
The component has a required signal input, usernames
.
@for
iterates the usernames
input and passes the username
to the username
input of the GithubProfileCardComponent
.
// app.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { GithubProfileListComponent } from './github/components/github-profile-list.component';
@Component({
selector: 'app-root',
imports: [GithubProfileListComponent],
template: '<app-github-profile-list [usernames]="usernames" />',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
readonly usernames = ['johnsoncodehk', 'antfu', 'railsstudent', 'danielkellyio', 'hootlex', 'MooseSaeed'];
}
The AppComponent
defines a readonly usernames
list and it is passed to the GithubProfileListComponent
.
We have successfully created GithubProfileList
and GithubProfileCard
components and passed the names from the parent to the child. The child component receives inputs input and displays them in the template.
Github Repositories
- Vue 3: https://github.com/railsstudent/vue-github-profile
- Svelte 5: https://github.com/railsstudent/svelte-github-profile
- Angular 20: https://github.com/railsstudent/angular-github-profile
Resources