Thanks to visit codestin.com
Credit goes to dev.to

DEV Community

Connie Leung
Connie Leung

Posted on • Edited on • Originally published at blueskyconnie.com

Day 19 - Github Card project Part 2 - Component Composition

Day 19 - Github Card project Part 2 - Component Composition

Table of Contents

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>
Codestin Search App Codestin Search App

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>
Codestin Search App Codestin Search App

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>
Codestin Search App Codestin Search App

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 
}
Codestin Search App Codestin Search App

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>
Codestin Search App Codestin Search App

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}
Codestin Search App Codestin Search App

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 || '');
}
Codestin Search App Codestin Search App

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}`
    }
}
Codestin Search App Codestin Search App

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>
    }
}
Codestin Search App Codestin Search App

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>
Codestin Search App Codestin Search App

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>
Codestin Search App Codestin Search App

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>
Codestin Search App Codestin Search App

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>
Codestin Search App Codestin Search App

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}
Codestin Search App Codestin Search App

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>
Codestin Search App Codestin Search App
{#if !profiles || profiles.length === 0}
    <p>No profiles found.</p>
{:else}
    <GithubProfileList {profiles} />
{/if}
Codestin Search App Codestin Search App

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[]>();   
}
Codestin Search App Codestin Search App

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'];   
}
Codestin Search App Codestin Search App

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

Top comments (0)