Drop the Blazor client runtime and load page JS as plain modules
- author
- Marijn Besseling <njirambem@gmail.com> · 2026-09-09 19:38 UTC
- commit
- 354201a7a418f8e51babbcec8937cee6a893e2c9
- parent
- 9d011c8738
- tree
- browse at this commit
28 files changed +204 -389
Blog/Components/App.razor +0 -7
| @@ -6,19 +6,12 @@ | ||
| 6 | 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| 7 | 7 | <base href="/"/> |
| 8 | 8 | <link rel="stylesheet" href="@Assets["app.css"]"/> |
| 9 | - <link rel="stylesheet" href="@Assets["Blog.styles.css"]"/> | |
| 10 | 9 | <ImportMap/> |
| 11 | 10 | <HeadOutlet/> |
| 12 | 11 | </head> |
| 13 | 12 | |
| 14 | 13 | <body class="center"> |
| 15 | 14 | <Routes/> |
| 16 | -<script src="@Assets["_framework/blazor.web.js"]" autostart="false"></script> | |
| 17 | -<script> | |
| 18 | - Blazor.start({ | |
| 19 | - webAssembly: {} | |
| 20 | - }); | |
| 21 | -</script> | |
| 22 | 15 | </body> |
| 23 | 16 | |
| 24 | 17 | </html> |
Blog/Components/Layout/MainLayout.razor +0 -6
| @@ -5,9 +5,3 @@ | ||
| 5 | 5 | |
| 6 | 6 | |
| 7 | 7 | <SiteFooter/> |
| 8 | - | |
| 9 | -<div id="blazor-error-ui" data-nosnippet> | |
| 10 | - An unhandled error has occurred. | |
| 11 | - <a href="." class="reload">Reload</a> | |
| 12 | - <span class="dismiss">x</span> | |
| 13 | -</div> | |
Blog/Components/Layout/MainLayout.razor.css +0 -19
| @@ -1,19 +0,0 @@ | ||
| 1 | -/* Shown by blazor.web.js when an unhandled error reaches the browser. */ | |
| 2 | -#blazor-error-ui { | |
| 3 | - display: none; | |
| 4 | - position: fixed; | |
| 5 | - inset-block-end: 0; | |
| 6 | - inset-inline: 0; | |
| 7 | - z-index: 1000; | |
| 8 | - padding: var() var(); | |
| 9 | - background-color: var(); | |
| 10 | - color: var(); | |
| 11 | - border-block-start: 2px solid var(); | |
| 12 | -} | |
| 13 | - | |
| 14 | -#blazor-error-ui .dismiss { | |
| 15 | - cursor: pointer; | |
| 16 | - position: absolute; | |
| 17 | - inset-inline-end: var(); | |
| 18 | - inset-block-start: var(); | |
| 19 | -} | |
Blog/Components/Pages/Calc.razor +1 -1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | "/Calc" |
| 2 | 2 | <PageTitle>Calculator</PageTitle> |
| 3 | -<PageScript Src="./Components/Pages/Calc.razor.js"/> | |
| 3 | +<script type="module" src="@Assets["Components/Pages/Calc.razor.js"]"></script> | |
| 4 | 4 | |
| 5 | 5 | <main> |
| 6 | 6 | <p>A rpn calculator</p> |
Blog/Components/Pages/Calc.razor.js +8 -19
| @@ -1,27 +1,16 @@ | ||
| 1 | 1 | import { getById, h, writeError } from "/common.module.js" |
| 2 | 2 | |
| 3 | -export function onLoad() { | |
| 4 | - console.log('Loaded'); | |
| 5 | - const form = getById("form"); | |
| 6 | - const input = getById("input"); | |
| 7 | - const log = getById("log"); | |
| 3 | +const form = getById("form"); | |
| 4 | +const input = getById("input"); | |
| 5 | +const log = getById("log"); | |
| 8 | 6 | |
| 9 | - form.addEventListener("submit", submitForm); | |
| 7 | +form.addEventListener("submit", submitForm); | |
| 10 | 8 | |
| 11 | - const urlParams = new URLSearchParams(window.location.search); | |
| 12 | - const queryInput = urlParams.get('in'); | |
| 9 | +const urlParams = new URLSearchParams(window.location.search); | |
| 10 | +const queryInput = urlParams.get('in'); | |
| 13 | 11 | |
| 14 | - if (input.value.length === 0) { | |
| 15 | - input.value = queryInput; | |
| 16 | - } | |
| 17 | -} | |
| 18 | - | |
| 19 | -export function onUpdate() { | |
| 20 | - console.log('Updated'); | |
| 21 | -} | |
| 22 | - | |
| 23 | -export function onDispose() { | |
| 24 | - console.log('Disposed'); | |
| 12 | +if (input.value.length === 0) { | |
| 13 | + input.value = queryInput; | |
| 25 | 14 | } |
| 26 | 15 | |
| 27 | 16 | /** @param {SubmitEvent} event */ |
Blog/Components/Pages/Concat.razor +1 -1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | "/Concat" |
| 2 | 2 | <PageTitle>Concatenator</PageTitle> |
| 3 | -<PageScript Src="./Components/Pages/Concat.razor.js"/> | |
| 3 | +<script type="module" src="@Assets["Components/Pages/Concat.razor.js"]"></script> | |
| 4 | 4 | <main> |
| 5 | 5 | <p>A "concatenative language"</p> |
| 6 | 6 | <form id="form"> |
Blog/Components/Pages/Concat.razor.js +11 -25
| @@ -2,35 +2,21 @@ import { getById, h, writeError } from "/common.module.js"; | ||
| 2 | 2 | import lzString from "/lz-string.module.js"; |
| 3 | 3 | |
| 4 | 4 | let definitions = {}; |
| 5 | -let log = undefined; | |
| 6 | -let input = undefined; | |
| 7 | - | |
| 8 | -export function onLoad() { | |
| 9 | - console.log('Loaded'); | |
| 10 | - const form = getById("form"); | |
| 11 | - input = getById("input"); | |
| 12 | - log = getById("log"); | |
| 13 | - | |
| 14 | - form.onsubmit = async (event) => { | |
| 15 | - event.preventDefault(); | |
| 16 | - await submitForm(); | |
| 17 | - }; | |
| 18 | 5 | |
| 19 | - const urlParams = new URLSearchParams(window.location.search); | |
| 20 | - const queryInput = urlParams.get("in"); | |
| 6 | +const form = getById("form"); | |
| 7 | +const input = getById("input"); | |
| 8 | +const log = getById("log"); | |
| 21 | 9 | |
| 22 | - if (input.value.length === 0) { | |
| 23 | - input.value = lzString.decompressFromEncodedURIComponent(queryInput); | |
| 24 | - } | |
| 25 | -} | |
| 10 | +form.onsubmit = async (event) => { | |
| 11 | + event.preventDefault(); | |
| 12 | + await submitForm(); | |
| 13 | +}; | |
| 26 | 14 | |
| 27 | -export function onUpdate() { | |
| 28 | - const urlParams = new URLSearchParams(window.location.search); | |
| 29 | - const queryInput = urlParams.get("in"); | |
| 15 | +const urlParams = new URLSearchParams(window.location.search); | |
| 16 | +const queryInput = urlParams.get("in"); | |
| 30 | 17 | |
| 31 | - if (input.value.length === 0) { | |
| 32 | - input.value = lzString.decompressFromEncodedURIComponent(queryInput); | |
| 33 | - } | |
| 18 | +if (input.value.length === 0) { | |
| 19 | + input.value = lzString.decompressFromEncodedURIComponent(queryInput); | |
| 34 | 20 | } |
| 35 | 21 | |
| 36 | 22 | async function submitForm() { |
Blog/Components/Pages/Generator.razor +1 -1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | "/Generator" |
| 2 | 2 | <PageTitle>Number generator</PageTitle> |
| 3 | -<PageScript Src="./Components/Pages/Generator.razor.js"></PageScript> | |
| 3 | +<script type="module" src="@Assets["Components/Pages/Generator.razor.js"]"></script> | |
| 4 | 4 | |
| 5 | 5 | <main> |
| 6 | 6 | <button id="bsn">Generate BSN</button> |
Blog/Components/Pages/Generator.razor.js +4 -6
| @@ -8,13 +8,11 @@ function writeInfo(label, msg) { | ||
| 8 | 8 | ); |
| 9 | 9 | } |
| 10 | 10 | |
| 11 | -export function onLoad() { | |
| 12 | - const gen_bsn = getById("bsn"); | |
| 13 | - gen_bsn.addEventListener("click", generateBsn); | |
| 11 | +const gen_bsn = getById("bsn"); | |
| 12 | +gen_bsn.addEventListener("click", generateBsn); | |
| 14 | 13 | |
| 15 | - const gen_iban = getById("iban"); | |
| 16 | - gen_iban.addEventListener("click", generateIban); | |
| 17 | -} | |
| 14 | +const gen_iban = getById("iban"); | |
| 15 | +gen_iban.addEventListener("click", generateIban); | |
| 18 | 16 | |
| 19 | 17 | function generateBsn() { |
| 20 | 18 | const nr9 = Math.floor(Math.random() * 7); |
Blog/Components/Pages/Letterflixd.razor +1 -1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | "/Letterflixd" |
| 2 | 2 | <PageTitle>Netflix to Letterboxd converter</PageTitle> |
| 3 | -<PageScript Src="./Components/Pages/Letterflixd.razor.js"/> | |
| 3 | +<script type="module" src="@Assets["Components/Pages/Letterflixd.razor.js"]"></script> | |
| 4 | 4 | <main> |
| 5 | 5 | <p>Convert Netflix viewing history to a Letterboxd import file.</p> |
| 6 | 6 | <p>Download your viewing history from Netflix at <a href="https://www.netflix.com/settings/viewed/" |
Blog/Components/Pages/Letterflixd.razor.js +3 -9
| @@ -1,15 +1,9 @@ | ||
| 1 | 1 | import { getById, resetLog, writeError, writeInfo, writeDebug, h } from "/common.module.js" |
| 2 | 2 | |
| 3 | -let netflix_file_input = undefined; | |
| 4 | - | |
| 5 | -export function onLoad() { | |
| 6 | - | |
| 7 | - const form = getById("form"); | |
| 8 | - netflix_file_input = getById("netflix-file"); | |
| 9 | - | |
| 10 | - form.addEventListener("submit", submitForm); | |
| 11 | -} | |
| 3 | +const form = getById("form"); | |
| 4 | +const netflix_file_input = getById("netflix-file"); | |
| 12 | 5 | |
| 6 | +form.addEventListener("submit", submitForm); | |
| 13 | 7 | |
| 14 | 8 | /** @param {SubmitEvent} event */ |
| 15 | 9 | function submitForm(event) { |
Blog/Components/Pages/Note.razor +1 -1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | "/Note" |
| 2 | 2 | <PageTitle>Note</PageTitle> |
| 3 | -<PageScript Src="./Components/Pages/Note.razor.js"></PageScript> | |
| 3 | +<script type="module" src="@Assets["Components/Pages/Note.razor.js"]"></script> | |
| 4 | 4 | |
| 5 | 5 | <main> |
| 6 | 6 | <textarea id="input" class="editor-tall" autofocus></textarea> |
Blog/Components/Pages/Note.razor.js +12 -19
| @@ -1,26 +1,19 @@ | ||
| 1 | 1 | import { getById, debounce, writeError, resetLog } from "/common.module.js"; |
| 2 | 2 | import lzString from "/lz-string.module.js"; |
| 3 | 3 | |
| 4 | -let input = undefined; | |
| 5 | -export function onLoad() { | |
| 6 | - input = getById("input"); | |
| 7 | - input.addEventListener("input", debounce(() => { | |
| 8 | - if (input.value === '') { | |
| 9 | - window.location.hash = '' | |
| 10 | - } | |
| 11 | - else { | |
| 12 | - window.location.hash = '#' + lzString.compressToEncodedURIComponent(input.value); | |
| 13 | - } | |
| 14 | - resetLog(); | |
| 15 | - }, 10)) | |
| 16 | - | |
| 17 | - window.addEventListener('hashchange', loadState); | |
| 18 | - loadState(); | |
| 19 | -} | |
| 4 | +const input = getById("input"); | |
| 5 | +input.addEventListener("input", debounce(() => { | |
| 6 | + if (input.value === '') { | |
| 7 | + window.location.hash = '' | |
| 8 | + } | |
| 9 | + else { | |
| 10 | + window.location.hash = '#' + lzString.compressToEncodedURIComponent(input.value); | |
| 11 | + } | |
| 12 | + resetLog(); | |
| 13 | +}, 10)) | |
| 20 | 14 | |
| 21 | -export function onUpdate() { | |
| 22 | - loadState(); | |
| 23 | -} | |
| 15 | +window.addEventListener('hashchange', loadState); | |
| 16 | +loadState(); | |
| 24 | 17 | |
| 25 | 18 | function loadState() { |
| 26 | 19 | if (window.location.hash !== '') { |
Blog/Components/Pages/QRCodeGenerator.razor +1 -1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | "/QRCodeGenerator" |
| 2 | 2 | <PageTitle>QR-Code generator</PageTitle> |
| 3 | -<PageScript Src="./Components/Pages/QRCodeGenerator.razor.js"></PageScript> | |
| 3 | +<script type="module" src="@Assets["Components/Pages/QRCodeGenerator.razor.js"]"></script> | |
| 4 | 4 | |
| 5 | 5 | <main> |
| 6 | 6 | <textarea id="input" rows="5" autofocus>https://bes.is/</textarea> |
Blog/Components/Pages/QRCodeGenerator.razor.js +6 -8
| @@ -1,13 +1,11 @@ | ||
| 1 | 1 | import { getById } from '/common.module.js'; |
| 2 | 2 | import {QRCode} from '/qrcode.js'; |
| 3 | 3 | |
| 4 | -export function onLoad() { | |
| 5 | - var qrcode = new QRCode("qrcode", | |
| 6 | - "https://bes.is/"); | |
| 4 | +const qrcode = new QRCode("qrcode", | |
| 5 | + "https://bes.is/"); | |
| 7 | 6 | |
| 8 | - const input = getById("input"); | |
| 9 | - input.oninput = () => { | |
| 10 | - const value = input.value; | |
| 11 | - qrcode.makeCode(value); | |
| 12 | - } | |
| 7 | +const input = getById("input"); | |
| 8 | +input.oninput = () => { | |
| 9 | + const value = input.value; | |
| 10 | + qrcode.makeCode(value); | |
| 13 | 11 | } |
| No newline at end of file | ||
Blog/Components/Pages/QRCodeScan.razor +1 -1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | "/QRCodeScan" |
| 2 | 2 | <PageTitle>QR-Code scanner</PageTitle> |
| 3 | -<PageScript Src="./Components/Pages/QRCodeScan.razor.js"></PageScript> | |
| 3 | +<script type="module" src="@Assets["Components/Pages/QRCodeScan.razor.js"]"></script> | |
| 4 | 4 | |
| 5 | 5 | <main> |
| 6 | 6 | <video id="video"></video> |
Blog/Components/Pages/QRCodeScan.razor.js +60 -62
| @@ -1,4 +1,4 @@ | ||
| 1 | -import { getById, writeError, writeInfo, a, div } from "/common.module.js"; | |
| 1 | +import { getById, writeError, writeInfo, h } from "/common.module.js"; | |
| 2 | 2 | |
| 3 | 3 | // regexr.com/2rj36 |
| 4 | 4 | const url_like_regex = |
| @@ -18,81 +18,79 @@ function arraysEqual(a, b) { | ||
| 18 | 18 | |
| 19 | 19 | function logQRValue(value) { |
| 20 | 20 | if (URL.canParse(value)) { |
| 21 | - writeInfo(div(a(new URL(url).href, value))); | |
| 21 | + writeInfo([h("a", { href: new URL(value).href }, value)]); | |
| 22 | 22 | } else if (url_like_regex.test(value)) { |
| 23 | 23 | // Not a true URL but close enough for me to create an anchor tag. |
| 24 | 24 | if (!value.startsWith("http://") && !value.startsWith("https://")) { |
| 25 | 25 | value = "https://" + value; |
| 26 | 26 | } |
| 27 | - writeInfo(div(a(value, value))); | |
| 27 | + writeInfo([h("a", { href: value }, value)]); | |
| 28 | 28 | } else { |
| 29 | 29 | writeInfo(value); |
| 30 | 30 | } |
| 31 | 31 | } |
| 32 | 32 | |
| 33 | -export function onLoad() { | |
| 34 | - // check compatibility | |
| 35 | - if (!("BarcodeDetector" in globalThis)) { | |
| 36 | - writeError( | |
| 37 | - "Barcode Detector is not supported by this browser. Make sure Shape Detection API is turned on.", | |
| 38 | - ); | |
| 39 | - } else { | |
| 40 | - writeInfo("Barcode Detector supported!"); | |
| 41 | - | |
| 42 | - // create new detector | |
| 43 | - const barcodeDetector = new BarcodeDetector({ | |
| 44 | - formats: ["qr_code"], | |
| 45 | - }); | |
| 33 | +// check compatibility | |
| 34 | +if (!("BarcodeDetector" in globalThis)) { | |
| 35 | + writeError( | |
| 36 | + "Barcode Detector is not supported by this browser. Make sure Shape Detection API is turned on.", | |
| 37 | + ); | |
| 38 | +} else { | |
| 39 | + writeInfo("Barcode Detector supported!"); | |
| 46 | 40 | |
| 47 | - const initBarcodescan = (stream) => { | |
| 48 | - writeInfo("Starting scanner..."); | |
| 49 | - let lastbarcodes = []; | |
| 50 | - setInterval(() => { | |
| 51 | - barcodeDetector | |
| 52 | - .detect(stream) | |
| 53 | - .then((barcodes) => { | |
| 54 | - if (barcodes.length === 0) { | |
| 55 | - } else { | |
| 56 | - const values = barcodes.map( | |
| 57 | - (barcode) => barcode.rawValue, | |
| 58 | - ); | |
| 59 | - if (arraysEqual(values, lastbarcodes)) { | |
| 60 | - return; | |
| 61 | - } | |
| 41 | + // create new detector | |
| 42 | + const barcodeDetector = new BarcodeDetector({ | |
| 43 | + formats: ["qr_code"], | |
| 44 | + }); | |
| 62 | 45 | |
| 63 | - values.forEach(logQRValue); | |
| 64 | - lastbarcodes = values; | |
| 46 | + const initBarcodescan = (stream) => { | |
| 47 | + writeInfo("Starting scanner..."); | |
| 48 | + let lastbarcodes = []; | |
| 49 | + setInterval(() => { | |
| 50 | + barcodeDetector | |
| 51 | + .detect(stream) | |
| 52 | + .then((barcodes) => { | |
| 53 | + if (barcodes.length === 0) { | |
| 54 | + } else { | |
| 55 | + const values = barcodes.map( | |
| 56 | + (barcode) => barcode.rawValue, | |
| 57 | + ); | |
| 58 | + if (arraysEqual(values, lastbarcodes)) { | |
| 59 | + return; | |
| 65 | 60 | } |
| 66 | - }) | |
| 67 | - .catch((err) => { | |
| 68 | - writeError(err); | |
| 69 | - }); | |
| 70 | - }, 1000); | |
| 71 | - }; | |
| 72 | 61 | |
| 73 | - const video = getById("video"); | |
| 74 | - video.setAttribute("playsinline", ""); | |
| 75 | - video.setAttribute("autoplay", ""); | |
| 76 | - video.setAttribute("muted", ""); | |
| 62 | + values.forEach(logQRValue); | |
| 63 | + lastbarcodes = values; | |
| 64 | + } | |
| 65 | + }) | |
| 66 | + .catch((err) => { | |
| 67 | + writeError(err); | |
| 68 | + }); | |
| 69 | + }, 1000); | |
| 70 | + }; | |
| 77 | 71 | |
| 78 | - /* Setting up the constraint */ | |
| 79 | - const facingMode = "environment"; // Can be 'user' or 'environment' to access back or front camera (NEAT!) | |
| 80 | - const constraints = { | |
| 81 | - audio: false, | |
| 82 | - video: { | |
| 83 | - facingMode: facingMode, | |
| 84 | - }, | |
| 85 | - }; | |
| 72 | + const video = getById("video"); | |
| 73 | + video.setAttribute("playsinline", ""); | |
| 74 | + video.setAttribute("autoplay", ""); | |
| 75 | + video.setAttribute("muted", ""); | |
| 86 | 76 | |
| 87 | - /* Stream it to video element */ | |
| 88 | - navigator.mediaDevices | |
| 89 | - .getUserMedia(constraints) | |
| 90 | - .then(function success(stream) { | |
| 91 | - video.srcObject = stream; | |
| 92 | - }) | |
| 93 | - .finally(() => { | |
| 94 | - writeInfo("Initialized video stream"); | |
| 95 | - initBarcodescan(video); | |
| 96 | - }); | |
| 97 | - } | |
| 77 | + /* Setting up the constraint */ | |
| 78 | + const facingMode = "environment"; // Can be 'user' or 'environment' to access back or front camera (NEAT!) | |
| 79 | + const constraints = { | |
| 80 | + audio: false, | |
| 81 | + video: { | |
| 82 | + facingMode: facingMode, | |
| 83 | + }, | |
| 84 | + }; | |
| 85 | + | |
| 86 | + /* Stream it to video element */ | |
| 87 | + navigator.mediaDevices | |
| 88 | + .getUserMedia(constraints) | |
| 89 | + .then(function success(stream) { | |
| 90 | + video.srcObject = stream; | |
| 91 | + }) | |
| 92 | + .finally(() => { | |
| 93 | + writeInfo("Initialized video stream"); | |
| 94 | + initBarcodescan(video); | |
| 95 | + }); | |
| 98 | 96 | } |
| No newline at end of file | ||
Blog/Components/Pages/Query.razor +1 -1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | "/Query" |
| 2 | 2 | <PageTitle>Query</PageTitle> |
| 3 | -<PageScript Src="./Components/Pages/Query.razor.js"/> | |
| 3 | +<script type="module" src="@Assets["Components/Pages/Query.razor.js"]"></script> | |
| 4 | 4 | |
| 5 | 5 | <main> |
| 6 | 6 | <div class="scroll-nav"> |
Blog/Components/Pages/Query.razor.js +37 -44
| @@ -23,58 +23,51 @@ const HISTORY_STORE = "queryHistory" | ||
| 23 | 23 | const FAVORITES_STORE = "queryFavorites" |
| 24 | 24 | const HISTORY_LIMIT = 50 |
| 25 | 25 | |
| 26 | -let dactal | |
| 27 | -let db | |
| 28 | -let input | |
| 29 | -let lang | |
| 30 | - | |
| 31 | -export async function onLoad() { | |
| 32 | - input = getById("input") | |
| 33 | - lang = getById("lang") | |
| 34 | - const favoriteButton = getById("favoriteButton") | |
| 35 | - dactal = new DACTAL() | |
| 36 | - | |
| 37 | - db = await openDB() | |
| 38 | - await renderHistory() | |
| 39 | - await renderFavorites() | |
| 40 | - | |
| 41 | - fetch("/brp.json").then(async res => { | |
| 42 | - const cdata = await res.json() | |
| 43 | - dactal.load(cdata, "personen") | |
| 44 | - writeDebug("Loaded brp data") | |
| 26 | +const input = getById("input") | |
| 27 | +const lang = getById("lang") | |
| 28 | +const favoriteButton = getById("favoriteButton") | |
| 29 | +const dactal = new DACTAL() | |
| 30 | + | |
| 31 | +const db = await openDB() | |
| 32 | +await renderHistory() | |
| 33 | +await renderFavorites() | |
| 34 | + | |
| 35 | +fetch("/brp.json").then(async res => { | |
| 36 | + const cdata = await res.json() | |
| 37 | + dactal.load(cdata, "personen") | |
| 38 | + writeDebug("Loaded brp data") | |
| 39 | + await runQuery(input.value, lang.value) | |
| 40 | +}) | |
| 41 | + | |
| 42 | +input.onkeydown = async (e) => { | |
| 43 | + if (e.key === "Enter" && !e.shiftKey) { | |
| 44 | + e.preventDefault() | |
| 45 | 45 | await runQuery(input.value, lang.value) |
| 46 | - }) | |
| 47 | - | |
| 48 | - input.onkeydown = async (e) => { | |
| 49 | - if (e.key === "Enter" && !e.shiftKey) { | |
| 50 | - e.preventDefault() | |
| 51 | - await runQuery(input.value, lang.value) | |
| 52 | - // Only real keystrokes count as a run worth remembering, not the | |
| 53 | - // synthetic Enter dispatched below to trigger the initial query. | |
| 54 | - if (e.isTrusted) { | |
| 55 | - await recordHistory(lang.value, input.value) | |
| 56 | - } | |
| 46 | + // Only real keystrokes count as a run worth remembering, not the | |
| 47 | + // synthetic Enter dispatched below to trigger the initial query. | |
| 48 | + if (e.isTrusted) { | |
| 49 | + await recordHistory(lang.value, input.value) | |
| 57 | 50 | } |
| 58 | 51 | } |
| 52 | +} | |
| 59 | 53 | |
| 60 | - lang.onchange = async () => { | |
| 61 | - input.value = defaultQueries[lang.value] | |
| 62 | - await runQuery(input.value, lang.value) | |
| 63 | - } | |
| 54 | +lang.onchange = async () => { | |
| 55 | + input.value = defaultQueries[lang.value] | |
| 56 | + await runQuery(input.value, lang.value) | |
| 57 | +} | |
| 64 | 58 | |
| 65 | - favoriteButton.onclick = async () => { | |
| 66 | - const name = await getFavoriteName() | |
| 67 | - if (name) { | |
| 68 | - await addFavorite(lang.value, input.value, name) | |
| 69 | - } | |
| 59 | +favoriteButton.onclick = async () => { | |
| 60 | + const name = await getFavoriteName() | |
| 61 | + if (name) { | |
| 62 | + await addFavorite(lang.value, input.value, name) | |
| 70 | 63 | } |
| 64 | +} | |
| 71 | 65 | |
| 72 | - getById("scrollTopButton").onclick = () => window.scrollTo({top: 0, behavior: "smooth"}) | |
| 73 | - getById("scrollFavoritesButton").onclick = () => | |
| 74 | - getById("favoritesList").closest(".panel").scrollIntoView({behavior: "smooth", block: "start"}) | |
| 66 | +getById("scrollTopButton").onclick = () => window.scrollTo({top: 0, behavior: "smooth"}) | |
| 67 | +getById("scrollFavoritesButton").onclick = () => | |
| 68 | + getById("favoritesList").closest(".panel").scrollIntoView({behavior: "smooth", block: "start"}) | |
| 75 | 69 | |
| 76 | - input.dispatchEvent(enterEvent) | |
| 77 | -} | |
| 70 | +input.dispatchEvent(enterEvent) | |
| 78 | 71 | |
| 79 | 72 | function openDB() { |
| 80 | 73 | return new Promise((resolve, reject) => { |
Blog/Components/Pages/Rvrb.razor +1 -1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | "/rvrb" |
| 2 | 2 | <PageTitle>rvrb bot</PageTitle> |
| 3 | -<PageScript Src="./Components/Pages/Rvrb.razor.js"></PageScript> | |
| 3 | +<script type="module" src="@Assets["Components/Pages/Rvrb.razor.js"]"></script> | |
| 4 | 4 | |
| 5 | 5 | <main> |
| 6 | 6 | <h1>rvrb bot</h1> |
Blog/Components/Pages/Rvrb.razor.js +3 -24
| @@ -5,24 +5,8 @@ | ||
| 5 | 5 | const REFRESH_KEY = "rvrb-auto-refresh"; |
| 6 | 6 | const REFRESH_MS = 30_000; |
| 7 | 7 | |
| 8 | -let timer; | |
| 9 | - | |
| 10 | -export function onLoad() { | |
| 11 | - wireAutoRefresh(); | |
| 12 | - startTicking(); | |
| 13 | -} | |
| 14 | - | |
| 15 | -export function onUpdate() { | |
| 16 | - // Enhanced navigation swaps the DOM without a page load, so whatever was ticking is now | |
| 17 | - // pointing at elements that are gone. | |
| 18 | - stopTicking(); | |
| 19 | - wireAutoRefresh(); | |
| 20 | - startTicking(); | |
| 21 | -} | |
| 22 | - | |
| 23 | -export function onDispose() { | |
| 24 | - stopTicking(); | |
| 25 | -} | |
| 8 | +wireAutoRefresh(); | |
| 9 | +startTicking(); | |
| 26 | 10 | |
| 27 | 11 | function wireAutoRefresh() { |
| 28 | 12 | const checkbox = document.getElementById("autoRefresh"); |
| @@ -52,12 +36,7 @@ function startTicking() { | ||
| 52 | 36 | }; |
| 53 | 37 | |
| 54 | 38 | tick(); |
| 55 | - timer = setInterval(tick, 1000); | |
| 56 | -} | |
| 57 | - | |
| 58 | -function stopTicking() { | |
| 59 | - clearInterval(timer); | |
| 60 | - timer = undefined; | |
| 39 | + setInterval(tick, 1000); | |
| 61 | 40 | } |
| 62 | 41 | |
| 63 | 42 | // The snapshot says how far into the track the bot was when it was taken, and how long ago that |
Blog/Components/Pages/Storage.razor +1 -1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | "/Storage" |
| 2 | 2 | <PageTitle>Storage</PageTitle> |
| 3 | -<PageScript Src="./Components/Pages/Storage.razor.js"></PageScript> | |
| 3 | +<script type="module" src="@Assets["Components/Pages/Storage.razor.js"]"></script> | |
| 4 | 4 | |
| 5 | 5 | <main> |
| 6 | 6 | <Panel> |
Blog/Components/Pages/Storage.razor.js +21 -23
| @@ -39,30 +39,28 @@ function InitDB(upgradeCallback) { | ||
| 39 | 39 | |
| 40 | 40 | } |
| 41 | 41 | |
| 42 | -export function onLoad() { | |
| 43 | - try { | |
| 44 | - db?.close(); | |
| 45 | - db = null; | |
| 46 | - InitDB(null); | |
| 47 | - | |
| 48 | - const newStoreButton = getById('newStoreButton'); | |
| 49 | - newStoreButton.addEventListener('click', async () => { | |
| 50 | - const newStoreName = await getInput("New store", "Store name"); | |
| 51 | - | |
| 52 | - if (newStoreName) { | |
| 53 | - writeDebug("New store submitted."); | |
| 54 | - InitDB((event) => { | |
| 55 | - writeDebug(`Creating new store: ${newStoreName}, version: ${event.newVersion}`); | |
| 56 | - event.target.result.createObjectStore(newStoreName, {autoIncrement: true}); | |
| 57 | - writeDebug("New store created: " + newStoreName); | |
| 58 | - }); | |
| 59 | - } | |
| 60 | - }); | |
| 42 | +try { | |
| 43 | + db?.close(); | |
| 44 | + db = null; | |
| 45 | + InitDB(null); | |
| 46 | + | |
| 47 | + const newStoreButton = getById('newStoreButton'); | |
| 48 | + newStoreButton.addEventListener('click', async () => { | |
| 49 | + const newStoreName = await getInput("New store", "Store name"); | |
| 50 | + | |
| 51 | + if (newStoreName) { | |
| 52 | + writeDebug("New store submitted."); | |
| 53 | + InitDB((event) => { | |
| 54 | + writeDebug(`Creating new store: ${newStoreName}, version: ${event.newVersion}`); | |
| 55 | + event.target.result.createObjectStore(newStoreName, {autoIncrement: true}); | |
| 56 | + writeDebug("New store created: " + newStoreName); | |
| 57 | + }); | |
| 58 | + } | |
| 59 | + }); | |
| 61 | 60 | |
| 62 | - } catch (e) { | |
| 63 | - writeError(e.message); | |
| 64 | - throw e; | |
| 65 | - } | |
| 61 | +} catch (e) { | |
| 62 | + writeError(e.message); | |
| 63 | + throw e; | |
| 66 | 64 | } |
| 67 | 65 | |
| 68 | 66 | function showStores() { |
Blog/Components/Routes.razor +0 -1
| @@ -1,6 +1,5 @@ | ||
| 1 | 1 | <Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)"> |
| 2 | 2 | <Found Context="routeData"> |
| 3 | 3 | <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/> |
| 4 | - <FocusOnNavigate RouteData="routeData" Selector="h1"/> | |
| 5 | 4 | </Found> |
| 6 | 5 | </Router> |
| No newline at end of file | ||
Blog/Components/_Shared/PageScript.razor +0 -7
| @@ -1,7 +0,0 @@ | ||
| 1 | -<page-script src="@Src"></page-script> | |
| 2 | - | |
| 3 | - { | |
| 4 | - [Parameter] | |
| 5 | - [EditorRequired] | |
| 6 | - public string Src { get; set; } = null!; | |
| 7 | -} | |
| No newline at end of file | ||
Blog/Program.cs +2 -4
| @@ -4,8 +4,7 @@ using Blog.Services; | ||
| 4 | 4 | var builder = WebApplication.CreateBuilder(args); |
| 5 | 5 | |
| 6 | 6 | // Add services to the container. |
| 7 | -builder.Services.AddRazorComponents() | |
| 8 | - .AddInteractiveServerComponents(); | |
| 7 | +builder.Services.AddRazorComponents(); | |
| 9 | 8 | |
| 10 | 9 | builder.Services.AddHttpClient(); |
| 11 | 10 | builder.Services.AddHybridCache(); |
| @@ -38,7 +37,6 @@ app.UseHttpsRedirection(); | ||
| 38 | 37 | app.UseAntiforgery(); |
| 39 | 38 | |
| 40 | 39 | app.MapStaticAssets(); |
| 41 | -app.MapRazorComponents<App>() | |
| 42 | - .AddInteractiveServerRenderMode(); | |
| 40 | +app.MapRazorComponents<App>(); | |
| 43 | 41 | |
| 44 | 42 | app.Run(); |
| No newline at end of file | ||
Blog/wwwroot/Blog.lib.module.js +0 -83
| @@ -1,83 +0,0 @@ | ||
| 1 | -const pageScriptInfoBySrc = new Map(); | |
| 2 | - | |
| 3 | -function registerPageScriptElement(src) { | |
| 4 | - if (!src) { | |
| 5 | - throw new Error('Must provide a non-empty value for the "src" attribute.'); | |
| 6 | - } | |
| 7 | - | |
| 8 | - let pageScriptInfo = pageScriptInfoBySrc.get(src); | |
| 9 | - | |
| 10 | - if (pageScriptInfo) { | |
| 11 | - pageScriptInfo.referenceCount++; | |
| 12 | - } else { | |
| 13 | - pageScriptInfo = { referenceCount: 1, module: null }; | |
| 14 | - pageScriptInfoBySrc.set(src, pageScriptInfo); | |
| 15 | - initializePageScriptModule(src, pageScriptInfo); | |
| 16 | - } | |
| 17 | -} | |
| 18 | - | |
| 19 | -function unregisterPageScriptElement(src) { | |
| 20 | - if (!src) { | |
| 21 | - return; | |
| 22 | - } | |
| 23 | - | |
| 24 | - const pageScriptInfo = pageScriptInfoBySrc.get(src); | |
| 25 | - | |
| 26 | - if (!pageScriptInfo) { | |
| 27 | - return; | |
| 28 | - } | |
| 29 | - | |
| 30 | - pageScriptInfo.referenceCount--; | |
| 31 | -} | |
| 32 | - | |
| 33 | -async function initializePageScriptModule(src, pageScriptInfo) { | |
| 34 | - if (src.startsWith("./")) { | |
| 35 | - src = new URL(src.substring(2), document.baseURI).toString(); | |
| 36 | - } | |
| 37 | - | |
| 38 | - const module = await import(src); | |
| 39 | - | |
| 40 | - if (pageScriptInfo.referenceCount <= 0) { | |
| 41 | - return; | |
| 42 | - } | |
| 43 | - | |
| 44 | - pageScriptInfo.module = module; | |
| 45 | - module.onLoad?.(); | |
| 46 | - module.onUpdate?.(); | |
| 47 | -} | |
| 48 | - | |
| 49 | -function onEnhancedLoad() { | |
| 50 | - for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) { | |
| 51 | - if (referenceCount <= 0) { | |
| 52 | - module?.onDispose?.(); | |
| 53 | - pageScriptInfoBySrc.delete(src); | |
| 54 | - } | |
| 55 | - } | |
| 56 | - | |
| 57 | - for (const { module } of pageScriptInfoBySrc.values()) { | |
| 58 | - module?.onUpdate?.(); | |
| 59 | - } | |
| 60 | -} | |
| 61 | - | |
| 62 | -export function afterWebStarted(blazor) { | |
| 63 | - console.log("afterWebStarted"); | |
| 64 | - customElements.define('page-script', class extends HTMLElement { | |
| 65 | - static observedAttributes = ['src']; | |
| 66 | - | |
| 67 | - attributeChangedCallback(name, oldValue, newValue) { | |
| 68 | - if (name !== 'src') { | |
| 69 | - return; | |
| 70 | - } | |
| 71 | - | |
| 72 | - this.src = newValue; | |
| 73 | - unregisterPageScriptElement(oldValue); | |
| 74 | - registerPageScriptElement(newValue); | |
| 75 | - } | |
| 76 | - | |
| 77 | - disconnectedCallback() { | |
| 78 | - unregisterPageScriptElement(this.src); | |
| 79 | - } | |
| 80 | - }); | |
| 81 | - | |
| 82 | - blazor.addEventListener('enhancedload', onEnhancedLoad); | |
| 83 | -} | |
| No newline at end of file | ||
CLAUDE.md +27 -13
| @@ -34,18 +34,31 @@ follow for any new interactive page. | ||
| 34 | 34 | |
| 35 | 35 | ### The `.razor` + `.razor.js` pairing |
| 36 | 36 | |
| 37 | -Blazor's `<PageScript Src="./Components/Pages/Foo.razor.js"/>` (wrapping the built-in | |
| 38 | -`<page-script>` custom element, registered in `wwwroot/Blog.lib.module.js`) loads a JS | |
| 39 | -module scoped to that page. The module can export `onLoad`, `onUpdate`, `onDispose` | |
| 40 | -lifecycle hooks, called as the custom element connects/re-renders/disconnects (this | |
| 41 | -matters across Blazor's enhanced navigation, which doesn't do a full page reload). | |
| 42 | -Page logic is written in vanilla JS DOM manipulation, not Blazor data binding — components | |
| 43 | -grab elements with `getById` and wire up listeners directly in `onLoad`. | |
| 44 | - | |
| 45 | -`PageScript.razor`, `Log.razor`, `Panel.razor`, and the `StackOp*.razor` components live in | |
| 46 | -`Components/_Shared/` and are reused across pages (e.g. `Log` renders a debug/error log | |
| 47 | -panel that JS writes into via `writeDebug`/`writeError`; `Panel` is a bordered | |
| 48 | -fieldset-with-legend used for docs/credits/grouped controls). | |
| 37 | +There is **no Blazor client runtime**. Every page is statically server-rendered and its JS is | |
| 38 | +loaded as a plain ES module: | |
| 39 | + | |
| 40 | +```razor | |
| 41 | +<script type="module" src="@Assets["Components/Pages/Foo.razor.js"]"></script> | |
| 42 | +``` | |
| 43 | + | |
| 44 | +`.razor.js` files colocated with a component are static web assets, so `@Assets[...]` resolves | |
| 45 | +them to a fingerprinted URL and `<ImportMap/>` in `App.razor` maps the bare `/common.module.js` | |
| 46 | +style imports onto their fingerprinted files too. Never hand-write the plain path — go through | |
| 47 | +`@Assets` or the module ships uncached. | |
| 48 | + | |
| 49 | +The module body *is* the page's setup code: it runs once, at top level, after the DOM is parsed | |
| 50 | +(`type="module"` is deferred). There are no `onLoad`/`onUpdate`/`onDispose` hooks, because | |
| 51 | +navigation is a real page load — the browser tears down listeners, timers and module state for | |
| 52 | +you. Page logic is vanilla JS DOM manipulation, not Blazor data binding: grab elements with | |
| 53 | +`getById` at top level and wire up listeners directly. | |
| 54 | + | |
| 55 | +If you ever reintroduce interactivity or enhanced navigation, this stops being true — module | |
| 56 | +state would then outlive the DOM it points at, and every page script would need teardown again. | |
| 57 | + | |
| 58 | +`Log.razor`, `Panel.razor`, and the `StackOp*.razor` components live in `Components/_Shared/` | |
| 59 | +and are reused across pages (e.g. `Log` renders a debug/error log panel that JS writes into via | |
| 60 | +`writeDebug`/`writeError`; `Panel` is a bordered fieldset-with-legend used for docs/credits/ | |
| 61 | +grouped controls). | |
| 49 | 62 | |
| 50 | 63 | ### `wwwroot/common.module.js` |
| 51 | 64 | |
| @@ -99,4 +112,5 @@ organized into numbered sections (Tokens → Reset/base → Typography → Layou | ||
| 99 | 112 | Components → Media) with CSS custom properties as the single source of design tokens |
| 100 | 113 | (colors, spacing scale, fonts). It uses `light-dark()` and `color-scheme` for automatic |
| 101 | 114 | dark mode — don't hardcode light/dark colors, extend the token set in section 1 instead. |
| 102 | -`MainLayout.razor.css` holds the one layout-specific scoped stylesheet. | |
| 115 | +There are no scoped `.razor.css` stylesheets, so `App.razor` links `app.css` only; adding one | |
| 116 | +means adding the `Blog.styles.css` bundle link back. | |