ในบทนี้ เป็นตอนต่อจาก EP.6 เนื่องจากมันยาวเกินไป เลยมีความจำเป็นอย่างยิ่งที่จะตัดมันออกมาเป็น 2 ตอน

เริ่มต้นจากตอนที่แล้ว เราเริ่มต้นกันโดยการเพิ่ม HeroesComponent และ DashboardComponent เพื่อดูแล view ต่างๆ แล้วเปลี่ยน AppComponent ไปเป็น router component ทำหน้าที่จัดการการแสดงผลหลังจากมีการกดลิงค์ และเปลี่ยนเพจไปมาๆ ที่เรียกว่า “routing”

สำหรับในตอนนี้ คือ การเก็บรายละเอียดต่างๆภายใน application ได้แก่

* เพิ่ม event แสดงรายละเอียดของฮีโร่แต่ละคน

* ปรับโค้ดในส่วนของ router ให้ดูดีมากขึ้น

* เพ่ิม style ให้กับทุกๆ view

Navigating to hero details

จากแผนภาพที่เขียนไว้ใน EP.6 จะพบว่ายังคงมีบางลิงค์ที่ขาดหายไป

ซึ่งมันก็คือ ส่วนของ HeroDetailComponent ที่เราละเลยไปนั้นเอง

สิ่งที่จำเป็นที่สุดสำหรับเพจใหม่ คือ การนิยาม route ใหม่ แต่ route ของ HeroDetailComponent มีความแตกต่างจาก route ปกติเล็กน้อย เพราะว่าในครั้งนี้ เราเริ่มต้นใช้ parameters เพื่อระบุว่า “ในเพจนี้เราจะแสดงข้อมูลของฮีโร่คนไหน??”

สิ่งที่เราต้องการคือ

“/hero/1” เพื่อแสดง hero คนที่ 1
“/hero/2” เพื่อแสดง hero คนที่ 2

เรียกรูปแบบนี้ว่า “Parameterized route”

นิยาม route เหมือนเดิม เพิ่มเติมคือ “:id” ทำหน้าที่เป็น parameter ระบุลำดับของฮีโร่ที่แสดงผล

Revise the HeroDetailComponent

กลับไปที่ HeroDetailComponent อีกครั้ง แน่นอนว่า ทุกๆส่วนที่ใช้ในการแสดงผลยังคงใช้ได้เหมือนเดิม แต่ทั้งนี้ส่วนที่จะต้องมีการเปลี่ยนแปลง คือ วิธีการดึงข้อมูล

เดิม HeroDetailComponent ได้รับข้อมูลจาก parent component ผ่านการ binding แต่หลังจากนี้ทุกอย่างกำลังเปลี่ยนแปลงไป เพราะว่า เราจะไม่เชื่อข้อมูลที่ได้รับมาจาก component อื่นอีกต่อไป และแทนที่ด้วยการดึงข้อมูลตามที่ระบุไว้ใน url ผ่าน HeroService

เพื่อที่จะดึงข้อมูล “:id” มาจาก url เราจำเป็นต้อง import modules ที่เกี่ยวข้องอีก 3-4 modules ประกอบไปด้วย ActivatedRoute, Params, Location และ switchMap (เอาไปใช้ภายหลัง)

หลังจากนั้น ก็ inject modules ทั้งหลายเข้าไปใน HeroDetailComponent พร้อมทั้งเพิ่ม OnInit interface

จากนั้นก็เริ่มทำการเขียน ngOnInit() ที่ต้องทำหน้าที่ดึงข้อมูลจาก HeroService ผ่านตัวแปร heroService ที่เราทำการ inject เอาไว้แล้ว

จะพบกว่า การดึงข้อมูลจาก params เปลี่ยนไปจาก angular1 หรือ js framework อื่นๆพอสมควร เดิมเรามักคุ้นเคยกับการใช้ this.route.params[“id”] เพื่อดึงข้อมูล id แล้ว assign ใส่ตัวแปร

แต่ใน angular2+ เปลี่ยนเป็นแนวคิดที่เรียกว่า “reactive programming” โดยแนวคิดนี้เป็นแนวคิดที่สร้างขึ้นสำหรับการดำเนินการที่เป็นแบบ async operation

ใน reactive programming แทนที่จะมองว่าการดำเนินการคือ action ที่ทำแล้วสิ้นสุด เราจะมองว่า action คือการเชื่อมโยงความสัมพันธ์ระหว่างตัวแปร ดังนั้นในตัวอย่างจะพบว่า เมื่อกำหนดให้ c มีค่าเชื่อมโยงความสัมพันธ์การบวกระหว่าง a,b ทุกครั้งที่มีการเปลี่ยนแปลง a หรือ b จะทำให้ตัวแปร c โดยเปลี่ยนแปลงไปด้วย

** แน่นอนว่า js ไม่ใช่ reactive paradigm

เพราะ ActivatedRoute โดย implement อยู่ในรูปแบบของ reactive ดังนั้นการดึงข้อมูลจาก this.route จึงกลายเป็นการเชื่อมโยงความสัมพันธ์ระหว่าง this.route เข้าไปในตัวแปร this.hero ผ่าน subscribe() function

ดังนั้นทุกครั้งที่ this.route เปลี่ยนแปลง ก็จะส่งผลให้ this.hero เปลี่ยนแปลงไปด้วย

แต่ปัญหาหนึ่งของ async operation คือ latency time หรือดีเลย์ แต่ละการดำเนินการอาจจะเสร็จไม่พร้อมกัน บางครั้งการดำเนินการที่มาทีหลังอาจจะเสร็จก่อนคำสั่งที่เราสั่งไปในลำดับแรก

ลองจิตนาการเมื่อมี เด็กน้อยกดเปลี่ยนเพจไปมาๆ อาจจะทำให้

* เข้าสู่ url “/detail/4”
* param เปลี่ยนไปเป็น “4”
* getHero(4) ดึงข้อมูล //แต่บังเอิญมาช้ามากๆ
* เด็กน้อยเปลี่ยนใจ เปลี่ยน url “/detail/1”
* param เปลี่ยนไปเป็น “1”
* getHero(1) ดึงข้อมูล //แต่บังเอิญเร็วมากๆ
* getHero(1) ดึงข้อมูลสำเร็จ แล้วเปลี่ยน hero
* getHero(4) พึ่งดึงข้อมูลสำเร็จ แล้วพยายามเปลี่ยน hero อีกครั้ง!! //BUG!!!

เพราะว่า เราไม่มีการยกเลิก getHero(4) ทำให้เกิดการทำงานซ้อนทับกับข้อมูลที่ถูกต้องที่มีอยู่แล้ว และรูปแบบการเขียนแบบเดิมๆมักแก้ปัญหานี้ได้ยากมากๆ แต่ใน reactive programming เรามีเครื่องมือที่เรียกว่า switchMap สำหรับจัดการปัญหาตรงนี้โดยเฉพาะ

switchMap จะทำหน้าที่ cancle action ก่อนหน้า ทุกครั้งที่มี action ใหม่เข้ามาแทนที่ นอกจากนี้แนวคิดแบบ reactive ยังมีเครื่องมืออีกหลายตัวสำหรับอีกหลายๆสถาณการณ์ เช่น flatMap หรือ concatMap

อ่านเพิ่มเติม : Angular 2: why use switchMap when retrieving route params?

** ข้อสำคัญอีกอย่าง คือ ค่าที่อยู่ใน params จะเป็น string เสมอ ดังนั้นจึงต้องมี +params['id'] เพื่อเปลี่ยนเป็นตัวเลข

และที่ลืมไม่ได้ คือ กลับไปเขียน getHero(id) สำหรับ HeroService

หลังจากเราสามารถดึงข้อมูลมาได้เรียบร้อยแล้ว ต่อมา ก็เริ่มทำการเพิ่มปุ่มสำหรับกด back ไปหน้าเดิม ผ่าน Location module

จากนั้น ก็เพิ่มปุ่มลงไปใน template และแยก html ทั้งหมดออกมาเป็น hero-detail.component.html สำหรับใครที่ชอบให้มี html อยู่ใน component อยู่แล้วก็ไม่จำเป็นต้องเปลี่ยนแปลงในส่วนนี้ (แต่ยังต้องเพิ่มปุ่มด้วยนะ!)

** ในส่วนนี้ /me สนับสนุนให้ย้ายออกมาเป็นไฟล์ html #ทีมแยกhtml

** สำหรับแอพจริงๆ มักจะมีเงื่อนไขต่างๆในการเข้าถึงหน้าบางหน้า angular จึงสร้าง “Route guards” เพื่อตรวจสอบลิงค์ก่อนที่จะ route เสมือนเป็นยามคุมประตู ซึ่งมันพึ่งมามีใน angular2+ /me เสียใจ เพราะติดปัญหาเรื่องนี้ตอนใช้ angular1 บ่อยมาก ฮืออออ

Refactor routes to a Routing Module

หลังจากแก้ไข HerodetailComponent กันเสร็จเรียบร้อยแล้ว สำหรับ flow การเขียนโค้ดที่ดี ก่อนที่จะเพิ่ม feature ใหม่ๆ We need refactoring.

Refactoring time @#$%^&

ลองกลับไปดู AppModule อีกครั้ง ลองคิดถึงความเป็นจริง โดยทั่วไปแล้วแอพหนึ่งๆไม่ได้มีแค่ 3 route แน่นอน นอกจากนี้มันมักประกอบไปด้วยเงื่อนไขยุบยิบๆอีกสารพัด สำหรับการ route แค่ 1 ครั้ง

การแยก routing ไปอีก module จึงเป็นไอเดียหนึ่งที่เหมาะสมกว่า แน่นอนว่าจริงๆแล้ว แทนที่เราจะสร้าง module เราก็สามารถใช้ service เพื่อแยกลอจิกตรงนี้ออกมาได้เช่นเดียวกัน

แต่.. การใช้ module จะทำให้เราสามารถ inherit RouterModule เดิมที่มีอยู่แล้วได้อย่างมีประสิทธิภาพมากกว่า

ภายในโค้ดจะประกอบไปด้วย 3 section เหมือนเดิม คือ
* import สำหรับ include ไฟล์ที่เกี่ยวข้องกับ AppRoutingModule

* export สำหรับแชร์ AppRoutingModule ทำให้ไฟล์อื่นๆสามารถเข้าถึง class นี้ได้

* @NgModule ส่วนนี้เป็นส่วนที่ยากที่สุด เพราะจะทำให้สับสนได้ง่าย โดยมันทำหน้าที่เป็น decorator เปิดให้เราเพิ่ม feature หรือ เขียน function เพิ่มไปกับ module เดิมได้

How does AppRoutingModule work?

* เริ่มต้นด้วยการกำหนดรายการ routing ต่างๆไว้ในตัวแปร routes

* แล้วส่งต่อไปให้ RouterModule.forRoot(routes) ที่โดน imports อยู่ภายใน decorator โดย “forRoot([…])” จะสร้าง instance ที่เป็น RouterModule ตามที่ระบุรูปแบบ routing

โดยเมื่อเรา imports มาแล้ว ภายใน AppRoutingModule จะสามารถ override function และใช้ความสามารถต่างๆที่อยู่ภายใน สิ่งที่เรา imports มาได้ทันที

* exports ไปเป็น RouterModule เพื่อบอก module อื่นๆว่า สามารถใช้ฟังก์ชั่นใน AppRoutingModule ได้เหมือนกับ RouterModule เลย หรือพูดอีกมุมว่า exports คือการลิสฟังก์ชั่นที่เปิดให้ module อื่นๆใช้งาน

** รายละเอียดส่วนนี้ /me ก็ไม่มั่นใจ 100% สามารถอ่านเพิ่มเติม ตามลิงค์ Role of imports / exports in Angular 2+ ngModule

จากนั้นก็กลับไปที่ AppModule เพื่อใช้งาน AppRoutingModule

Select a hero

กลับมาที่ แต่ละ view กันอีกครั้ง

สำหรับ DashboardComponent เราลืมที่จะสร้าง link เพื่อสร้างลิงค์ไปที่ /detail/:id

จะพบว่า คราวนี้ เราใช้ routerLink เปลี่ยนไปจากเดิม ที่อยู่ใน app.component.ts เพราะว่าเดิมมันเป็น static url แปลว่า ชื่อลิงค์ไม่เปลี่ยนแปลง

แต่ในกรณีนี้ เราจำเป็นต้องใช้ลิงค์ที่แตกต่างกันสำหรับฮีโร่แค่ละคน หรือ dynamic url รูปแบบการเขียนจึงเปลี่ยนไปเป็นการรับ params 2 ค่า โดยตัวแปรที่ 2 จะโดนเพิ่มไปอยู่ใน “/:id”

สำหรับ HeroesComponent เพราะว่าเราย้าย HeroDetailComponent ไปอยู่ใน view ใหม่เรียบร้อยแล้ว ดังนั้น เราจึงจะแทนที่ <hero-detail> ด้วย mini-detail พร้อมปุ่มกดไปดูรายละเอียดแทน

จะสังเกตเห็น “| uppercase” เป็น operation ที่เราไม่เคยเห็นมาก่อน

สำหรับ Pipe หรือ “|” เป็นการทำ filter เพื่อเปลี่ยนแปลงรูปแบบข้อมูลสำหรับการแสดงผลที่ดียิ่งขึ้น และมันก็เป็นแค่ไม่กี่ feature ที่หลงเหลือจาก angular1 มาสู่ angular2+

สำหรับ uppercase ก็คือชื่อ filter ที่แปลงตัวอักษรพิมพ์เล็กไปเป็นอักษรพิมพ์ใหญ่ แค่นั้นเอง~ นอกจากนี้ก็จะเป็นพวกจัดการรูปแบบวันที่ บลาๆ หรือ อาจจะเขียนขึ้นมาใหม่เป็นของตัวเองก็ได้

ก่อนที่จะเริ่มเขียน gotoDetail()

Refactoring time @#$%^&

สำหรับการ refactoring รอบนี้ ก็แค่ย้าย html, styles ไปเป็น heroes.component.html และ heroes.component.css เท่านั้นเอง

จากนั้นก็แก้ metadata เป็น

จากนั้นก็สร้าง gotoDetail() โดยการ inject Router module เพื่อใช้ .navigate()

จริงๆแล้ว สามารถใช้ routerLink เพื่อเปลี่ยน url ก็จะได้ผลลัพย์(แทบจะ)เหมือนกัน

Style the app

และขั้นตอนสุดท้ายตกแต่ง ทุกส่วนด้วย css โดยการเพิ่ม styleUrls

** จะมีข้อสังเกตว่า styleUrls จะรับค่าเป็น array ทำให้เราสามารถใช้หลายๆ styles ใน 1 component ได้

hero-detail.component.css

dashboard.component.css

และ app.component.css

แก้ไข AppComponent เล็กน้อย โดยการเพิ่ม routerLinkActive เพื่อให้มันใส่ class=”active” หลังจากลิงค์ active ไปแล้ว

และสุดท้ายก็คือ global style เป็น style ที่จะใช้กับทุกๆหน้า โดยส่วนใหญ่มักใช้เป็นการเครียร์ค่า offset หรือ จัดการกับ font

จบตอนที่ 6 แล้วเราจะต่อกันด้วยตอนที่ 7 เป็นตอนสุดท้าย สำหรับการติดต่อเพื่อดึงข้อมูลจาก http request

PS.

เนื้อหาทั้งหมด จะแบ่งเป็น 7 ตอน
EP1. introduction
EP2. The Hero Editor
EP3. Master/Detail
EP4. Multiple Components
EP5. Services
EP6. Routing
EP6-2. Routing II
EP7. HTTP
ตามดูโค้ดได้ที่ Github

Thank you

Angular.io บทความนี้อยู่ภายใต้ลิขสิทธิ์แบบ CC BY 4.0

Tell your friend about thisShare on Facebook
Facebook
0Tweet about this on Twitter
Twitter
Share on Google+
Google+
0