Hướng dẫn angular render html

Lần trước mình đã có một bài hướng dẫn các bạn biến một file pts thành 1 giao diện web, hôm nay chúng ta sẽ bắt tay vào làm một trang web dự báo thời tiết mà hoàn toàn có thể đem ra sử dụng thật luôn, từ các bước thiết kế cho đến lập trình sử dụng Adobe XD, Angular 7 và Firebase.

Bước 1: Thiết kế

Bạn có thể down bản thiết kế tại đây để xem các layer xếp chồng lên nhau thế nào để tạo ra được bản thiết kế cuối cùng.

A. Thương hiệu

Hướng dẫn angular render html

Thương hiệu sẽ thể hiện được giá trị cốt lõi của nó thông qua bản thiết kế, và ở đây thể hiện sự tối giản, gọn gàng và dễ sử dụng.

  • Màu sắc

    2 màu sắc cơ bản đã bão hòa đem lại sự tươi mới cho giao diện

  • Kiểu chữ

    Chỉ sử dụng đúng font 'Sans Serif' nên chúng ta không phải tải bất cứ font nào khác, giúp tăng hiệu suất

  • Logo

Hướng dẫn angular render html

Việc thiết kế logo nói dễ không dễ, nói khó cũng không hẳn, căn bản là phải phù hợp với mục đích của thương hiệu. Như logo của Nike, chỉ là một dấu chữ check, mất có 35$, trông khi logo của pepsi, cũng chỉ là 3 màu cơ bản xoay qua lại một chút, tốn 1.000.000$

Hướng dẫn angular render html
Còn đây là logo chúng ta sẽ dùng, chỉ là một chữ M đơn giản, sử dụng 2 hình tam giác giao nhau, vs 2 màu là 2 màu cơ bản của trang web, hiệu quả và chỉ tốn có 0 đồng.

B. UI/UX

Ứng dụng chủ yếu sử dụng các thẻ có đổ bóng giống như những mảnh giấy trôi nổi. Chỉ những thông tin quan trọng mới được hiển thị ở phía trước để tránh khiến giao diện lộn xộn và sử dụng các hành ảnh động để tăng điểm UX.

  • Light mode (Mặc định)

Hướng dẫn angular render html

Dark mode

Hướng dẫn angular render html

  • Icon

    Người dùng sẽ phải được thông báo về tình hình thời tiết trong nháy mắt, vì thế nên chúng ta sẽ sử dụng bộ icon sau cho cả trang web

Hướng dẫn angular render html

  • Tranh minh họa

Có một cách để người dùng có thể dễ dàng đoán ra địa điểm mà lại lấp đầy khoảng trống một cách cực kì trực quan, đấy là thêm một bức tranh minh họa địa điểm, họ sẽ chẳng cần phải đọc chữ, mà hình ảnh thì lúc nào cũng để lại ấn tượng hơn. Cùng nghía qua 1 số hình minh họa nhé!

  1. Tunisia

Hướng dẫn angular render html
2. Qatar

Hướng dẫn angular render html
3. Nhật bản

Hướng dẫn angular render html
4. Pháp

Hướng dẫn angular render html

Bước 2: Lập trình

Đã gọi là từ A tới Z thì phải bắt đầu từ người chưa biết gì, nên chúng ta sẽ bắt đầu từ việc install nodejs và angular CLI nhé. Ai biết rồi thì có thể bỏ qua đoạn này.

Install nodejs từ đây, sau đó mở console, install Angular CLI và typescript bằng câu lệnh sau

npm i -g typescript 
npm i -g @angular/cli

Thêm hàm này vào file app.component.ts để đóng mở sidenav

toggleMenu() {
    this.showMenu = !this.showMenu;
 }

SVG icon

SVG icon và logo bạn có thể lấy ở đây (copy + paste)

  • hamburger icon
  • logo
  • icon thời tiết

Thêm style cho root component

Giờ là thời gian dành cho css, hãy xem nhanh đoạn css bên dưới và xem kết quả đạt được, sau đó tự viết css của bạn, vì mỗi người 1 quan niệm về cái đẹp =))

.root__container {
  width: 100vw;
  height: 100vh;
  display: grid;
  grid-template-columns: auto;
  grid-template-rows: 0.5fr auto;
  position: relative;
}

/*
================
    Header
================
*/

/*
    Slide Menu
= = = = = = = = =
*/
.side-menu__container {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  pointer-events: none;
  z-index: 25;
}

.side-menu__container-active {
  pointer-events: auto;
}

.side-menu__container::before {
  content: '';
  cursor: pointer;
  position: absolute;
  display: block;
  top: 0;
  left: 0;
  height: 100%;
  width: 100%;
  background-color: #0c1066;
  opacity: 0;
  transition: opacity 300ms linear;
  will-change: opacity;
}

.side-menu__container-active::before {
  opacity: 0.3;
}

.slide-menu {
  box-sizing: border-box;
  transform: translateX(-103%);
  position: relative;
  top: 0;
  left: 0;
  z-index: 10;
  height: 100%;
  width: 90%;
  max-width: 26rem;
  background-color: white;
  box-shadow: 0 0 2rem rgba(0, 0, 255, 0.1);
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 2fr 4fr 1fr;
  grid-gap: 1rem;
  transition: transform 300ms linear;
  will-change: transform;
}

.slide-menu-active {
  transform: none;
}

.menu-header {
  background: linear-gradient(to right, #00FF9B, #5f84fb);
  display: grid;
  grid-template-rows: 1fr 4fr;
  grid-template-columns: 1fr 4fr;
  grid-template-areas: "greeting greeting" "image details";
  box-sizing: border-box;
  width: 100%;
  align-content: center;
  color: white;
  box-shadow: 0 0.5rem 2rem rgba(0, 0, 255, 0.2);
}

.greeting__text {
  grid-area: greeting;
  font-size: 1.25rem;
  letter-spacing: 0.15rem;
  text-transform: uppercase;
  margin-top: 1rem;
  justify-self: center;
  align-self: center;
}

.account-details {
  grid-area: details;
  display: flex;
  flex-flow: column;
  margin-left: 1rem;
  align-self: center;
}

.name__text {
  font-size: 1.15rem;
  margin-bottom: 0.5rem;
}

.email__text {
  font-size: 0.9rem;
  letter-spacing: 0.1rem;
}

.menu-body {
  display: grid;
  width: 100%;
}

.profile-image__container {
  grid-area: image;
  margin-right: 0.5rem;
  border-radius: 50%;
  height: 4rem;
  width: 4rem;
  overflow: hidden;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: white;
  align-self: center;
  margin-left: 2rem;
}

.profile__image {
  max-width: 4rem;
}

/*Header*/
.main__header {
  width: 100%;
  display: grid;
  grid-template-columns: 1fr 1fr 0.25fr;
  grid-template-rows: 1fr;
  box-shadow: 0 0 2rem rgba(0, 0, 255, 0.1);
  height: 4rem;
  margin: 0;
  align-items: center;
  transition: background-color 500ms linear;
  animation: 1s ease-in-out 0ms 1 fadein;
}

.main__header-dark {
  background-color: #2B244D;
  color: white;
}

.toggle-button__container {
  cursor: pointer;
  position: relative;
  margin: 0 0.5rem;
}

.mode-toggle__input {
  -webkit-appearance: none;
  -moz-appearance: none;
}

.mode-toggle__bg {
  height: 1rem;
  width: 2rem;
  border-radius: 0.5rem;
  background-color: rgba(0, 0, 0, 0.5);
  display: inline-block;
  transition: background-color 300ms linear;
}

.mode-toggle__circle {
  height: 1.30rem;
  width: 1.30rem;
  background-color: #2B244D;
  position: absolute;
  top: -0.2rem;
  border-radius: 50%;
  box-shadow: 0 0 0 rgba(0, 0, 255, 0.5);
  transition: left 300ms linear;
  left: 0.1rem;
}

.mode-toggle__circle-checked {
  background-color: white;
  left: 1.75rem;
}

.mode-toggle__bg-checked {
  background-color: #FF0070;
}

.mode-toggle__text {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.1rem;
}

/*Content*/
.left__section {
  display: grid;
  grid-template-rows: 1fr;
  grid-template-columns: 1fr 1fr;
  max-width: 5rem;
}

.date__text {
  text-transform: uppercase;
  letter-spacing: 0.1rem;
  display: inline;
  margin: 0.5rem 0;
}

/*SVGs*/
.hamburger__icon {
  position: relative;
  z-index: 35;
  height: 1rem;
  padding: 0.5rem 1.5rem;
  margin-right: 1rem;
  cursor: pointer;
}

.logo__icon {
  height: 2rem;
  margin-left: 1rem;
}

.logo__text {
  fill: #2B244D;
}

.logo__text-dark {
  fill: #ffff;
}

.hamburger__icon__fill {
  fill: #2B244D;
}

.hamburger__icon__fill-dark {
  fill: #ffff;
}

/*
================
    Body
================
*/

.main-container__bg {
  height: 100%;
  width: 100%;
  position: absolute;
  top: 0;
  left: 0;
  z-index: -2;
  opacity: 0;
  background: white;
  transition: opacity 300ms linear;
}

.main-container__bg-dark {
  opacity: 1;
  background: linear-gradient(to bottom, #B290FF, #2E1D65);
  transition: opacity 300ms linear;
}

/*
================-
    Footer
================
*/
.main__footer {
  background: transparent;
  position: absolute;
  bottom: 1rem;
  left: 1.5rem;
  z-index: 100;
}

.copyright__text {
  letter-spacing: 0.1rem;
  color: white;
}

@media only screen and (max-width: 300px) {
  .slide-menu {
    width: 100%;
  }
}

Hoa mắt nhỉ, giờ giải thích cụ thể hơn đoạn CSS trên nhé

  • Bố cục
display: grid;  
grid-template-columns: auto;
grid-template-rows: 0.5fr auto;

Ở đây ta dùng CSS grid để chia bố cục, 1 phần nhỏ phía trên cho navbar, phần to phía dưới cho router outlet, cũng chính là nội dung của trang, giống như thế này:

Hướng dẫn angular render html

  • sidenav
.side-menu__conatiner {
    position: fixed; 
    left: 0;
    top: 0 
}

fix cứng vị trí của sidenav là trên cùng bên tay trái

.slide-menu { transform: translateX(-103%); }

translateX ở đây sẽ dịch chuyển slide-menu theo trục X 1 đoạn -103%, tức là giấu nó vào bên trái ý, sau đó ta thêm vào class .slide-menu-active

.slide-menu-active {  transform: none; }

để reset lại, slide-menu sẽ hiện thị ra luôn, ko bị giấu đi nữa.

Bố cục của chúng ta như thế này

Hướng dẫn angular render html

Demo

Hướng dẫn angular render html

Chế độ ban đêm

Bạn có thể thấy có 1 số css thêm hậu tố -dark là để dùng cho chế độ ban đêm, vẫn dùng ngClass để chuyển đổi class. Ví dụ trong trường hợp này, khi chuyển sang chế độ ban đêm

Hướng dẫn angular render html

  • Add card component

Ở component add card thì ta cũng thêm ngClass cho chế độ ban đêm, và thêm routerLink để điều hướng người dùng đến trang thêm thành phố khi click vào card đó

<div class="add__card" routerLink="/add" [ngClass]="{'add__card-dark': darkMode}">
  <div class="header__container">
  <span class="card__title">Add city</span>
  </div>
  <div class="body__container">
    <svg class="add__icon"></svg>
    <svg class="city__illustration"></svg>
  </div>
</div>

Css về cơ bản cũng không khác quá nhiều, vì nó cần đồng bộ vs các card khác, vẫn sử dụng grid layout

.add__card {
  background-color: #ffffff;
  box-shadow: 0 0 2rem rgba(0, 0, 255, 0.1);
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr 1fr;
  padding: 2rem;
  margin: 2rem;
  width: 19rem;
  height: 30rem;
  justify-items: center;
  cursor: pointer;
  border-radius: 1.75rem;
  animation: 1.25s ease-in-out 0ms 1 fadein;
  color: #443282;
}

.add__card-dark {
  background: linear-gradient(to bottom, #711B86, #00057A);
  color: white;
}

.card__title {
  text-transform: uppercase;
  letter-spacing: 0.1rem;
}

.city__illustration {
  width: 20rem;
}

.body__container {
  align-self: end;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-flow: column;
}

.add__icon {
  width: 10rem;
  margin-bottom: 1.15rem;
}

Kết quả

Hướng dẫn angular render html

  • Detail component

Đây là component để hiển thị chi tiết thời tiết 1 thành phố. Ta sử dụng WeatherService để lấy dữ liệu thời tiết trong ngày hôm nay và 5 ngày tới, lưu vào các biến riêng để hiển thị ngoài màn hình

import {Component, OnDestroy, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {WeatherService} from '../../services/weather/weather.service';
import {Subscription} from 'rxjs';

@Component({
  selector: 'app-details',
  templateUrl: './details.component.html',
  styleUrls: ['./details.component.css']
})
export class DetailsComponent implements OnInit, OnDestroy {

  city: string;
  state: string;
  temp: number;
  hum: number;
  wind: number;

  today: string;

  day1Name: string;
  day1State: string;
  day1Temp: number;


  day2Name: string;
  day2State: string;
  day2Temp: number;

  day3Name: string;
  day3State: string;
  day3Temp: number;

  day4Name: string;
  day4State: string;
  day4Temp: number;

  day5Name: string;
  day5State: string;
  day5Temp: number;

  sub1: Subscription;
  sub2: Subscription;
  sub3: Subscription;
  sub4: Subscription;
  sub5: Subscription;

  constructor(public activeRouter: ActivatedRoute, public weather: WeatherService) {
  }

  ngOnInit() {

    const todayNumberInWeek = new Date().getDay();
    const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
    this.today = days[todayNumberInWeek];

    this.activeRouter.paramMap.subscribe((route: any) => {

      this.city = route.params.city;
      this.sub1 = this.weather.getWeatherState(this.city).subscribe((state) => this.state = state);
      this.sub2 = this.weather.getCurrentTemp(this.city).subscribe((temperature) => this.temp = temperature);
      this.sub3 = this.weather.getCurrentHum(this.city).subscribe((humidity) => this.hum = humidity);
      this.sub4 = this.weather.getCurrentWind(this.city).subscribe((windspeed) => this.wind = windspeed);
      this.sub5 = this.weather.getForecast(this.city).subscribe((data: any) => {
        console.log(data);
        for (let i = 0; i < data.length; i++) {
          const date = new Date(data[i].dt_txt).getDay();
          console.log(days[date]);
          if (((date === todayNumberInWeek + 1) || (todayNumberInWeek === 6 && date === 0)) && !this.day1Name) {
            this.day1Name = days[date];
            this.day1State = data[i].weather[0].main;
            this.day1Temp = Math.round(data[i].main.temp);

          } else if (!!this.day1Name && !this.day2Name && days[date] !== this.day1Name) {
            this.day2Name = days[date];
            this.day2State = data[i].weather[0].main;
            this.day2Temp = Math.round(data[i].main.temp);

          } else if (!!this.day2Name && !this.day3Name && days[date] !== this.day2Name) {
            this.day3Name = days[date];
            this.day3State = data[i].weather[0].main;
            this.day3Temp = Math.round(data[i].main.temp);

          } else if (!!this.day3Name && !this.day4Name && days[date] !== this.day3Name) {
            this.day4Name = days[date];
            this.day4State = data[i].weather[0].main;
            this.day4Temp = Math.round(data[i].main.temp);

          } else if (!!this.day4Name && !this.day5Name && days[date] !== this.day4Name) {
            this.day5Name = days[date];
            this.day5State = data[i].weather[0].main;
            this.day5Temp = Math.round(data[i].main.temp);

          }
        }
      });

    });

  }

  ngOnDestroy() {
    this.sub1.unsubscribe();
    this.sub2.unsubscribe();
    this.sub3.unsubscribe();
    this.sub4.unsubscribe();
    this.sub5.unsubscribe();
  }
}

Phần HTML của trang này có rất nhiều svgs nên rất là dài, vì thế sẽ không đề cập đến trong bài này, bạn có thể vào đây để xem full code nhé.

Bài dài quá rồi nên lần sau mình sẽ nói tiếp phần service và router nhé. :3

Nguồn: https://medium.com/@hamedbaatour/build-a-real-world-beautiful-web-app-with-angular-6-a-to-z-ultimate-guide-2018-part-i-e121dd1d55e