Angular ile Api Haberleşmeleri
Merhaba,
Geçmiş yazımda Angular’a yeni başlamak isteyenlere yönelik kurulumlarımızı tamamlayıp, ilk projemizi oluşturup proje yapısını incelemiştik. Bu yazımda ilgili component’ları doldurup, routing yapısını tamamlayıp, component’ların API ile haberleşmesini sağlayacağız.
Not: Konu Angular olduğu için ilgili Api kodları makalede bulunmamaktadır.
Kütüphaneler
Proje içerisinde çeşitli kütüphaneler kullanacağımız için bu kütüphaneleri projemize entegre etmemiz gerekiyor. İlgili kütüphaneleri aşağıdaki gibi entegre edebiliriz.
npm i @ng-bootstrap/ng-bootstrap
npm i bootstrap
npm i bootstrap-icons
npm i ngx-bootstrap
Kütüphanelerin indirme işlemleri tamamlandıktan sonra bootstrap kullanabilmemiz için angular.json dosyası içerisinde styles ve scripts alanlarını aşağıdaki gibi güncellememiz gerekiyor.
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.css",
"node_modules/bootstrap/scss/bootstrap.scss",
],
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.js",
"node_modules/bootstrap/dist/js/bootstrap.js"
],
Generic Service
Öncelikle projemizde ortak kullanacağımız yapılar olacağı için src/app altına shared klasörünü, shared klasörü altına ise ortak kullanacağımız servisler için services klasörünü oluşturuyoruz. Projemizde get, post, getList işlemleri içinse services altına generic-service.service.ts adında servisimizi oluşturup aşağıdaki gibi dolduruyoruz.
import { Injector,Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export class GenericHttpService<T> {
private headers: any;
private httpClient: HttpClient;
constructor(private injector: Injector,
private url: string,
private controllerName : string
) {
this.httpClient = this.injector.get(HttpClient);
this.headers = new HttpHeaders({
"Access-Control-Allow-Origin": "*",
'Access-Control-Allow-Methods': 'HEAD, GET',
'Content-Type': 'application/json'
})
}
public get(id: number, action: string) : Observable<T> {
return this.httpClient
.get<T>(this.getRequestUrl(action + '/' + id), {headers: this.headers})
.pipe(map((data) => data as T));
}
public post(item: any, action: string, httpParameters?: HttpParams) : Observable<T> {
return this.httpClient
.post<T>(this.getRequestUrl(action), item, {
headers: this.headers, params: httpParameters
})
.pipe(map((data) => data as T));
}
public getList(query : any = null, action: string) : Observable<Array<T>> {
let fullPath = this.getRequestUrl(action);
if(query) {
fullPath = `${fullPath}?${Object.keys(query)
.map((key) => key + '=' + query[key])
.join('&')}`;
}
return this.httpClient
.get<T>(fullPath, {headers: this.headers})
.pipe(map((data: any) => data as Array<T>));
}
private getRequestUrl(action : string) {
let path = `${this.url}`
if(this.controllerName != '') {
path = path + '/' + `${this.controllerName}`;
}
path = path + '/' + action;
return path;
}
}
Yukarıdaki url, action, controller gibi değişken isimleri aslında api tarafta istek atmak istediğimiz alanlara denk geliyor.
FullPath -> BaseApiUrl/ActionName/ControllerName
Movie List
movie-list.html
<div class="content">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">Movie List</h4>
</div>
<div class="card-body">
<div>
<button (click)="addMovie()" class="btn btn-sm btn-primary" placement="bottom" ngbTooltip="Add">
<i class="fa fa-plus"></i>
</button>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead class=" text-primary">
<tr>
<th>
Id
</th>
<th>
Title
</th>
<th>
Description
</th>
<th>
Vote Average
</th>
<th>
Original Language
</th>
<th>
Action
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let movie of movies "
>
<td >{{movie.id}}</td>
<td class="small-text">{{movie.title}}</td>
<td class="long-text">{{movie.description}}</td>
<td>{{movie.voteAverage}}</td>
<td>{{movie.originalLanguage}}</td>
<td>
<button (click)="showDetail(movie.id)" class="btn btn-sm btn-secondary" placement="bottom" ngbTooltip="Detail">
<i class="fa fa-search" aria-hidden="true"></i>
</button>
<button (click)="updateMovie(movie.id)" class="btn btn-sm btn-primary" placement="bottom" ngbTooltip="Update">
<i class="fa fa-pencil" aria-hidden="true"></i>
</button>
<button class="btn btn-sm btn-danger" placement="bottom" ngbTooltip="Delete">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
movie-list.ts
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { ProcessMode } from '../../shared/models/shared-model';
import { MovieService } from '../movie.service';
@Component({
selector: 'app-movie-list',
templateUrl: './movie-list.component.html',
styleUrl: './movie-list.component.css'
})
export class MovieListComponent implements OnInit {
movies : any [];
constructor(private movieService : MovieService, private router : Router) { }
ngOnInit(): void {
this.getMovies();
}
getMovies() {
this.movieService.getMovies().subscribe({
next: (response) => {
this.movies = response;
},
error: (error) => {
console.log("Could not get movies")
}});
}
showDetail(id) {
this.router.navigate(['moviedetail',id, ProcessMode.Show]);
}
addMovie() {
this.router.navigate(['moviedetail',0, ProcessMode.Add]);
}
updateMovie(id) {
if(id != null && id != undefined) {
this.router.navigate(['moviedetail',id, ProcessMode.Update]);
}
}
}
İlgili dosyaları doldurduktan sonra movie klasörümüzün altına movie.service.ts ve movie.model.ts dosyalarını oluşturup aşağıdaki gibi dolduruyoruz.
movie.model.ts
export class Movie {
Id : number;
Title : string;
Description : string;
VoteAverage : number;
OriginalLanguage : string;
ProcessMode : string;
}
movie.service.ts
import { HttpClient } from "@angular/common/http";
import { Injectable, Injector } from "@angular/core";
import { Observable } from "rxjs";
import { environment } from "../../environments/environment";
import { GenericHttpService } from "../shared/services/generic-service.service";
import { Movie } from "./movie.model";
@Injectable({
providedIn: 'root',
})
export class MovieService extends GenericHttpService<any> {
constructor(private http : HttpClient, injector: Injector) {
super(
injector,
environment.baseUrl,
'Movie'
)
}
public getMovies() : Observable<any[]> {
return this.getList({}, 'GetMovies');
}
public getMovieById(id: number) : Observable<any> {
return this.get(id, 'GetMovieById');
}
public saveMovie(movie: Movie) : Observable<any> {
return this.post(movie, 'SaveMovie');
}
}
Movie Detail
movie-detail.html
<div class="row content">
<div class="card card-user">
<div class="card-header">
<button (click)="goBack()" class="btn btn-sm btn-warning pull-right"
ngbTooltip="Go Back">
<i class="fa fa-long-arrow-left" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<form [formGroup]="form" (ngSubmit)="onSubmit(form.value)">
<div class="row">
<div class="col-md-6 pl-1">
<div class="form-group">
<label>Title</label>
<div *ngIf="form.controls['title'].invalid && (form.controls['title'].dirty
|| form.controls['title'].touched)" >
<small class="text-danger"
*ngIf="form.get('title').hasError('required')">
Title field is required.
</small>
</div>
<input type="text" class="form-control" formControlName="title"
placeholder="" value="" required
>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>
Original Language
</label>
<div *ngIf="form.controls['language'].invalid && (form.controls['language'].dirty
|| form.controls['language'].touched)" >
<small class="text-danger"
*ngIf="form.get('language').hasError('required')">
Original Language field is required.
</small>
</div>
<input type="text" class="form-control" formControlName="language"
placeholder="" value="" required
>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>
Vote Average
</label>
<div *ngIf="form.controls['average'].invalid && (form.controls['average'].dirty
|| form.controls['average'].touched)" >
<small class="text-danger"
*ngIf="form.get('average').hasError('required')">
Vote Average field is required.
</small>
</div>
<input type="number" class="form-control" formControlName="average"
placeholder="" value="" required
>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label>
Description
</label>
<div *ngIf="form.controls['description'].invalid && (form.controls['description'].dirty
|| form.controls['description'].touched)" >
<small class="text-danger"
*ngIf="form.get('description').hasError('required')">
Description field is required.
</small>
</div>
<textarea formControlName="description"
class="form-control textarea"
required >
</textarea>
</div>
</div>
</div>
<div id="submit-button" class="row">
<div class="update ml-auto mr-auto">
<div class="form-group">
<button type="submit"
[disabled]="disableMode"
class="btn btn-outline-primary btn-round pull-right">Save</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
movie-detail.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import Utils from '../../shared/helpers/utils';
import { ProcessMode } from '../../shared/models/shared-model';
import { NotificationService } from '../../shared/services/notification.service';
import { Movie } from '../movie.model';
import { MovieService } from '../movie.service';
@Component({
selector: 'app-movie-detail',
templateUrl: './movie-detail.component.html',
styleUrl: './movie-detail.component.css'
})
export class MovieDetailComponent implements OnInit{
id : any;
processMode: any;
disableMode: boolean = false;
validForm : boolean = false;
model : Movie;
form: FormGroup;
constructor(private fb: FormBuilder,
private route: ActivatedRoute,
private router : Router,
private movieService : MovieService,) {
}
ngOnInit() {
this.id =this.route.snapshot.paramMap.get('id');
this.processMode =this.route.snapshot.paramMap.get('processMode');
this.initializeForm();
this.getMovie();
this.enableDisableControls();
}
initializeForm() {
this.form = this.fb.group({
title: new FormControl('', Validators.required),
average: new FormControl(0, Validators.required),
description: new FormControl('', Validators.required),
language: new FormControl('', Validators.required),
});
}
getMovie() {
if(this.processMode != ProcessMode.Add) {
this.movieService.getMovieById(this.id).subscribe({
next: (response) => {
this.model = response;
this.fillFormData();
},
error: (error) => {
console.log("Could not get movie! " + error.message)
}});
}
}
fillFormData() {
this.form = this.fb.group({
title: new FormControl(this.model['title'], Validators.required),
average: new FormControl(this.model['voteAverage'], Validators.required),
description: new FormControl(this.model['description'],Validators.required),
language: new FormControl(this.model['originalLanguage'], Validators.required),
});
}
enableDisableControls() {
if(this.processMode == ProcessMode.Show ) {
this.disableMode = true;
this.form.disable()
}
else {
this.disableMode = false;
this.form.enable()
}
}
onSubmit(formData) : void {
let message = this.validateForm();
if(message == '') {
this.model = new Movie();
this.model.Id = this.id;
this.model.Title = formData.title;
this.model.OriginalLanguage = formData.language;
this.model.Description = formData.description;
this.model.VoteAverage = formData.average;
this.model.ProcessMode = ProcessMode[this.processMode];
this.saveMovie()
}
else {
console.log(message);
}
}
validateForm() {
let message = "";
this.validForm = this.form.valid;
if(this.validForm == false) {
message += "Please fill the form fields! ";
}
return message;
}
saveMovie() {
this.movieService.saveMovie(this.model).subscribe({
next: (response) => {
console.log("Movie created successfully!")
Utils.sleep(3000);
this.router.navigate(['/']);
},
error: (error) => {
console.log("Could not save movie! " + error.message)
}});
}
goBack() {
this.router.navigate(['/'])
}
}
Shared
movie-list ve movie-detail component’larında bazı ortak yapılar kullandık. Bunları da aşağıdaki gibi ekliyoruz.
shared/models altına shared-model.ts
export enum ProcessMode {
Add = "Add",
Update = "Update",
Show = "Show"
}
shared/helpers altına utils.ts
export default class Utils {
static sleep(milliseconds : number) {
var start = new Date().getTime();
var end = 0;
while((end - start) < milliseconds){
end = new Date().getTime();
}
}
}
Aşağıdaki komutu çalıştırıp not-found component oluşturuyoruz.
ng g c shared/not-found
Component oluştuktan sonra not-found.html dosyasını aşağıdaki gibi dolduruyoruz.
<section class="page_404">
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="col-sm-10 col-sm-offset-1- text-center">
<div class="four_zero_four_bg">
<h1 class="text-center">
404
</h1>
</div>
<div class="contant_box_404">
<h3 > Look like you're lost</h3>
<p>The page you are looking for not available!</p>
<a [routerLink]="['/']" class="link_404">Go Home</a>
</div>
</div>
</div>
</div>
</div>
</section>
Routing
app-routing.module.ts dosyasını aşağıdaki gibi dolduruyoruz.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MovieDetailComponent } from './movie/movie-detail/movie-detail.component';
import { MovieListComponent } from './movie/movie-list/movie-list.component';
import { NotFoundComponent } from './shared/not-found/not-found.component';
const routes: Routes = [
{ path:'', component: MovieListComponent },
{ path:'moviedetail/:id/:processMode', component: MovieDetailComponent },
{ path:'**', component: NotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
app.component.html
<router-outlet></router-outlet>
Sonuç
Angular projemiz içerisindeki component’ları doldurup oluşturduğumuz generic-service vasıtasıyla api ile haberleşmelerini sağladık ve routing işlemini tamamladık.
İlerleyen süreçte projemizi navbar, navmenu gibi alanlar ekleyip layout işlemlerini tamamlayacağız ve hazır template ile projemizi güzelleştireceğiz.
Bir sonraki yazımda görüşmek üzere…
Ford Otosan — Dijital Ürünler ve Servisler
Software Development Team Member
Mert Keçer