iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 5
0
Modern Web

新手也能懂的JS30系列 第 5

JS30-Day5-Flex Panels

Day5-課題內容

今天進入JS30的第五天,今天將透過非常酷炫的畫面特效,來認識CSS3中的Flex屬性。[1]
實作連結

首先在作者給的範例當中,我們可以看到頁面被分成了五個區塊,分別包含不同背景以及文字。當我們按下其中的任一區塊,該區塊的寬度會增加,並且裡面的字體會放大,再次按下則會恢復原本的樣貌。而轉換時的動畫特效就是使用第二天所學到的transformtransition的CSS屬性。

認識Flex

要精準的使用flex屬性,我們就需要了解其兩個重要的特性:Flexible Box Layout Module以及Flex Property。

1. Flexible Box Layout Module

根據w3schools的參考資料,Flex的Flexible Box特性,是一種新的Layout模型。如同其字面意思,它可以讓我們靈活的安排頁面的排版結構,取代原本舊CSS中的float以及position屬性。[2]
接下來將透過幾個參考網站中的gif圖檔,來介紹幾個常用的Flexible Box Layout Module屬性。[3]

  1. Display: Flex
    透過將容器的display修改為flex屬性,內部元素的排版方式,將被更改為類似inline-block屬性的樣貌,並且可以使用flex專屬的CSS屬性。
#container {
  display: flex;
}

  1. Flex Direction
    display:flex屬性,會將頁面上的方向,分成Main-Axis(主要軸)以及Cross-Axis(交叉軸),而flex-direction指定的方向,將成為主要軸的方向,其垂直的方向則成為交叉軸。
    這個屬性值,分成row(行)column(列)reverse-row(反向行)reverse-column(反向列),若無特別指定,預設值為flex-direction:row
#container {
  display: flex;
  flex-direction: column;
}


  1. Justify Content
    Justify-content屬性,為控制主要軸上各元素對齊方式的屬性,其中總共包含了五種條件,flex-startflex-endcenterspace-betweenspace-around
#container {
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
}

  1. Align Items
    Align-items屬性,則是控制交叉軸上各元素對齊方式的屬性,其中也包含了五種條件,flex-startflex-endcenterstretchbaseline
#container {
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
}

透過justify-content以及align-items屬性,可以做出輕鬆做出絕對置中,但是當flex-direction的不同時,所呈現的置中行為也會有所不同。

  1. Align Self
    align-selfalign-items屬性類似,都是控制交叉軸上的對齊方式,但前者是單一元素自己,後者則是控制容器中的所有元素。
#one {
  align-Self: flex-start;
}

#two {
  align-Self: flex-start;
}

2. Flex property

flex property為使用flex時最重要的屬性,該屬性由flex-growflex-shrinkflex-basis三個子屬性組合而成,只填一個數值的話將default給定為flex-growth屬性值:

{
flex:flex-grow flex-shrink flex-basis;
}

flex property最特別的地方,就是他能讓flex容器內的元素,利用此flex屬性,讓元素隨著容器的大小變化也跟著變化。[4][5]
當flex容器的主要軸長度,大於所有元素的長度總和,也就是說當剩餘空間 > 0,裡面元素就會隨著其flex-grow設定延伸,來補滿整個容器。而當flex容器的主要軸長度,小於所有元素的長度總和,也就是說剩餘空間 < 0時,裡面元素就會隨著其flex-shrink設定壓縮,避免元素突出容器。
因此我們能利用其以上的特點,來做出響應式的頁面效果,以下就來介紹各個子屬性:

  1. flex-grow
    元素延伸時的,該元素獲得容器剩餘空間的分配比例。此屬性值無單位,預設值為1。設定0的話不會被分配到剩餘空間,該元素將不會延伸。
  2. flex-shrink
    元素壓縮時的,該元素獲得容器剩餘空間的分配比例。此屬性值無單位,預設值為1,設定0的話不會被分配到剩餘空間,該元素將不會壓縮。
  3. flex-basis
    flex條件中元素的基本大小,若無設定該值,其預設值為元素本身的大小。若有設定,其值會覆蓋掉設定的高或寬(主要軸上的長度),也當作flex-shrink時的分配比例之一。

實際範例:

  1. flex-grow:
延伸空間 = (該元素flex-grow/所有元素flex-grow總和)*剩餘空間

原本每個元素的寬為100px,但因為flex容器的大小為1000px,所以還有600px的剩餘空間,而每個元素的flex-grow都為1,因此每個元素的延伸空間都為 1 / 4 x 600 = 150px,因此延伸後每個元素的寬度都為100+150=250px。

container{
    width:1000px;
    display:flex;
}
item{
  flex-basis: 100px;
}

container{
    width:1000px;
    display:flex;
}
item{
  flex-grow: 1;
  flex-basis: 100px;
}

  1. flex-shrink:
壓縮空間 = -(該元素flex-shrink*flex-basis/所有元素flex-shrink*flex-basis總和)*剩餘空間

原本每個元素的寬為200px,但因為flex容器的大小為1000px,所以剩餘空間為1000-8x200 = -600px,而每個元素flex-shrink預設值都為1,且flex-basis = 100px,因此每個元素的壓縮空間都為 (1x100/8x1x100)x600,因此壓縮後每個元素的大小都為200-75=125px。

container{
    width:1000px;
    display:flex;
}
item{
  flex-basis: 200px;
}

container{
    width:1000px;
    display:flex;
}
item{
  flex-shrink: 1;
  flex-basis: 200px;
}

設定CSS屬性及頁面顯示

了解以上關於flex的特性之後,我們可以開始著手今天的課題。第一件事就是將元素們加上flex屬性,可以看到草稿中的DOM以及目前頁面如下:

<div class="panels">
    <div class="panel panel1">
      <p>Hey</p>
      <p>Let's</p>
      <p>Dance</p>
    </div>
    <div class="panel panel2">
      <p>Give</p>
      <p>Take</p>
      <p>Receive</p>
    </div>
    <div class="panel panel3">
      <p>Experience</p>
      <p>It</p>
      <p>Today</p>
    </div>
    <div class="panel panel4">
      <p>Give</p>
      <p>All</p>
      <p>You can</p>
    </div>
    <div class="panel panel5">
      <p>Life</p>
      <p>In</p>
      <p>Motion</p>
    </div>
  </div>

首先我們需要先將五個.panel元素轉成與作者相同的排列方式,因此我們在其容器元素.panels的CSS屬性中,加上display:flex;並指定加上flex-direction:row。但.panel元素並沒有被給定寬度,因此其寬度目前是由內容物的大小所撐出來的,因此會看到以下這樣的情況。

.panels {
      min-height:100vh;
      overflow: hidden;
      display:flex;
      flex-direction: row;
    }


可以看到黃色的區域,就是前面提到的剩餘空間,因此我們在.panel元素中加入flex-grow = 1的屬性,讓每個元素均勻分布並將容器填滿。

但我們的.panel元素中的<p>元素依然擠在畫面上方,因此我們也要將其容器.panel元素,加上display:flex;屬性,並指定方向flex-direction:column
除此之外,<p>元素也須加上幾個flex的相關屬性:

  1. flex-grow:1
    透過平分容器高度,而達到填滿容器的視覺效果。
  2. display:flex
    為了使用flex的方法將文字置中,所以將display屬性由原本inline改為flex。
  3. justify-content: center
    利用此屬性將文字做水平置中。
  4. align-items: center
    利用此屬性將文字做垂直置中。
//新增的CSS屬性
.panel {
      flex-grow:1;
      display: flex;
      flex-direction: column;
      
}
.panel p {
      flex-grow:1;
      display:flex;
      justify-content: center;
      align-items: center;
}

完成新增這些的CSS屬性設定之後,就可以看到我們的頁面呈現完整的均勻分配。

但是在作者範例中,頁面剛開啟的時候,最上面跟最下面的<p>元素,並沒有顯示在頁面上,而是點下滑鼠之後,才從上方與下方飛入。這邊我們就要用到第二天中所使用到的CSS transform屬性,透過其子屬性translate(),將這兩個<p>元素移出畫面之外。
這邊透過.panel > *:first-child以及.panel > *:last-child的方法,取得.panel中的第一及最後一個元素,並加上translateY()屬性,來使這兩個元素被移出畫面之外

.panel > *:first-child { transform: translateY(-100%); }
.panel > *:last-child { transform: translateY(100%); }

加上觸發後的class與CSS屬性

.panel被按下的時候,其所佔的範圍會變大,最上層與最下層的<p>元素會移入頁面中,且該.panel元素中的字體都放大。
再次按下的時候,所佔的範圍會變小,最上層與最下層的<p>元素會移回原本的位置。
如同JS30第一天學到的內容,透過按下按鍵,觸發函式來轉換選定元素的CSS屬性。首先我們需要先設定觸發函式後的元素,需要加上哪些屬性:

  1. flex-grow
    因為被按下的元素會變寬,因此可以透過改變其flex-grow的分配比例,來達到變大的效果。
  2. font-size
    透過更改此屬性值,將字體放大。
  3. transform:translateY()
    因剛開始最上層與最下層的<p>元素都不在頁面中,因此需更改此屬性,將這兩個元素移回頁面中。
    將以上的條件組合起來,我們需要新增的CSS屬性如下,當元素被按下時,會被新增一.open的class,而最上層與最下層的<p>元素會被加上.open-active的class。如此一來,我們就能完成透過class轉化,做出這次課題中的特效。
.panel.open {
      flex-grow:5;
      font-size:40px;
}
.panel.open-active > *:first-child { transform: translateY(0); }
.panel.open-active > *:last-child { transform: translateY(0); }

監聽事件

首先我們將每個.panel元素加上監聽事件,透過選折器將每一個元素選起來,並將選取器得到的陣列,利用前一章所學到的forEach()方法,將每個元素都加上監聽事件click,而這邊作者為了讓特效完全表現出來,所以將最上層與最下層的<p>元素移動的時間移到完成字體放大縮小之後,因此這邊需額外監聽transitionend這個事件:

const allPanel = document.querySelectorAll('.panel');
allPanel.forEach(function (panel) { panel.addEventListener('click', toggleOpen) });
allPanel.forEach(function (panel) { panel.addEventListener('transitionend', toggleOpenActive) });

函示設定

  1. click事件函式:
    因為都是在相同的事件click後觸發class的轉換,因此這邊使用了classList.toggle(),這個方法可以判斷元素當中是否有指定的class,如果沒有則新增上去,若有則移除該class:
function toggleOpen(event) {
    //event.target為觸發事件的目標元素
    event.target.classList.toggle('open');
};
  1. transitionend事件函示:
    因要等到字體放大或縮小完成之後,再進行移動,但我們直接將函式寫成如上方的函示,只將open修改為open-active,會發現我們的<p>元素並不會進入畫面,這是為什麼呢?
    透過在函示中加入console.log(event),我們可以從開發者模式中,觀察到有兩個transitionend事件被觸發,因此第一個transitionend事件結束後會將class加上,但是第二個transitionend事件結束後又把class改回來,因此我們的<p>元素便無法成功加上open-active這個class。
    因此我們需加上一判斷式,當transitonend事件中包含其中一個轉換的屬性,便執行加上open-active這個class的動作。
function toggleOpenActive(event) {
    console.log(event);
    if (event.propertyName.includes('flex')) {
        this.classList.toggle('open-active');
    }
};

總結

在今天的課題當中,我們學到以下的技能:

  1. Flexible Box Layout Module
  2. Flex property
  3. classlist.toggle()

Flex是一個非常強大的屬性,不管是其安排子元素排列方式的屬性,或是剩餘空間的運用,都能讓網頁的響應式互動提升,讓使用者在瀏覽的同時,能有更生動的體驗。以上是JS30第五天的分享心得,歡迎大家交流討論,感謝您的閱讀。

參考資料

  1. javascript30
  2. w3schools-Flexible Box Layout Module
  3. How Flexbox works 
  4. 深入解析 CSS Flexbox
  5. The Difference Between Width and Flex Basis

上一篇
JS30-Day4-Array Cardio
下一篇
JS30-Day6-Type Ahead
系列文
新手也能懂的JS3030
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Xuan
iT邦新手 5 級 ‧ 2017-12-25 21:16:54

HELLO
2. Flex property中的
1.flex-frow:
應該是grow?

另外 好文推推!

王郁翔 iT邦新手 5 級 ‧ 2017-12-25 22:06:49 檢舉

謝謝你幫忙抓出錯誤!!

我要留言

立即登入留言