---
# System prepended metadata

title: Angular Fade In/Out Div ( 此為 Component 範本 )
tags: [Angular]

---

# Angular Fade In/Out Div ( 此為 Component 範本 )
###### tags: `Angular`

## 先上測試結果圖
![](https://i.imgur.com/ypgGyTH.gif)

## 步驟/簡介
1. 用一個變數存取區塊(div)狀態 ( Default(其實就是隱藏)、FadeIn、FadeOut )
2. 提供一個 function 判斷 window 和 element 之間的位置關係去更改此區塊(div)狀態
3. 註冊 window 的 OnScroll、OnResize 去呼叫第 2 點的 function
4. ngAfterViewInit ( 讓 url 更換/首次進入頁面 trigger ) 中使用 requestAnimationFrame 方法 ( 為了等待元素正確渲染完畢計算位置 )，讓其呼叫第 2 點的 function
5. 依照狀態、方向 在 css 中訂好對應的 class name
6. 使用 [ngClass] 依照狀態去更改套用對應的 class


## 訂定 Fade In Or Out Component
### Component.html
```
<div #fadeDiv class="fadeDiv-main"
     [ngClass]="applyClass()">
  <ng-content select="[div-main-content]"></ng-content>
</div>
```

### Component.ts
```
import { Component, HostListener, Input, OnInit, ViewChild, ViewContainerRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-fade-in-or-out-div',
  templateUrl: './fade-in-or-out-div.component.html',
  styleUrls: ['./fade-in-or-out-div.component.css']
})
export class FadeInOrOutDivComponent implements OnInit, AfterViewInit {
  @Input() isFromLeft = true;

  @ViewChild('fadeDiv', { read: ViewContainerRef }) fadeDiv: ViewContainerRef;

  // 註冊 window scroll 事件
  // 不需要特別刪除事件，參考: https://stackoverflow.com/a/50015632
  @HostListener('window:scroll', ['$event']) onScroll(e: Event): void {
    this.fadeInOrOutInTheScreen();
  }

  // 註冊 window resize 事件
  @HostListener('window:resize', ['$event']) onResize(e: Event): void {
    this.fadeInOrOutInTheScreen();
  }

  // ※ 棄用原因: angular route change 的時候不會 trigger window onload
  // 註冊 window onload 事件
  // 如果 div 已經在畫面內直接顯示
  // ※: 原本使用 ngAfterViewInit 一樣沒有渲染完畢，導致 element offsetTop 還是錯的位置
  // 故使用 load 事件，等元素加載完畢後再檢測
  // @HostListener('window:load', ['$event']) onLoad(e: Event): void {
  //   if (this.isFirstLoaded == false) {
  //     this.fadeInOrOutInTheScreen();
  //     this.isFirstLoaded = true;
  //     console.log("window load")
  //   }
  // }

  isFirstLoaded: boolean = false;
  fadeStatus: E_FadeStatus = E_FadeStatus.Default;

  constructor() { }

  ngOnInit(): void {

  }

  ngAfterViewInit(): void {
    // 嘗試等待下一個動畫幀等 html 元素渲染完畢
    // 參考:
    // 1. JavaScript學習日記 : Day30 - JavaScript動畫: https://ithelp.ithome.com.tw/articles/10280810
    // 2. 計時與動畫: https://pjchender.dev/webapis/webapis-timer-and-scheduling/

    // console.log(this.isFirstLoaded)
    if (this.isFirstLoaded == false) {
      requestAnimationFrame(() => {
        this.fadeInOrOutInTheScreen();
      })
      this.isFirstLoaded = true;
    }
  }

  applyClass() {
    let baseClass = "";
    if (this.fadeStatus == E_FadeStatus.Default) {
      // console.log("hide")
      return "hide";
    }
    else if (this.fadeStatus == E_FadeStatus.FadeIn) {
      baseClass = "fadeIn";
    }
    else if (this.fadeStatus == E_FadeStatus.FadeOut) {
      baseClass = "fadeOut";
    }

    if (this.isFromLeft) {
      // console.log(baseClass + " left-to-right")
      return baseClass + " left-to-right";
    }
    else {
      // console.log(baseClass + " right-to-left")
      return baseClass + " right-to-left";
    }
  }

  fadeInOrOutInTheScreen() {
    const element = this.fadeDiv.element.nativeElement;

    // console.log("object offsetTop: " + element.offsetTop)
    // console.log("object clientHeight: " + element.clientHeight)
    // console.log("window scrollY: " + window.scrollY)
    // console.log("window outerHeight: " + window.outerHeight)

    // 棄用原因: 看到底部才跑出來，這樣區塊太長會看不到最上方的資訊，故改為看到區塊的最上方就開始 Fade In
    // var bottom_of_object = element.offsetTop + element.clientHeight;
    // var bottom_of_window = window.scrollY + window.outerHeight;
    // console.log("object bottom: " + bottom_of_object)
    // console.log("window bottom: " + bottom_of_window)

    var top_of_object = element.offsetTop + 200; // +200 讓其再下滑一點才 FadeIn，這樣能更清楚看到 FadeIn 效果
    var windowHeight = window.scrollY + window.outerHeight;

    // console.log("object top: " + top_of_object)
    // console.log("window height: " + windowHeight)


    // console.log(this.fadeStatus)

    if (windowHeight > top_of_object) {
      this.fadeStatus = E_FadeStatus.FadeIn;
    }
    else {
      // 如果是剛剛 Fade In 進入畫面的才需要 Fade Out
      if (this.fadeStatus == E_FadeStatus.FadeIn) {
        this.fadeStatus = E_FadeStatus.FadeOut;
      }
    }

    // console.log(this.fadeStatus)
  }
}

enum E_FadeStatus {
  Default = "Default",
  FadeIn = "FadeIn",
  FadeOut = "FadeOut",
}
```

### 測試荒唐鏡板 Component.css
```
div.fadeDiv-main {
  margin: 100px;
  padding: 100px;
  background-color: lightgreen;
}

div.fadeDiv-main.hide {
  opacity: 0;
}

div.fadeIn.right-to-left {
  background-image: url("https://pic1.xuehuaimg.com/proxy/baijia/https://f12.baidu.com/it/u=3910127792,1794832412&fm=173&app=25&f=JPEG?w=640&h=367&s=E0C296472A4108550ED12CA20300D002&access=215967316");
  background-position: center;
  background-repeat: no-repeat;
  background-size: contain;
  animation: fadeIn-right-to-left-animation 1.5s ease-out forwards;
}

@keyframes fadeIn-right-to-left-animation {
  0% {
    opacity: 0;
    transform: translateX(100%);
  }

  100% {
    opacity: 1;
    transform: translateX(0%);
  }
}

div.fadeIn.left-to-right {
  background-image: url("https://pic1.xuehuaimg.com/proxy/baijia/https://f12.baidu.com/it/u=3910127792,1794832412&fm=173&app=25&f=JPEG?w=640&h=367&s=E0C296472A4108550ED12CA20300D002&access=215967316");
  background-position: center;
  background-repeat: no-repeat;
  background-size: contain;
  animation: fadeIn-left-to-right-animation 1.5s ease-out forwards;
}

@keyframes fadeIn-left-to-right-animation {
  0% {
    opacity: 0;
    transform: translateX(-100%);
  }

  100% {
    opacity: 1;
    transform: translateX(0%);
  }
}

div.fadeOut.right-to-left {
  background-image: url("https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQV-1hvRnGwpIgG2RmB8eYtBv9nnVNtoNYuDHoOrKkQ5RmXaGDwMhZgrmsDC1qEr3BpT-c&usqp=CAU");
  background-position: center;
  background-repeat: no-repeat;
  background-size: contain;
  animation: fadeOut-right-to-left-animation 1.5s ease-out forwards;
}

@keyframes fadeOut-right-to-left-animation {
  0% {
    opacity: 1;
    transform: translateX(0%);
  }

  100% {
    opacity: 0;
    transform: translateX(100%);
  }
}

div.fadeOut.left-to-right {
  background-image: url("https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQV-1hvRnGwpIgG2RmB8eYtBv9nnVNtoNYuDHoOrKkQ5RmXaGDwMhZgrmsDC1qEr3BpT-c&usqp=CAU");
  background-position: center;
  background-repeat: no-repeat;
  background-size: contain;
  animation: fadeOut-left-to-right-animation 1.5s ease-out forwards;
}

@keyframes fadeOut-left-to-right-animation {
  0% {
    opacity: 1;
    transform: translateX(0%);
  }

  100% {
    opacity: 0;
    transform: translateX(-100%);
  }
}

```

### 正常使用版 Component.css
```
div.fadeDiv-main {
  width: 100%;
  height: 100%;
  padding: 5px;
}

div.fadeDiv-main.hide {
  opacity: 0;
}

div.fadeIn.right-to-left {
  animation: fadeIn-right-to-left-animation 1.5s ease-out forwards;
}

@keyframes fadeIn-right-to-left-animation {
  0% {
    opacity: 0;
    transform: translateX(100%);
  }

  100% {
    opacity: 1;
    transform: translateX(0%);
  }
}

div.fadeIn.left-to-right {
  animation: fadeIn-left-to-right-animation 1.5s ease-out forwards;
}

@keyframes fadeIn-left-to-right-animation {
  0% {
    opacity: 0;
    transform: translateX(-100%);
  }

  100% {
    opacity: 1;
    transform: translateX(0%);
  }
}

div.fadeOut.right-to-left {
  animation: fadeOut-right-to-left-animation 1.5s ease-out forwards;
}

@keyframes fadeOut-right-to-left-animation {
  0% {
    opacity: 1;
    transform: translateX(0%);
  }

  100% {
    opacity: 0;
    transform: translateX(100%);
  }
}

div.fadeOut.left-to-right {
  animation: fadeOut-left-to-right-animation 1.5s ease-out forwards;
}

@keyframes fadeOut-left-to-right-animation {
  0% {
    opacity: 1;
    transform: translateX(0%);
  }

  100% {
    opacity: 0;
    transform: translateX(-100%);
  }
}
```



## 呼叫 Fade In Or Out Component

### 呼叫時特別注意

錯誤示範↓
![](https://i.imgur.com/Ng6ZPx6.png)
今日頭條6以後，最下面兩個div不該出現的

img 沒有給 height/width attribute 的情況下 
而是 style 設定 width/height 都 100% ( 自適應大小 )

這樣會因為 img 剛剛還沒 loading 出來 ( img 沒設定固定高度 => 沒讀到圖片的情況下高度為 0 )

導致下面的 div 位置在渲染階段的時候被認為該出現 ( 還沒被圖片推下去 )
但其實不應該

所以使用時，<span style="color:red;font-weight: bold">區塊最好先設定好該有的高度</span>

### html

```
<!-- class 可有可無，若要覆蓋基本 style 再給 class -->
<app-fade-in-or-out-div class="testDiv" [isFromLeft]="false">
  <div div-main-content>
    <h2>今日頭條</h2>
  </div>
</app-fade-in-or-out-div>
```

### css
若要覆蓋 style 的寫法
```
::ng-deep .testDiv>div.fadeDiv-main {
  margin: 100px;
  padding: 100px;
  background-color: rgb(255, 255, 255) !important;
}
```

### 進階使用: 呼叫方使用 float 進行兩側分割畫面 ( RWD 適應 )

#### 結果
![](https://i.imgur.com/u3SZMXH.gif)


#### css
```
.news-wrap {
  /* 可以用來清除浮動，去除 margin 合併 */
  display: flow-root;
}

.news-wrap>.left {
  width: 50%;
  line-height: 30px;
  padding: 5px;
  border: 2px blue solid;
  float: left;
}

.news-wrap>.right {
  width: 50%;
  line-height: 30px;
  padding: 5px;
  border: 2px green solid;
  float: left;
}

@media screen and (max-width: 600px) {
  .news-wrap>.left {
    width: 100%;
  }

  .news-wrap>.right {
    width: 100%;
  }
}

```

#### html
```
<div class="news-wrap">
  <div class="left">
    <app-fade-in-or-out-div [isFromLeft]="true">
      <div div-main-content
           class="rounded shadow bg-white">
        <h2 class="m-0 py-2">今日頭條1</h2>
        <h4><a href="https://ynews.page.link/EjrC">俄烏戰停火有解？ 美媒推測普丁在等「完全解放」頓巴斯</a></h4>
        <img class="w-75 rounded shadow"
             style="max-width: 500px;"
             onerror="this.src='assets/Images/404-error.png'"
             [src]="'https://images.pexels.com/photos/3526022/pexels-photo-3526022.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'" />
        <p class="mt-3 text-start px-3">
          俄烏戰爭開打迄今已經超過4個多月，而隨著全球經濟所受到的衝擊愈來愈深，何時才能畫下句點，成了各國最關注的焦點，而現在最新消息傳出，俄羅斯總統普丁有可能在完全佔領頓巴斯（Donbas）地區後，提出停火協議。

          根據《CNBC》新聞網的報導，本周一普丁慶賀俄羅斯「解放」了盧甘斯克地區後，另一個宣布脫離烏克蘭的頓巴斯地區成了普丁下一個重點，據當地官員透露，俄羅斯的軍隊現正集中精力攻打頓巴斯，該地區週​​日遭到猛烈砲擊。

          對此專為國防產業提供建議的奧斯托亞諮詢（Ostoya Consulting）負責人阿布拉莫維奇（Victor Abramowicz）表示，俄羅斯「幾乎可以肯定」能夠在頓巴斯實現其更有限的目標，而普丁提議停火的前提，只要給他自己一個機會，來鞏固他迄今為止所取得的成果。

          阿布拉莫維奇表示，如果普丁在取得頓巴斯後提議停火，將會讓烏克蘭總統澤倫斯基陷入困境，如果他拒絕了停火協議，最嚴重的後果將會失去西方國家的支持。尤其美國今年11月將面臨期中選舉，在自身經濟持續低迷的情況下，拜登總統對於烏克蘭的堅定支持會不會出現鬆動，澤倫斯基必須慎重考量。
        </p>
      </div>
    </app-fade-in-or-out-div>
    <app-fade-in-or-out-div [isFromLeft]="true">
      <div div-main-content
           class="rounded shadow bg-white w-100">
        <h2 class="m-0 py-2">今日頭條3</h2>
        <h4><a href="https://ynews.page.link/BB3y">金曲33／小S當眾「襲胸」具俊曄 網虧：過氣主持風格</a></h4>
        <img class="w-75 rounded shadow"
             style="max-width: 500px;"
             onerror="this.src='assets/Images/404-error.png'"
             [src]="'https://images.pexels.com/photos/713149/pexels-photo-713149.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'" />
        <p class="mt-3 text-start px-3">
          「第33屆金曲獎」於昨（2）日晚間7時在高雄巨蛋隆重登場，由資深藝人羅時豐獨挑大樑主持典禮，並請來藝人小S（徐熙娣）與陶晶瑩一同搭檔演出，更邀請到大S老公、人稱「國民姊夫」的具俊曄擔任神秘嘉賓，不僅具俊曄於台上熱情的展現自我，小S更當眾對姊夫「襲胸」，眾多搞笑互動把現場氣氛炒熱到最高點。
          身為今年金曲獎神秘嘉賓具俊曄，在台上勇於表現自己，除了大秀一口流利的中文，更與小S大跳酷龍經典歌曲〈Bing Bing Bing〉，此外，小S還大膽伸手狂摸姊夫的胸部，具俊曄隨即撥開她的手並大喊「不可以這樣」、「安堆（韓文不行）」，有趣的互動笑翻台下眾人。
        </p>
      </div>
    </app-fade-in-or-out-div>
  </div>
  <div class="right">
    <app-fade-in-or-out-div [isFromLeft]="false">
      <div div-main-content
           class="rounded shadow bg-white w-100">
        <h2 class="m-0 py-2">今日頭條2</h2>
        <h4><a href="https://ynews.page.link/6gHv">本土34499例95死！ 1歲男童發燒、嘔吐「一天就心跳突停」不治</a></h4>
        <img class="w-75 rounded shadow"
             style="max-width: 500px;"
             onerror="this.src='assets/Images/404-error.png'"
             [src]="'https://images.pexels.com/photos/3952231/pexels-photo-3952231.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'" />
        <p class="mt-3 text-start px-3">
          中央流行疫情指揮中心今（6）日公布國內新增34577例COVID-19確定病例，分別為34499例本土個案及78例境外移入；另確診個案中新增95例死亡，中重症新增275例。今日新增死亡個案中，最小僅1歲男童，6月30日發病，出現嘔吐、呼吸喘、發燒等症狀，隔天送到急診時即突發心跳停止，急救不治。

          今日新增本土個案，依縣市區分，以新北市新增5324例最多，其次為台中市新增4669例、桃園市新增3687例、高雄市新增3608例、台北市新增3110例、台南市新增2902例。

          死亡個案95例當中，最年幼的1歲男童，本身有先天性神經系統疾病，長期使用呼吸器，6月30日發病，出現嘔吐、呼吸喘、發燒、活力及餵食量下降，7月1日送至醫院急診，一到急診後就突發心跳停止，經急救仍不治。死亡原因為新冠病毒感染跟本身疾病，歸類為共病死亡案例。

          另外，今日中重症新增275例，中症患者新增99例、重症患者新增68例。當中一口氣新增3例兒童重症MIS-C（兒童多系統發炎症候群），分別為0歲女童、2歲男童、12歲女童（曾接種一劑疫苗），均在6月初確診，6月底又開始出現發燒合併相關的腹瀉、眼睛紅、頸部腫等症狀，於6月29日至7月2日間就醫，經醫師診斷疑似MIS-C住院，目前都住院治療中。

          指揮中心統計，國內兒童（12歲以下）重症累計有88例，已有22例死亡；分別為腦炎22例、肺炎16例、MIS-C 30例、敗血症3例、哮吼症8症、家中死亡6例以及共病2例。
        </p>
      </div>
    </app-fade-in-or-out-div>
    <app-fade-in-or-out-div [isFromLeft]="false">
      <div div-main-content
           class="rounded shadow bg-white w-100">
        <h2 class="m-0 py-2">今日頭條4</h2>
        <h4><a href="https://udn.com/news/story/7266/6439509">高溫上看36度！午後大雷雨 吳德榮：周五起雨漸少但酷熱</a></h4>
        <img class="w-75 rounded shadow"
             style="max-width: 500px;"
             onerror="this.src='assets/Images/404-error.png'"
             [src]="'https://images.pexels.com/photos/1102915/pexels-photo-1102915.jpeg'" />
        <p class="mt-3 text-start px-3">
          中央大學大氣科學系兼任副教授吳德榮今天在「三立準氣象．老大洩天機」專欄表示，各國模式模擬顯示，熱帶低壓在日本本州南方近海，向東移動；未來有再恢復成輕颱的趨勢，並轉向沿本州東側北上。最新歐洲系集模式及美國模式模擬圖皆顯示，下周菲律賓東方至南海海面仍是有利熱帶擾動發展的海域，但模擬結果仍分歧，需持續密切觀察。

          他說，今天各地大多為多雲時晴的天氣；大氣不穩定，迎風面的南台灣不定時及中部以北的午後，皆有明顯降雨的機率，應注意大雷雨、伴隨劇烈天氣如雷擊、強風、瞬間強降雨，也要注意氣象局的特報。天氣偏熱，北部23至36度，中部22至35度，南部23至35度，東部22至35度。

          明天太平洋高壓仍偏弱，天氣晴熱，持續不穩定，午後易有強對流發生，仍應注意劇烈天氣。

          吳德榮表示，周五至下周二太平洋高壓逐日增強，晴朗炎熱，高溫升至36度左右，要防曬、防中暑；午後對流逐日減弱、範圍逐日縮小，大多在山區及少部分鄰近平地。
        </p>
      </div>
    </app-fade-in-or-out-div>
  </div>
</div>

```