wordpress_blog

This is a dynamic to static website.

Modern JavaScript (3)

(Complete guide, from Novice to Ninja)

Learning Udemy Course:Modern JavaScript

建立者:The Net Ninja (Shaun Pelling)

Learn Modern JavaScript from the very start to ninja-level & build awesome JavaScript applications.

您會學到

  • Learn how to program with modern JavaScript, from the very beginning to more advanced topics
  • Learn all about OOP (object-oriented programming) with JavaScript, working with prototypes & classes
  • Learn how to create real-world front-end applications with JavaScript (quizes, weather apps, chat rooms etc)
  • Learn how to make useful JavaScript driven UI components like popups, drop-downs, tabs, tool-tips & more.
  • Learn how to use modern, cutting-edge JavaScript features today by using a modern workflow (Babel & Webpack)
  • Learn how to use real-time databases to store, retrieve and update application data
  • Explore API’s to make the most of third-party data (such as weather information)

Project – Weather App

Project Preview & Setup

操作步驟

  • 創建一個專案資料夾 weather_app
  • 新增一個 index.html 檔案
  • index.html 使用 doc + tab 建立環境、載入 CSS、Bootstrap CDN
  • 新增一個 scripts 資料夾、並建立 app.js、forecast.js 檔案
  • 在 index.html 載入 JS
// index.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
  <link rel="stylesheet" href="style.css">
  <title>Ninja Weather</title>
</head>
<body>
  

  <script src="scripts/forecast.js"></script>
  <script src="scripts/app.js"></script>
</body>
</html>

HTML & CSS Template

資源

// index.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
  <link rel="stylesheet" href="style.css">
  <title>Ninja Weather</title>
</head>
<body>

  <div class="container my-5 mx-auto">

    <h1 class="text-muted text-center my-4">Ninja Weather</h1>

    <form class="change-location my-4 text-center text-muted">
      <label for="city">Enter a location for weather information</label>
      <input type="text" name="city" class="form-control p-4">
    </form>

    <div class="card shadow-lg rounded">
      <img src="https://via.placeholder.com/400x300" class="time card-img-top" alt="">
      <div class="icon bg-light mx-auto text-center">
        <!-- icon -->
      </div>
      <div class="text-muted text-uppercase text-center details">
        <h5 class="my-3">City name</h5>
        <div class="my-3">Weather condition</div>
        <div class="display-4 my-4">
          <span>temp</span>
          <span>&deg;C</span>
        </div>
      </div>
    </div>

  </div>
  

  <script src="scripts/forecast.js"></script>
  <script src="scripts/app.js"></script>
</body>
</html>
// style.css

body{
  background: #eeedec;
  letter-spacing: 0.2em;
  font-size: 0.8em;
}
.container{
  max-width: 400px;
}

Placeholder Image

AccuWeather API

資源

API Reference

  • Locations API
    • Text Search – City Search
  • Current Conditions API
    • Current Conditions – Current Conditions

個人練習

  • 政府資料凱放平臺
  • 中央氣象局開放資料平臺之資料擷取 API

Get City API Call

// forecast.js

const key = 'rdec-key-123-45678-011121314';

const getCity = async (city) => {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001';
  const query = `?Authorization=${key}&locationName=${city}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data);
  // console.log(data.records.location[0]);
  
return data.records.location[0];

};

getCity('臺南市')
  .then(data => console.log(data))
  .catch(err => console.log(err));

Get Weather API Call

// forecast.js - 1

const key = 'rdec-key-123-45678-011121314';

// get weather information
const getWeather = async (id = 'F-C0032-001') => {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/';
  const query = `${id}?Authorization=${key}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data.records.location);
  return data.records.location;

};

// get city information
const getCity = async (city) => {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001';
  const query = `?Authorization=${key}&locationName=${city}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data);
  // console.log(data.records.location[0]);
  
return data.records.location;

};

getCity('臺南市').then(data => {
    return getWeather(data.key);
  }).then(data => {
    console.log(data);
  }).catch(err => console.log(err));
// forecast.js - 2

const key = 'rdec-key-123-45678-011121314';

// get weather information
const getWeather = async (id) => {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/';
  const query = `${id}?Authorization=${key}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data.records.location);
  return data;

};

// get city information
const getCity = async (city) => {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001';
  const query = `?Authorization=${key}&locationName=${city}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data);
  // console.log(data.records.location[0]);
  
return data;

};

getCity('臺南市').then(data => {
    return getWeather(data.result.resource_id);
  }).then(data => {
    console.log(data);
  }).catch(err => console.log(err));

Updating the Location

// app.js

const cityForm = document.querySelector('form');

const updateCity = async (city) => {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(cityDets.result.resource_id);

  return {
    cityDets: cityDets,
    weather: weather
  };

};

cityForm.addEventListener('submit', e => {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data => console.log(data))
  .catch(err => console.log(err));

});

Object Shorthand Notation

// app.js

const cityForm = document.querySelector('form');

const updateCity = async (city) => {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(cityDets.result.resource_id);

  return { cityDets, weather };

};

cityForm.addEventListener('submit', e => {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data => console.log(data))
  .catch(err => console.log(err));

});

Updating the UI

// index.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
  <link rel="stylesheet" href="style.css">
  <title>Ninja Weather</title>
</head>
<body>

  <div class="container my-5 mx-auto">

    <h1 class="text-muted text-center my-4">Ninja Weather</h1>

    <form class="change-location my-4 text-center text-muted">
      <label for="city">Enter a location for weather information</label>
      <input type="text" name="city" class="form-control p-4">
    </form>

    <div class="card shadow-lg rounded d-none">
      <img src="https://via.placeholder.com/400x300" class="time card-img-top" alt="">
      <div class="icon bg-light mx-auto text-center">
        <!-- icon -->
      </div>
      <div class="text-muted text-uppercase text-center details">
        <h5 class="my-3">City name</h5>
        <div class="my-3">Weather condition</div>
        <div class="display-4 my-4">
          <span>temp</span>
          <span>&deg;C</span>
        </div>
      </div>
    </div>

  </div>
  

  <script src="scripts/forecast.js"></script>
  <script src="scripts/app.js"></script>
</body>
</html>
// app.js

const cityForm = document.querySelector('form');
const card = document.querySelector('.card');
const details = document.querySelector('.details');

const updateUI = (data) => {
  
  const cityDets = data.cityDets;
  const weather = data.weather;

  // update details template
  details.innerHTML = `
    <h4 class="my-3">今明36小時天氣預報</h4>
    <h5 class="my-3">${cityDets.records.location[0].locationName}</h5>
    <div class="my-3">${cityDets.records.location[0].weatherElement[0].time[0].parameter.parameterName}</div>
    <div>↓</div>
    <div class="my-3"><br>${cityDets.records.location[0].weatherElement[0].time[1].parameter.parameterName}</div>
    <div>↓</div>
    <div class="my-3"><br>${cityDets.records.location[0].weatherElement[0].time[2].parameter.parameterName}</div>
    <div class="display-4 my-4">
      <span>temp</span><br>
      <span>${cityDets.records.location[0].weatherElement[2].time[0].parameter.parameterName} - ${cityDets.records.location[0].weatherElement[4].time[0].parameter.parameterName}  &deg;C</span>
      <div>↓</div>
      <span>${cityDets.records.location[0].weatherElement[2].time[1].parameter.parameterName} - ${cityDets.records.location[0].weatherElement[4].time[1].parameter.parameterName}  &deg;C</span>
      <div>↓</div>
      <span>${cityDets.records.location[0].weatherElement[2].time[2].parameter.parameterName} - ${cityDets.records.location[0].weatherElement[4].time[2].parameter.parameterName}  &deg;C</span>
    </div>
  `;

  // remove the d-none class if present
  if(card.classList.contains('d-none')){
    card.classList.remove('d-none');
  }

};

const updateCity = async (city) => {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(cityDets.result.resource_id);

  console.log(cityDets);
  return { cityDets, weather };

};

cityForm.addEventListener('submit', e => {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data => updateUI(data))
  .catch(err => console.log(err));

});
// forecast.js

const key = 'rdec-key-123-45678-011121314';

// get weather information
const getWeather = async (id) => {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/';
  const query = `${id}?Authorization=${key}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data.records.location);
  
  return data;

};

// get city information
const getCity = async (city) => {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001';
  const query = `?Authorization=${key}&locationName=${city}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data);
  // console.log(data.records.location[0]);
  
  return data;

};

Destructuring (解構賦值)

// app.js

const cityForm = document.querySelector('form');
const card = document.querySelector('.card');
const details = document.querySelector('.details');

const updateUI = (data) => {
  
  // console.log(data);
  // const cityDets = data.cityDets;
  // const weather = data.weather;

  // destructure properties
  const { cityDets, weather } = data;

  // update details template
  details.innerHTML = `
    <h4 class="my-3">今明36小時天氣預報</h4>
    <h5 class="my-3">${cityDets.records.location[0].locationName}</h5>
    <div class="my-3">${weather.records.location[0].weatherElement[0].time[0].parameter.parameterName}</div>
    <div>↓</div>
    <div class="my-3"><br>${weather.records.location[0].weatherElement[0].time[1].parameter.parameterName}</div>
    <div>↓</div>
    <div class="my-3"><br>${weather.records.location[0].weatherElement[0].time[2].parameter.parameterName}</div>
    <div class="display-4 my-4">
      <span>temp</span><br>
      <span>${weather.records.location[0].weatherElement[2].time[0].parameter.parameterName} - ${weather.records.location[0].weatherElement[4].time[0].parameter.parameterName}  &deg;C</span>
      <div>↓</div>
      <span>${weather.records.location[0].weatherElement[2].time[1].parameter.parameterName} - ${weather.records.location[0].weatherElement[4].time[1].parameter.parameterName}  &deg;C</span>
      <div>↓</div>
      <span>${weather.records.location[0].weatherElement[2].time[2].parameter.parameterName} - ${weather.records.location[0].weatherElement[4].time[2].parameter.parameterName}  &deg;C</span>
    </div>
  `;

  // remove the d-none class if present
  if(card.classList.contains('d-none')){
    card.classList.remove('d-none');
  }

};

const updateCity = async (city) => {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(city);

  console.log(cityDets);
  console.log(weather);
  return { cityDets, weather };

};

cityForm.addEventListener('submit', e => {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data => updateUI(data))
  .catch(err => console.log(err));

});
// index.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
  <link rel="stylesheet" href="style.css">
  <title>Ninja Weather</title>
</head>
<body>

  <div class="container my-5 mx-auto">

    <h1 class="text-muted text-center my-4">Ninja Weather</h1>

    <form class="change-location my-4 text-center text-muted">
      <label for="city">Enter a location for weather information</label>
      <input type="text" name="city" class="form-control p-4">
    </form>

    <div class="card shadow-lg rounded d-none">
      <img src="https://via.placeholder.com/400x300" class="time card-img-top" alt="">
      <div class="icon bg-light mx-auto text-center">
        <!-- icon -->
      </div>
      <div class="text-muted text-uppercase text-center details">
        <h5 class="my-3">City name</h5>
        <div class="my-3">Weather condition</div>
        <div class="display-4 my-4">
          <span>temp</span>
          <span>&deg;C</span>
        </div>
      </div>
    </div>

  </div>
  

  <script src="scripts/forecast.js"></script>
  <script src="scripts/app.js"></script>
</body>
</html>
// forecast.js

const key = 'rdec-key-123-45678-011121314';
const id = 'F-C0032-001';

// get weather information
const getWeather = async (city) => {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/';
  const query = `${id}?Authorization=${key}&locationName=${city}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data.records.location);
  
  return data;

};

// get city information
const getCity = async (city) => {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001';
  const query = `?Authorization=${key}&locationName=${city}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data);
  // console.log(data.records.location[0]);
  
  return data;

};

Weather Icons & images

// index.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
  <link rel="stylesheet" href="style.css">
  <title>Ninja Weather</title>
</head>
<body>

  <div class="container my-5 mx-auto">

    <h1 class="text-muted text-center my-4">臺灣天氣</h1>

    <form class="change-location my-4 text-center text-muted">
      <label for="city">輸入地點取得天氣資訊</label><br>
      <span class="text-muted">需輸入完整縣市名稱(例如:臺北市)</span><br><br>
      <input type="text" name="city" class="form-control p-4">
    </form>

    <div class="card shadow-lg rounded d-none">
      <img src="https://via.placeholder.com/400x300" class="time card-img-top" alt="">
      <div class="icon bg-light mx-auto text-center">
        <!-- icon -->
        <img src="" alt="">
      </div>
      <div class="text-muted text-uppercase text-center details">
        <h5 class="my-3">City name</h5>
        <div class="my-3">Weather condition</div>
        <div class="display-4 my-4">
          <span>temp</span>
          <span>&deg;C</span>
        </div>
      </div>
    </div>

  </div>
  

  <script src="scripts/forecast.js"></script>
  <script src="scripts/app.js"></script>
</body>
</html>
// app.js

const cityForm = document.querySelector('form');
const card = document.querySelector('.card');
const details = document.querySelector('.details');
const time = document.querySelector('img.time');
const icon = document.querySelector('.icon img');

const updateUI = (data) => {
  
  // console.log(data);
  // const cityDets = data.cityDets;
  // const weather = data.weather;

  // destructure properties
  const { cityDets, weather } = data;

  // update details template
  details.innerHTML = `
    <h4 class="my-3">12小時內天氣預報</h4>
    <h5 class="my-3">${cityDets.records.location[0].locationName}</h5>
    <div class="my-3">${weather.records.location[0].weatherElement[0].time[0].parameter.parameterName}</div>
    <div class="display-4 my-4">
      <span>氣溫</span><br>
      <span>${weather.records.location[0].weatherElement[2].time[0].parameter.parameterName} - ${weather.records.location[0].weatherElement[4].time[0].parameter.parameterName}  &deg;C</span>
    </div>
  `;

  // update the night/day & icon images
  const iconSrc = `img/icons/${weather.records.location[0].weatherElement[0].time[0].parameter.parameterValue}.svg`;
  icon.setAttribute('src', iconSrc);

  let timeSrc = null;
  if(weather.records.location[0].weatherElement[0].time[0].startTime.indexOf("06:00:00") >= 0){
    timeSrc = 'img/day.svg';
  } else {
    timeSrc = 'img/night.svg';
  }
  time.setAttribute('src', timeSrc);

  // remove the d-none class if present
  if(card.classList.contains('d-none')){
    card.classList.remove('d-none');
  }

};

const updateCity = async (city) => {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(city);

  console.log(cityDets);
  console.log(weather);
  // console.log(weather.records.location[0].weatherElement[0].time[0].startTime);
  return { cityDets, weather };

};

cityForm.addEventListener('submit', e => {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data => updateUI(data))
  .catch(err => console.log(err));

});
// style.css

body{
  background: #eeedec;
  letter-spacing: 0.2em;
  font-size: 0.8em;
}
.container{
  max-width: 400px;
}
.icon{
  position: relative;
  top: -50px;
  border-radius: 50%;
  width: 100px;
  margin-bottom: -50px;
}

Ternary Operator (三元運算子)

// app.js

const cityForm = document.querySelector('form');
const card = document.querySelector('.card');
const details = document.querySelector('.details');
const time = document.querySelector('img.time');
const icon = document.querySelector('.icon img');

const updateUI = (data) => {
  
  // console.log(data);
  // const cityDets = data.cityDets;
  // const weather = data.weather;

  // destructure properties
  const { cityDets, weather } = data;

  // update details template
  details.innerHTML = `
    <h4 class="my-3">12小時內天氣預報</h4>
    <h5 class="my-3">${cityDets.records.location[0].locationName}</h5>
    <div class="my-3">${weather.records.location[0].weatherElement[0].time[0].parameter.parameterName}</div>
    <div class="display-4 my-4">
      <span>氣溫</span><br>
      <span>${weather.records.location[0].weatherElement[2].time[0].parameter.parameterName} - ${weather.records.location[0].weatherElement[4].time[0].parameter.parameterName}  &deg;C</span>
    </div>
  `;

  // update the night/day & icon images
  const iconSrc = `img/icons/${weather.records.location[0].weatherElement[0].time[0].parameter.parameterValue}.svg`;
  icon.setAttribute('src', iconSrc);


  let timeSrc = weather.records.location[0].weatherElement[0].time[0].startTime.indexOf("06:00:00") >= 0 ? 'img/day.svg' : 'img/night.svg';

  // let timeSrc = null;
  // if(weather.records.location[0].weatherElement[0].time[0].startTime.indexOf("06:00:00") >= 0){
  //   timeSrc = 'img/day.svg';
  // } else {
  //   timeSrc = 'img/night.svg';
  // }
  time.setAttribute('src', timeSrc);

  // remove the d-none class if present
  if(card.classList.contains('d-none')){
    card.classList.remove('d-none');
  }

};

const updateCity = async (city) => {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(city);

  console.log(cityDets);
  console.log(weather);
  // console.log(weather.records.location[0].weatherElement[0].time[0].startTime);
  return { cityDets, weather };

};

cityForm.addEventListener('submit', e => {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data => updateUI(data))
  .catch(err => console.log(err));

});

// ternary operator
// const result = true ? 'value 1' : 'value 2';
// console.log(result);

Local Storage

What is Local Storage?

Application Data

  • Set up a database to store & retrieve data
  • Use local storage to store and retrieve data

Local Storage

// Google Console
>  window
<  Window {window: Window, self: Window, document: document, name: '', location: Location, …}
>

Storing & Getting Data

  • Google Console
  • sandbox.js
  • Google Application → Storage → Local Storage
// Google Console - 1
>  window.localStorage
<  Storage {length: 0}
>  localStorage
<  Storage {length: 0}
>
// sandbox.js - 2

// store data in local storage
localStorage.setItem('name', 'mario');
localStorage.setItem('age', 50);


// get data from local storage
let name = localStorage.getItem('name');
let age = localStorage.getItem('age');

console.log(name, age);

// updating data
localStorage.setItem('name', 'luigi');
localStorage.age = '40';

name = localStorage.getItem('name');
age = localStorage.getItem('age');
console.log(name, age);
// Google Console - 2

Deleting Storage Data

  • sandbox.js
  • Google Console
  • Google Application
// sandbox.js - 1

// store data in local storage
localStorage.setItem('name', 'mario');
localStorage.setItem('age', 50);

// get data from local storage
let name = localStorage.getItem('name');
let age = localStorage.getItem('age');

console.log(name, age);

// deleting data from local storage

// Google Console - 1
   mario 50
>  localStorage
<  Storage {name: 'mario', age: '50', length: 2}
>
// sandbox.js - 2

// store data in local storage
localStorage.setItem('name', 'mario');
localStorage.setItem('age', 50);

// get data from local storage
let name = localStorage.getItem('name');
let age = localStorage.getItem('age');

console.log(name, age);

// deleting data from local storage
// localStorage.removeItem('name');
localStorage.clear();

name = localStorage.getItem('name');
age = localStorage.getItem('age');

console.log(name, age);
// Google Console - 2
   mario 50
   null null
>

Stringifying & Parsing Data

  • sandbox.js
  • Google Console
  • Google Application
// sandbox.js

const todos = [
  {text: 'play mariokart', author: 'shaun'},
  {text: 'buy some milk', author: 'mario'},
  {text: 'buy some bread', author: 'luigi'}
];

// console.log(JSON.stringify(todos));
localStorage.setItem('todos', JSON.stringify(todos));

const stored = localStorage.getItem('todos');

console.log(JSON.parse(stored));
// Google Console
   (3) [{...}, {...}, {...}]
>

Updating the Weather App

  • app.js
  • Google Application
// app.js

const cityForm = document.querySelector('form');
const card = document.querySelector('.card');
const details = document.querySelector('.details');
const time = document.querySelector('img.time');
const icon = document.querySelector('.icon img');

const updateUI = (data) => {
  
  // console.log(data);
  // const cityDets = data.cityDets;
  // const weather = data.weather;

  // destructure properties
  const { cityDets, weather } = data;

  // update details template
  details.innerHTML = `
    <h4 class="my-3">12小時內天氣預報</h4>
    <h5 class="my-3">${cityDets.records.location[0].locationName}</h5>
    <div class="my-3">${weather.records.location[0].weatherElement[0].time[0].parameter.parameterName}</div>
    <div class="display-4 my-4">
      <span>氣溫</span><br>
      <span>${weather.records.location[0].weatherElement[2].time[0].parameter.parameterName} - ${weather.records.location[0].weatherElement[4].time[0].parameter.parameterName}  &deg;C</span>
    </div>
  `;

  // update the night/day & icon images
  const iconSrc = `img/icons/${weather.records.location[0].weatherElement[0].time[0].parameter.parameterValue}.svg`;
  icon.setAttribute('src', iconSrc);


  let timeSrc = weather.records.location[0].weatherElement[0].time[0].startTime.indexOf("06:00:00") || weather.records.location[0].weatherElement[0].time[0].startTime.indexOf("12:00:00") >= 0 ? 'img/day.svg' : 'img/night.svg';

  // let timeSrc = null;
  // if(weather.records.location[0].weatherElement[0].time[0].startTime.indexOf("06:00:00") >= 0){
  //   timeSrc = 'img/day.svg';
  // } else {
  //   timeSrc = 'img/night.svg';
  // }
  time.setAttribute('src', timeSrc);

  // remove the d-none class if present
  if(card.classList.contains('d-none')){
    card.classList.remove('d-none');
  }

};

const updateCity = async (city) => {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(city);

  console.log(cityDets);
  console.log(weather);
  // console.log(weather.records.location[0].weatherElement[0].time[0].startTime);
  return { cityDets, weather };

};

cityForm.addEventListener('submit', e => {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data => updateUI(data))
  .catch(err => console.log(err));

  // set local storage
  localStorage.setItem('city', city);

});

// ternary operator
// const result = true ? 'value 1' : 'value 2';
// console.log(result);

if(localStorage.getItem('city')){
  updateCity(localStorage.getItem('city'))
    .then(data => updateUI(data))
    .catch(err => console.log(err));
};

Object Oriented JavaScript (物件導向)

What is OOP? (物件導向程式設計)

// Google Console
>  const names = ['shaun', 'crystal']
<  undefined
>  names
<  (2) ["shaun", "crystal"]
>  const ages = new Array(20,25,30)
<  undefined
>  ages
<  (3) [20, 25, 30]
>  const userOne = {}
<  undefined
>  userOne
<  {}
>  const userTwo = new Object();
<  undefined
>  userTwo
<  {}
>  const name = 'mario'
<  undefined
>  name
<  "mario"
>  name.length
<  5
>  name.toUpperCase()
<  "MARIO"
>  const nameTwo = new String('ryu');
<  undefined
>  nameTwo
<  String {"ryu"}
>  new Number(5)
<  Number {5}
>  new Boolean(true)
<  Boolean {true}
>

Object Literal Recap (物件實字回顧)

// index.html

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="style.css">
    <title>Modern JavaScript</title>
</head>
<body>
    <h1>Object Oriented JavaScript</h1>
    
    
    <script src="sandbox.js"></script>
</body>
</html>
// sandbox.js - 1

const userOne = {
  username: 'ryu',
  email: 'ryu@thenetninja.co.uk',
  login(){
    console.log('the user logged in');
  },
  logout(){
    console.log('the user logged out');
  }
};

console.log(userOne.email, userOne.username);
userOne.login();

const userTwo = {
  username: 'chun li',
  email: 'chun.li@thenetninja.co.uk',
  login(){
    console.log('the user logged in');
  },
  logout(){
    console.log('the user logged out');
  }
};

console.log(userTwo.email, userTwo.username);
userTwo.login();

// Google Console - 1
   ryu@thenetninja.co.uk ryu
   the user logged in
   chun.li@thenetninja.co.uk chun li
   the user logged in
>
// sandbox.js - 2

const userOne = {
  username: 'ryu',
  email: 'ryu@thenetninja.co.uk',
  login(){
    console.log('the user logged in');
  },
  logout(){
    console.log('the user logged out');
  }
};

console.log(userOne.email, userOne.username);
userOne.login();

const userTwo = {
  username: 'chun li',
  email: 'chun.li@thenetninja.co.uk',
  login(){
    console.log('the user logged in');
  },
  logout(){
    console.log('the user logged out');
  }
};

console.log(userTwo.email, userTwo.username);
userTwo.login();

const userThree = new User('shaun@thenetninja.co.uk', 'shaun');

Classes (類別)

// sandbox.js

const userOne = {
  username: 'ryu',
  email: 'ryu@thenetninja.co.uk',
  login(){
    console.log('the user logged in');
  },
  logout(){
    console.log('the user logged out');
  }
};

console.log(userOne.email, userOne.username);
userOne.login();

const userTwo = {
  username: 'chun li',
  email: 'chun.li@thenetninja.co.uk',
  login(){
    console.log('the user logged in');
  },
  logout(){
    console.log('the user logged out');
  }
};

console.log(userTwo.email, userTwo.username);
userTwo.login();

const userThree = new User('shaun@thenetninja.co.uk', 'shaun');

Classes

  • A Class is like a blueprint(藍圖) for an object (it describes how one should be made)

Class Constructors (類別建構子)

// sandbox.js - 1

class User {
  constructor(){
    // set up properties
    this.username = 'mario';
  }
}

const userOne = new User();
const userTwo = new User();

console.log(userOne, userTwo);

// the 'new' keyword
// 1 - it creates a new empty object {}
// 2 - it binds the value of 'this' to the new empty object
// 3 - it calls the constructor function to 'build' the object
// Google Console - 1
   User {username: 'mario'} User {username: 'mario'}
>
// sandbox.js - 2

class User {
  constructor(username){
    // set up properties
    this.username = username;
  }
}

const userOne = new User('mario');
const userTwo = new User('luigi');

console.log(userOne, userTwo);

// the 'new' keyword
// 1 - it creates a new empty object {}
// 2 - it binds the value of 'this' to the new empty object
// 3 - it calls the constructor function to 'build' the object
// Google Console - 2
   User {username: 'mario'} User {username: 'luigi'}
>
// sandbox.js - 3

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);

// the 'new' keyword
// 1 - it creates a new empty object {}
// 2 - it binds the value of 'this' to the new empty object
// 3 - it calls the constructor function to 'build' the object
// Google Console - 3
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
>

Class Methods & Method Chaining

// sandbox.js - 1

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
  }
  login(){
    console.log(`${this.username} just logged in`);
  }
  logout(){
    console.log(`${this.username} just logged out`);
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.login();
userTwo.login();
// Google Console - 1
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   mario just logged in
   luigi just logged in
>
// sandbox - 2

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
  }
  login(){
    console.log(`${this.username} just logged in`);
  }
  logout(){
    console.log(`${this.username} just logged out`);
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.logout();
userTwo.logout();
// Google Console - 2
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   mario just logged out
   luigi just logged out
>
// sandbox.js - 3

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
    this.score = 0;
  }
  login(){
    console.log(`${this.username} just logged in`);
    return this;
  }
  logout(){
    console.log(`${this.username} just logged out`);
    return this;
  }
  incScore(){
    this.score += 1;
    console.log(`${this.username} has a score of ${this.score}`);
    return this;
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);

userOne.login().incScore().incScore().logout();
// Google Console - 3
   User {username: 'mario', email: 'mario@thenetninja.co.uk', score: 0}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk', score: 0}
   mario just logged in
   mario has a score of 1
   mario has a score of 2
   mario just logged out
>

Class Inheritance (類別繼承) (subclasses) (子類別)

// sandbox.js - 1

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
    this.score = 0;
  }
  login(){
    console.log(`${this.username} just logged in`);
    return this;
  }
  logout(){
    console.log(`${this.username} just logged out`);
    return this;
  }
  incScore(){
    this.score += 1;
    console.log(`${this.username} has a score of ${this.score}`);
    return this;
  }
}

class Admin extends User{

}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree = new Admin('shaun', 'shaun@thenetninja.co.uk');

console.log(userOne, userTwo, userThree);

// Google Console - 1
   User {username: 'mario', email: 'mario@thenetninja.co.uk', score: 0}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk', score: 0}
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', score: 0}
>
// Google Console - 2
   User {username: 'mario', email: 'mario@thenetninja.co.uk', score: 0}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk', score: 0}
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', score: 0}
>  userThree.login();
   shaun just logged in
<  Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', score: 0}
>
// sandbox - 3

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
    this.score = 0;
  }
  login(){
    console.log(`${this.username} just logged in`);
    return this;
  }
  logout(){
    console.log(`${this.username} just logged out`);
    return this;
  }
  incScore(){
    this.score += 1;
    console.log(`${this.username} has a score of ${this.score}`);
    return this;
  }
}

class Admin extends User{
  deleteUser(user){
    users = users.filter((u) => {
      return u.username !== user.username;
    });
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree = new Admin('shaun', 'shaun@thenetninja.co.uk');

console.log(userOne, userTwo, userThree);

let users = [userOne, userTwo, userThree];
console.log(users);

userThree.deleteUser(userTwo);
console.log(users);
// Google Console - 3
   User {username: 'mario', email: 'mario@thenetninja.co.uk', score: 0}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk', score: 0}
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', score: 0}
   (3) [User, User, Admin]
   (2) [User, Admin]
>
// sandbox.js - 3 縮寫

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
    this.score = 0;
  }
  login(){
    console.log(`${this.username} just logged in`);
    return this;
  }
  logout(){
    console.log(`${this.username} just logged out`);
    return this;
  }
  incScore(){
    this.score += 1;
    console.log(`${this.username} has a score of ${this.score}`);
    return this;
  }
}

class Admin extends User{
  deleteUser(user){
    users = users.filter(u => u.username !== user.username);
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree = new Admin('shaun', 'shaun@thenetninja.co.uk');

console.log(userOne, userTwo, userThree);

let users = [userOne, userTwo, userThree];
console.log(users);

userThree.deleteUser(userTwo);
console.log(users);
// Google Console - 3 縮寫
   User {username: 'mario', email: 'mario@thenetninja.co.uk', score: 0}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk', score: 0}
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', score: 0}
   (3) [User, User, Admin]
   (2) [User, Admin]
>

Super()

// sandbox.js

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
    this.score = 0;
  }
  login(){
    console.log(`${this.username} just logged in`);
    return this;
  }
  logout(){
    console.log(`${this.username} just logged out`);
    return this;
  }
  incScore(){
    this.score += 1;
    console.log(`${this.username} has a score of ${this.score}`);
    return this;
  }
}

class Admin extends User{
  constructor(username, email, title){
    super(username, email);
    this.title = title;
  }
  deleteUser(user){
    users = users.filter(u => u.username !== user.username);
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree = new Admin('shaun', 'shaun@thenetninja.co.uk', 'black-belt-ninja');

console.log(userThree);
// Google Console
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', score: 0, title: 'black-belt-ninja'}
>

Constructors (under the hood)

// sandbox.js

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
  this.login = function(){
    console.log(`${this.username} has logged in`);
  };
}

// class User {
//   constructor(username, email){
//     // set up properties
//     this.username = username;
//     this.email = email;
//   }
// }

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.login();

// the 'new' keyword
// 1 - it creates a new empty object {}
// 2 - it binds the value of 'this' to the new empty object
// 3 - it calls the constructor function to 'build' the object
// Google Console
   User {username: 'mario', email: 'mario@thenetninja.co.uk', login: ƒ}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk', login: ƒ}
   mario has logged in
>

Prototype Model (原型模型)

不懂可以重複觀看、練習。

// sandbox.js - 1

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
  this.login = function(){
    console.log(`${this.username} has logged in`);
  };
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.login();
// Google Console - 1
   User User
   mario has logged in
>  const nums = [1,2,3,4,5]
<  undefined
>  nums
<  (5) [1,2,3,4,5]
>

Prototypes (原型)

  • Every object in JavaScript has a prototype
  • Prototypes contain all the methods for that object type
// Google Console - 2
   User User
   mario has logged in
>  const nums = [1,2,3,4,5]
<  undefined
>  nums
<  (5) [1,2,3,4,5]
>  Array.prototype
<  [constructor: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ, find: ƒ, …]
>  Date.prototype
<  {constructor: ƒ, toString: ƒ, toDateString: ƒ, toTimeString: ƒ, toISOString: ƒ, …}
>  User.prototype
<  {constructor: ƒ}
>
// sandbox.js - 3

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
};

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.login();
// Google Console - 3
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   mario has logged in
>  User.prototype
<  {login: f, constructor: f}
>
// sandbox.js - 4

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
};

User.prototype.logout = function(){
  console.log(`${this.username} has logged out`);
};

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.login();
userOne.logout();
// Google Console - 4
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   mario has logged in
   mario has logged out
>
// sandbox.js - 5
// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
  return this;
};

User.prototype.logout = function(){
  console.log(`${this.username} has logged out`);
  return this;
};

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.login().logout();
// Google Console - 5
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   mario has logged in
   mario has logged out
>

Prototypal Inheritance

不懂可以重複觀看、練習。

// sandbox.js - 1

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
  return this;
};

User.prototype.logout = function(){
  console.log(`${this.username} has logged out`);
  return this;
};

function Admin(username, email){
  User.call(this, username, email);
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree =  new Admin('shaun', 'shaun@thenetninja.co.uk');

console.log(userOne, userTwo, userThree);
userOne.login().logout();
// Google Console - 1
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk'}
   mario has logged in
   mario has logged out
>
// sandbox.js - 2

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
  return this;
};

User.prototype.logout = function(){
  console.log(`${this.username} has logged out`);
  return this;
};

function Admin(username, email, title){
  User.call(this, username, email);
  this.title = title;
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree =  new Admin('shaun', 'shaun@thenetninja.co.uk', 'black-belt-ninja');

console.log(userOne, userTwo, userThree);
userOne.login().logout();
// Google Console - 2
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', title: 'black-belt-ninja'}
   mario has logged in
   mario has logged out
>
// Google Console - 3
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', title: 'black-belt-ninja'}
   mario has logged in
   mario has logged out
>  Admin.prototype
<  {constructor: f}
>
// sandbox.js - 4

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
  return this;
};

User.prototype.logout = function(){
  console.log(`${this.username} has logged out`);
  return this;
};

function Admin(username, email, title){
  User.call(this, username, email);
  this.title = title;
}

Admin.prototype = Object.create(User.prototype);

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree =  new Admin('shaun', 'shaun@thenetninja.co.uk', 'black-belt-ninja');

console.log(userOne, userTwo, userThree);
userOne.login().logout();
// Google Console - 4
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', title: 'black-belt-ninja'}
   mario has logged in
   mario has logged out
>  Admin.prototype
<  User {}
>
// sandbox.js - 5

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
  return this;
};

User.prototype.logout = function(){
  console.log(`${this.username} has logged out`);
  return this;
};

function Admin(username, email, title){
  User.call(this, username, email);
  this.title = title;
}

Admin.prototype = Object.create(User.prototype);

Admin.prototype.deleteUser = function(){
  // delete a user
};

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree =  new Admin('shaun', 'shaun@thenetninja.co.uk', 'black-belt-ninja');

console.log(userOne, userTwo, userThree);
userOne.login().logout();
// Google Console - 5
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', title: 'black-belt-ninja'}
   mario has logged in
   mario has logged out
>

Built-in Objects

// Google Console
>  new Array(1,2,3,4,5)
<  (5) [1,2,3,4,5]
>  new Object
<  {}
>  new XMLHttpRequest
<  XMLHttpRequest {onreadystatechange: null, readyState: 0, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, …}
>

Making a Forecast Class (weather app)

// scripts/forecast.js

class Forecast{
  constructor(){
    this.key = 'rdec-key-123-45678-011121314';
    this.weatherURI = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/';
    this.cityURI = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001';
    this.id = 'F-C0032-001';
  }
  async updateCity(city){
    const cityDets = await this.getCity(city);
    const weather = await this.getWeather(city);
    return { cityDets, weather };
  }
  async getCity(city){
    const query = `?Authorization=${this.key}&locationName=${city}`;
    const response = await fetch(this.cityURI + query);
    const data = await response.json();
    return data;
  }
  async getWeather(city){
    const query = `${this.id}?Authorization=${this.key}&locationName=${city}`;
    const response = await fetch(this.weatherURI + query);
    const data = await response.json();
    return data;
  }
}
// scripts/app.js

const cityForm = document.querySelector('form');
const card = document.querySelector('.card');
const details = document.querySelector('.details');
const time = document.querySelector('img.time');
const icon = document.querySelector('.icon img');
const forecast = new Forecast();

// console.log(forecast);

const updateUI = (data) => {
  // destructure properties
  const { cityDets, weather } = data;

  // update details template
  details.innerHTML = `
    <h4 class="my-3">12小時內天氣預報</h4>
    <h5 class="my-3">${cityDets.records.location[0].locationName}</h5>
    <div class="my-3">${weather.records.location[0].weatherElement[0].time[0].parameter.parameterName}</div>
    <div class="display-4 my-4">
      <span>氣溫</span><br>
      <span>${weather.records.location[0].weatherElement[2].time[0].parameter.parameterName} - ${weather.records.location[0].weatherElement[4].time[0].parameter.parameterName}  &deg;C</span>
    </div>
  `;

  // update the night/day & icon images
  const iconSrc = `img/icons/${weather.records.location[0].weatherElement[0].time[0].parameter.parameterValue}.svg`;
  icon.setAttribute('src', iconSrc);


  const timeSrc = weather.records.location[0].weatherElement[0].time[0].startTime.indexOf("06:00:00") || weather.records.location[0].weatherElement[0].time[0].startTime.indexOf("12:00:00") >= 0 ? 'img/day.svg' : 'img/night.svg';
  time.setAttribute('src', timeSrc);

  // remove the d-none class if present
  if(card.classList.contains('d-none')){
    card.classList.remove('d-none');
  }

};

cityForm.addEventListener('submit', e => {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  forecast.updateCity(city)
  .then(data => updateUI(data))
  .catch(err => console.log(err));

  // set local storage
  localStorage.setItem('city', city);

});

if(localStorage.getItem('city')){
  forecast.updateCity(localStorage.getItem('city'))
    .then(data => updateUI(data))
    .catch(err => console.log(err));
};

Databases (Firebase)

NoSQL Databases

Storing Data

  • Websites work with data
    • blog posts, todos, comments, user info, scores etc
  • We can store this data in database
    • Firebase (by Google)

Storing Data

Storing Data

NoSQL Databases

NoSQL Databases

Firebase & Firestore

操作步驟

  • GO TO CONSOLE
  • 新的 Firebase 專案 > 新增專案
  • 專案名稱 udemy-modern-javascript
  • 適用於 Firebase 專案的 Google Analytics (分析)
    • 這裡我先不使用
  • 建立專案
  • 建構 > Firestore Database
  • 建立資料庫
  • Cloud Firestore 的安全規則
    • 以測試模式啟動 (在這裡先使用這個)
      以正式版模式啟動 (在未來使用上安全是重要的)
  • 設定 Cloud Firestore 位置
    • 選擇東亞啟用
  • 新增集合 (Add collection)
    • 集合 ID (Collection ID) – recipes
  • 新增第一份文件 (Add first document)
    • 欄位(Field) title、類型(Type) string、值(Value) veg & tofu ninja curry
    • 欄位(Field) author、類型(Type) string、值(Value) ryu
    • 欄位(Field) created_at 日期(Date) 2022年1月24日、類型(Type) timestamp
  • 自動產生的 ID、然後儲存
  • 新增文件 (Add a document)
    • 欄位(Field) title、類型(Type) string、值(Value) spring green burrito
    • 欄位(Field) author、類型(Type) string、值(Value) chun li
    • 欄位(Field) created_at 日期(Date) 2022年1月25日、類型(Type) timestamp
  • 自動產生的 ID、然後儲存

可以參考 The Net Ninja Youtube Channel 的其他課程。

課程補充文件

// In index.html file put the script tag like this:

<script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-firestore.js"></script>
<script>
  // replace "xxxx" with your own generated firebase config
  const firebaseConfig = {
  apiKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  authDomain: 'xxxxxxxxxxxxxxxxxxxxx.com',
  projectId: 'xxxxx-js',
  storageBucket: 'xxxxxxxxxxx.com',
  messagingSenderId: 'xxxxxxxxxxxxxxxxx',
  appId: 'xxxxxxxxxxxxxxxxx',
  measurementId: 'xxxxxxxxxxxx'
  };
    
  firebase.initializeApp(firebaseConfig);
  const db = firebase.firestore();
</script>
<script src="sandbox.js"></script>
// in sandbox.js file

db.collection('recipes')
   .get()
   .then((snapshot) => {
      snapshot.docs.forEach((doc) => {
         console.log(doc.data());
      });
   })
   .catch((err) => {
      console.log(err);
   });

Connecting to Firestore

// index.html

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="style.css">
    <title>Modern JavaScript</title>
</head>
<body>
    <h1>Databases(Firebase)</h1>
    
    
    <script src="sandbox.js"></script>
</body>
</html>

操作步驟

  • 建立一個 index.html 檔案
  • 專案總覽 udemy-modern-javascript
  • 首先請新增應用程式 > 選擇網頁
  • 將 Firebase 新增至您的網頁應用程式
    • 註冊應用程式 >應用程式暱稱 udemy-modern-javascript > 註冊應用程式
    • 新增 Firebase SDK > 使用 <script> 標記 (選擇這個)
  • 這個教學影片版本是使用 5.9.1
  • 載入 Bootstrap v4.6
  • 製作 index.html 的 HTML Template

注意:
Make sure you use the same version of Firebase as me – 5.9.1 – to make sure everything works the same way as in the videos.

There will be a chapter at the end of the course showing how to use the most recent version of Firebase as well (version 9)

// index.html - HTML Template

  <div class="container my-5">
    <h2>Recipes</h2>
    <ul>
      <li>Mushroom pie</li>
      <li>Veg curry</li>
    </ul>
    <form>
      <label for="recipe">Add a new recipe:</label>
      <div class="input-group">
        <input type="text" class="form-control" id="recipe" required>
        <div class="input-group-append">
          <input type="submit" value="add" class="btn btn-outline-secondary">
        </div>
      </div>
    </form>
  </div>
// index.html - 串接資料查閱 Firebase

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css">
    <title>Modern JavaScript</title>
</head>
<body>
    
  <div class="container my-5">
    <h2>Recipes</h2>
    <ul>
      <li>Mushroom pie</li>
      <li>Veg curry</li>
    </ul>
    <form>
      <label for="recipe">Add a new recipe:</label>
      <div class="input-group">
        <input type="text" class="form-control" id="recipe" required>
        <div class="input-group-append">
          <input type="submit" value="add" class="btn btn-outline-secondary">
        </div>
      </div>
    </form>
  </div>
    
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-firestore.js"></script>
    <script>
      // Initialize Firebase
      const firebaseConfig = {
          apiKey: "",
          authDomain: "",
          projectId: "",
          storageBucket: "",
          messagingSenderId: "",
          appId: ""
      };
      
      // Initialize Firebase
      firebase.initializeApp(firebaseConfig);
      const db = firebase.firestore();
    </script>
    <script src="sandbox.js"></script>
</body>
</html>

Getting Collections

// sandbox.js

// index.html - 串接資料查閱 firebase

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css">
    <title>Modern JavaScript</title>
</head>
<body>
    
  <div class="container my-5">
    <h2>Recipes</h2>
    <ul>
    </ul>
    <form>
      <label for="recipe">Add a new recipe:</label>
      <div class="input-group">
        <input type="text" class="form-control" id="recipe" required>
        <div class="input-group-append">
          <input type="submit" value="add" class="btn btn-outline-secondary">
        </div>
      </div>
    </form>
  </div>
    
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-firestore.js"></script>
    <script>
      // Initialize Firebase
      const firebaseConfig = {
          apiKey: "",
          authDomain: "",
          projectId: "",
          storageBucket: "",
          messagingSenderId: "",
          appId: ""
      };
      
      // Initialize Firebase
      firebase.initializeApp(firebaseConfig);
      const db = firebase.firestore();
    </script>
    <script src="sandbox.js"></script>
</body>
</html>
// sandbox.js

const list = document.querySelector('ul');

const addRecipe = (recipe) => {
  // console.log(recipe.created_at.toDate());
  let time = recipe.created_at.toDate();
  let html = `
    <li>
      <div>${recipe.title}</div>
      <div>${time}</div>
    </li>
  `;

  // console.log(html);
  list.innerHTML += html;
}

// get documents
db.collection('recipes').get().then(snapshot => {
  // when we have the data
  snapshot.docs.forEach(doc => {
    // console.log(doc.data());
    addRecipe(doc.data());
  });
}).catch(err => {
  console.log(err);
});

Saving Documents

// sandbox.js

const list = document.querySelector('ul');
const form = document.querySelector('form');

const addRecipe = (recipe) => {
  // console.log(recipe.created_at.toDate());
  let time = recipe.created_at.toDate();
  let html = `
    <li>
      <div>${recipe.title}</div>
      <div>${time}</div>
    </li>
  `;

  // console.log(html);
  list.innerHTML += html;
}

// get documents
db.collection('recipes').get().then(snapshot => {
  // when we have the data
  snapshot.docs.forEach(doc => {
    // console.log(doc.data());
    addRecipe(doc.data());
  });
}).catch(err => {
  console.log(err);
});

// add documents
form.addEventListener('submit', e => {
  e.preventDefault();

  const now = new Date();
  const recipe = {
    title: form.recipe.value,
    created_at: firebase.firestore.Timestamp.fromDate(now)
  };

  db.collection('recipes').add(recipe).then(() => {
    console.log('recipe added');
  }).catch(err => {
    console.log(err);
  });

});

Deleting Documents

不懂可重複觀看、練習。

// index.html - 串接資料查閱 Firebase

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css">
    <title>Modern JavaScript</title>
</head>
<body>
    
  <div class="container my-5">
    <h2>Recipes</h2>
    <ul>
    </ul>
    <form>
      <label for="recipe">Add a new recipe:</label>
      <div class="input-group">
        <input type="text" class="form-control" id="recipe" required>
        <div class="input-group-append">
          <input type="submit" value="add" class="btn btn-outline-secondary">
        </div>
      </div>
    </form>
  </div>
    
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-firestore.js"></script>
    <script>
      // Initialize Firebase
      const firebaseConfig = {
          apiKey: "",
          authDomain: "",
          projectId: "",
          storageBucket: "",
          messagingSenderId: "",
          appId: ""
      };
      
      // Initialize Firebase
      firebase.initializeApp(firebaseConfig);
      const db = firebase.firestore();
    </script>
    <script src="sandbox.js"></script>
</body>
</html>
// sandbox.js

const list = document.querySelector('ul');
const form = document.querySelector('form');

const addRecipe = (recipe, id) => {
  // console.log(recipe.created_at.toDate());
  let time = recipe.created_at.toDate();
  let html = `
    <li data-id="${id}">
      <div>${recipe.title}</div>
      <div>${time}</div>
      <button class="btn btn-danger btn-sm my-2">delete</button>
    </li>
  `;

  // console.log(html);
  list.innerHTML += html;
}

// get documents
db.collection('recipes').get().then(snapshot => {
  // when we have the data
  snapshot.docs.forEach(doc => {
    // console.log(doc.data());
    // console.log(doc.id);
    addRecipe(doc.data(), doc.id);
  });
}).catch(err => {
  console.log(err);
});

// add documents
form.addEventListener('submit', e => {
  e.preventDefault();

  const now = new Date();
  const recipe = {
    title: form.recipe.value,
    created_at: firebase.firestore.Timestamp.fromDate(now)
  };

  db.collection('recipes').add(recipe).then(() => {
    console.log('recipe added');
  }).catch(err => {
    console.log(err);
  });

});

// deleting data
list.addEventListener('click', e => {
  // console.log(e);
  if(e.target.tagName === 'BUTTON'){
    const id = e.target.parentElement.getAttribute('data-id');
    // console.log(id);
    db.collection('recipes').doc(id).delete().then(() => {
      console.log('recipe deleted');
    });
  }
});

Real-time Listeners

不懂可重複觀看、練習。

// sandbox.js

const list = document.querySelector('ul');
const form = document.querySelector('form');

const addRecipe = (recipe, id) => {
  // console.log(recipe.created_at.toDate());
  let time = recipe.created_at.toDate();
  let html = `
    <li data-id="${id}">
      <div>${recipe.title}</div>
      <div>${time}</div>
      <button class="btn btn-danger btn-sm my-2">delete</button>
    </li>
  `;

  // console.log(html);
  list.innerHTML += html;
}

const deleteRecipe = (id) => {
  const recipes = document.querySelectorAll('li');
  recipes.forEach(recipe => {
    if(recipe.getAttribute('data-id') === id){
      recipe.remove();
    }
  });
}

// get documents
db.collection('recipes').onSnapshot(snapshot => {
  // console.log(snapshot.docChanges());
  snapshot.docChanges().forEach(change => {
    // console.log(change);
    const doc = change.doc;
    // console.log(doc);
    if(change.type === 'added'){
      addRecipe(doc.data(), doc.id);
    } else if (change.type === 'removed'){
      deleteRecipe(doc.id);
    }
  })
});

// add documents
form.addEventListener('submit', e => {
  e.preventDefault();

  const now = new Date();
  const recipe = {
    title: form.recipe.value,
    created_at: firebase.firestore.Timestamp.fromDate(now)
  };

  db.collection('recipes').add(recipe).then(() => {
    console.log('recipe added');
  }).catch(err => {
    console.log(err);
  });

});

// deleting data
list.addEventListener('click', e => {
  // console.log(e);
  if(e.target.tagName === 'BUTTON'){
    const id = e.target.parentElement.getAttribute('data-id');
    // console.log(id);
    db.collection('recipes').doc(id).delete().then(() => {
      console.log('recipe deleted');
    });
  }
});

Unsubscribing

// index.html - 串接資料查閱 Firebase

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css">
    <title>Modern JavaScript</title>
</head>
<body>
    
  <div class="container my-5">
    <h2>Recipes</h2>
    <ul>
    </ul>
    <form>
      <label for="recipe">Add a new recipe:</label>
      <div class="input-group">
        <input type="text" class="form-control" id="recipe" required>
        <div class="input-group-append">
          <input type="submit" value="add" class="btn btn-outline-secondary">
        </div>
      </div>
    </form>
    <button>unsubscribe from changes</button>
  </div>
    
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-firestore.js"></script>
    <script>
      // Initialize Firebase
      const firebaseConfig = {
          apiKey: "",
          authDomain: "",
          projectId: "",
          storageBucket: "",
          messagingSenderId: "",
          appId: ""
      };
      
      // Initialize Firebase
      firebase.initializeApp(firebaseConfig);
      const db = firebase.firestore();
    </script>
    <script src="sandbox.js"></script>
</body>
</html>
// sandbox.js

const list = document.querySelector('ul');
const form = document.querySelector('form');
const button = document.querySelector('button');

const addRecipe = (recipe, id) => {
  // console.log(recipe.created_at.toDate());
  let time = recipe.created_at.toDate();
  let html = `
    <li data-id="${id}">
      <div>${recipe.title}</div>
      <div>${time}</div>
      <button class="btn btn-danger btn-sm my-2">delete</button>
    </li>
  `;

  // console.log(html);
  list.innerHTML += html;
}

const deleteRecipe = (id) => {
  const recipes = document.querySelectorAll('li');
  recipes.forEach(recipe => {
    if(recipe.getAttribute('data-id') === id){
      recipe.remove();
    }
  });
}

// get documents
const unsub = db.collection('recipes').onSnapshot(snapshot => {
  // console.log(snapshot.docChanges());
  snapshot.docChanges().forEach(change => {
    // console.log(change);
    const doc = change.doc;
    // console.log(doc);
    if(change.type === 'added'){
      addRecipe(doc.data(), doc.id);
    } else if (change.type === 'removed'){
      deleteRecipe(doc.id);
    }
  })
});

// add documents
form.addEventListener('submit', e => {
  e.preventDefault();

  const now = new Date();
  const recipe = {
    title: form.recipe.value,
    created_at: firebase.firestore.Timestamp.fromDate(now)
  };

  db.collection('recipes').add(recipe).then(() => {
    console.log('recipe added');
  }).catch(err => {
    console.log(err);
  });

});

// deleting data
list.addEventListener('click', e => {
  // console.log(e);
  if(e.target.tagName === 'BUTTON'){
    const id = e.target.parentElement.getAttribute('data-id');
    // console.log(id);
    db.collection('recipes').doc(id).delete().then(() => {
      console.log('recipe deleted');
    });
  }
});

// unsub from database changes
button.addEventListener('click', () => {
  unsub();
  console.log('unsubscribed from collection changes');
});

Project – Real-time Chatroom

非常有趣,不懂可以重複觀看、練習。

Project Preview & Setup

操作步驟

  • 建立一個專案資料夾 ninja_chat
  • 建立 index.html 檔案
  • 建立一個 scripts 資料夾
  • 在 scripts 資料夾裡面建立以下檔案
    • app.js
    • ui.js
    • chat.js
  • 載入 JS 檔案,chat.js、ui.js、app.js
  • 載入 CSS 檔案,bootstrap v4.6 cdn
  • 建立 style.css 檔案
  • 載入 CSS 檔案,style.css
  • 開啟 Live Server

HTML Template

// index.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
  <link rel="stylesheet" href="style.css">
  <title>Ninja Chat</title>
</head>
<body>

  <!-- container & title -->
  <div class="container my-4">
    <h1 class="my-4 text-center">Ninja Chat</h1>


    <!-- buttons for chatrooms -->
    <div class="chat-rooms mb-3 text-center">
      <div class="my-2">Choose a chatroom:</div>
      <button class="btn" id="general">#general</button>
      <button class="btn" id="gaming">#gaming</button>
      <button class="btn" id="music">#music</button>
      <button class="btn" id="ninjas">#ninjas</button>
    </div>

    <!-- chat list / window -->
    <div class="chat-window">
      <ul class="chat-list list-group"></ul>
    </div>

    <!-- new chat form -->
    <form class="new-chat my-3">
      <div class="input-group">
        <div class="input-group-prepend">
          <div class="input-group-text">Your message:</div>
        </div>
        <input type="text" id="message" class="form-control" required>
        <div class="input-group-append">
          <input type="submit" class="btn" value="send">
        </div>
      </div>
    </form>

    <!-- update name form -->
    <form class="new-name my-3">
      <div class="input-group">
        <div class="input-group-prepend">
          <div class="input-group-text">Update name:</div>
        </div>
        <input type="text" id="name" class="form-control" required>
        <div class="input-group-append">
          <input type="submit" class="btn" value="update">
        </div>
      </div>
      <div class="update-mssg"></div>
    </form>
    
  </div>

  <script src="scripts/chat.js"></script>
  <script src="scripts/ui.js"></script>
  <script src="scripts/app.js"></script>
</body>
</html>
// style.css

.container{
  max-width: 600px;
}
.btn{
  background: #43d9be;
  color: white;
  outline: none !important;
  box-shadow: none !important;
}
.btn:focus{
  outline: none !important;
}

Connecting to Firebase

Make sure you use the same version of Firebase as me – 5.9.1 – to make sure everything works the same way as in the videos.

There will be a chapter at the end of the course showing how to use the most recent version of Firebase as well (version 9)

操作步驟

  • 載入 SDK 設定和配置 (使用 udermy-modern-javascript 來練習)
  • 點選 udemy-modern-javascript、選擇 Firestore Database
  • 並不是每次都需要手動新增集合(collection),這邊是想要新增兩個虛假的文件
  • 開始新增集合 (Set collection ID)
    • 集合ID (Collection ID) chats
  • 新增第一份文件、自動產生 ID,然後儲存
    • 欄位(Field) message、類型(Types) string、值(Value) hey guys
    • 欄位(Field) username、類型(Types) string、值(Value) yoshi
    • 欄位(Field) room、類型(Types) string、值(Value) general
    • 欄位(Field) created_at、類型(Types) timestamp 2022年1月27日
  • 新增文件、自動產生 ID、然後儲存
    • 欄位(Field) message、類型(Types) string、值(Value) Yo Yoshi
    • 欄位(Field) username、類型(Types) string、值(Value) mario
    • 欄位(Field) room、類型(Types) string、值(Value) general
    • 欄位(Field) created_at、類型 timestamp 2022年1月27日
// index.html 串接資料查閱 Firebase

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
  <link rel="stylesheet" href="style.css">
  <title>Ninja Chat</title>
</head>
<body>

  <!-- container & title -->
  <div class="container my-4">
    <h1 class="my-4 text-center">Ninja Chat</h1>


    <!-- buttons for chatrooms -->
    <div class="chat-rooms mb-3 text-center">
      <div class="my-2">Choose a chatroom:</div>
      <button class="btn" id="general">#general</button>
      <button class="btn" id="gaming">#gaming</button>
      <button class="btn" id="music">#music</button>
      <button class="btn" id="ninjas">#ninjas</button>
    </div>

    <!-- chat list / window -->
    <div class="chat-window">
      <ul class="chat-list list-group"></ul>
    </div>

    <!-- new chat form -->
    <form class="new-chat my-3">
      <div class="input-group">
        <div class="input-group-prepend">
          <div class="input-group-text">Your message:</div>
        </div>
        <input type="text" id="message" class="form-control" required>
        <div class="input-group-append">
          <input type="submit" class="btn" value="send">
        </div>
      </div>
    </form>

    <!-- update name form -->
    <form class="new-name my-3">
      <div class="input-group">
        <div class="input-group-prepend">
          <div class="input-group-text">Update name:</div>
        </div>
        <input type="text" id="name" class="form-control" required>
        <div class="input-group-append">
          <input type="submit" class="btn" value="update">
        </div>
      </div>
      <div class="update-mssg"></div>
    </form>
    
  </div>

  <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-firestore.js"></script>
  <script>
    // Initialize Firebase
    const firebaseConfig = {
        apiKey: "",
        authDomain: "",
        projectId: "",
        storageBucket: "",
        messagingSenderId: "",
        appId: ""
    };
    
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
    const db = firebase.firestore();
  </script>
  <script src="scripts/chat.js"></script>
  <script src="scripts/ui.js"></script>
  <script src="scripts/app.js"></script>
</body>
</html>

Chatroom Class & Adding Chats

chat.js 從零開始撰寫程式碼,
不懂可重複觀看、練習。

// chat.js

// adding new chat documents
// setting up a real-time listener to get new chats
// updating the username
// updating the room

class Chatroom {
  constructor(room, username){
    this.room = room;
    this.username = username;
    this.chats = db.collection('chats');
  }
  async addChat(message){
    // format a chat object
    const now = new Date();
    const chat = {
      message,
      username: this.username,
      room: this.room,
      created_at: firebase.firestore.Timestamp.fromDate(now)
    };
    // save the chat document
    const response = await this.chats.add(chat);
    return response;
  }
}

const chatroom = new Chatroom('gaming', 'shaun');
// console.log(chatroom);

chatroom.addChat('hello everyone')
  .then(() => console.log('chat added'))
  .catch(err => console.log(err));

Setting up a Real-time Listener

// chat.js

// adding new chat documents
// setting up a real-time listener to get new chats
// updating the username
// updating the room

class Chatroom {
  constructor(room, username){
    this.room = room;
    this.username = username;
    this.chats = db.collection('chats');
  }
  async addChat(message){
    // format a chat object
    const now = new Date();
    const chat = {
      message,
      username: this.username,
      room: this.room,
      created_at: firebase.firestore.Timestamp.fromDate(now)
    };
    // save the chat document
    const response = await this.chats.add(chat);
    return response;
  }
  getChats(callback){
    this.chats
      .onSnapshot(snapshot => {
        snapshot.docChanges().forEach(change => {
          if(change.type === 'added'){
            // update the ui
            callback(change.doc.data());
          }
        });
    });
  }
}

const chatroom = new Chatroom('gaming', 'shaun');

chatroom.getChats((data) => {
  console.log(data);
});

測試 – 在 Firestore Database 新增文件是否會 console.log 出物件資料

  • 新增文件
    • 欄位(Field) message、類型(Types) string、值(Value) test

Complex Queries

不懂可重複觀看、練習。

// chat.js

// adding new chat documents
// setting up a real-time listener to get new chats
// updating the username
// updating the room

class Chatroom {
  constructor(room, username){
    this.room = room;
    this.username = username;
    this.chats = db.collection('chats');
  }
  async addChat(message){
    // format a chat object
    const now = new Date();
    const chat = {
      message,
      username: this.username,
      room: this.room,
      created_at: firebase.firestore.Timestamp.fromDate(now)
    };
    // save the chat document
    const response = await this.chats.add(chat);
    return response;
  }
  getChats(callback){
    this.chats
      .where('room', '==', this.room)
      .orderBy('created_at')
      .onSnapshot(snapshot => {
        snapshot.docChanges().forEach(change => {
          if(change.type === 'added'){
            // update the ui
            callback(change.doc.data());
          }
        });
    });
  }
}

const chatroom = new Chatroom('general', 'shaun');

chatroom.getChats((data) => {
  console.log(data);
});

操作步驟

  • 在 Firestore Database 移除 test 文件
  • 在 orderBy(‘created_at) 產生錯誤時 Google Console 有顯示如何建立 index、開啟後按建立

Updating the Room & Username

不懂可重複觀看、練習。

// chat.js

// adding new chat documents
// setting up a real-time listener to get new chats
// updating the username
// updating the room

class Chatroom {
  constructor(room, username){
    this.room = room;
    this.username = username;
    this.chats = db.collection('chats');
    this.unsub;
  }
  async addChat(message){
    // format a chat object
    const now = new Date();
    const chat = {
      message,
      username: this.username,
      room: this.room,
      created_at: firebase.firestore.Timestamp.fromDate(now)
    };
    // save the chat document
    const response = await this.chats.add(chat);
    return response;
  }
  getChats(callback){
    this.unsub = this.chats
      .where('room', '==', this.room)
      .orderBy('created_at')
      .onSnapshot(snapshot => {
        snapshot.docChanges().forEach(change => {
          if(change.type === 'added'){
            // update the ui
            callback(change.doc.data());
          }
        });
    });
  }
  updateName(username){
    this.username = username;
  }
  updateRoom(room){
    this.room = room;
    console.log('room updated');
    if(this.undub){
      this.unsub();
    }
  }
}

const chatroom = new Chatroom('general', 'shaun');

chatroom.getChats((data) => {
  console.log(data);
});

setTimeout(() => {
  chatroom.updateRoom('gaming');
  chatroom.updateName('yoshi');
  chatroom.getChats((data) => {
    console.log(data);
  });
  chatroom.addChat('hello');
}, 3000);

Creating a ChatUI Class

不懂可重複觀看、練習。

// chat.js

// adding new chat documents
// setting up a real-time listener to get new chats
// updating the username
// updating the room

class Chatroom {
  constructor(room, username){
    this.room = room;
    this.username = username;
    this.chats = db.collection('chats');
    this.unsub;
  }
  async addChat(message){
    // format a chat object
    const now = new Date();
    const chat = {
      message,
      username: this.username,
      room: this.room,
      created_at: firebase.firestore.Timestamp.fromDate(now)
    };
    // save the chat document
    const response = await this.chats.add(chat);
    return response;
  }
  getChats(callback){
    this.unsub = this.chats
      .where('room', '==', this.room)
      .orderBy('created_at')
      .onSnapshot(snapshot => {
        snapshot.docChanges().forEach(change => {
          if(change.type === 'added'){
            // update the ui
            callback(change.doc.data());
          }
        });
    });
  }
  updateName(username){
    this.username = username;
  }
  updateRoom(room){
    this.room = room;
    console.log('room updated');
    if(this.undub){
      this.unsub();
    }
  }
}

// app.js

// dom queries
const chatList = document.querySelector('.chat-list');

// class instances
const chatUI = new ChatUI(chatList);
const chatroom = new Chatroom('general', 'shaun');

// get chats and render
chatroom.getChats(data => chatUI.render(data));
// ui.js

// render chat templates to the DOM
// clear the list of chats (when the room changes)

class ChatUI {
  constructor(list){
    this.list = list;
  }
  render(data){
    const html = `
      <li class="list-group-item">
        <span class="username">${data.username}</span>
        <span class="message">${data.message}</span>
        <div class="time">${data.created_at.toDate()}</div>
      </li>
    `;

    this.list.innerHTML += html;
  }
}

Formatting the Dates

GitHub Course

// index.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
  <link rel="stylesheet" href="style.css">
  <title>Ninja Chat</title>
</head>
<body>

  <!-- container & title -->
  <div class="container my-4">
    <h1 class="my-4 text-center">Ninja Chat</h1>


    <!-- buttons for chatrooms -->
    <div class="chat-rooms mb-3 text-center">
      <div class="my-2">Choose a chatroom:</div>
      <button class="btn" id="general">#general</button>
      <button class="btn" id="gaming">#gaming</button>
      <button class="btn" id="music">#music</button>
      <button class="btn" id="ninjas">#ninjas</button>
    </div>

    <!-- chat list / window -->
    <div class="chat-window">
      <ul class="chat-list list-group"></ul>
    </div>

    <!-- new chat form -->
    <form class="new-chat my-3">
      <div class="input-group">
        <div class="input-group-prepend">
          <div class="input-group-text">Your message:</div>
        </div>
        <input type="text" id="message" class="form-control" required>
        <div class="input-group-append">
          <input type="submit" class="btn" value="send">
        </div>
      </div>
    </form>

    <!-- update name form -->
    <form class="new-name my-3">
      <div class="input-group">
        <div class="input-group-prepend">
          <div class="input-group-text">Update name:</div>
        </div>
        <input type="text" id="name" class="form-control" required>
        <div class="input-group-append">
          <input type="submit" class="btn" value="update">
        </div>
      </div>
      <div class="update-mssg"></div>
    </form>
    
  </div>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.9.0/date_fns.min.js"></script>
  <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-firestore.js"></script>
  <script>
    // Initialize Firebase
    const firebaseConfig = {
        apiKey: "",
        authDomain: "",
        projectId: "",
        storageBucket: "",
        messagingSenderId: "",
        appId: ""
    };
    
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
    const db = firebase.firestore();
  </script>
  <script src="scripts/chat.js"></script>
  <script src="scripts/ui.js"></script>
  <script src="scripts/app.js"></script>
</body>
</html>
// ui.js

// render chat templates to the DOM
// clear the list of chats (when the room changes)

class ChatUI {
  constructor(list){
    this.list = list;
  }
  render(data){
    const when = dateFns.distanceInWordsToNow(
      data.created_at.toDate(),
      { addSuffix: true }
    );
    const html = `
      <li class="list-group-item">
        <span class="username">${data.username}</span>
        <span class="message">${data.message}</span>
        <div class="time">${when}</div>
      </li>
    `;

    this.list.innerHTML += html;
  }
}
// style.css

.container{
  max-width: 600px;
}
.btn{
  background: #43d9be;
  color: white;
  outline: none !important;
  box-shadow: none !important;
}
.btn:focus{
  outline: none !important;
}
.username{
  font-weight: bold;
}
.time{
  font-size: 0.7em;
  color: #999;
}

Sending New Chats

不懂可重複觀看、練習。

// app.js

// dom queries
const chatList = document.querySelector('.chat-list');
const newChatForm = document.querySelector('.new-chat');

// add a new chat
newChatForm.addEventListener('submit', e => {
  e.preventDefault();
  const message = newChatForm.message.value.trim();
  chatroom.addChat(message)
    .then(() => newChatForm.reset())
    .catch(err => console.log(err));
});

// class instances
const chatUI = new ChatUI(chatList);
const chatroom = new Chatroom('general', 'shaun');

// get chats and render
chatroom.getChats(data => chatUI.render(data));

Changing Username & Local Storage

重要,不懂可重複觀看、練習。

// app.js - 1

// dom queries
const chatList = document.querySelector('.chat-list');
const newChatForm = document.querySelector('.new-chat');
const newNameForm = document.querySelector('.new-name');
const updateMssg = document.querySelector('.update-mssg');

// add a new chat
newChatForm.addEventListener('submit', e => {
  e.preventDefault();
  const message = newChatForm.message.value.trim();
  chatroom.addChat(message)
    .then(() => newChatForm.reset())
    .catch(err => console.log(err));
});

// update username
newNameForm.addEventListener('submit', e => {
  e.preventDefault();
  // update name via chatroom
  const newName = newNameForm.name.value.trim();
  chatroom.updateName(newName);
  // reset the form
  newNameForm.reset();
  // show then hide the update message
  updateMssg.innerText = `Your name was updated to ${newName}`;
  setTimeout(() => updateMssg.innerText = '', 3000);
});

// class instances
const chatUI = new ChatUI(chatList);
const chatroom = new Chatroom('general', 'shaun');

// get chats and render
chatroom.getChats(data => chatUI.render(data));
// style.css

.container{
  max-width: 600px;
}
.btn{
  background: #43d9be;
  color: white;
  outline: none !important;
  box-shadow: none !important;
}
.btn:focus{
  outline: none !important;
}
.username{
  font-weight: bold;
}
.time{
  font-size: 0.7em;
  color: #999;
}
.update-mssg{
  text-align: center;
  margin: 20px auto;
}
// Google Console
>  chatroom
<  Chatroom {room: 'general', username: 'luigi', chats: t, unsub: ƒ}
// chat.js

// adding new chat documents
// setting up a real-time listener to get new chats
// updating the username
// updating the room

class Chatroom {
  constructor(room, username){
    this.room = room;
    this.username = username;
    this.chats = db.collection('chats');
    this.unsub;
  }
  async addChat(message){
    // format a chat object
    const now = new Date();
    const chat = {
      message,
      username: this.username,
      room: this.room,
      created_at: firebase.firestore.Timestamp.fromDate(now)
    };
    // save the chat document
    const response = await this.chats.add(chat);
    return response;
  }
  getChats(callback){
    this.unsub = this.chats
      .where('room', '==', this.room)
      .orderBy('created_at')
      .onSnapshot(snapshot => {
        snapshot.docChanges().forEach(change => {
          if(change.type === 'added'){
            // update the ui
            callback(change.doc.data());
          }
        });
    });
  }
  updateName(username){
    this.username = username;
    localStorage.setItem('username', username);
  }
  updateRoom(room){
    this.room = room;
    console.log('room updated');
    if(this.undub){
      this.unsub();
    }
  }
}

// app.js - 2

// dom queries
const chatList = document.querySelector('.chat-list');
const newChatForm = document.querySelector('.new-chat');
const newNameForm = document.querySelector('.new-name');
const updateMssg = document.querySelector('.update-mssg');

// add a new chat
newChatForm.addEventListener('submit', e => {
  e.preventDefault();
  const message = newChatForm.message.value.trim();
  chatroom.addChat(message)
    .then(() => newChatForm.reset())
    .catch(err => console.log(err));
});

// update username
newNameForm.addEventListener('submit', e => {
  e.preventDefault();
  // update name via chatroom
  const newName = newNameForm.name.value.trim();
  chatroom.updateName(newName);
  // reset the form
  newNameForm.reset();
  // show then hide the update message
  updateMssg.innerText = `Your name was updated to ${newName}`;
  setTimeout(() => updateMssg.innerText = '', 3000);
});

// check local storage for a name
const username = localStorage.username ? localStorage.username : 'anon';

// class instances
const chatUI = new ChatUI(chatList);
const chatroom = new Chatroom('general', username);

// get chats and render
chatroom.getChats(data => chatUI.render(data));
// Google Console - test
>  chatroom
<  Chatroom {room: 'general', username: 'anon', chats: t, unsub: ƒ}
>
// Goolge Console - Update name: luigi
>  chatroom
<  Chatroom {room: 'general', username: 'anon', chats: t, unsub: ƒ}
>  chatroom
<  Chatroom {room: 'general', username: 'luigi', chats: t, unsub: ƒ}
>

Updating the Room

不懂可重複觀看、練習。

// app.js

// dom queries
const chatList = document.querySelector('.chat-list');
const newChatForm = document.querySelector('.new-chat');
const newNameForm = document.querySelector('.new-name');
const updateMssg = document.querySelector('.update-mssg');
const rooms = document.querySelector('.chat-rooms');

// add a new chat
newChatForm.addEventListener('submit', e => {
  e.preventDefault();
  const message = newChatForm.message.value.trim();
  chatroom.addChat(message)
    .then(() => newChatForm.reset())
    .catch(err => console.log(err));
});

// update username
newNameForm.addEventListener('submit', e => {
  e.preventDefault();
  // update name via chatroom
  const newName = newNameForm.name.value.trim();
  chatroom.updateName(newName);
  // reset the form
  newNameForm.reset();
  // show then hide the update message
  updateMssg.innerText = `Your name was updated to ${newName}`;
  setTimeout(() => updateMssg.innerText = '', 3000);
});

// update the chat room
rooms.addEventListener('click', e => {
  // console.log(e);
  if(e.target.tagName === 'BUTTON'){
    chatUI.clear();
    chatroom.updateRoom(e.target.getAttribute('id'));
    chatroom.getChats(chat => chatUI.render(chat));
  }
});

// check local storage for a name
const username = localStorage.username ? localStorage.username : 'anon';

// class instances
const chatUI = new ChatUI(chatList);
const chatroom = new Chatroom('general', username);

// get chats and render
chatroom.getChats(data => chatUI.render(data));
// ui.js

// render chat templates to the DOM
// clear the list of chats (when the room changes)

class ChatUI {
  constructor(list){
    this.list = list;
  }
  clear(){
    this.list.innerHTML = '';
  }
  render(data){
    const when = dateFns.distanceInWordsToNow(
      data.created_at.toDate(),
      { addSuffix: true }
    );
    const html = `
      <li class="list-group-item">
        <span class="username">${data.username}</span>
        <span class="message">${data.message}</span>
        <div class="time">${when}</div>
      </li>
    `;

    this.list.innerHTML += html;
  }
}

Testing the App

資源

More ES6 Features

Spread & Rest

// index.html

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css">
    <title>Modern JavaScript</title>
</head>
<body>
    
    <script src="sandbox.js"></script>
</body>
</html>
// sandbox.js

// rest parameter - 其餘參數
const double = (...nums) => {
  console.log(nums);
  return nums.map(num => num*2);
}

const result = double(1,3,5,7,9,2,4,6,8);
console.log(result);

// spread syntax (arrays) - 展開語法 (陣列)

const people = ['shaun', 'ryu', 'crystal'];
console.log(...people);
const members = ['mario', 'chun-li', ...people];
console.log(members);

// spread syntax (objects) - 展開語法 (物件)

const person = { name: 'shaun', age: 30, job: 'net ninja'};
const personClone = {...person, location: 'manchester'};

console.log(personClone);

Sets

// index.html

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css">
    <title>Modern JavaScript</title>
</head>
<body>
    
    <script src="sandbox.js"></script>
</body>
</html>
// sandbox.js

// sets
const namesArray = ['ryu', 'chun-li', 'ryu', 'shaun'];
console.log(namesArray);

// const namesSet = new Set(['ryu', 'chun-li', 'ryu', 'shaun']);
// const namesSet = new Set(namesArray);
// console.log(namesSet);

// const uniqueNames = [...namesSet];
const uniqueNames = [...new Set(namesArray)];
console.log(uniqueNames);

const ages = new Set();
ages.add(20);
ages.add(25).add(30);
ages.add(25);
ages.delete(25);

console.log(ages, ages.size);
console.log(ages.has(30), ages.has(25));

ages.clear();
console.log(ages);

const ninjas = new Set([
  {name: 'shaun', age: 30},
  {name: 'crystal', age: 29},
  {name: 'chun-li', age: 32}
]);

ninjas.forEach(ninja => {
  console.log(ninja.name, ninja.age);
});

Symbols (符號)

// index.html

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css">
    <title>Modern JavaScript</title>
</head>
<body>
    
    <script src="sandbox.js"></script>
</body>
</html>
// sandbox.js

const symbolOne = Symbol('a generic name');
const symbolTwo = Symbol('a generic name');

console.log(symbolOne, symbolTwo, typeof symbolOne);
console.log(symbolOne === symbolTwo);

const ninja = {};

ninja.age = 30;
ninja['belt'] = 'orange';
ninja['belt'] = 'black';

ninja[symbolOne] = 'ryu';
ninja[symbolTwo] = 'shaun';

console.log(ninja);