Zum Inhalt springen

Day 19 – Github Card project Part 2 – Component Composition

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

Resources

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert