Kliensoldali webprogramozás
Horváth Győző
Egyetemi docens
1117 Budapest, Pázmány Péter sétány 1/c., 2.408-as szoba
Tel: (1) 372-2500/8469
horvath.gyozo@inf.elte.hu
<form action="" method="get">
<input type="password" name="password1" value="secret1" data-show-password>
<input type="password" name="password2" value="secret2" data-show-password>
<button>Submit</button>
</form>
<style></style>
class ShowPassword {
// ...
}
document.querySelectorAll('[data-show-password]').forEach(
el => new ShowPassword(el))
<show-password>
<input type="password" name="password1" value="secret">
</show-password>
<input type="password" name="password1" value="secret" is="show-password">
Adat → UI
(UI → adat)
Mátrix
→
???
Táblázat
class Game {
constructor() {
this.n = 0
this.m = 0
this.board = []
this.gameState = 0
}
initBoard(n, m) { /* ... */ }
select(i, j) { /* ... */ }
get solved() { /* ... */ }
}
class Game {
constructor() {
this.n = 0
this.m = 0
this.board = []
this.gameState = 0 // 0: initial, 1: playing, 2: end
}
initBoard(n, m) {
this.n = n
this.m = m
const numbers = Array(n*m/2).fill(null).map((e, i) => i+1)
const values = [...numbers, ...numbers]
this.board = Array(n).fill(null).map(() => Array(m).fill(null).map(() => ({
value: values.shift(),
flipped: false,
solved: false,
})))
this.gameState = 1
}
select(i, j) {
if (this.isFlipped(i, j) || this.isSolved(i, j)) {
return
}
const flipped = this.board.flatMap(row => row.filter(card => card.flipped))
// turn back
if (flipped.length === 2) {
flipped.forEach(card => card.flipped = false)
flipped.shift()
flipped.shift()
}
// flip over
flipped.push(this.board[i][j])
flipped.forEach(card => card.flipped = true)
// check cards
if (flipped.length === 2 && flipped[0].value === flipped[1].value) {
flipped.forEach(card => card.solved = true)
}
// check win
if (this.isWin()) {
this.gameState = 2
}
}
isFlipped(i, j) {
return this.board[i][j].flipped
}
isSolved(i, j) {
return this.board[i][j].solved
}
isWin() {
return this.board.every(row => row.every(card => card.solved))
}
get solved() {
return this.board.flatMap(row => row.filter(card => card.solved)).length
}
}
<div id="main">
<form action="">
n = <input type="number" id="n" value="3">
m = <input type="number" id="m" value="4">
<button type="button">Start new game</button>
</form>
<div id="board"></div>
<div id="status"></div>
</div>
Globális változók és függvények
const game = new Game()
const form = document.querySelector('form')
const button = form.querySelector('button')
const boardDiv = document.querySelector('#board')
const status = document.querySelector('#status')
function xyCoord(card) {
const td = card.closest('td')
const x = td.cellIndex
const tr = td.parentNode
const y = tr.sectionRowIndex
return { x, y }
}
button.addEventListener('click', onGenerate)
function onGenerate(e) {
e.preventDefault()
const n = form.querySelector('#n').valueAsNumber
const m = form.querySelector('#m').valueAsNumber
game.initBoard(n, m)
renderBoard(game.board)
renderStatus(game.solved, game.gameState)
}
boardDiv.addEventListener('click', onSelectCard)
function onSelectCard(e) {
const card = e.target.closest('.card')
if (boardDiv.contains(card)) {
const {x, y} = xyCoord(card)
game.select(y, x)
renderBoard(game.board)
renderStatus(game.solved, game.gameState)
}
}
function renderBoard(board) {
boardDiv.innerHTML = `
<table>
${board.map(row => `
<tr>
${row.map(card => `
<td>
<div class="card ${card.flipped || card.solved ? 'flipped' : ''}">
<div class="front">${card.value}</div>
<div class="back"></div>
</div>
</td>
`).join('')}
</tr>
`).join('')}
</table>
`
}
function renderStatus(solved, gameState) {
status.innerHTML = `
<p>Game state: ${gameState}</p>
<p>You have already solved ${solved} cards.</p>
`
}
const game = new Game()
const view = new View(game)
function xyCoord(card) { /* ... */ }
class View {
constructor(game) {
this.game = game
this.form = document.querySelector('form')
this.button = this.form.querySelector('button')
this.boardDiv = document.querySelector('#board')
this.statusDiv = document.querySelector('#status')
this.onGenerate = this.onGenerate.bind(this)
this.onSelectCard = this.onSelectCard.bind(this)
this.button.addEventListener('click', this.onGenerate)
this.boardDiv.addEventListener('click', this.onSelectCard)
}
onGenerate(e) {
e.preventDefault()
const n = this.form.querySelector('#n').valueAsNumber
const m = this.form.querySelector('#m').valueAsNumber
this.game.initBoard(n, m)
this.renderBoard(this.game.board)
this.renderStatus(this.game.solved, this.game.gameState)
}
onSelectCard(e) {
const card = e.target.closest('.card')
if (this.boardDiv.contains(card)) {
const { x, y } = xyCoord(card)
this.game.select(y, x)
this.renderBoard(this.game.board)
this.renderStatus(this.game.solved, this.game.gameState)
}
}
renderBoard(board) {
this.boardDiv.innerHTML = `...`
}
renderStatus(solved, gameState) {
this.statusDiv.innerHTML = `...`
}
}
<div id="main">
<div id="form"></div>
<div id="board"></div>
<div id="status"></div>
</div>
export class AppView {
constructor(game) {
this.game = game
this.render = this.render.bind(this)
this.formView = new FormView(this.game, this.render)
this.boardView = new BoardView(this.game, this.render)
this.statusView = new StatusView(this.game, this.render)
this.render()
}
render() {
const game = this.game
this.formView.render(game.gameState, game.n, game.m)
this.boardView.render(game.board)
this.statusView.render(game.solved, game.gameState)
}
}
FormView, BoardView, StatusView
export class FormView {
constructor(game, render) {
this.game = game
this.renderAll = render
this.form = document.querySelector('#form')
this.onGenerate = this.onGenerate.bind(this)
this.form.addEventListener('click', this.onGenerate)
}
onGenerate(e) {
if (e.target.matches('button')) {
e.preventDefault()
const n = this.form.querySelector('#n').valueAsNumber
const m = this.form.querySelector('#m').valueAsNumber
this.game.initBoard(n, m)
this.renderAll()
}
}
render(gameState, n, m) {
this.form.innerHTML = `
<form action="">
n = <input type="number" id="n" value="${gameState === 0 ? 3 : n}"> <br>
m = <input type="number" id="m" value="${gameState === 0 ? 4 : m}">
<button type="button">Start new game</button>
</form>
`
}
}
Analizálni a rerendert; probléma: animációk
export class BoardView {
render(n, m, board) {
if (n !== this.prevN || m !== this.prevM) {
// declarative
this.prevN = n
this.prevM = m
this.boardDiv.innerHTML = `<table>...</table>`
} else {
// imperative
board.forEach((row, i) => row.forEach((card, j) =>
this.boardDiv.querySelector(`table tr:nth-child(${i+1}) td:nth-child(${j+1}) .card`)
.classList.toggle('flipped', card.flipped || card.solved)
))
}
}
}
import {html, render} from 'lit-html';
// A lit-html template uses the `html` template tag:
let sayHello = (name) => html`<h1>Hello ${name}</h1>`;
// It's rendered with the `render()` function:
render(sayHello('World'), document.body);
// And re-renders only update the data that changed, without VDOM diffing!
render(sayHello('Everyone'), document.body);
<div id="main"></div>
import {html, render} from 'https://unpkg.com/lit-html?module';
export class AppView {
constructor(game) {
this.game = game
this.render = this.render.bind(this)
this.formView = new FormView(this.game, this.render)
this.boardView = new BoardView(this.game, this.render)
this.statusView = new StatusView(this.game, this.render)
this.render()
}
template(game) {
return html`
<div id="form">
${this.formView.render(game.gameState, game.n, game.m)}
</div>
<div id="board">
${this.boardView.render(game.board)}
</div>
<div id="status">
${this.statusView.render(game.solved, game.gameState)}
</div>
`
}
render() {
const game = this.game
render(this.template(game), document.querySelector('#main'))
}
}
import {html, render} from 'https://unpkg.com/lit-html?module';
export class FormView {
constructor(game, render) {
this.game = game
this.renderAll = render
this.onGenerate = this.onGenerate.bind(this)
}
onGenerate(e) {
e.preventDefault()
const n = document.querySelector('#n').valueAsNumber
const m = document.querySelector('#m').valueAsNumber
this.game.initBoard(n, m)
this.renderAll()
}
render(gameState, n, m) {
return html`
<form action="">
n = <input type="number" id="n" value="${gameState === 0 ? 3 : n}"> <br>
m = <input type="number" id="m" value="${gameState === 0 ? 4 : m}">
<button type="button" @click=${this.onGenerate}>Start new game</button>
</form>
`
}
}
export class FormView {
constructor(game, render) {
this.game = game
this.renderAll = render
this.onGenerate = this.onGenerate.bind(this)
}
onGenerate(e) {
e.preventDefault()
const n = document.querySelector('#n').valueAsNumber
const m = document.querySelector('#m').valueAsNumber
this.game.initBoard(n, m)
this.renderAll()
}
render(gameState, n, m) {
return html`
<form action="">
n = <input type="number" id="n" value="${gameState === 0 ? 3 : n}"> <br>
m = <input type="number" id="m" value="${gameState === 0 ? 4 : m}">
<button type="button" @click=${this.onGenerate}>Start new game</button>
</form>
`
}
}
export function formView(game, renderAll) {
function onGenerate(e) {
e.preventDefault()
const n = document.querySelector('#n').valueAsNumber
const m = document.querySelector('#m').valueAsNumber
game.initBoard(n, m)
renderAll()
}
return (gameState, n, m) => html`
<form action="">
n = <input type="number" id="n" value="${gameState === 0 ? 3 : n}"> <br>
m = <input type="number" id="m" value="${gameState === 0 ? 4 : m}">
<button type="button" @click=${onGenerate}>Start new game</button>
</form>
`
}
Történeti kontextus
Model-View-Controller (MVC)
Miért működött jól szerveroldalon?
Miért nem működik jól kliensoldalon?
Nézet és DOM absztrakció
// component definition
function HelloMessage(props) {
return <div>Hello {props.name}!</div>;
}
// show component
ReactDOM.render(<HelloMessage name="Győző" />, mountNode);
npx create-react-app <alkalmazás neve>
vagy
npm create vite@latest <alkalmazás neve> -- --template react
vagy
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Add React in One Minute</title>
</head>
<body>
<!-- We will put our React component inside this div. -->
<div id="like_button_container"></div>
<!-- Load React. -->
<!-- Note: when deploying, replace "development.js" with "production.min.js". -->
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<!-- Load our React component. -->
<script src="like_button.js"></script>
</body>
</html>
JSX: alternatív formátum (Babel támogatja)
ReactDOM.render(
<h1 className="greeting">Hello, world!</h1>,
document.querySelector('#example')
);
JavaScript:
ReactDOM.render(
React.createElement('h1', {className: 'greeting'}, 'Hello, world!'),
document.querySelector('#example')
);
// Expressions
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
// Attributes
const element = <img className="big" src={user.avatarUrl}></img>;
// no children: autoclosing
const element = <img src={user.avatarUrl} />;
// children
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
<div id="react-container">
<!-- Managed by React -->
</div>
const random = (a, b) => Math.floor(Math.random() * (b - a + 1)) + a;
✒>ReactDOM.render<✒(
<div style={{ backgroundColor: `hsl(${random(0, 360)}, 50%, 50%)` }}>
React rendered box, random value: {random(1, 100)}
</div>,
document.querySelector("#react-container")
);
Az oldal bármely kis/nagy része lehet
<body>
<h1>My React Sandbox</h1>
<p>With some static content before</p>
<div id="react-container">
<!-- Managed by React -->
</div>
<p>With some static content after</p>
<div id="other-container">
<!-- Managed by other library -->
</div>
</body>
React.render()
egyszer rendereli ki az elemeket<div class="wrapper-container">
<div id="react-container"><!-- Managed by React --></div>
</div>
function render() {
✒>ReactDOM.render<✒(
<div style={{ backgroundColor: `hsl(${random(0, 360)}, 50%, 80%)` }}>
React rendered box, random value: {random(1, 100)}.
</div>,
document.querySelector("#react-container")
);
}
document.querySelector('.wrapper-container')
.addEventListener('mouseover', render)
render()
<div class="wrapper-container">
<div id="react-container-1"></div>
</div>
<div id="react-container-2"></div>
function render() {
✒>ReactDOM.render<✒(
<div style={{ backgroundColor: `hsl(${random(0, 360)}, 50%, 80%)` }}>
React rendered box, random value: {random(1, 100)}.
</div>,
document.querySelector("#react-container-1")
);
}
function render2() {
✒>ReactDOM.render<✒(
<div style={{ backgroundColor: `hsl(${random(0, 360)}, 50%, 80%)` }}>
React rendered box, random value: {random(1, 100)}.
</div>,
document.querySelector("#react-container-2")
)
}
function init() {
document.querySelector('.wrapper-container')
.addEventListener('mouseover', ✒>render<✒)
setInterval(✒>render2<✒, 1000)
render(); render2()
}
init()
render
-ek számaconst colorBox =
<div style={{ backgroundColor: `hsl(${random(0, 360)}, 50%, 50%)` }}>
React rendered box, random value: {random(1, 100)}
</div>
ReactDOM.render(
colorBox,
document.querySelector("#react-container")
);
function ColorBox() {
return (
<div style={{ backgroundColor: `hsl(${random(0, 360)}, 50%, 50%)` }}>
React rendered box, random value: {random(1, 100)}
</div>
)
}
ReactDOM.render(
<>{ColorBox()}</>,
document.querySelector("#react-container")
);
function ColorBox() {
return (
<div style={{ backgroundColor: `hsl(${random(0, 360)}, 50%, 50%)` }}>
React rendered box, random value: {random(1, 100)}
</div>
)
}
ReactDOM.render(
<ColorBox />,
document.querySelector("#react-container")
);
// functional component
function ColorBox() {
return (
<div style={{ backgroundColor: `hsl(${random(0, 360)}, 50%, 50%)` }}>
React rendered box, random value: {random(1, 100)}
</div>
)
}
// class component
class ColorBox extends React.Component {
render() {
return (
<div style={{ backgroundColor: `hsl(${random(0, 360)}, 50%, 50%)` }}>
React rendered box, random value: {random(1, 100)}
</div>
)
}
}
<div class="container">
<div class="card text-center">
<div class="card-header">
Code list
</div>
<div class="card-body">
<form class="form-inline">
<label for="inlineFormInputName2">Filter</label>
<input type="text" class="form-control m-2" id="text-filter">
</form>
<ul id="code-list" class="list-group">
<li class="list-group-item">title1</li>
<li class="list-group-item">title2</li>
</ul>
</div>
<div class="card-footer text-muted">
3 item(s) listed
</div>
</div>
</div>
export function App() {
return (
<div ✒>className<✒="container">
<div className="card text-center">
<div className="card-header">
Code list
</div>
<div className="card-body">
<form className="form-inline">
<label for="inlineFormInputName2">Filter</label>
✒><input type="text" className="form-control m-2" id="text-filter" /><✒
</form>
<ul id="code-list" className="list-group">
<li className="list-group-item">title1</li>
<li className="list-group-item">title2</li>
</ul>
</div>
<div className="card-footer text-muted">
3 item(s) listed
</div>
</div>
</div>
)
}
npm i bootstrap --save
import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.css';
import { App } from "./App";
ReactDOM.render(
<App />,
document.getElementById('root')
);
export function App() {
return (
<div className="container">
<Card />
</div>
)
}
export function Card() {
return (
<div className="card text-center">
<CardHeader />
<div className="card-body">
<FilterForm />
<FilterList />
</div>
<CardFooter />
</div>
)
}