blob: 5feef6e23aa7b4b97688b9a00d847c97b7d4de22 (
plain) (
blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
// view.js
// Pure view functions
/**
* Pure view functions for the application.
*
* Why pure functions returning HTML strings?
* - Keeps rendering logic stateless and easy to test.
* - Ensures the UI is always a direct function of state, avoiding UI bugs from incremental DOM updates.
* - Using template literals is minimal and browser-native, with no dependencies.
*
* Why escape output?
* - Prevents XSS and ensures all user/content data is safely rendered.
*
* Why semantic/accessible HTML?
* - Ensures the app is usable for all users, including those using assistive tech, and is easy to reason about.
*/
/**
* Render the app UI as an HTML string
* @param {object} state
* @returns {string}
*/
export function view(state) {
return `
<h1>PokéDex</h1>
<container>
<form id="search-form" autocomplete="off">
<label for="pokemon-query">Pokémon Name (or number)</label>
<input id="pokemon-query" type="text" value="${escape(state.query)}" placeholder="e.g. pikachu" aria-label="Pokémon Name" required />
<button type="submit" ${state.loading ? 'disabled' : ''}>${state.loading ? 'Loading...' : 'Search'}</button>
</form>
${state.error ? `<div class="error" role="alert" tabindex="-1">${escape(state.error)}</div>` : ''}
${state.pokemon ? renderResult(state.pokemon) : ''}
</container>
`;
}
function renderResult(pokemon) {
return `
<div class="result">
<h2>${capitalize(pokemon.name)} (#${pokemon.id})</h2>
<img class="pokemon-sprite" src="${pokemon.sprites.front_default}" alt="${escape(pokemon.name)} sprite" />
<ul>
<li>Type: <b>${pokemon.types.map(t => capitalize(escape(t.type.name))).join(', ')}</b></li>
<li>Height: <b>${pokemon.height / 10} m</b></li>
<li>Weight: <b>${pokemon.weight / 10} kg</b></li>
</ul>
</div>
`;
}
function escape(str) {
/**
* Escapes HTML special characters to prevent XSS.
*
* Why escape here? Keeps all rendering safe by default, so no accidental injection is possible.
*/
return String(str).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
|