Setelah rekan-rekan mengerti lebih dalam mengenai Angular dan cara kerjanya, maka kita akan melanjutkan project kita.
Designing Application Component
Tampilan aplikasi yang akan kita buat akan terlihat seperti ini.
Aplikasi yang kita buat akan dapat melakukan inquiry data to-do yang sudah kita tambahkan, terdapat form yang dapat digunakan untuk menambahkan data to-do, serta toggle pada setiap data to-do untuk mengubah status dari suatu to-do item.
Adapun aplikasi frontend kita ini terdiri dari beberapa component berdasarkan pembagian berikut.
Kita akan membuat tiga component yaitu:
Header component, yang merupakan shared component untuk menampilkan header di setiap page
Todo List component, menampilkan data to-do list kita
Todo Add component, adalah form yang digunakan untuk menambahkan data to-do.
Header Component
Angular CLI telah menyediakan command untuk dapat digunakan dalam men-generate component, yakni
ng generate component <nama-component>
. Karena component Header ini adalah shared component, maka demi kerapihan project, saya tempatkan component ini di dalam folder components/header
.Sehingga command yang dijalankan adalah:
ng g c --skipTests=true components/header
Untuk simplifikasi project pembelajaran ini, kita tidak menerapkan testing pada aplikasi kita. Sehingga
--skipTests=true
ditambahkan untuk mencegah pembuatan file .spec
yang ditujukan untuk keperluan tersebut.Command ini akan membantu kita dalam men-generate file-file di dalam folder
components/header
dengan file naming dan kode yang sudah siap untuk dapat kita gunakan langsung. Tentu saja kita dapat membuat sendiri file-file ini, hanya saja, kita tidak boleh lupa untuk menambahkan deklarasi component tersebut karena semua component di Angular project harus dideklarasikan di dalam app.module.ts
. Sedangkan dengan menggunakan CLI, Angular akan secara otomatis juga menambahkan deklarasi component tersebut.File yang terbentuk ada tiga, yakni
.css
file, .ts
file dan .html
file. .html
file digunakan sebagai template seperti yang sudah dijelaskan sebelumnya. .ts
file akan menjadi view logic component tersebut, .css
digunakan untuk styling, namun karena kita menggunakan Tailwind CSS, file ini tidak akan banyak kita gunakan.Untuk component Header ini, kita tidak memiliki view logic apapun, oleh karena itu kita hanya perlu mengubah component template
header.component.html
.<div class="flex bg-white border-b border-gray-200 fixed top-0 inset-x-0 z-100 h-16 items-center" > <div class="w-full max-w-screen-xl relative mx-auto px-6"> <div class="flex items-center -mx-6"> <div class="lg:w-1/4 xl:w-1/5 pl-6 pr-6 lg:pr-8"> <div class="flex items-center"> <a class="block lg:mr-4" href="/" ><img class="w-14" id="profile-img" src="https://img.icons8.com/fluent/344/year-of-tiger.png" alt="Logo" /></a> </div> </div> <div class="flex flex-grow min-w-0 lg:w-3/4 xl:w-4/5"> <div class="w-full min-w-0 lg:px-6 xl:w-3/4 xl:px-12"> <div class="relative"> <h1 class="font-semibold text-2xl text-indigo-500"> BCA Advisory Program 2022 </h1> </div> </div> <div class="hidden lg:flex lg:items-center lg:justify-between xl:w-1/4 px-6" > <div class="flex justify-start items-center text-gray-500"> <a class="block flex items-center hover:text-gray-700 mr-5" href="https://github.com/chandragie/bca-advisory-program-2022/" target="_blank" ><svg class="fill-current w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" > <title>GitHub</title> <path d="M10 0a10 10 0 0 0-3.16 19.49c.5.1.68-.22.68-.48l-.01-1.7c-2.78.6-3.37-1.34-3.37-1.34-.46-1.16-1.11-1.47-1.11-1.47-.9-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.52 2.34 1.08 2.91.83.1-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.1.39-1.99 1.03-2.69a3.6 3.6 0 0 1 .1-2.64s.84-.27 2.75 1.02a9.58 9.58 0 0 1 5 0c1.91-1.3 2.75-1.02 2.75-1.02.55 1.37.2 2.4.1 2.64.64.7 1.03 1.6 1.03 2.69 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85l-.01 2.75c0 .26.18.58.69.48A10 10 0 0 0 10 0" ></path></svg></a ><a class="block flex items-center hover:text-gray-700 mr-5" target="_blank" href="https://twitter.com/chandragie" ><svg class="fill-current w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" > <title>Twitter</title> <path d="M6.29 18.25c7.55 0 11.67-6.25 11.67-11.67v-.53c.8-.59 1.49-1.3 2.04-2.13-.75.33-1.54.55-2.36.65a4.12 4.12 0 0 0 1.8-2.27c-.8.48-1.68.81-2.6 1a4.1 4.1 0 0 0-7 3.74 11.65 11.65 0 0 1-8.45-4.3 4.1 4.1 0 0 0 1.27 5.49C2.01 8.2 1.37 8.03.8 7.7v.05a4.1 4.1 0 0 0 3.3 4.03 4.1 4.1 0 0 1-1.86.07 4.1 4.1 0 0 0 3.83 2.85A8.23 8.23 0 0 1 0 16.4a11.62 11.62 0 0 0 6.29 1.84" ></path></svg ></a> </div> </div> </div> </div> </div> </div>
Untuk dapat me-render component Header, kita perlu menempatkannya pada root component kita yaitu
app.component.html
. Hapus semua baris kode di dalam file tersebut dan gantikan dengan memanggil selector component Header.<app-header></app-header>
Cek kembali aplikasi kita melalui browser dan Header component kita sudah ter-render.
To-do List Component
Selanjutnya, kita akan membuat component untuk menampilkan daftar to-do yang kita miliki. Seperti sebelumnya, saya menjalankan command
ng g c --skipTests=true todo/todo-list
. Berikut inilah perubahan kode yang perlu dilakukan.<div class="flex justify-center pt-16"> <div class="w-1/2 mt-7"> <div class="flex flex-col justify-center items-center w-full"> <div class="flex justify-between w-full"> <button type="button" class="w-32 rounded-md bg-indigo-400 hover:bg-indigo-300 p-2 text-md focus:outline-none text-white font-semibold mb-3" > Add to do </button> </div> <!-- Cards --> <p *ngIf="!todos">No data</p> <div *ngIf="todos" class="divide-y divide-gray-200 w-full"> <ul *ngFor="let todo of todos" role="list"> <li class="pl-3 pr-4 py-3 flex items-center justify-between text-lg mb-1 border border-gray-300 rounded-md" > <div class="flex-1 flex items-center"> <p class="text-gray-600" aria-hidden="true"> {{ todo.title }} </p> </div> <div class="ml-4 flex-shrink-0"> <button type="button" class="font-medium text-indigo-600 hover:text-indigo-400" *ngIf="!todo.done" > Done </button> <button type="button" class="font-medium text-gray-600 hover:text-gray-500" *ngIf="todo.done" > Undone </button> </div> </li> </ul> </div> </div> </div> </div>
Kita menggunakan directive
ngFor
untuk melakukan looping data to-do yang akan didapatkan dari backend API. Kita perlu mendeklarasikan properti ini pada file TypeScript kita.import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-todo-list', templateUrl: './todo-list.component.html', styleUrls: ['./todo-list.component.css'], }) export class TodoListComponent implements OnInit { constructor() {} todos: any; ngOnInit(): void { } }
Tidak seperti Header component yang ditampilkan pada semua halaman, Todo List component merupakan component yang diakses melalui URL atau biasa disebut page. Oleh karena itu, kita perlu meng-update router aplikasi kita untuk dapat mengakses component ini. Pada inisialisasi project, kita melakukan setting router menjadi enabled, sehingga kita dapat langsung menggunakan routing system Angular.
Pertama, kita perlu mendefinisikan route atau path ke component Todo List. Buka file
app-routing.module.ts
dan tambahkan kode berikut.const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'todos' }, { path: 'todos', component: TodoListComponent} ];
Path
''
dimaksudkan untuk me-redirect request ke halaman Todo List sebagai default page.Kedua, tambahkan component Todo List ini ke dalam app.module.html seperti sebelumnya. Namun karena kali ini kita menggunakan router dari Angular untuk take over proses rendering berdasarkan URL yang dikirim, kita bukan menambahkan tag
<app-todo-list></app-todo-list>
melainkan <router-outlet></router-outlet>
.<app-header></app-header> <router-outlet></router-outlet>
Nah, sekarang coba kita akses kembali aplikasi kita dan by default kita akan langsung diarahkan ke component Todo List. Perhatikan pada address bar bahwa request kita diarahkan ke
/todos
.Karena belum ada data apapun yang kita supply ke properti
todos
, sesuai dengan conditional rendering logic yang kita terapkan, maka kita mendapatkan teks "No data". Kita akan tambahkan integrasi ke backend API kita pada bagian selanjutnya.To-do Add Component
Next kita buat form untuk menambahkan to-do item. Sama seperti sebelumnya, mari kita generate component dan lakukan perubahan sebagai berikut.
import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; @Component({ selector: 'app-todo-add', templateUrl: './todo-add.component.html', styleUrls: ['./todo-add.component.css'], }) export class TodoAddComponent implements OnInit { addForm!: FormGroup; form: any; constructor( ) { this.form = { title: '', }; } ngOnInit(): void { this.addForm = new FormGroup({ title: new FormControl('', Validators.required), }); } }
Ubah
todo-add.component.html
dengan code berikut.<div class="flex justify-center pt-16"> <div class="w-1/2 mt-7"> <div class="flex flex-col justify-center items-center w-full"> <form autocomplete="off" class="w-full" [formGroup]="addForm"> <div class="items-center"> <div class="w-full mb-2"> <label class="block text-gray-500 font-bold text-left" for="title" > What do you want to do? </label> </div> <div class="w-full"> <input class="bg-gray-100 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-indigo-500" id="title" type="text" placeholder="Do something..." [formControlName]="'title'" required /> <span *ngIf="!addForm.get('title')?.valid && addForm.get('title')?.touched" class="text-red-600 text-sm">Really? Nothing to do?</span> </div> </div> <div class="flex justify-between w-full mt-10"> <button type="button" class="w-32 rounded-md bg-indigo-400 hover:bg-indigo-300 p-2 text-md focus:outline-none text-white font-semibold mb-3" [routerLink]="['/']" > Cancel </button> <button type="submit" class="w-32 rounded-md bg-indigo-400 hover:bg-indigo-300 p-2 text-md focus:outline-none text-white font-semibold mb-3" > Submit </button> </div> </form> </div> </div> </div>
Untuk dapat menggunakan form pada Angular, kita perlu mengaktifkan module tersebut melalui
app.module.ts
.... import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [ ... ], imports: [ ... ReactiveFormsModule ], ... }) export class AppModule { }
Jangan lupa kita juga perlu menambahkan route ke component ini.
const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'todos' }, { path: 'todos', component: TodoListComponent}, { path: 'add', component: TodoAddComponent}, ];
Tambahkan juga
routerLink
sebagai event binding pada tombol "Add to do" kita.<div class="flex justify-between w-full"> <button [routerLink]="['/add']" type="button" class="w-32 rounded-md bg-indigo-400 hover:bg-indigo-300 p-2 text-md focus:outline-none text-white font-semibold mb-3" > Add to do </button> </div>
Simpan semua file tersebut dan cek aplikasi. Tombol "Add to do" kita sudah dapat melakukan perpindahan halaman ke halaman dari component Todo Add. Perhatikan juga bahwa validasi form yang kita terapkan sudah dapat berjalan 👍
Glue'em all together with services
Meskipun dapat melakukan perpindahan halaman, aplikasi kita masih belum menggunakan backend API yang sudah kita buat. Mari kita integrasikan agar aplikasi kita dapat fully functional. Pertama, kita perlu melakukan generate service. Sama seperti component, kita dapat menggunakan command berikut.
ng g service services/todo
Kita akan mendapatkan file
todo.service.ts
pada folder /services
. Lakukan perubahan berikut.import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) export class TodoService { constructor(private httpClient: HttpClient) {} getAll = (): Observable<any> => { return this.httpClient.get(`${environment.baseUrl}/todo`, { responseType: 'json', }); }; add = (body: any): Observable<any> => { return this.httpClient.post(`${environment.baseUrl}/todo`, { ...body }); }; update = (id: string): Observable<any> => { return this.httpClient.put(`${environment.baseUrl}/todo`, { id: id }); }; }
Code beauty tips: gunakan environment properties untuk configurational value. Tambahkan
baseUrl: 'http://localhost:8080/adam'
pada environment.ts
. Sesuaikan port pada URL tersebut dengan mengarah ke port dimana backend API kita berjalan.Untuk memanggil backend API, kita memerlukan HTTP Client. Angular sudah menyediakan module ini, kita hanya perlu mengaktifkannya seperti berikut.
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { HttpClientModule } from '@angular/common/http'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { HeaderComponent } from './components/header/header.component'; import { TodoListComponent } from './todo/todo-list/todo-list.component'; import { TodoAddComponent } from './todo/todo-add/todo-add.component'; import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [ AppComponent, HeaderComponent, TodoListComponent, TodoAddComponent ], imports: [ BrowserModule, AppRoutingModule, ReactiveFormsModule, HttpClientModule, ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Observable
Beberapa hal yang perlu dipelajari yaitu bahwa kita menggunakan Observable untuk berkomunikasi dengan backend API kita. Observable sendiri merupakan topik advance namun sederhananya, ibarat sebuah keran air, kita dapat sewaktu-waktu membuka keran tersebut di saat kita membutuhkan air dan mematikannya apabila sudah selesai menggunakannya. Apabila fellowdevs familiar dengan konsep Promise, Observable tidaklah sama. Observable memungkinkan kita untuk melakukan reactive programming sedangkan Promise sebatas asynchronous.
Wait, what?? You are not alone! Ya, as I said, this is actually an advance topic tapi saya akan mencoba menjelaskannya sesederhana mungkin. Observable berasal dari kata to observe, to watch, or notice. Melalui Observable, kita dapat melakukan data streaming melalui proses subscription pada event emitter. Sehingga apabila terjadi perubahan atau biasa disebut event kita akan mendapatkan informasi atau update terhadap kejadian tersebut apabila kita masih listen atau subscribe.
Another brilliant example yang menjelaskan perbedaan Promise dan Observable adalah: andaikan Anda memenangkan tiket untuk menghadiri Angular conference, ng-conf. Anda mengemas koper Anda, terbang ke lokasi konferensi dan mendapati bahwa bapak dari Angular, Miško Hevery sendirilah yang memberikan talk pertama. Apa yang Anda lakukan?
- Pop in for a second, catch only one word of his talk and leave?
- Atau Anda akan duduk dengan nyaman dan "observe" apapun yang dia katakan?
Tentu saja Anda akan melakukan yang kedua bukan? Dengan kata lain, Promise adalah ilustrasi yang pertama. Segera setelah Promise mendapatkan response, even only a word of the whole talk, it would just leave and complete. Sedangkan Observable adalah ilustrasi kedua dimana Anda mendengarkan dan melakukan observasi terhadap materi yang dijelaskan oleh Miško.
Saat Anda mendengarkan ceramah Angular tersebut, Anda mulai sedikit merasa bosan, Anda mengambil handphone Anda dan mulai browsing atau berselancar di sosial media selama 15 menit. Lalu Anda sadar bahwa Anda tidak lagi mendengarkan materi yang dibawakan. Kondisi ini disebut juga sebagai "unsubscribe". Dan kemudian Anda memutuskan untuk meletakkan handphone Anda dan menjadi selayaknya good observer.
Okay, cukup tentang Observable. Sekarang kita perlu memanggil / "subscribe" ke Observable ini dari component kita. Mari kita mulai dari proses inquiry to-do list.
import { Component, OnInit } from '@angular/core'; import { TodoService } from 'src/app/services/todo.service'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-todo-list', templateUrl: './todo-list.component.html', styleUrls: ['./todo-list.component.css'], }) export class TodoListComponent implements OnInit { constructor( private todoService: TodoService, private activatedRoute: ActivatedRoute, private toastr: ToastrService ) {} todos: any; ngOnInit(): void { this.getTodos(); this.activatedRoute.queryParams.subscribe((params) => { if (params['save'] !== undefined && params['save'] === 'true') { this.toastr.success('Todo saved succesfully!'); } }); } getTodos = () => { this.todoService.getAll().subscribe({ next: (data) => { this.todos = data; }, error: (error) => { this.toastr.error('Failed to get todo list'); throw error; }, }); }; }
Saya menambahkan dependency tambahan untuk notifikasi sederhana berupa toaster. Dependency ini perlu kita install terlebih dahulu. Kemudian untuk pemanis, kita juga tambahkan animasi.
npm i ngx-toastr @angular/animations
Tertulis pada dokumentasi resminya, kita perlu meng-copy file toastr.css ini pada folder
/src
project kita dan kemudian untuk Angular dapat memasukkan style tersebut, kita perlu sedikit mengubah file angular.json
dan menambahkan konfigurasi berikut."architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist/bcaadam-frontend", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.css", "src/toastr.css" ], "scripts": [] }, "configurations": {}
Seperti halnya semua module perlu dideklarasikan, mari kita daftarkan module toaster ini.
... import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ToastrModule } from 'ngx-toastr'; @NgModule({ declarations: [ ... ], imports: [ ... BrowserAnimationsModule, ToastrModule.forRoot({ positionClass: 'toast-bottom-right', }), ], ... }) export class AppModule {}
Simpan semua file tersebut dan kita sudah dapat melihat bahwa aplikasi frontend kita sudah berhasil melakukan call ke backend API dan mendapatkan data to-do yang tersimpan.
Coba matikan service backend API kita dan refresh halaman aplikasi. Maka kita akan mendapatkan notifikasi dalam bentuk toaster. Notifikasi semacam ini berguna untuk memberi user info apabila terjadi masalah pada aplikasi kita.
Status info using toastr
Selanjutnya, kita akan mengubah beberapa file untuk membuat fungsi add dan update bekerja.
<form autocomplete="off" class="w-full" [formGroup]="addForm" (ngSubmit)="addTodo()" >
... import { Router } from '@angular/router'; import { ToastrService } from 'ngx-toastr'; import { TodoService } from 'src/app/services/todo.service'; @Component({ ... }) export class TodoAddComponent implements OnInit { ... constructor( private todoService: TodoService, private router: Router, private toastr: ToastrService ) { ... addTodo() { if (this.addForm.invalid) { this.toastr.error('Name what you want to do dude!'); return; } this.form.title = this.addForm.get('title')?.value; this.todoService.add(this.form).subscribe({ next: () => this.router.navigate(['/'], { queryParams: { save: 'true' } }), error: () => this.toastr.error('Failed to save todo! Please try again'), }); } }
<div class="ml-4 flex-shrink-0"> <button (click)="updateTodo(todo.id)" ... > Done </button> <button (click)="updateTodo(todo.id)" ... > Undone </button> </div>
... updateTodo = (id: string) => { this.todoService.update(id).subscribe({ next: () => { this.toastr.success('Todo updated!'); this.getTodos(); }, error: (error) => { this.toastr.error('Failed to update todo ☹'); throw error; }, }); };
And that should be it guys! Cek aplikasi kita melalui browser dan semua operasi sudah dapat dilakukan.
Practice
Apabila fellowdevs perhatikan pada component
todo-list.component.html
saya memberikan segment <!-- Cards -->
. Untuk saat ini saya masih menuliskan to-do list dalam tag <ul>
. Nah untuk latihan, coba kalian kembangkan cara ini dengan memisahkan list menjadi card component.Untuk source code dari project yang kita kerjakan dapat fellowdevs checkout melalui Git repository ini.
What's Next?
Kita telah belajar mengenai bagaimana melakukan frontend development dengan menggunakan framework Angular, serta mengintegrasikannya ke backend API kita yang dibangun dengan menggunakan Spring Boot. Selanjutnya, kita akan kembangkan aplikasi kita ini dengan menambahkan unsur security yaitu authentication dan authorization pada Chapter 3.