學習資源

學習資源

Back-end

Ruby on Rails

Front-end

JavaScript

Vue.js

Markdown

在 Table 裡面使用 Form


把 form 放在 table, tbody, 或 tr 底下,瀏覽器會把 form 移動位子,而裡面的元素則會留在原位。

<!-- 原來應該是這樣子 -->
<form>
    ... input fields ...
</form>
<!-- 現在變成這樣子 -->
<form></form>
... inputs fields ...

如果將整個 table 封裝在 form 中,問題在於所有表單元素都將在 submit 時發送。此方法允許您為每個 row (tr) 定義一個表單,並在 submit 時僅發送該行資料。

將 form 包著 tr 周圍(或將 tr 包著 form)的問題在於它是無效的 html,因為 DOM 已損壞。

解法1 - HTML form attribute

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefform

<form method="GET" id="my_form"></form>

<table>
    <tr>
        <td>
            <input type="text" name="company" form="my_form" />
            <button type="button" form="my_form">ok</button>
        </td>
    </tr>
</table>

把 form fields 跟 form 標籤分離,利用 form tag 的 id 讓 form field 去做關聯。 (IE不支援)

解法2 - 做個假 table

<style>
  DIV.table {
      display:table;
  }
  FORM.tr, DIV.tr{
      display:table-row;
  }
  SPAN.td{
      display:table-cell;
  }
</style>

...

<div class="table">
  <form class="tr" method="post" action="blah.html">
    <span class="td"><input type="text"/></span>
    <span class="td"><input type="text"/></span>
  </form>
  <div class="tr">
    <span class="td">(cell data)</span>
    <span class="td">(cell data)</span>
  </div>
  ...
</div>

IE 7 不支援 CSS table, IE8 需要宣告 doctype 才可以使用。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

Reference:
Form inside a table

JS: 簡易 Date Picker

如何使用 JavaScript 實作一個 date picker。參考教學: Custom Date Picker in JavaScript & CSS

這邊做一下筆記如何完成以及哪邊該注意的。

起始結構

- index.html
- reset.css
- style.css
- app.js
- simple-date-picker
        - simple-date-picker.css
        -    simple-date-picker.js

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Simple Date Picker</title>
  <link rel="stylesheet" href="reset.css">
  <link rel="stylesheet" href="style.css">
  <link rel="stylesheet" href="./simple-date-picker/simple-date-picker.css">
</head>
<body>
  <div class="container">
    <div id="date-picker"></div>
  </div>

  <script src="./simple-date-picker/simple-date-picker.js"></script>
  <script src="app.js"></script>
</body>
</html>

style.css

.container {
  width: 100vw;
  height: 100vh;
  background: rgb(249,242,206);
  background: linear-gradient(156deg, rgba(249,242,206,1) 2%, rgba(184,230,246,1) 96%);
  display: flex;
  justify-content: center;
  align-items: center;
}

#date-picker {
  width: 350px;
}

simple-date-picker.css

@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700&display=swap');

.simple-date-picker {
  font-family: 'Roboto', sans-serif;
  position: relative;
  width: 350px;
  height: 70px;
  background-color: mintcream;
  margin: 10px auto;
  box-shadow: 0px 2px 6px rgba(0,0,0,.2);
  cursor: pointer;
  user-select: none;
}

.simple-date-picker:hover {
  background-color: #e9f9ff;
}

.simple-date-picker .selected-date {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #313131;
  font-size: 30px;
}

.simple-date-picker .dates {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background-color: #fff;
}

// active 展開
.simple-date-picker .dates.active {
  display: block;
}

simple-date-picker.js

const datePicker = ({root}) => {
  root.innerHTML = `
    <div class="simple-date-picker">
      <div class="selected-date">08-25-2020</div>
      <div class="dates">
        <div class="month">
          <div class="btns prev-month"><<</div>
          <div class="month-label">八月</div>
          <div class="btns next-month">>></div>          
        </div>
          <div class="days"></div>
      </div>
    </div>
  `;

    const datepickerElement = root.querySelector('.simple-date-picker');
  const selectedDateElement = root.querySelector('.simple-date-picker .selected-date');
  const datesElement = root.querySelector('.simple-date-picker .dates');

  datepickerElement.addEventListener('click', toggleDatepicker);

  function toggleDatepicker(e){
    console.log('展開 datepicker')
  }
}

這邊的先用一個函式打包,方便以後可以在其他地方使用,也可以一次使用多個,不會互相打架。

app.js

datePicker({
  root: document.querySelector('#date-picker'),
});

在 app.js 裡呼叫,生成一個 date picker,順便指定要載入的元素 #date-picker。

展開/關閉日期選擇器

toggle 開關

現在日期選擇器可以展開,但是如果滑鼠點擊『月份選擇』區塊時,它會關閉。這時候需要檢查滑鼠所點擊的位置有沒有在 .dates 區塊。

在 simple-date-picker.js 裡面新增一個 helper function。

...(略)

datepickerElement.addEventListener('click', toggleDatepicker);

function toggleDatepicker(e){
  console.log(e.path);
  if (!checkEventPathForClass(e.path, 'dates')) {
    datesElement.classList.toggle('active');
  }
}

function checkEventPathForClass (path, selector) {
  for (let i = 0; i < path.length; i++) {
    if (path[i].classList && path[i].classList.contains(selector)) {
      console.log('path[i].classList', path[i].classList)
      console.log('contains selector? ', path[i].classList.contains(selector))
      return true;
    }
  }
  return false;
}

當 date picker 展開時,滑鼠點擊下方『月份選擇』區塊時,e.path 是有包含 selector dates,所以會 return true

if (!checkEventPathForClass(e.path, 'dates')) {
  datesElement.classList.toggle('active');
}

而我們要 checkEventPathForClass 非 true 時,才關閉日期選擇器。

點擊以外元素&關閉

如果要點擊 #date-picker (root) 以外的地方就關閉日期選擇棄,也可以用類似方法達到。

document.addEventListener('click', function(e){
  if (!root.contains(e.target)){
    datesElement.classList.remove('active');
  }
});

判斷 root element (date picker 所有元素)有沒有包含滑鼠點擊的元素(e.target)。頭兩次的 e.target 都沒有包含所以選擇器不會關閉,到了第三個才會 remove class active

月份 & 日期選擇器

CSS Style

/* 月份選擇 */
.simple-date-picker .dates .month {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 3px solid #eee;
  padding: 5px;
}

/* 月份選擇 左右按鈕 */
.simple-date-picker .dates .month .btns {
  width: 35px;
  height: 35px;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #313131;
  font-size: 20px;
  border-radius: 50%;
}

.simple-date-picker .dates .month .btns:hover {
  background-color: whitesmoke;
  color: #999;
}

.simple-date-picker .dates .month .btns:active {
  background-color: azure;
  color: #bbb;
}

在月份選擇區塊加入樣式。

/* 日期選擇器 */
.simple-date-picker .dates .days {
  height: 240px;
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  border-radius: 5px;
}

.simple-date-picker .dates .days .day {
  display: flex;
  justify-content: center;
  align-items: center;
  color: #313131;
  font-size: 15px;
  font-weight: 300;
  border-radius: 50%;
  border: 1px solid #eee;
  background-color: #eee;
  margin: 5px;
}

.simple-date-picker .dates .days .day:hover {
  font-weight: 700;
  background-color: #fff;
  border: 1px solid #eee;
}

.simple-date-picker .dates .days .day:active,
.simple-date-picker .dates .days .day.selected  {
  font-weight: 700;
  background-color: lightsalmon;
  border: 1px solid lightsalmon;
  color: #fff;
}

在日期區塊加入樣式。

const datePicker = ({root}) => {
  root.innerHTML = `
    <div class="simple-date-picker">
      <div class="selected-date">08-25-2020</div>
      <div class="dates">
        <div class="month">
          <div class="btns prev-month"><<</div>
          <div class="month-label">八月</div>
          <div class="btns next-month">>></div>          
        </div>
        <div class="days">

                    <!-- 這部分先加入日期,測試一下樣式 -->
          <div class="day">1</div>
          <div class="day">2</div>
          <div class="day">3</div>
          <div class="day">4</div>
          <div class="day">5</div>
          <div class="day">6</div>
          <div class="day">7</div>
          <div class="day">8</div>
          <div class="day">9</div>
          <div class="day">10</div>
          <div class="day">11</div>
          <div class="day">12</div>
          <div class="day">13</div>
          <div class="day">14</div>
          <div class="day">15</div>
          <div class="day">16</div>
          <div class="day">17</div>
          <div class="day">18</div>
          <div class="day">19</div>
          <div class="day">20</div>
          <div class="day">21</div>
          <div class="day">22</div>
          <div class="day">23</div>
          <div class="day">24</div>
          <div class="day">25</div>
          <div class="day">26</div>
          <div class="day">27</div>
          <div class="day">28</div>
          <div class="day">29</div>
          <div class="day">30</div>
          <div class="day">31</div>
        </div>
      </div>

    </div>
  `;

.... 略

Date Picker 預設日期

const datePicker = ({root}) => {
  root.innerHTML = `
    <div class="simple-date-picker">
            <!-- 填入的日期先清空,要讓 JS 自動填入當下的日期 -->
      <div class="selected-date"></div>
      <div class="dates">
        <div class="month">
          <div class="btns prev-month"><<</div>
          <div class="month-label"></div>
          <div class="btns next-month">>></div>          
        </div>
        <div class="days"></div>
      </div>

    </div>
  `;

... 略

selectedDateElement.textContent = formatDate(date);

function formatDate(d) {
  let day = d.getDate();
  let month = d.getMonth();
  let year = d.getFullYear();

    if (day < 10) {
    day = `0${day}`;
  }

  if (month < 10){
    month = `0${month+1}`
  }
  return `${year}年 ${month}月 ${day}日`;
}

展開時顯示月份年份

const monthElement = root.querySelector('.simple-date-picker .dates .month .month-label');
const prevBtn = root.querySelector('.simple-date-picker .dates .month .prev-month');
const nextBtn = root.querySelector('.simple-date-picker .dates .month .next-month');
const daysElement = root.querySelector('.simple-date-picker .dates .days');

const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

// 當下日期
let date = new Date();
let day = date.getDate();
let month = date.getMonth();
let year = date.getFullYear();

// 選擇的日期
let selectedDate = date;
let selectedDay = day;
let selectedMonth = month;
let selectedYear = year;

monthElement.textContent = `${ month+1 }月  ${year}年`;  // 展開時顯示的月份&年份

月份往前/後移動&更新畫面

prevBtn.addEventListener('click', goPrevMonth); // 往前一個月事件監聽
nextBtn.addEventListener('click', goNextMonth); // 往後一個月事件監聽

// 當月份往前到 -1 時,要換成 11 (十二月),年也要跟著減1
function goPrevMonth() {
  month--;
  if (month < 0) {
    month = 11;
    year--;
  }
  monthElement.textContent = `${ month+1 }月  ${year}年`;  // 展開時顯示的月份&年份
}

// 當月份走到12時 (即十三月),要換成 0 (一月),年也要跟著加1
function goNextMonth() {
  month++;
  if (month > 11) {
    month = 0;
    year++;
  }
  monthElement.textContent = `${ month+1 }月  ${year}年`;  // 展開時顯示的月份&年份
}

加入對應日期

populateDates(month, year);

function goPrevMonth() {
    ... 略
  populateDates(month, year);
}

function goNextMonth() {
  ... 略
  populateDates(month, year);
}

function populateDates(mth, yr) {
  daysElement.innerHTML = '';
  let amountDays = getTotalDays(mth, yr); // 取得月份共幾天

  for (let i = 0; i < amountDays; i++) {
    const dayElement = document.createElement('div');
    dayElement.classList.add('day');
    dayElement.textContent = i + 1;

   daysElement.appendChild(dayElement);
  }
}

function getTotalDays(month, year) {
  return new Date(year, month+1, 0).getDate();
}

當要加入對應的日期數量需要先知道被選擇的月份有幾天,所以下面的 getTotalDays() 函式會回傳月份日數,再用這個去跑回圈。

日期事件監聽

當進入 populateDates() 回圈時,需要幫 dayElement 元素個別加入事件監聽,這樣才知道到底選擇了哪一天。

function populateDates(mth, yr) {
  daysElement.innerHTML = '';
  let amountDays = getTotalDays(mth, yr); // 取得月份共幾天

  for (let i = 0; i < amountDays; i++) {
    const dayElement = document.createElement('div');
    dayElement.classList.add('day');
    dayElement.textContent = i + 1;

    ++ // 遇到當下日期/被選擇的日期,加上 css style
    ++ if (selectedDay == (i + 1) && selectedYear == year && selectedMonth == month) {
    ++   dayElement.classList.add('selected');
    ++ }

    ++ dayElement.addEventListener('click', function() {
    ++   selectedDate = new Date(year + '-' + (month+1) + '-' + (i+1));
    ++   selectedDay = (i + 1);
    ++   selectedMonth = month;
    ++   selectedYear = year;

      ++ selectedDateElement.textContent = formatDate(selectedDate);
      ++ selectedDateElement.dataset.value = dataSetFormat(selectedDate); // 在元素上加入 dataset,之後要取得日期比較容易。

      ++ populateDates(selectedMonth, selectedYear); // 當選擇了某一天,日期區塊需要重新更新。
    ++ });

   daysElement.appendChild(dayElement);
  }
}

function dataSetFormat(d) {
  const year = d.getFullYear();
  const month = d.getMonth()+1;
  const day = d.getDate();
  return [year, month, day].join("-");
}

之後要取得選擇的日期可以直接從 data-value 取得。

加入週

/* WEEK */
.simple-date-picker .dates .weekday {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  border-radius: 5px;
  padding: 8px;
}

.simple-date-picker .dates .weekday .week {
  display: flex;
  justify-content: center;
  align-items: center;
  color: #313131;
  font-size: 13px;
  font-weight: 600;
  margin: 15px 0 0 0;
}

.simple-date-picker .dates .days .space {
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 5px;
  height: 36px;
}

function populateDates(mth, yr) {
  daysElement.innerHTML = '';

  let amountDays = getTotalDays(mth, yr); // 取得月份共幾天

++ let firstDay = getStartedWeekDay(mth, yr);
++   let spacer = ``;

++   for (let i = 0; i < firstDay; i++) {
++     spacer += `<div class="space"></div>`;
++   }

++   daysElement.innerHTML = spacer;

... 略
}

function getStartedWeekDay(month, year) {
  return new Date(year, month, 1).getDay();
}

使用 getStartedWeekDay() 取得月份起始日是週幾。 getDay() 會回傳本地時間星期中的日子(0-6,從星期日開始)。之後在 daysElement 加入幾個空格,把月份的 1 號撐開至對應的星期日子。


Sample
Code

Reference:
Custom Date Picker in JavaScript & CSS

Chrome Dev Tool: CSS Overview

在 Chrome 瀏覽器有個實驗性質的功能,很棒。有時候在瀏覽網站時發現有些設計很棒,想要偷師一下,CSS Overview 裡有幾個的功能真的要推一下。

  1. 首先需要打開 DevTools

Mac: command + option + i
Win: control + shift + i

  1. 進入 Settings

    在 DevTools 最上方右側有個齒輪 icon 可以進入 settings。
    Mac: function + F1
    Win: F1
    接著把左邊欄位第三行 Experiments 裡面 第四個 CSS Overview 打勾

重新打開瀏覽器。

  1. 打開 DevTools 裡面的 CSS Overview: Capture overview

    按下按鈕瀏覽器就會開始分析了。

  2. 分析報告

    除了摘要一些網頁裡共使用了幾個 HTML 元素,下了幾個 style 定義,用了幾個 css selector 之外還有顏色解析。


這個部分我很喜歡,清楚看到用了多少種背景顏色、文字顏色,甚至連 border 顏色都有。


媽呀,用了哪種字型、字體大小,出現幾次都有紀錄。


這個網站裡面有已定義卻未使用的 CSS 高達 14 個。這功能對於優化一個網站很棒!
下方還有 media queries。目前我觀察了幾個大型網站,media queries 都好長一串,看到眼花。

—

Chrome 這個實驗功能對於初學者我剛要練習切版真的太有幫助了。偶而遇到想要臨攀練習的網站,可以透過 CSS Overview 更理解有些元素怎麼規劃會比較恰當。

JS Day 49: 如何使用 axios 傳遞 token

AXIO GET Request

axios
  .get('http://www.abcd1234.com/api/users')
  .then( res => {
    console.log(res);
  })
  .catch( err => {
    console.log(err)
  })

一個 GET 請求可以這樣完成。

如果需要更多設定,例如回傳的格式、帳密驗證,就可以把需要設定的一些 key:value 寫在 config 裡面傳遞給 axios。

axios(url[, config])

const api = 'https://challenge.thef2e.com/api/thef2e2019/stage6/rooms';
const token = 'token_value';
const roomInfo = axios({
      method: 'GET',
      url: api,
      responseType: 'json', // responseType 也可以寫在 header 裡面
      headers: {
        // accept: 'application/json',
      },
        auth: {
        username: 'xiaoming',
        password: 'abc123'
      },
    })
    .then(function (response) {
  console.log(response);
    })
    .catch(function (error) {
      console.log('錯誤',error);
    });

這邊的 auth 是一個基本的 HTTP 驗證。它會生成一個 Authorization header 並覆蓋掉原有的。官方文件有特別說明,如果要傳遞 Bearer tokens 需要在 headers 裡頭自定義 Authorization

// auth indicates that HTTP Basic auth should be used, and supplies credentials.
// This will set an Authorization header, overwriting any existing
// Authorization custom headers you have set using headers.
// Please note that only HTTP Basic auth is configurable through this parameter.
// For Bearer tokens and such, use Authorization custom headers instead.

這邊有更多的 request config 可以參考。

const api = 'https://challenge.thef2e.com/api/thef2e2019/stage6/rooms';
const token = 'token_value';
const roomInfo = axios({
      method: 'GET',
      url: api,
      responseType: 'json', // responseType 也可以寫在 header 裡面
      headers: {
            Authorization: `Bearer ${token}` // Bearer 跟 token 中間有一個空格
      }
    })
    .then(function (response) {
  console.log(response);
    })
    .catch(function (error) {
      console.log('錯誤',error);
    });

Reference:

JS Day 47: AJAX Request - XMLHttpRequest, Fetch, AXIOS

XMLHttpRequest

  • JavaScript 原生的方法傳送請求
  • 不支援 Promises
  • 語法攏長,較多 callbacks,不好記

GET request

// GET request
const myReq = new XMLHttpRequest();

myReq.onload = function() {
  const data = JSON.parse(this.responseText);
};

myReq.onerror = function(err) {
  console.log('Error!', err);
};

myReq.open('get', 'https://www.icanhazdadjoke.com/', true');
myReq.setRequestHeader('Accept', 'application/json');
myReq.send();

POST request

// POST request
let xhr = new XMLHttpRequest();
xhr.open('POST', 'https://hexschool-tutorial.herokuapp.com/api/signup', true);

// 格式:application/x-www-form-urlencoded
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send('email=abcde@gmail.com');

// 格式:JSON
// let account = {
//   email:'abcdef@gmail.com',
//   password: '1234'
// };

// xhr.setRequestHeader("Content-type", "application/json");
// xhr.send(JSON.stringify(account));

範例:GET Request StarWars API (www.swapi.dev)

const req = new XMLHttpRequest();
req.addEventListener('load', function() {
  console.log('請求成功!');
  const data = JSON.parse(this.responseText);
  for (let planet of data.results) {
    console.log(planet.name); // 列出星球名稱
  }
});

req.addEventListener('error', () => {
  console.log('錯誤');
});

req.open('GET', 'https://swapi.dev/api/planets/')
req.send();
console.log('request send')

Planet Request 結果:第一筆資料

GET /api/planets/
HTTP 200 OK
Content-Type: application/json
Vary: Accept
Allow: GET, HEAD, OPTIONS

{
    "count": 60, 
    "next": "http://swapi.dev/api/planets/?page=2", 
    "previous": null, 
    "results": [
        {
            "name": "Tatooine", 
            "rotation_period": "23", 
            "orbital_period": "304", 
            "diameter": "10465", 
            "climate": "arid", 
            "gravity": "1 standard", 
            "terrain": "desert", 
            "surface_water": "1", 
            "population": "200000", 
            "residents": [
                "http://swapi.dev/api/people/1/", 
                "http://swapi.dev/api/people/2/", 
                "http://swapi.dev/api/people/4/", 
                "http://swapi.dev/api/people/6/", 
                "http://swapi.dev/api/people/7/", 
                "http://swapi.dev/api/people/8/", 
                "http://swapi.dev/api/people/9/", 
                "http://swapi.dev/api/people/11/", 
                "http://swapi.dev/api/people/43/", 
                "http://swapi.dev/api/people/62/"
            ], 
            "films": [
                "http://swapi.dev/api/films/1/", 
                "http://swapi.dev/api/films/3/", 
                "http://swapi.dev/api/films/4/", 
                "http://swapi.dev/api/films/5/", 
                "http://swapi.dev/api/films/6/"
            ], 
            "created": "2014-12-09T13:50:49.641000Z", 
            "edited": "2014-12-20T20:58:18.411000Z", 
            "url": "http://swapi.dev/api/planets/1/"
        },

...略

Chaining XMLHttpRequest

每一個星球下還有很多資料,例如 films,但是要取的 films 還需要發送 request 才可以取得資料。

Films request 結果

// Film Instance

GET /api/films/5/
HTTP 200 OK
Content-Type: application/json
Vary: Accept
Allow: GET, HEAD, OPTIONS

{
    "title": "Attack of the Clones", 
    "episode_id": 2, 
    "opening_crawl": "There is unrest in the Galactic\r\nSenate. Several thousand solar\r\nsystems have declared their\r\nintentions to leave the Republic.\r\n\r\nThis separatist movement,\r\nunder the leadership of the\r\nmysterious Count Dooku, has\r\nmade it difficult for the limited\r\nnumber of Jedi Knights to maintain \r\npeace and order in the galaxy.\r\n\r\nSenator Amidala, the former\r\nQueen of Naboo, is returning\r\nto the Galactic Senate to vote\r\non the critical issue of creating\r\nan ARMY OF THE REPUBLIC\r\nto assist the overwhelmed\r\nJedi....", 
    "director": "George Lucas", 
    "producer": "Rick McCallum", 
    "release_date": "2002-05-16", 
    "characters": [
        "http://swapi.dev/api/people/2/", 
        "http://swapi.dev/api/people/3/", 
        "http://swapi.dev/api/people/6/", 
        "http://swapi.dev/api/people/7/", 
        "http://swapi.dev/api/people/10/", 

       .... 略

Film 底下還有很多資料可以 request,例如人物。如果用 XMLHttpRequest 就可以一直 nested 下去沒完沒了。

Fetch

  • ES6 原生方法
  • 支援 Promises
  • 不支援 IE

POST Request

// 來發個 POST Request:

postData('http://example.com/answer', {answer: 42})
  .then(data => console.log(data)) // JSON from `response.json()` call
  .catch(error => console.error(error))

function postData(url, data) {
  // Default options are marked with *
  return fetch(url, {
    body: JSON.stringify(data), // must match 'Content-Type' header
    cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
    credentials: 'same-origin', // include, same-origin, *omit
    headers: {
      'user-agent': 'Mozilla/4.0 MDN Example',
      'content-type': 'application/json'
    },
    method: 'POST', // *GET, POST, PUT, DELETE, etc.
    mode: 'cors', // no-cors, cors, *same-origin
    redirect: 'follow', // manual, *follow, error
    referrer: 'no-referrer', // *client, no-referrer
  })
  .then(response => response.json()) // 輸出成 json
}

HexSchool 註冊帳號範例:

const api = 'https://hexschool-tutorial.herokuapp.com/api/signup';
let account = {
    email:'abcdef@gmail.com',
    password: '1234'
  };

postData(api, account)
  .then(data => console.log(data)) // JSON from `response.json()` call
  .catch(error => console.error(error))

function postData(url, data) {
  return fetch(url, {
    body: JSON.stringify(data),
    headers: {
      'content-type': 'application/json'
    },
    method: 'POST',
  })
  .then(response => response.json()) // 輸出成 json
}

// {success: false, result: {…}, message: "此帳號已被使用"}

GET Request

const myReq = fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(myJson);
  });

範例:改寫 XMLHttpRequest 例子

const fetchedData = fetch('https://swapi.dev/api/planets/')
  .then( response => {
    // status: 404/500 promise 一樣會是 resolved (ok 值會從 true 變成 false)
    console.log(response);
    if (response.status !== 202) {
      console.log('發生錯誤:', response.status);
      return;
    }
    response.json().then ( data => {
      for (let planet of data.results) {
        console.log(planet.name);
      }
    })
  })
  .catch( err => {
    // 只有在網路發生錯誤或是請求中斷才會是 promise rejected
    console.log('Error: ', err);
  })

fetch() 回傳的 promise 物件, resolve 和 reject 的使用方式有差異, 當遇到 HTTP Status 404, 500 時會使用 resolve 但會將 status 的值從 ok 變為 false, reject 只有在網路發生錯誤或是任何會中斷網路請求時才會使用。 — MDN web docs

除了上方的方式,也可以利用下方的 catch error,即是把錯誤丟給他

const fetchedData = fetch('https://swapi.dev/api/planetsss/') // 網址錯誤
  .then( response => {
    // status: 404/500 promise 一樣會是 resolved (ok 值會從 true 變成 false)
    console.log(response);
    if (response.status !== 202) { // 這邊寫成 if (!response.ok) ** not ok ** 也可以
      throw new Error(`Status: ${response.status}`);
            return;
    }
    response.json().then ( data => {
      for (let planet of data.results) {
        console.log(planet.name);
      }
    })
  })
  .catch( err => {
    // 只有在網路發生錯誤或是請求中斷才會是 promise rejected
    console.log('Catch 錯誤: ', err);
  })

錯誤跑到下方顯示了。
https://drive.google.com/uc?export=view&id=1dK5aSjMRe4_TdzL6xya9dKoKzZSTKVhC

const fetchedData = fetch('https://swapi.dev/api/planets/')
  .then( response => {
    // status: 404/500 promise 一樣會是 resolved (ok 值會從 true 變成 false)
    console.log(response);
    if (!response.ok) {
      throw new Error(`Status: ${response.status}`);
      return;
    }
    return response.json(); // return promise
  })

  ////////////////////////////////
  // 從上方 response.json() 往下移,less nested
  .then ( data => {
    console.log(data);
    for (let planet of data.results) {
      console.log(planet.name);
    }
  })
  ////////////////////////////////

    .catch( err => {
    // 只有在網路發生錯誤或是請求中斷才會是 promise rejected
    console.log('Catch 錯誤: ', err);
  })

Chaining Fetch Requests

const fetchedData = fetch('https://swapi.dev/api/planets/')
  .then( response => {
    // status: 404/500 promise 一樣會是 resolved (ok 值會從 true 變成 false)
    console.log('星球', response);
    if (!response.ok) {
      throw new Error(`Status: ${response.status}`);
      return;
    }
    return response.json(); // return promise
  })
  .then ( data => {
    //////////////////////////////
    //  request FILM
    const filmAPI = data.results[0].films[0];
    fetch(filmAPI)
      .then( response => {
        console.log('影片', response);
        if (!response.ok) {
          throw new Error(`Status: ${response.status}`);
          return;
        }
        return response.json(); // return promise
      })
      .then ( filmData => {
        console.log('filmData', filmData);
      })
      .catch( err => {
        console.log('Catch 錯誤: ', err);
      })
  })
  ////////////////////////////////
  .catch( err => {
    // 只有在網路發生錯誤或是請求中斷才會是 promise rejected
    console.log('Catch 錯誤: ', err);
  })
const fetchedData = fetch('https://swapi.dev/api/planets/')
  .then( response => {
    // status: 404/500 promise 一樣會是 resolved (ok 值會從 true 變成 false)
    console.log(response);
    if (!response.ok) {
      throw new Error(`Status: ${response.status}`);
      return;
    }
    return response.json(); // return promise
  })

  ////////////////////////////////
  // 請求 film data
  .then ( data => {
    const filmAPI = data.results[0].films[0];
    return fetch(filmAPI); // return promise
  })
  //** 這邊跟上方的 response 是做一樣的事 **//
  .then( response => {
    console.log(response);
    if (!response.ok) {
      throw new Error(`Status: ${response.status}`);
      return;
    }
    return response.json(); // return promise
  })
  .then( filmData => {
    console.log(filmData);
  })
  //**                                **//
  ////////////////////////////////

  .catch( err => {
    // 只有在網路發生錯誤或是請求中斷才會是 promise rejected
    console.log('Catch 錯誤: ', err);
  })

這樣子看起來比較平整話,而且有部分重複的語法。

AXIOS

  • 支援 promises
  • 在瀏覽器中創建 XMLHttpRequests
  • 支援IE

Post Request

axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

HexSchool 註冊帳號範例:

const api = 'https://hexschool-tutorial.herokuapp.com/api/signup';
axios.post(api, {
  email:'abcdefddd@gmail.com',
  password: '1234'
})
.then(function (response) {
  console.log(response);
})
.catch(function (error) {
  console.log(error);
});

Reference:

The Modern JavaScript Bootcamp
Using Fetch — MDN web docs
AXIOS

JS Day 44: window.location

取得目前的網址與參數

語法

let locationObj = window.location;

console.log(locationObj);

屬性

範例網址:http://127.0.0.1:5506/44/index.html?q=%27username%27&status=true#title

https://drive.google.com/uc?export=view&id=1IPhpvsOWkeGiwpO7bv9VjQpSAk-ySEU6

跳頁

回到上一頁

window.history.go(-1);
history.go(-1); // 省略 window, 一樣可以動作

window.history.back(); // 另一個方法

回到下一頁

window.history.go(1);

window.history.forward();

回到第 N 頁: window.history.go(arg)

範例:

window.history.go(-2); // 參數是正數就往上 2 頁 (forward), www.google.com
window.history.go(1);  // 參數是負數就往下 1 頁 (back), www.lego.com
window.history.go(0);  // 參數是 0 則重新載入現在頁面, refresh www.testla.com

重新整理當前頁面

除了上面提到的 window.history.go(0) 還有 reload() 方法可以用:

window.location.reload();

window.location.reload(true); 
// window.location.reload(forcedReload) 裡面的 forceReload 是 boolean

頁面跳轉

// window 皆可省略
window.location = "http://www.lego.com"; // location property
window.location.href = "http://www.lego.com"; // href property
window.location.assign('http://www.lego.com'); // assign method
window.location.replace('http://www.lego.com'); // replace method

replace 是置換掉現在的document,不會留下被置換掉的頁面紀錄,所以當你按下”上一頁”時回到的其實是你”上上個”看到的頁面 (reference-1)

Reference:
JavaScript 筆記 - window.location 類別
JavaScript 刷新頁面的幾種方法
window.location

JS Day 42: Date() 方法&日期格式

Getter

取得時間格式

const  today = new Date(); // Date Sat Aug 08 2020 00:28:32 GMT+0800 (Taipei Standard Time)

// 回傳年份
today.getFullYear(); // 2020

// 回傳月份 (0-11) ** 從 0 開始
today.getMonth(); // 7

// 回傳日期(1-31)
today.getDate(); // 8

// 回傳小時(0-23)
today.getHours(); // 0

// 回傳分鐘(0-59)
today.getMinutes(); // 28

// 回傳日期(0-6)** 從 0 開始
today.getDay(); // 6

Setter

設定時間格式

// 設定月份中的日期
today.setDate();

const bday = new Date('August 08, 2020 11:26:15');
event.setDate(05); // 設定程 5 號
// => 1596597975000
console.log(event); // Date Wed Aug 05 2020 11:26:15 GMT+0800 (Taipei Standard Time)

// 設定完整年份
today.setFullYear();

// 設定月份
today.setMonth();

// 設定小時
today.setHours();

// 設定分鐘
today.setMinutes();

// 設定秒數
today.setSeconds();

時間格式

設定時間格式 例:2020/08/05

const setDate = new Date(2020, 07, 05);
let df = (date) => {

  const year = date.getFullYear();
  const month = date.getMonth()+1;
  const day = date.getDate();

  return [year, month, day].join("/");
};

console.log(df(setDate));

不過這樣寫不夠靈活。如果遇到格式是 2020-8-5 或是 2020-08-05 整式函式都要重寫。

Date.prototype.dateFormat = function(format){
  var obj = {
        // 這邊的 this 會指向 Date()
    "y+": this.getFullYear(),                //年
    "M+": this.getMonth()+1,                 //月份
    "d+": this.getDate(),                    //日
    "h+": this.getHours(),                   //小時
    "m+": this.getMinutes(),                 //分
    "s+": this.getSeconds(),                 //秒
    "S" : this.getMilliseconds()             //毫秒
  };
    // 物件的 key 代表 regular expression 的規則
  // +號:匹配前一字元 1 至多次,等同於 {1,}。
    // 例如:/a+/ 匹配「candy」中的 a,以及所有「caaaaaaandy」中的 a。

  for (let key in obj) {
    // create a regular expression
        const regex = new RegExp(`(${key})`); // 這邊的 key 需要用括號包住( ),參考 capturing parentheses
                                                // 括號會記住此次的匹配,之後用以用 RegExp.$1..$n 呼叫

    if (regex.test(format)) {
      if (regex == '/(M+)/' || regex == '/(d+)/') {
        format = format.replace(
                    regex, (RegExp.$1.length < 2 ? obj[key] : ("00" + obj[key]).substring(1,obj[key].length))
                    // RegExp.$1 會回傳 M 跟 d 的長度
                    // 如果是 MM 就會組合成 00+月 = 008 再去刪掉最前方的0 也就是 obj[key].length
                );
      } else {
        format = format.replace(regex, obj[key]);
      }
    }
  }
  console.log(format);
  return format;
}

let ccc = new Date().dateFormat('yyyy/MM/d'); // 2020/08/8
let aaa = new Date().dateFormat('yyyy-M-dd'); // 2020-8-08
let bbb = new Date().dateFormat('yyyy/MM/dd'); // 2020/08/08

Reference:
JavaScript Date (日期) 對象
RegExp.prototype.test() – MDN web docs
Regular Expression: + (plus sign) — MDN web docs
Regular Expression: Capturing Parentheses — MDN web docs

JS Day 41: Date() 型別

Date 物件

建立 Date 物件的 4 種方法

// 1.
new Date();
// 2.
new Date(value);
// 3.
new Date(dateString);
// 4.
new Date(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]);

1. new Date()

var current = new Date();
// Thu Aug 06 2020 20:02:42 GMT+0800 (Taipei Standard Time)

var current = Date();
// "Thu Aug 06 2020 20:02:42 GMT+0800 (Taipei Standard Time)"

回傳當下日期時間、時間。如果少了 new 則是會回傳一個當下時間的字串。

2. new Date(value)

# 取得現在時間(毫秒)
const dateNow = Date.now(); // 1596715024359
const getTime = new Date().getTime(); // 1596715024359
const getValue = new Date().valueOf(); // 1596715024359

new Date(dateNow); // Thu Aug 06 2020 19:57:04 GMT+0800 (Taipei Standard Time)
new Date(getTime); // Thu Aug 06 2020 19:57:04 GMT+0800 (Taipei Standard Time)
new Date(getValue);

new Date(1596715024359);
// Thu Aug 06 2020 19:57:04 GMT+0800 (Taipei Standard Time)
// 只要傳入參數是 timestamp,new Date() 會轉換成我們可以字串,我們可閱讀的格式

Date.now() 回傳對應於當下時間的數值 - 1970/01/01 00:00:00 (UTC) 到當下的毫秒數。

new Date().getTime() 取得格林威治標準時間 1970/01/01 00:00:00 (UTC) 到現在的時間(毫秒)。

new Date().valueOf() 取得 Date 的原始值。

3. new Date(dataString)

var today = new Date('August 06, 2020 20:42:15');
// Thu Aug 06 2020 20:42:15 GMT+0800 (Taipei Standard Time)
var today = new Date('2020-08-06T20:42:15');
// Thu Aug 06 2020 20:42:15 GMT+0800 (Taipei Standard Time)

var today = new Date(2020, 08, 06);
// Sun Sep 06 2020 00:00:00 GMT+0800 (Taipei Standard Time)
// 雖然 dateString 是 2002, 08, 06 (星期四),但回傳的卻是 星期日 九月六日 🤔
// 啊,原來忘記用 quotation 包住。誤會一場。

var today = new Date('2020, 08, 06');
// Thu Aug 06 2020 00:00:00 GMT+0800 (Taipei Standard Time)

瀏覽器間解析日期字串的方式有所差異,MDN 不建議用此方法建立日期物件。

new Date('2020-08-06');
// Thu Aug 06 2020 08:00:00 GMT+0800 (Taipei Standard Time)

上方範例回傳08:00:00 ,Chrome 瀏覽器會解讀成 格林威治標準時間(GMT) 的日期格式,而本地時間是 GMT+8 所以時間顯示會再加上 8 小時。

下方範例回傳 00:00:00 ,顯示為本地時間,上下兩者相差了 8 小時。如果瀏覽器位於不同時區打開,顯示的本地時間不一定相同。

new Date('2020-8-06');
// Thu Aug 06 2020 00:00:00 GMT+0800 (Taipei Standard Time)

new Date('2020/08/06');
// Thu Aug 06 2020 00:00:00 GMT+0800 (Taipei Standard Time)

new Date('2020/8/06');
// Thu Aug 06 2020 00:00:00 GMT+0800 (Taipei Standard Time)

new Date('06 August, 2020');
// Thu Aug 06 2020 00:00:00 GMT+0800 (Taipei Standard Time)

4. new Date(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]);

這邊的參數傳入不是字串,不需要用 quotation ,但是傳入的數字有其規範。

year

表示年份的整數。當數值落在 0 到 99 之間,表示 1900 到 1999 之間的年份。參考下面的範例.

month

表示月份的整數。由 0 開始(一月)到 11 (十二月)。

day

選用。表示月份中第幾天的整數值。

hour

選用。表示小時數的整數值。

minute

選用。表示分鐘數的整數值。

second

選用。表示秒數的整數值。

millisecond

選用。表示毫秒數的整數值。

– 擷取自 MDN web doc

var today = new Date(2020, 08, 06);
// Sun Sep 06 2020 00:00:00 GMT+0800 (Taipei Standard Time)

// month 的起始值是 0(一月),08 則是九月,所以整個回傳的日期是 九月六日,二零二零年。
// new Date(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]);
// 把上面這個方法看成下方這樣對我來說比較好理解
// new Date(year, month, day, hour, minutes, seconds, milliseconds)

new Date(98);
// Thu Jan 01 1970 08:00:00 GMT+0800 (Taipei Standard Time)
new Date(2020); 
// Thu Jan 01 1970 08:00:02 GMT+0800 (Taipei Standard Time)

這部分有點不解!傳入一個參數回傳都是從 1970 開始。

new Date(2020, 07); // month 是從 0 開始, 07 等於八月
// Sat Aug 01 2020 00:00:00 GMT+0800 (Taipei Standard Time)
// 參數只有年、月,所以日期會被設為 1

new Date(2020, 07, 06);
// Thu Aug 06 2020 00:00:00 GMT+0800 (Taipei Standard Time)

規則:

  • 如果沒有傳入任務參數到建構子,會依系統設定建立出代表當下時間的 Date 物件。
  • 如果傳入至少兩個參數,缺少日期的話會設為 1,其它參數則會被設定為 0。

日期字串格式規範

ECMA-262 Edition 5.1 對 Date Time String Format 做出個規範:

// ISO 8601 Extended Format
YYYY-MM-DDTHH:mm:ss.SSSZ
// 即使只有年份也可以
new Date('2020');
// Wed Jan 01 2020 08:00:00 GMT+0800 (Taipei Standard Time)
new Date('2020-08');
// Sat Aug 01 2020 08:00:00 GMT+0800 (Taipei Standard Time)
new Date('2020-08-06');
// Thu Aug 06 2020 08:00:00 GMT+0800 (Taipei Standard Time)

如果要傳入時間(時:分),日期(DD) 的部分則不能省略,秒(ss) 則可以省略。。

YYYY-MM-DDTHH:mm:ss.SSSZ 日期跟時間中間會用英文字母 T 來做分隔。

YYYY-MM-DDTHH:mm:ss.SSSZ 最後面的 Z 代表時區,它可以被省略。

new Date('2020-08-06T20:55');
// Thu Aug 06 2020 20:55:00 GMT+0800 (Taipei Standard Time)
new Date('2020-08-06T20:55:33');
// Thu Aug 06 2020 20:55:33 GMT+0800 (Taipei Standard Time)

Reference:
Date - MDN web docs
前端工程研究:關於 JavaScript 中 Date 型別的常見地雷與建議作法 — The Will Will Web

JS Day 39: Arrow Function & this

箭頭函式 Arrow Function

如何寫箭頭函式

Function 範例:

function getProductName() {
    console.log('MacBook Pro');
};
getProductName(); // MacBook Pro

用 箭頭函式 Arrow Function 改寫:

let getProductName = () => {
    console.log('MacBook Pro');    
};
getProductName(); // MacBook Pro

如果只有一個參數傳遞進來,( ) 號可以省略。沒有任何參數時,( ) 是不可以省略的。

let sayHello = name => {
    console.log(`Hello ${name}`);
};
sayHello('George'); // Hello George

大於一個參數傳遞進來 ( ) 需要保留

let multiply = (num1, num2) => {
    return num1*num2;
};
console.log( multiply(2,3) ); // 6

上面這個例子可以再精簡。

如果函式本身不做太複雜的運算,單純要 return 一個值的話, { }return 都可以省略。

let multiply = (num1, num2) => return num1*num2; // 只剩一行
console.log( multiply(2,3) ); // 6

箭頭函式使用例子

範例1 - addEventListener:

button.addEventListener('click', function() {
    alert('Do Not Click');
});

範例1 改寫一:

button.addEventListener('click', () => {
    alert('Do Not Click');
});

範例1 改寫二:

let showAlert = () => {
    alert('Do Not Click');
};

button.addEventListener('click', showAlert);

範例2 - map:

const products = [
    {brand: 'Apple', name: 'iPhone 11 Pro', price: 800},
    {brand: 'Samsung', name: 'Galaxy S8', price: 788},
    {brand: 'Google', name: 'Pixel 4', price: 588}
];

let prices = products.map(function(item) {
    return item.price;
});

console.log(prices);

範例2 改寫:

const products = [
    {brand: 'Apple', name: 'iPhone 11 Pro', price: 800},
    {brand: 'Samsung', name: 'Galaxy S8', price: 788},
    {brand: 'Google', name: 'Pixel 4', price: 588}
];

let prices = products.map( item => item.price);

console.log(prices);

What is ‘this’?

console.log(this); // this 指向 Window

var a = 'A';
console.log(a); // A
console.log(this.a); // A
console.log(window.a); // A
console.log( this.a === window.a ); // true

目前 this 會變成 window。

function greeting() {
    console.log(this);
};
greeting(); // window

////

var name = 'Gary';
function greeting(name) {
  console.log(this.name);
};
greeting('hello'); // Gary

當下 this 還是 window 而不是 greeting() 這個函式。

this 代表的是 function 執行時所屬的物件,而不是 function 本身。 — Kuro

let getUsername = function() {
  console.log(this); // 印出 this
  return this.name;
};

const user1 = {
  id: 1,
  name: 'William',
  getUsername
};

const user2 = {
  id: 2,
  name: 'Alan',
  getUsername
};

console.log( user1.getUsername() ); // William
console.log( user2.getUsername() ); // Alan

另一個範例:

var obj = {
  func: function greeting(){
    return console.log(this);
  }
};

obj.func(); // Object { func: greeting() }

一但脫離了物件,this 的值就沒什麼意義,在沒意義的情況底下就會有個預設值,而預設值也很好記,嚴格模式就是undefined,非嚴格模式底下就是全域物件。 — Huli

console.log(this); // 非嚴格模式下就是全域物件 - window
var func = function (){
  "use strict" // 嚴格模式
  console.log(this); // undefined
};
func();

箭頭函式下的 this

將上面的範例改成 arrow function

let getUsername = () => {
    console.log(this); 
  return this.name;
};

const user1 = {
  id: 1,
  name: 'William',
  getUsername
};

const user2 = {
  id: 2,
  name: 'Alan',
  getUsername
};
console.log( user1.getUsername() ); // '' (empty string)
console.log( user2.getUsername() ); // '' (empty string)

this 指向全域物件 window

在箭頭函式下,this 會被強制綁定到其定義時所在的物件,上面的範例則是 window。

Arrow Function in Vue.js

  var app = new Vue({
    el: '#app',
    data: {
      text: '',
      newText: ''
    },
    methods: {
      reverseText: () => {
        console.log(this);
        console.log(app);
        this.newText = this.text.split('').reverse().join('');
      }
    },
  });

在 Vue.js 裡的 arrow function,this 也是指向 window

  var app = new Vue({
    el: '#app',
    data: {
      text: '',
      newText: ''
    },
    methods: {
      reverseText: function() {
        console.log(this);
        console.log(app);
        this.newText = this.text.split('').reverse().join('');
      }
    }
  });

一般 function 的 this 是指向 Vue instance, 在這個範例也就是 app.

Reference:
Arrow Function - MDN web docs
This - MDN web docs
[筆記] JavaScript ES6 中的箭頭函數(arrow function)及對 this 的影響 - PJCHENder
[筆記] 談談 JavaScript 中的 “this” 和它的問提 - PJCHENder
ECMAScript 6 入門
淺談 JavaScript 頭號難題 this:絕對不完整,但保證好懂 - TechBridge
鐵人賽:JavaScript 的 this 到底是誰? - 卡斯伯
What’s THIS in JavaScript? [上] [中] [下] - Kuro’s Blog