คุณเคยได้ยินเรื่องนี้มาก่อน คุณมีแอปพลิเคชันที่รันอยู่บน API แบ็กเอนด์ที่กระจายศูนย์และทำงานเต็มที่ และฟรอนต์เอนด์ที่ทำด้วยเครื่องมือทั่วไป
ตอนนี้ คุณอยากย้ายไป Angular หรือ อาจแค่มองหาวิธีรวม Angular กับโปรเจกต์ Rails ของคุณเพราะคุณชอบแบบนี้ เราไม่ винить คุณ
ด้วยแนวทางนี้ คุณสามารถใช้ประโยชน์จากทั้งสองโลก และตัดสินใจว่าคุณอยากใช้ฟีเจอร์จาก Rails หรือ Angular เพื่อจัดรูปแบบสิ่งต่างๆ ตัวอย่างเช่น
สิ่งที่เราจะสร้าง (What We'll Build)
ไม่ต้องกังวล บทเรียนนี้เขียนขึ้นเพื่อวัตถุประสงค์นี้ เราจะดำดิ่งสู่การสร้างแอปพลิเคชัน CRUD ที่ทำงานเต็มรูปแบบบนโดเมนของผู้ใช้
ในตอนท้ายของบทความ คุณจะได้เรียนรู้แนวคิดพื้นฐานบางอย่างเกี่ยวกับ Angular และวิธีตั้งค่าโปรเจกต์แบ็กเอนด์ Rails ที่รวมกับ Angular โดยตรงสำหรับฟรอนต์เอนด์ ตามที่แสดงด้านล่าง:
CRUD ของผู้ใช้ที่ทำด้วย Rails และ Angular
แอปพลิเคชันจะจัดการ CRUD ทั้ง 4 operations บนโดเมนของผู้ใช้ที่ดึงมาจากบริการเว็บทดสอบปลอมภายนอก แอปจะสร้างบนสถาปัตยกรรม MVC โดยแต่ละเลเยอร์ของ Angular จะได้รับการอธิบายรายละเอียดเพื่อช่วยให้คุณเข้าใจได้ดีว่าสิ่งต่างๆ ผูกมัดกันอย่างไร สไตล์ขึ้นอยู่กับ Bootstrap
การตั้งค่า (Setup)
เหมือนที่คุณคาดเดา เราจะต้องการซอฟต์แวร์ต่อไปนี้:
- Ruby (ผมเลือกเวอร์ชัน 2.7.0preview1),
- Ruby และ Rails (ผมใช้เวอร์ชัน 5.0.7.2),
- Node.js (ผมเลือก v13.7.0),
- Yarn (อย่างน้อยเวอร์ชัน 1.22.4)
ตรวจสอบให้แน่ใจว่าทุกอย่างติดตั้งอย่างถูกต้อง จากนั้นเราจะไปต่อโปรเจกต์ เลือกโฟลเดอร์ที่คุณชอบและรันคำสั่งต่อไปนี้:
rails new crud-rails-angular รอให้การตั้งค่าเสร็จและเปิดโปรเจกต์ใน IDE ที่คุณชอบ เราจะทำงานกับ VS Code สำหรับบทความนี้ เพราะมันเรียบง่าย ทรงพลัง และรองรับทั้ง syntax ของ Rails และ Angular ได้อย่างราบรื่น
หากคุณใช้ Rails 5 มาสักพัก คุณอาจสังเกตว่าคำสั่ง new ของมันสร้างบั๊กใน Gemfile สำหรับ config SQLite มันมาที่ไม่มี minimum version และนั่นจะทำให้มันรันพร้อมข้อผิดพลาด มาแก้ปัญหานี้โดยอัปเดตเป็น:
gem 'sqlite3', '~> 1.3.10' สมบูรณ์แบบ!
การตั้งค่า Webpacker (Webpacker Setup)
วิธีที่ดีที่สุดในการจัดการแอปพลิเคชันที่คล้าย JavaScript ใน Rails คือผ่าน Webpacker มันใช้ Webpack ข้างหลังเพื่อให้ฟีเจอร์อย่าง pre-processing และ bundling JavaScript apps เช่น Angular เข้าไปในแอปพลิเคชัน Rails ที่มีอยู่
เพื่อติดตั้ง ให้เพิ่มบรรทัดใหม่ใน Gemfile ของคุณ:
gem 'webpacker', '~> 4.3.x' สิ่งนี้จะ assure ว่าคุณจะติดตั้งเวอร์ชันล่าสุด ต่อไป ให้รันคำสั่งต่อไปนี้:
bundle install bundle exec rake webpacker:install bundle exec rake webpacker:install:angular คำสั่งแรกจะดาวน์โหลดและอัปเดต Rails dependencies ที่เพิ่มเข้ามา
คำสั่งที่สองเทียบเท่ากับ npm install เนื่องจากมันสร้างโฟลเดอร์ node_modules และติดตั้ง dependencies ของ Angular ที่จำเป็นมากมาย เช่น Babel, Sass, Browserlist และ Webpack ตอนนี้เรามีทั้ง Node และ Rails app ในโปรเจกต์เดียวกัน
ในคำสั่งล่าสุด เรามีเทียบเท่ากับ npm install angular ซึ่งจะดาวน์โหลด dependencies ที่ Angular ต้องการทั้งหมดและทำให้มันทำงานร่วมกับโปรเจกต์ Rails ของเรา
ในท้ายคำสั่งเหล่านี้ คุณยังเห็นไฟล์ package.json ที่สร้างขึ้น ทุก dependencies ที่จำเป็นของเราวางอยู่ที่นี่ และคุณสามารถเพิ่มอะไรก็ได้ที่คุณต้องการในอนาคต
นอกจากนี้ โฟลเดอร์และไฟล์บางอย่างถูกสร้างภายใต้โฟลเดอร์ /app เช่น /javascript ใหม่ ในโฟลเดอร์นี้ คุณมีโฟลเดอร์ /hello_angular ที่สร้างไว้แล้วเพื่อสนับสนุนการเริ่มต้นการพัฒนาของคุณ
เพื่อประหยัดเวลา ผมขอให้คุณ mirror โฟลเดอร์และโครงสร้างไฟล์กับตัวต่อไปนี้:

การปรับปรุง Angular บางอย่าง (Some Angular Adjustments)
Webpacker แนะนำชุดการปรับปรุงภายในโปรเจกต์ Rails ที่สร้างขึ้น มาใช้เวลาจัดระเบียบบ้านกัน
ขั้นแรก เปิดไฟล์ application.js ที่วางอยู่ภายใต้โฟลเดอร์ /packs (แสดงในรูปข้างบน) และเพิ่มโค้ดต่อไปนี้:
import "core-js/stable"; import "regenerator-runtime/runtime"; การ import เหล่านี้ทำงานเป็นแรงเสริมเพื่อทำให้สภาพแวดล้อม JavaScript ภายในโปรเจกต์ Rails มั่นคง
ตอนนี้ เราต้องบอก Rails ว่าต้องไปหา output สำหรับหน้าของมันจากไหน เมื่อ Webpacker จบการ pack สิ่งต่างๆ มันจะสร้าง static files ที่กระจายได้ (distributable) มากมายที่ Rails ต้องรู้จัก
ไปที่ไฟล์ application.html.erb ภายใต้โฟลเดอร์ app/views/layout และเปลี่ยนเนื้อหาแท็ก <head> เป็นดังนี้:
<head> <title>CrudRailsAngular</title> <base href="/" /> <!-- 1 --> <%= csrf_meta_tags %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous" /> <!-- 2 --> <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application' %> <!-- 3 --> </head> มาแยกวิเคราะห์นิดหน่อย:
- ที่นี่ เราเพิ่มแท็ก
baseซึ่งบอก Rails ว่าต้องมองที่ไหนเมื่อแอปพลิเคชันเริ่มต้น - เราจะใช้ Bootstrap เพื่ออนุมานสไตล์ให้หน้า เพื่อให้เราสามารถโฟกัสที่การนำไปใช้งานได้อย่างเดียว
- ที่นี่คือที่ที่คุณต้องวาง Webpacker tag ที่แมปไปยังเนื้อหาโฟลเดอร์ /packs (เหมือนกับที่จะถูกสร้างอัตโนมัติโดย Webpacker หลังจากการคอมไพล์ทุกครั้ง)
โมเดลและฐานข้อมูล (Model and Database)
ไปที่การตั้งค่าฐานข้อมูล เพื่อทำให้เร็วขึ้น เราจะ scaffold โมเดลใหม่ชื่อ User นี่คือคำสั่งที่คุณต้องรันเพื่อบรรลุเป้าหมาย:
rails g scaffold User name:string age:integer address:text && rake db:migrate มันจะสร้างโฟลเดอร์และไฟล์ของโมเดลทั้งหมด ซึ่งเราจะต้องการเพื่อให้ Rails จัดการข้อมูลฐานข้อมูล (จาก SQLite) และเก็บไว้สำหรับ CRUD operations ของเรา
จากนั้น คุณจะเห็นว่าไฟล์ใหม่ XXX1_create_users.rb ถูกสร้างภายใต้โฟลเดอร์ db/migrate/ เปิดมัน และคุณจะเห็นเรคอร์ด CreateUsers ที่สร้างใหม่
ภายในโฟลเดอร์ app/models/ คุณจะเห็นโมเดล User ที่สร้างขึ้นที่ไฟล์ user.rb
ตอนนี้ เปิดไฟล์ db/seeds.rb และเพิ่มโค้ดต่อไปนี้:
User.create(name: 'Luke Wan', age: 23, address: '123 Test St.') User.create(name: 'Mary Poppins', age: 41, address: '123 ABC St.') User.create(name: 'John Neilman', age: 76, address: '000 Test St.') โค้ดนี้จะ initialize ตาราง Users ของเราด้วยข้อมูลบางอย่างเมื่อเริ่มต้นเซฟ เรียบร้อยแล้ว และรันคำสั่ง:
rake db:seed นี่จะ seed ตารางผ่านคำสั่งที่ระบุไว้ข้างบน ต่อไป คุณสามารถเข้า SQLite database และตรวจสอบว่าทำงานหรือไม่ โดยออกคำสั่ง:
sqlite3 db/development.sqlite3 จากนั้น เลือกข้อมูลตาราง:
select * from users; และคุณอาจเห็นผลลัพธ์
คอมโพเนนต์ User (The User Component)
คุณจะต้องติดตั้ง dependencies อีกสองสามตัวเพื่อช่วยในการแปลง HTML และ CSS ไปยังหน้า Rails ของเรา เพิ่ม Angular router, forms libs และ ngx-bootstrap ซึ่งเราจะใช้เพื่ออำนวยความสะดวกในการสร้างและจัดการ Bootstrap components ดังนั้น ให้ออกคำสั่งต่อไปนี้:
yarn add @angular/router @angular/forms html-loader css-loader ngx-bootstrap อย่างไรก็ตาม ก่อนที่เราจะกระโดดเข้าโค้ดคอมโพเนนต์ มีแนวคิดสำคัญบางอย่างที่เราต้องชี้ให้เห็น เริ่มจากโครงสร้างของคอมโพเนนต์ Angular
คอมโพเนนต์คืออะไร? (What is a Component?)
ใน Angular คอมโพเนนต์มีอยู่เพื่อเชื่อมต่อ views ของคุณกับ application logic ที่ทำใน TypeScript
กล่าวอีกนัยหนึ่ง คอมโพเนนต์เหมือน container สำหรับ logic ทั้งหมดที่ views ของคุณต้องการเพื่อสนับสนุนการทำงานของมัน มันกำหนดค่าที่ views จะนำเสนอ และควบคุม flow ของพวกมัน มันเทียบเท่ากับ 'controller' ในเฟรมเวิร์กที่คล้ายกัน
เพื่อสร้างคอมโพเนนต์ ทั้งสิ่งที่คุณต้องทำคือกำหนดคลาสใหม่ implement interface OnInit และ annotate คลาสด้วย decorator @Component:
export class UserIndexComponent implements OnInit { constructor() { ... } ngOnInit() { ... } } @Component และ OnInit
Decorator @Component สำคัญเพราะมันทำเครื่องหมายให้คลาสนี้เป็นคอมโพเนนต์ Angular ที่จดจำได้ และให้ metadata configurations ที่ช่วย Angular จัดการกับพวกมันเกี่ยวกับการประมวลผล การสร้างอินสแตนซ์ และการใช้งานระหว่าง runtime
มาดู metadata configs ต่อไปนี้:
@Component({ selector: "users", template: templateString, }) ที่นี่ selector บอก Angular ว่าค่าที่ให้มาคือ CSS selector ที่มันอาจใช้เพื่อระบุ directive ปัจจุบันเข้าไปใน template ใช่ มันคือ template เดียวกันที่ให้มาใน metadata property ถัดไป
Interface OnInit อย่างไรก็ตาม เป็น optional และเป็นวิธีที่ดีในการ initialize สิ่งต่างๆ ก่อนที่คอมโพเนนต์จะจบ lifecycle มันทำงานเหมือน post-construct method
Dependency Injection (การฉีดพึ่งพา)
Angular เป็นเฟรมเวิร์ก DI (Dependency Injection) ลักษณะที่เพิ่ม modularity และ productivity
Dependencies ใน Angular อาจแตกต่างกัน ตั้งแต่ services และ repositories ของคุณ ไปจนถึง ordinary object ใดๆ ที่คุณรู้สึกว่าเหมาะสมที่จะ inject ไปที่อื่นในโค้ด
เพื่อทำให้คลาส "injectable" คุณแค่ต้อง annotate มันด้วย decorator @Injectable:
@Injectable({ providedIn: "root", }) export class UserService { ... } providedIn ระบุว่า injector ไหนจะ provide injectable ที่คุณสร้าง ค่า root บอก Angular ว่า injector ควรเป็นระดับแอปพลิเคชัน มีอีกมากมายที่คุณสามารถตรวจสอบได้ที่นี่
เพื่อ inject คลาสเข้าไปในคอมโพเนนต์ ตัวอย่างเช่น คุณขอ Angular ทำใน constructor ของคอมโพเนนต์:
constructor( private userService: UserService, ) {} ง่ายแค่นั้น!
คอมโพเนนต์เสร็จสมบูรณ์ (The Finished Component)
ด้านล่าง คุณสามารถหารายการโค้ดสุดท้ายสำหรับคอมโพเนนต์ User ของเรา วางมันลงใน index.component.ts ภายใต้โฟลเดอร์ javascript/hello_angular/app/
import { Component, OnInit, TemplateRef } from "@angular/core"; import { FormGroup, FormBuilder } from "@angular/forms"; import { BsModalRef, BsModalService } from "ngx-bootstrap/modal"; import templateString from "./index.component.html"; import { UserService } from "../user.service"; import { User } from "../user.class"; @Component({ selector: "users", template: templateString, }) export class UserIndexComponent implements OnInit { users: User[]; modalRef: BsModalRef; userForm: FormGroup; isNew: Boolean; constructor(public fb: FormBuilder, private userService: UserService, private modalService: BsModalService) {} public newUser(template: TemplateRef<any>) { this.reset(); this.modalRef = this.modalService.show(template); } public createUser() { this.userService.create(this.userForm.value).subscribe(() => { console.log("User created!"); this.reset(); this.modalRef.hide(); }); } public editUser(user, template: TemplateRef<any>) { this.isNew = false; this.userForm = this.fb.group({ id: [user.id], name: [user.name], age: [user.age], address: [user.address], }); this.modalRef = this.modalService.show(template); } public updateUser() { const { id } = this.userForm.value; this.userService.update(id, this.userForm.value).subscribe(() => { console.log("User updated!"); this.reset(); this.modalRef.hide(); }); } public deleteUser(id) { if (confirm("Are you sure?")) { this.userService.delete(id).subscribe(() => { console.log("User deleted!"); this.reset(); }); } } ngOnInit() { this.reset(); } public reset() { this.isNew = true; this.userService.getUsers().subscribe((users) => { this.users = users; }); this.userForm = this.fb.group({ id: [""], name: [""], age: [""], address: [""], }); } } อาร์เรย์ users จะเก็บข้อมูลตารางปัจจุบันที่แสดงบนหน้าจอและดึงมาจากเมธอด reset ซึ่งในที่สุดเรียก Rails API ของเราผ่าน UserService (ที่จะสร้าง)
userForm เพียงแค่ reference ช่วยสร้างและอัปเดตผู้ใช้ของเรา เนื่องจากฟอร์มเดียวกันจะใช้สำหรับทั้งสอง operations isNew ก็ช่วยเหมือนกัน ระบุว่าเราอยู่ใน flow ไหนในขณะนั้น
ที่นี่ เรามีเมธอดเทียบเท่า CRUD สำหรับแต่ละ operation ทุกตัวเรียกเมธอด UserService ที่เกี่ยวข้องเพื่อ commit กระบวนการใน Rails API
เราจะต้องตั้งค่า HTML module เพื่อแปลง templates ของเราเป็น HTML (เราจะเห็นเพิ่มเติมเกี่ยวกับ modules เร็วๆ นี้) ดังนั้น เปิดไฟล์ html.d.ts ภายในโฟลเดอร์เดียวกันและเพิ่ม:
declare module "*.html" { const content: string; export default content; } Angular Service และ Model (The Angular Service and Model)
มาไปที่การสร้าง UserService ของ Angular Angular เป็นเฟรมเวิร์ก เหมือน Rails ดังนั้น หมายความว่ามันโอเคที่จะปฏิบัติตามกฎของพวกมัน แม้จะหมายถึงการมี models ที่ซ้ำ (หรือคล้ายกันมาก) ตัวอย่างเช่น
โมเดลคืออะไร? (What is a Model?)
Angular models เป็นออบเจ็กต์ง่ายๆ ที่เก็บแอตทริบิวต์ข้อมูลที่มีความหมายร่วมกัน (คือ พวกมันแทนชิ้นส่วนที่กระชับของโดเมนของคุณ) พวกมันเหมือนโมเดลอื่นๆ ในภาษาและเฟรมเวิร์กส่วนใหญ่
มันช่วยมากที่จะมีข้อมูลของคุณ集中在ที่เดียว แทนที่จะทำซ้ำมันทั่วโค้ดอย่างที่เราทำกับโมเดล user:
export class User { constructor(public id: number, public name: string, public age: number, public address: string) {} } จำไว้ว่าเป็น TypeScript ดังนั้นแอตทริบิวต์ของโมเดลของคุณต้องมี type ที่กำหนดเสมอ
สร้างไฟล์ใหม่ชื่อ user.class.ts ภายใต้โฟลเดอร์ javascript/hello_angular/app/user/ และวางโค้ดข้างบนลงไป
บริการ (Service) คืออะไร? (What about a Service?)
Services เป็นแนวคิดกว้าง แต่เราสามารถเข้าใจได้ว่าเป็นออบเจ็กต์ที่กำหนดและมีวัตถุประสงค์ชัดเจน พวกมันช่วยคอมโพเนนต์ด้วย logic ที่ซับซ้อนมากขึ้น บริการพวกมันด้วยข้อมูลที่ประมวลผลและแปลงแล้ว ปกติมาจากบริการภายนอกหรือฐานข้อมูล
Service ไม่ต้องการ annotation หรือ interface เฉพาะใดๆ คุณแค่สร้างคลาสและทำให้มัน injectable ตามที่เราเห็นก่อนหน้านี้ จากนั้น คุณสามารถ inject มันเข้าไปในคอมโพเนนต์ของคุณได้
Observable Services (บริการที่สังเกตได้)
ฟีเจอร์น่าสนใจอีกอย่างของ Angular คือ มันอนุญาตให้คุณใช้ RxJS กับคลาสของคุณ
ตัวอย่างเช่น HTTP client เริ่มต้นของ Angular ซึ่งเราจะใช้เพื่อดึงข้อมูลจากบริการภายนอก คืนค่า RxJS Observables นี่คือเหตุผลที่เมื่อคุณเรียกเมธอด UserService ใดๆ ภายใน user component คุณอาจ subscribe ไปที่ผลลัพธ์ Observable:
this.userService.getUsers().subscribe((users) => { this.users = users; }); สังเกตว่า หากคุณไม่คุ้นเคยกับ RxJS ผมแนะนำให้อ่านเอกสารมันสั้นๆ มันไม่ยากเลย! ;)
อีกครั้ง ในโฟลเดอร์ javascript/hello_angular/app/user/ สร้างไฟล์อีกไฟล์ชื่อ user.service.ts นี่คือเนื้อหาของมัน:
import { Injectable } from "@angular/core"; import { HttpClient, HttpHeaders } from "@angular/common/http"; import { map } from "rxjs/operators"; import { Observable } from "rxjs"; import { User } from "./user.class"; @Injectable({ providedIn: "root", }) export class UserService { constructor(private http: HttpClient) {} httpOptions = { headers: new HttpHeaders({ "Content-Type": "application/json", }), }; getUsers(): Observable<User[]> { return this.http.get("/users.json").pipe( map((users: User[]) => users.map((user) => { return new User(user.id, user.name, user.age, user.address); }) ) ); } create(user): Observable<User> { return this.http.post<User>("/users.json", JSON.stringify(user), this.httpOptions); } update(id, user): Observable<User> { return this.http.put<User>("/users/" + id + ".json", JSON.stringify(user), this.httpOptions); } delete(id) { return this.http.delete<User>("/users/" + id + ".json", this.httpOptions); } } คุณเห็นความคล้ายคลึงระหว่างนี้กับคอมโพเนนต์ที่เราสร้างเมื่อสักครู่นี้ไหม? นี่เพราะเราต้องการ operations ที่ตรงกันเพื่อสนับสนุนอันในคอมโพเนนต์
สังเกตว่า HttpClient ต้องถูก inject ภายใน constructor ของคลาสด้วย เพื่อให้เราได้ใช้มันควบคู่กับคลาส
แต่ละ operation ทำ HTTP call ไปที่ Rails API ของเรา ตัวที่สร้างอัตโนมัติ
Views (มุมมอง)
Angular ทำงานกับ templates สำหรับ views ของมัน Template เป็นการผสม HTML-and-JavaScript แบบลำดับชั้นที่บอก Angular ว่าจะ render แต่ละคอมโพเนนต์อย่างไร
อย่างไรก็ตาม ก่อนจะไปต่อกับการสร้าง view ของเรา มาเข้าใจก่อนว่า Angular แยกระบบ template ของมันอย่างไร
Angular Directives (คำสั่ง Angular)
เนื่องจาก Angular templates เป็นแบบไดนามิกในตัว บาง directives จึงจำเป็นเพื่อขับ Angular ไปทางที่ถูกต้องในการ render สิ่งต่างๆ
Directives เพียงแค่เป็นคลาสที่มี decorator @Directive เหมือนคอมโพเนนต์ ใช่ @Component สืบทอดจาก @Directive ดังนั้นมันเป็น directive อย่างเป็นทางการด้วย
อย่างไรก็ตาม มีอีกสองประเภท: structural และ attribute directives
Structural Directives (คำสั่งโครงสร้าง)
Directives เหล่านี้แทนโครงสร้างเงื่อนไขและลูปที่แปลจาก JavaScript เข้าไปใน Angular template พวกมันช่วยทำให้ template ไดนามิกมากที่สุด เท่าที่คุณจะเขียนโปรแกรมใน vanilla JavaScript code ได้ มาดูตัวอย่าง:
<tr *ngFor="let user of users"> <td>{{ user.name }}</td> </tr> คำสั่ง *ngFor บอก Angular ให้ iterate ผ่านอาร์เรย์ของ users และพิมพ์ชื่อของแต่ละ user ไปที่ DOM
Attribute Directives (คำสั่งแอตทริบิวต์)
พวกมันทำงานโดยตรงกับลักษณะหรือพฤติกรรมของ elements มาดูตัวอย่าง:
<form [formGroup]="userForm" (ngSubmit)="isNew ? createUser() : updateUser()" novalidate></form> ที่นี่ เรากำลังปรับพฤติกรรมของฟอร์มโดยการตั้งค่าฟังก์ชัน submit แบบมีเงื่อนไข และใช้ FormGroup ของ Angular เพื่อ data-bind แต่ละฟิลด์อินพุตของฟอร์ม
Data Binding (การผูกข้อมูล)
การสร้างฟอร์มกับเว็บเฟรมเวิร์กอาจเป็นงานยุ่งยากและพลาดได้ง่าย หากมันไม่ให้ data binding
Angular รองรับ two-way data binding ซึ่งหมายความว่าคุณสามารถเชื่อมต่อชิ้นส่วนของ template ไปยังคอมโพเนนต์และในทางกลับกันได้โดยตรง
ฟอร์มข้างบนเป็นตัวอย่างที่ดีของพลัง data binding ของ FormGroup มันผูกแต่ละฟิลด์ฟอร์มกับออบเจ็กต์ userForm ที่สร้างภายในคอมโพเนนต์ของเราอย่างอัตโนมัติ
ในเมธอด editUser ตัวอย่างเช่น คุณเห็น version กลับกันของ binding ซึ่งค่าของ userForm ถูกตั้งค่าภายในคอมโพเนนต์และจะสะท้อนฟอร์มบน view
การสร้าง Index View (Building the Index View)
มาแยกเนื้อหาสำหรับ index.component.html เป็นสองส่วน นี่คือส่วนแรก:
<div class="container pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center"> <h1 class="display-4">User's Listing</h1> <p class="lead">A quick CRUD example of how to integrate Rails with Angular</p> <table class="table"> <tr> <th>Id</th> <th>Name</th> <th>Age</th> <th>Address</th> <th>Actions</th> </tr> <tbody> <tr *ngFor="let user of users"> <td>{{ user.id }}</td> <td>{{ user.name }}</td> <td>{{ user.age }}</td> <td>{{ user.address }}</td> <td colspan="2"> <button class="btn btn-secondary" (click)="editUser(user, template)">Edit</button> | <button class="btn btn-danger" (click)="deleteUser(user.id)">Delete</button> </td> </tr> </tbody> </table> <button class="btn btn-primary float-right mt-4" (click)="newUser(template)">Insert New</button> </div> ส่วนใหญ่ประกอบด้วย HTML ธรรมดา เราจะไม่ไปรายละเอียดเกี่ยวกับ Bootstrap classes
ส่วนสำคัญที่นี่คือคำสั่ง ngFor บนแถวของตาราง มันช่วย iterate ผ่านอาร์เรย์ users (จำมันไว้ไหม?) พิมพ์แอตทริบิวต์แต่ละตัวของมันไปที่ HTML output ผ่าน operator {{ … }}
ทุกครั้งที่คุณต้องการเพิ่ม DOM events หนึ่ง เช่น onClick เพียงแค่อัดชื่อ event ไว้ในวงเล็บและเพิ่มฟังก์ชันคอมโพเนนต์ที่มันจะเรียกเมื่อคลิก
การสร้าง Modal View (Building the Modal View)
ส่วนที่สอง เกี่ยวกับเนื้อหา modal ดังนั้น เพิ่มมันด้านล่างส่วนก่อนหน้า:
<ng-template #template> <div class="modal-header"> <h4 class="modal-title pull-left">{{ isNew ? "New User" : "Update User" }}</h4> <button type="button" class="close pull-right" aria-label="Close" (click)="modalRef.hide()"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <form [formGroup]="userForm" (ngSubmit)="isNew ? createUser() : updateUser()" novalidate> <input type="hidden" formControlName="id" class="form-control" /> <div class="form-group"> <label>Name</label> <input type="text" formControlName="name" class="form-control" /> </div> <div class="form-group"> <label>Age</label> <input type="text" formControlName="age" class="form-control" /> </div> <div class="form-group"> <label>Address</label> <textarea class="form-control" formControlName="address" rows="3"></textarea> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </ng-template> สังเกตว่าเราใช้แท็ก <ng-template> ซึ่งอนุญาตให้คุณ anchor elements ระหว่าง HTML และ Angular Template ID มา ngay หลังเครื่องหมาย #
ภายในฟอร์ม สังเกตด้วยว่าเราใช้ตัวแปรคอมโพเนนต์ isNew เพื่อตรวจสอบว่าการใช้งานฟอร์มครั้งนี้เกี่ยวกับการสร้างหรืออัปเดตผู้ใช้
สุดท้าย เราต้อง inject แอปพลิเคชัน hello_angular ทั้งหมดเข้าไปในหน้า index.html.erb ของ Rails ดังนั้น เปิดไฟล์นี้ภายใต้โฟลเดอร์ views/users/ และเปลี่ยนเนื้อหาเป็น:
<hello-angular>We're almost done...</hello-angular> <%= javascript_pack_tag 'hello_angular' %> Angular Modules (โมดูล Angular)
ตอนนี้ เราต้องบอก Angular ว่าจะหาสิ่งต่างๆ ที่ไหน มันเกิดขึ้นภายใน configurations ของโมดูล
มาเริ่มด้วยการเพิ่มเนื้อหาไปที่ app-bootstrap.module.ts:
import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { ModalModule } from "ngx-bootstrap/modal"; @NgModule({ imports: [CommonModule, ModalModule.forRoot()], exports: [ModalModule], }) export class AppBootstrapModule {} นี่จำกัดเฉพาะคอมโพเนนต์ Bootstrap ที่เราสืบทอดจาก ngx-bootstrap คอมโพเนนต์เดียวที่เราใช้ตอนนี้คือ Bootstrap modal
จากนั้น เปิดไฟล์ app-routing.module.ts และเปลี่ยนเนื้อหาเป็น:
import { RouterModule, Routes } from "@angular/router"; import { NgModule } from "@angular/core"; import { UserIndexComponent } from "./user/index/index.component"; const appRoutes: Routes = [ { path: "users", component: UserIndexComponent }, { path: "", redirectTo: "/users", pathMatch: "full" }, ]; @NgModule({ imports: [RouterModule.forRoot(appRoutes, { scrollPositionRestoration: "enabled" })], exports: [RouterModule], }) export class AppRoutingModule {} นี่จะ assure ว่า Angular จับคู่คอมโพเนนต์ User ที่ถูกต้องเมื่อ path /users ถูกเรียก
และ สุดท้าย ลงทะเบียนทั้งหมดภายในคลาส AppModule หลัก เปิดไฟล์ app.module.ts และตรวจสอบให้แน่ใจว่ามันมีลักษณะดังนี้:
import { BrowserModule } from "@angular/platform-browser"; import { NgModule } from "@angular/core"; import { HttpClientModule } from "@angular/common/http"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { AppComponent } from "./app.component"; import { AppRoutingModule } from "./app-routing.module"; import { AppBootstrapModule } from "./app-boostrap.module"; import { UserIndexComponent } from "./user/index/index.component"; @NgModule({ declarations: [AppComponent, UserIndexComponent], imports: [HttpClientModule, AppRoutingModule, BrowserModule, FormsModule, ReactiveFormsModule, AppBootstrapModule], providers: [], bootstrap: [AppComponent], }) export class AppModule {} ที่นี่ ทุกอย่างถูกแมป จากฟอร์ม HTTP client และ user component ไปจนถึง Bootstrap module configs และ routing
จบการตั้งค่า (Finishing Configs Up)
ก่อนจะกระโดดไปทดสอบ เราต้องจบบางสิ่งบ้าง เริ่มจากไฟล์ app.component.ts:
import { Component } from "@angular/core"; @Component({ selector: "hello-angular", template: "<router-outlet></router-outlet>", }) export class AppComponent { name = "Angular!"; } คอมโพเนนต์แอปหลักต้องรู้ว่าจะ route paths อย่างไร ดังนั้น RouterOutlet จะทำงาน
จากนั้น เราต้องแน่ใจว่า Webpacker เข้าใจ副檔名 HTML ที่เราทำงานอยู่ จะทำได้โดยการเปิดไฟล์ webpacker.yml และ ภายใต้โฟลเดอร์ /config ค้นหาส่วน extensions และเพิ่มรายการต่อไปนี้:
- .html Webpacker รู้จักเพียง built-in TypeScript loader ที่มาพร้อม Angular โดยค่าเริ่มต้น เราต้องประมวลผล HTML ซึ่งเป็นเหตุผลที่เราติดตั้ง dependency html-loader ก่อนหน้านี้ เพื่อตั้งค่ามัน ให้เปิดไฟล์ environment.js ภายใต้โฟลเดอร์ config/webpack และเพิ่ม loader config ต่อไปนี้:
environment.loaders.append("html", { test: /\.html$/, use: [ { loader: "html-loader", options: { minimize: true, }, }, ], }); สุดท้าย เพื่อป้องกัน Angular service ของเราจากการรับข้อผิดพลาดบน HTTP calls ของพวกมัน เราต้องปิดการตรวจสอบ CSRF token ที่ Rails ดำเนินการ สำหรับนี้ ให้เปิดไฟล์ application_controller.rb ภายใต้โฟลเดอร์ app/controllers และเปลี่ยนเนื้อหาเป็น:
class ApplicationController < ActionController::Base protect_from_forgery with: :null_session end การทดสอบ (Testing)
เสร็จแล้ว! มันดูยุ่งยากนิดหน่อย เพราะการตั้งค่าต้องการการปรับแต่งมาก แต่ผลลัพธ์คุ้มค่า
เพื่อทดสอบ เซฟทุกอย่าง และเริ่มเซิร์ฟเวอร์โดยออกคำสั่ง rails s
จากนั้น ไปที่เว็บเบราว์เซอร์ของคุณและพิมพ์ที่อยู่ https://localhost:3000/users ไปข้างหน้าและเล่นกับ CRUD web application
สรุป (Conclusion)
เป็นทางที่ยาวนานในการทำให้ CRUD นี้ทำงาน หลังจากการทดลองครั้งแรก คุณจะเห็นว่าสิ่งต่างๆ ง่ายขึ้นสำหรับโปรเจกต์อนาคตของคุณ ผมหวังว่าโปรเจกต์นี้จะช่วยตั้งจุดเริ่มต้นสำหรับ أولئكที่อยากเริ่มโปรเจกต์เร็วโดยรวมทั้งสองเทคโนโลยี
แม้เราจะไม่มีโปรเจกต์ scaffolding โอเพนซอร์สช่วยเหลือ แต่เราพึ่งพาความพยายามของกันและกันเพื่อให้มีวัสดุอย่างนี้ ตอนนี้ถึงคุณ了 ฟอร์กโปรเจกต์ (หรือสร้างจากศูนย์) และเริ่มทำการปรับแต่งของคุณ
GitHub repository สำหรับตัวอย่างนี้พบได้ที่นี่ สนุกกับมัน!