Berikut adalah materi tertulis untuk acara workshop di Surabaya yang diselenggarakan Mei 2023.
Untuk mengikuti materi ini, teman-teman membutuhkan beberapa alat berikut:
cd /Users/riza/playground/webscrape-tutorial
npm run dev
ngrok http 8080
Ujicoba dulu...
curl localhost:8080/followers
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:
mkdir webapp
cd webapp
code .
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>© 2023 HACKTIV8 🦊</footer>
</body>
</html>
npx live-server
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);
}
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>© 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);
}
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!");
});
.
├── 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!');
});
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>© 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);
}
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);
}
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.
footer-component.js
class FooterComponent extends HTMLElement {
connectedCallback() {
this.innerHTML = `<footer>© 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>
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();