Open In App

News App Using Angular

Last Updated : 02 Sep, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

We will be developing a News Application using the Angular framework. This interactive app will allow users to search for news articles and filter them by category.

With a clean and responsive design, it will dynamically display articles, providing an engaging and user-friendly experience. The app will use Angular Material components for a modern and visually appealing interface.

Project Preview

Screenshot-2024-08-19-at-13-57-34-NewsAngularApp
Project Preview

Prerequisites

Approach

  • Create a responsive layout with Angular Material components for consistent styling.
  • Implement a search bar to filter news articles based on user input.
  • Use Angular’s Reactive Forms to handle search queries and category selection.
  • Display news articles dynamically and ensure a user-friendly interface with clear navigation.

Steps to Create News app using Angular

Step 1: Install Angular CLI

If you haven’t installed Angular CLI yet, install it using the following command

npm install -g @angular/cli

Step 2: Create a New Angular Project

ng new news-app --no-standalone
cd news-app

Step 3: Create Standalone Component

Create a standalone component. You can generate a standalone component using the Angular CLI:

ng generate component news-app

Step 4: Install Packages

Execute the below command to install additional packages.

npm install @angular/material @angular/cdk @fortawesome/angular-fontawesome @fortawesome/fontawesome-free @fortawesome/free-solid-svg-icons

Dependencies

"dependencies": {
"@angular/animations": "^18.2.0",
"@angular/cdk": "^18.2.0",
"@angular/common": "^18.2.0",
"@angular/compiler": "^18.2.0",
"@angular/core": "^18.2.0",
"@angular/forms": "^18.2.0",
"@angular/material": "^18.2.0",
"@angular/platform-browser": "^18.2.0",
"@angular/platform-browser-dynamic": "^18.2.0",
"@angular/router": "^18.2.0",
"@fortawesome/angular-fontawesome": "^0.15.0",
"@fortawesome/fontawesome-free": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.10"
}

Folder Structure

PS
Folder Structure

Example: Create the required files as seen in the folder structure and add the following codes.

App Component

Below is the App Component demonstrating the selector of news-app in HTML file and all the necessary imports in app.module.ts file.

HTML
<!--src/app/app-component.html-->
<div class="app-container">
    <app-news-app></app-news-app>
</div>
JavaScript
// src/app/app-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { NewsAppComponent } from './news-app/news-app.component';

const routes: Routes = [
    { path: '', component: NewsAppComponent },
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }
JavaScript
// src/app/app.component.ts

import { Component } from '@angular/core';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {
    title = 'news-angular-app';
}
JavaScript
// src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { provideHttpClient, withInterceptorsFromDi }
    from '@angular/common/http';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule }
    from '@angular/material/button-toggle';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule }
    from '@angular/platform-browser/animations';
import { MatToolbarModule } from '@angular/material/toolbar';
import { AppComponent } from './app.component';
import { NewsAppComponent } from './news-app/news-app.component';

@NgModule({
    declarations: [
        AppComponent,
        NewsAppComponent
    ],
    imports: [
        BrowserModule,
        BrowserAnimationsModule,
        FormsModule,
        ReactiveFormsModule,
        MatButtonModule,
        MatButtonToggleModule,
        MatFormFieldModule,
        MatInputModule,
        MatToolbarModule
    ],
    providers: [provideHttpClient(withInterceptorsFromDi())],
    bootstrap: [AppComponent]
})
export class AppModule { }

News-App Component

Below is the NewsApp Component having the UI for New App in HTML file followed by its CSS in news-app.component.css and all the logical implementation in news-app.component.ts.

HTML
<!--app/news-app/news-app.component.html-->

<div class="news-app-container">
    <mat-toolbar class="navbar">
        <span class="navbar-title">GeeksforGeeks</span>
    </mat-toolbar>

    <div class="search-container">
        <mat-form-field class="search-field">
            <input matInput [formControl]="searchControl" placeholder="Search for news">
        </mat-form-field>
    </div>


    <mat-button-toggle-group class="category-buttons" (change)="onCategoryChange($event.value)">
        <mat-button-toggle *ngFor="let category of categories" [value]="category">
            {{ category | titlecase }}
        </mat-button-toggle>
    </mat-button-toggle-group>

    <div class="news-list">
        <div class="news-item" *ngFor="let article of filteredArticles">
            <img [src]="article.urlToImage ? article.urlToImage : 
     'https://media.geeksforgeeks.org/wp-content/uploads/20240819134655/news.png'" alt="News Image"
                 class="news-image">
            <div class="news-content">
                <h4 class="news-title">{{ article.title }}</h4>
                <p class="news-description">{{ article.description }}</p>
                <a [href]="article.url" target="_blank" class="news-link">Read More...</a>
            </div>
        </div>
    </div>

    <button mat-raised-button color="primary" (click)="loadMore()" *ngIf="!isLoading">
        Load More
    </button>
    <p *ngIf="isLoading">Loading...</p>
</div>
CSS
/*src/app/news-app/news-app.component.css*/

.navbar {
    background-color: #007bff;
    color: white;
    padding: 10px 20px;
    display: flex;
    justify-content: center;
    align-items: center;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}

.navbar-title {
    font-size: 24px;
    font-weight: bold;
    margin: 0;
}


.search-container {
    display: flex;
    justify-content: center;
    margin: 20px 0;
}

.search-field {
    max-width: 600px;
    width: 100%;
}

.mat-form-field {
    width: 100%;
    border-radius: 4px;
}

.mat-form-field-appearance-fill .mat-form-field-flex {
    border: 1px solid #007bff;
    border-radius: 4px;
    background-color: #f8f9fa;
}

.mat-form-field-appearance-fill .mat-form-field-infix {
    border: none;
}

input.mat-input-element {
    padding: 12px;
    border: none;
    border-radius: 4px;
    font-size: 16px;
}


input.mat-input-element::placeholder {
    color: #666;
    opacity: 1;
}


.news-app-container {
    text-align: center;
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
}

.header {
    color: green;
    font-size: 36px;
    margin-bottom: 10px;
}

.subheader {
    font-size: 24px;
    margin-bottom: 20px;
}

.category-buttons {
    margin-bottom: 20px;
}

.news-list {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 20px;
}

.news-item {
    width: 100%;
    max-width: 300px;
    border: 1px solid #ddd;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
    transition: transform 0.3s, box-shadow 0.3s;
}

.news-item:hover {
    transform: translateY(-10px);
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}

.news-image {
    width: 100%;
    height: 200px;
    object-fit: cover;
}

.news-content {
    padding: 15px;
    text-align: left;
}

.news-title {
    font-size: 18px;
    margin: 10px 0;
    font-weight: bold;
}

.news-description {
    font-size: 14px;
    margin-bottom: 10px;
    color: #666;
}

.news-link {
    color: #007bff;
    text-decoration: none;
    font-weight: bold;
}

.news-link:hover {
    text-decoration: underline;
}

button {
    margin-top: 20px;
}
JavaScript
//src/app/news-app/news-app.component.ts
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { debounceTime } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';

@Component({
    selector: 'app-news-app',
    templateUrl: './news-app.component.html',
    styleUrls: ['./news-app.component.css']
})
export class NewsAppComponent implements OnInit {
    newsArticles: any[] = [];
    filteredArticles: any[] = [];
    searchControl = new FormControl<string>('');
    page = 1;
    totalResults = 0;
    isLoading = false;
    categories = ['general', 'entertainment', 'technology', 'sports', 'business', 'health', 'science'];
    selectedCategory = 'general';

    private apiKey = 'ecfaf9eaaa8d40a5b5d769210f5ee616'; // Make sure this is your valid API key

    constructor(private http: HttpClient) { }

    ngOnInit(): void {
        this.fetchNews();
        this.searchControl.valueChanges.pipe(debounceTime(300)).subscribe(searchTerm => {
            this.filterArticles(searchTerm ?? '');
        });
    }

    fetchNews(): void {
        this.isLoading = true;
        const url = `https://newsapi.org/v2/top-headlines?country=in&category=${this.selectedCategory}
        &page=${this.page}&apiKey=${this.apiKey}`;

        this.http.get<any>(url).subscribe({
            next: (response) => {
                this.newsArticles = response.articles;
                this.totalResults = response.totalResults;
                this.isLoading = false;
                this.filterArticles(this.searchControl.value ?? '');
            },
            error: (err) => {
                console.error('API error occurred:', err);
                this.isLoading = false;
                // Display a user-friendly error message
                alert('Something went wrong; please try again later.');
            }
        });
    }

    loadMore(): void {
        if (this.newsArticles.length < this.totalResults) {
            this.page += 1;
            this.fetchNews();
        }
    }

    filterArticles(searchTerm: string): void {
        if (searchTerm) {
            this.filteredArticles = this.newsArticles.filter(article =>
                article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
                article.description?.toLowerCase().includes(searchTerm.toLowerCase())
            );
        } else {
            this.filteredArticles = this.newsArticles;
        }
    }

    onCategoryChange(category: string): void {
        this.selectedCategory = category;
        this.page = 1;
        this.fetchNews();
    }
}


Complete Code:

HTML
<!--src/app/app-component.html-->
<div class="app-container">
    <app-news-app></app-news-app>
</div>
HTML
<!--app/news-app/news-app.component.html-->

<div class="news-app-container">
    <mat-toolbar class="navbar">
        <span class="navbar-title">GeeksforGeeks</span>
    </mat-toolbar>

    <div class="search-container">
        <mat-form-field class="search-field">
            <input matInput [formControl]="searchControl" placeholder="Search for news">
        </mat-form-field>
    </div>


    <mat-button-toggle-group class="category-buttons" (change)="onCategoryChange($event.value)">
        <mat-button-toggle *ngFor="let category of categories" [value]="category">
            {{ category | titlecase }}
        </mat-button-toggle>
    </mat-button-toggle-group>

    <div class="news-list">
        <div class="news-item" *ngFor="let article of filteredArticles">
            <img [src]="article.urlToImage ? article.urlToImage : 
     'https://media.geeksforgeeks.org/wp-content/uploads/20240819134655/news.png'" alt="News Image"
                 class="news-image">
            <div class="news-content">
                <h4 class="news-title">{{ article.title }}</h4>
                <p class="news-description">{{ article.description }}</p>
                <a [href]="article.url" target="_blank" class="news-link">Read More...</a>
            </div>
        </div>
    </div>

    <button mat-raised-button color="primary" (click)="loadMore()" *ngIf="!isLoading">
        Load More
    </button>
    <p *ngIf="isLoading">Loading...</p>
</div>
CSS
/*src/app/news-app/news-app.component.css*/

.navbar {
    background-color: #007bff;
    color: white;
    padding: 10px 20px;
    display: flex;
    justify-content: center;
    align-items: center;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}

.navbar-title {
    font-size: 24px;
    font-weight: bold;
    margin: 0;
}


.search-container {
    display: flex;
    justify-content: center;
    margin: 20px 0;
}

.search-field {
    max-width: 600px;
    width: 100%;
}

.mat-form-field {
    width: 100%;
    border-radius: 4px;
}

.mat-form-field-appearance-fill .mat-form-field-flex {
    border: 1px solid #007bff;
    border-radius: 4px;
    background-color: #f8f9fa;
}

.mat-form-field-appearance-fill .mat-form-field-infix {
    border: none;
}

input.mat-input-element {
    padding: 12px;
    border: none;
    border-radius: 4px;
    font-size: 16px;
}


input.mat-input-element::placeholder {
    color: #666;
    opacity: 1;
}


.news-app-container {
    text-align: center;
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
}

.header {
    color: green;
    font-size: 36px;
    margin-bottom: 10px;
}

.subheader {
    font-size: 24px;
    margin-bottom: 20px;
}

.category-buttons {
    margin-bottom: 20px;
}

.news-list {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 20px;
}

.news-item {
    width: 100%;
    max-width: 300px;
    border: 1px solid #ddd;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
    transition: transform 0.3s, box-shadow 0.3s;
}

.news-item:hover {
    transform: translateY(-10px);
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}

.news-image {
    width: 100%;
    height: 200px;
    object-fit: cover;
}

.news-content {
    padding: 15px;
    text-align: left;
}

.news-title {
    font-size: 18px;
    margin: 10px 0;
    font-weight: bold;
}

.news-description {
    font-size: 14px;
    margin-bottom: 10px;
    color: #666;
}

.news-link {
    color: #007bff;
    text-decoration: none;
    font-weight: bold;
}

.news-link:hover {
    text-decoration: underline;
}

button {
    margin-top: 20px;
}
JavaScript
// src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { provideHttpClient, withInterceptorsFromDi }
    from '@angular/common/http';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule }
    from '@angular/material/button-toggle';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule }
    from '@angular/platform-browser/animations';
import { MatToolbarModule } from '@angular/material/toolbar';
import { AppComponent } from './app.component';
import { NewsAppComponent } from './news-app/news-app.component';

@NgModule({
    declarations: [
        AppComponent,
        NewsAppComponent
    ],
    imports: [
        BrowserModule,
        BrowserAnimationsModule,
        FormsModule,
        ReactiveFormsModule,
        MatButtonModule,
        MatButtonToggleModule,
        MatFormFieldModule,
        MatInputModule,
        MatToolbarModule
    ],
    providers: [provideHttpClient(withInterceptorsFromDi())],
    bootstrap: [AppComponent]
})
export class AppModule { }
JavaScript
// src/app/app.component.ts

import { Component } from '@angular/core';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {
    title = 'news-angular-app';
}
JavaScript
// src/app/app-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { NewsAppComponent } from './news-app/news-app.component';

const routes: Routes = [
    { path: '', component: NewsAppComponent },
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }
JavaScript
//src/app/news-app/news-app.component.ts
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { debounceTime } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';

@Component({
    selector: 'app-news-app',
    templateUrl: './news-app.component.html',
    styleUrls: ['./news-app.component.css']
})
export class NewsAppComponent implements OnInit {
    newsArticles: any[] = [];
    filteredArticles: any[] = [];
    searchControl = new FormControl<string>('');
    page = 1;
    totalResults = 0;
    isLoading = false;
    categories = ['general', 'entertainment', 'technology', 'sports', 'business', 'health', 'science'];
    selectedCategory = 'general';

    private apiKey = 'ecfaf9eaaa8d40a5b5d769210f5ee616'; // Make sure this is your valid API key

    constructor(private http: HttpClient) { }

    ngOnInit(): void {
        this.fetchNews();
        this.searchControl.valueChanges.pipe(debounceTime(300)).subscribe(searchTerm => {
            this.filterArticles(searchTerm ?? '');
        });
    }

    fetchNews(): void {
        this.isLoading = true;
        const url = `https://newsapi.org/v2/top-headlines?country=in&category=${this.selectedCategory}
        &page=${this.page}&apiKey=${this.apiKey}`;

        this.http.get<any>(url).subscribe({
            next: (response) => {
                this.newsArticles = response.articles;
                this.totalResults = response.totalResults;
                this.isLoading = false;
                this.filterArticles(this.searchControl.value ?? '');
            },
            error: (err) => {
                console.error('API error occurred:', err);
                this.isLoading = false;
                // Display a user-friendly error message
                alert('Something went wrong; please try again later.');
            }
        });
    }

    loadMore(): void {
        if (this.newsArticles.length < this.totalResults) {
            this.page += 1;
            this.fetchNews();
        }
    }

    filterArticles(searchTerm: string): void {
        if (searchTerm) {
            this.filteredArticles = this.newsArticles.filter(article =>
                article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
                article.description?.toLowerCase().includes(searchTerm.toLowerCase())
            );
        } else {
            this.filteredArticles = this.newsArticles;
        }
    }

    onCategoryChange(category: string): void {
        this.selectedCategory = category;
        this.page = 1;
        this.fetchNews();
    }
}


Open the terminal, run this command from your root directory to start the application

ng serve --open

Open your browser and navigate to http://localhost:4200

Output


Next Article

Similar Reads