< Kembali

Membangun Tampilan Web dengan HTML, CSS & JS

✏️ Edit catatan ini.

Berikut adalah materi tertulis untuk acara workshop di Surabaya yang diselenggarakan Mei 2023.

Prasarat

Untuk mengikuti materi ini, teman-teman membutuhkan beberapa alat berikut:

Persiapan

cd /Users/riza/playground/webscrape-tutorial
npm run dev
ngrok http 8080

Ujicoba dulu...

curl localhost:8080/followers

Pengantar

Kesempatan kali ini kita akan membangun halaman web dengan HTML, CSS dan JavaScript. Dimuai dari tanpa menggunakan framework hingga bertahap diubah dengan memperkenalkan framework. Di akhir sesi diharapkan teman-teman yang mengikuti sesi ini sudah dapat melakukan:

Persiapan Proyek

mkdir webapp
cd webapp
code .

Halaman HTML

index.html

<!doctype 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>Web App</title>
  </head>

  <body>
    <header>
      <h1>Web Dashboard</h1>
    </header>
    <main>
      <div class="media">
        <div class="media-name">Twitter</div>
        <h2 class="media-followers">5,320</h2>
        <span class="media-label">followers</span>
      </div>
    </main>
    <footer>&copy; 2023 HACKTIV8 🦊</footer>
  </body>
</html>
npx live-server

Menambahkan CSS

style.css

:root {
  --bg_main: #0a1f44;
  --text_light: #fff;
  --text_med: #53627c;
  --text_dark: #1e2432;
  --red: #ff1e42;
  --darkred: #c3112d;
  --orange: #ff8c00;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-weight: normal;
}

body {
  font:
    1rem/1.3 "Roboto",
    sans-serif;
  background: var(--bg_main);
  color: var(--text_dark);
  padding: 50px;
}

header > h1 {
  font-weight: bold;
  font-size: 4rem;
  letter-spacing: 0.02em;
  padding: 0 0 30px 0;
  color: var(--text_light);
}

.media {
  position: relative;
  padding: 40px 10%;
  border-radius: 20px;
  background: var(--text_light);
  color: var(--text_med);
  width: 35%;
}

.media-name {
  font-size: 1.5rem;
}

.media-followers {
  font-size: 2rem;
  font-weight: bold;
}

.media-label {
  color: var(--text_med);
}

footer {
  text-align: center;
  font-size: 1rem;
  color: var(--text_light);
  margin-top: 40px;
}

footer span {
  color: var(--red);
}

Menambahkan JavaScript

index.html

 <!DOCTYPE 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>Web App</title>
 </head>

 <body>
   <header>
     <h1>Web Dashboard</h1>
   </header>
   <main>
     <div class="media">
       <div class="media-name">Twitter</div>
       <h2 class="media-followers">5,320</h2>
       <span class="media-label">followers</span>
     </div>
   </main>
   <footer>&copy; 2023 HACKTIV8 🦊</footer>
+   <script src="app.js"></script>
 </body>

 </html>

app.js

async function getData() {
  const response = await fetch("https://blabla.ngrok.io/followers");
  const { followers } = await response.json();
  return followers;
}

async function main() {
  try {
    const followers = await getData();
    render(followers);
  } catch (error) {
    console.log(error);
  }
}

main();

function render(followers) {
  const followersEl = document.querySelector(".media-followers");
  followersEl.textContent = formatNumber(followers[0].count);
}

function formatNumber(num) {
  const formatter = new Intl.NumberFormat("id-ID");
  return formatter.format(num);
}

Backend

npm init -y
npm install [email protected]

package.json

{
  "name": "webapp-tutorial",
  "version": "0.0.1",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "keywords": [],
  "author": "Riza Fahmi <[email protected]> (https://rizafahmi.com/)",
  "license": "MIT",
  "dependencies": {
    "express": "5.0.0-beta.1"
  },
  "type": "module"
}

server.js

import express from "express";
import path from "path";

const app = express();

app.get("/", function (req, res) {
  res.sendFile(path.resolve("index.html"));
});

app.listen(3000, function () {
  console.log("Example app listening on port 3000!");
});

Refactor Folder

.
├── public
│   ├── app.js
│   ├── index.html
│   └── style.css
├── package.json
├── pnpm-lock.yaml
└── server.js

server.js

import express from 'express';
import path from 'path';

const app = express();

+ app.use(express.static('public'));

app.get('/', function (req, res) {
- res.sendFile(path.resolve('index.html'));
+ res.sendFile(path.resolve('public/index.html'));
});

app.listen(3000, function () {
  console.log('Example app listening on port 3000!');
});

Menambahkan Media Sosial Lainnya

Statis/Hardcoded/manual: app.js

function render(followers) {
  const mainEl = document.querySelector("main");
  const mediaEl = document.createElement("div");
  mediaEl.className = "media";

  const nameEl = document.createElement("div");
  nameEl.className = "media-name";
  nameEl.textContent = followers[0].media;
  mediaEl.append(nameEl);

  const followerEl = document.createElement("h2");
  followerEl.className = "media-followers";
  followerEl.textContent = formatNumber(followers[0].count);
  mediaEl.append(followerEl);

  const labelEl = document.createElement("span");
  labelEl.className = "media-label";
  labelEl.textContent = "followers";
  mediaEl.append(labelEl);

  mainEl.append(mediaEl);
}

index.html

 <!DOCTYPE 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>Web App</title>
 </head>

 <body>
   <header>
     <h1>Web Dashboard</h1>
   </header>
   <main>
-     <div class="media">
-       <div class="media-name">Twitter</div>
-       <h2 class="media-followers">5,320</h2>
-       <span class="media-label">followers</span>
-     </div>
   </main>
   <footer>&copy; 2023 HACKTIV8 🦊</footer>
 </body>

 </html>

style.css

 :root {
   --bg_main: #0a1f44;
   --text_light: #fff;
   --text_med: #53627c;
   --text_dark: #1e2432;
   --red: #ff1e42;
   --darkred: #c3112d;
   --orange: #ff8c00;
 }

 * {
   margin: 0;
   padding: 0;
   box-sizing: border-box;
   font-weight: normal;
 }

 body {
   font: 1rem/1.3 'Roboto', sans-serif;
   background: var(--bg_main);
   color: var(--text_dark);
   padding: 50px;
 }

 header > h1 {
   font-weight: bold;
   font-size: 4rem;
   letter-spacing: 0.02em;
   padding: 0 0 30px 0;
   color: var(--text_light);
 }

 .media {
   position: relative;
   padding: 40px 10%;
   border-radius: 20px;
   background: var(--text_light);
   color: var(--text_med);
   width: 35%;
 }

 .media-name {
   font-size: 1.5rem;
+   text-transform: capitalize;
 }

 .media-followers {
   font-size: 2rem;
   font-weight: bold;
 }

 .media-label {
   color: var(--text_med);
 }

 footer {
   text-align: center;
   font-size: 1rem;
   color: var(--text_light);
   margin-top: 40px;
 }

 footer span {
   color: var(--red);
 }

Dinamis: app.js

function render(followers) {
  const mainEl = document.querySelector("main");

  for (let i = 0; i < followers.length; i += 1) {
    const mediaEl = document.createElement("div");
    mediaEl.className = "media";

    const nameEl = document.createElement("div");
    nameEl.className = "media-name";
    nameEl.textContent = followers[i].media;
    mediaEl.append(nameEl);

    const followerEl = document.createElement("h2");
    followerEl.className = "media-followers";
    followerEl.textContent = formatNumber(followers[i].count);
    mediaEl.append(followerEl);

    const labelEl = document.createElement("span");
    labelEl.className = "media-label";
    labelEl.textContent = "followers";
    mediaEl.append(labelEl);

    mainEl.append(mediaEl);
  }
}

style.css

 :root {
   --bg_main: #0a1f44;
   --text_light: #fff;
   --text_med: #53627c;
   --text_dark: #1e2432;
   --red: #ff1e42;
   --darkred: #c3112d;
   --orange: #ff8c00;
 }

 * {
   margin: 0;
   padding: 0;
   box-sizing: border-box;
   font-weight: normal;
 }

 body {
   font: 1rem/1.3 'Roboto', sans-serif;
   background: var(--bg_main);
   color: var(--text_dark);
   padding: 50px;
 }

 header > h1 {
   font-weight: bold;
   font-size: 4rem;
   letter-spacing: 0.02em;
   padding: 0 0 30px 0;
   color: var(--text_light);
 }

+ main {
+   display: flex;
+   flex-wrap: wrap;
+   gap: 24px;
+ }

 .media {
   position: relative;
   padding: 40px 10%;
   border-radius: 20px;
   background: var(--text_light);
   color: var(--text_med);
   width: 35%;
 }

 .media-name {
   font-size: 1.5rem;
   text-transform: capitalize;
 }

 .media-followers {
   font-size: 2rem;
   font-weight: bold;
 }

 .media-label {
   color: var(--text_med);
 }

 footer {
   text-align: center;
   font-size: 1rem;
   color: var(--text_light);
   margin-top: 40px;
 }

 footer span {
   color: var(--red);
 }

Menggunakan Framework

Rewrite ke Web Components

Pengantar

Adalah framework native yang didukung hampir semua browser modern. Kode dapat lebih modular dan komponen yang dibuat dapat digunakan berkali-kali. Performa tentu yang paling bagus karena sudah didukung web browser secara native tanpa perlu menambahkan pustaka atau kerangka kerja tambahan lagi. Terdiri dari empat teknologi utama: ES Module, Custom Elements, Shadow DOM, dan HTML Template.

Web Components Pertama

Custom Components: footer-component.js

class FooterComponent extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<footer>&copy; 2023 HACKTIV8 🦊</footer>`;
  }
}
customElements.define("footer-component", FooterComponent);

index.html

<!doctype 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>Web App</title>
  </head>

  <body>
    <header>
      <h1>Web Dashboard</h1>
    </header>
    <main>
      <div class="media">
        <div class="media-name">Twitter</div>
        <h2 class="media-followers" id="followers">5,320</h2>
        <span class="media-label">followers</span>
      </div>
    </main>
    <footer-component></footer-component>

    <script src="app.js"></script>
    <script src="footer-component.js"></script>
  </body>
</html>

Demo Web Component

ES Module: index.html

 <!DOCTYPE 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>Web App</title>
 </head>

 <body>
     <header>
         <h1>Web Dashboard</h1>
     </header>
     <div id="root">
+         <followers-count></followers-count>
     </div>

     <footer-component></footer-component>

-     <script src="app.js"></script>
+     <script src="app.js" type="module"></script>
     <script src="footer-component.js" type="module"></script>
 </body>

 </html>

followers-count.js

export class FollowersCount extends HTMLElement {
  constructor() {
    super();
    this.data = [];
  }

  set socials(value) {
    this.data = value;
    this.render();
  }

  render() {
    this.innerHTML = `
				  	  <main>
				  	      ${this.data.map(function (social) {
                    return `
				                  <div class="media">
				                  <div class="media-name">${social.media}</div>
				                  <h2 class="media-followers">${social.count}</h2>
				                  <span class="media-label">followers</span>
				                  </div>
				                `;
                  })}
				  	  </main>
				      `;
  }
}

customElements.define("followers-count", FollowersCount);

app.js

 import './followers-count.js';

 async function getData() {
   const response = await fetch('http://localhost:8080/followers');
   const { followers } = await response.json();
   return followers;
 }

- function render() {
-   // ...
- }

 function formatNumber(num) {
   const formatter = new Intl.NumberFormat('id-ID');
   return formatter.format(num);
 }

 async function main() {
   const followers = await getData();
+   const followersEl = document.querySelector('followers-count');
+   followersEl.socials = followers;
-	render(render);
 }

 main();

Bonus: Rewrite ke Framework lainnya

Kesimpulan