├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── cli.ts ├── package.json ├── scripts ├── du.js ├── hash.sh ├── size.sh └── update-hash.sh ├── template ├── .gitignore ├── README.md ├── package.json ├── public │ ├── app.html │ ├── base.js │ ├── examples │ │ ├── article.html │ │ ├── bind.html │ │ ├── counter.html │ │ ├── data-show.html │ │ ├── edit.html │ │ ├── form.html │ │ ├── fragment.html │ │ ├── icon-list.html │ │ ├── list.html │ │ └── search.html │ ├── footer.html │ ├── header.html │ ├── index.html │ └── style.css ├── server │ ├── api.ts │ ├── env.ts │ ├── error.ts │ └── main.ts └── tsconfig.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | *.min.js 6 | *.gz 7 | *.tgz 8 | dist/ 9 | tsconfig.tsbuildinfo 10 | cli.js 11 | base.js 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .dccache 2 | *.tgz 3 | pnpm-lock.yaml 4 | cli.ts 5 | scripts/ 6 | tsconfig.json 7 | !template/tsconfig.json 8 | tsconfig.tsbuildinfo 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) [2022], [Beeno Tung (Tung Cheung Leong)] 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # data-template 2 | 3 | Lightweight and minimal HTML template helpers powered by native DOM 4 | 5 | [![npm Package Version](https://img.shields.io/npm/v/data-template)](https://www.npmjs.com/package/data-template) 6 | [![Minified Package Size](https://img.shields.io/bundlephobia/min/data-template)](https://bundlephobia.com/package/data-template) 7 | [![Minified and Gzipped Package Size](https://img.shields.io/bundlephobia/minzip/data-template)](https://bundlephobia.com/package/data-template) 8 | 9 | Server-side-rendering (SSR) mode available via [node-data-template](https://github.com/beenotung/node-data-template) 10 | 11 | ## Installation (with CDN) 12 | 13 | Drop below line in your html with automatic patch updates: 14 | 15 | ```html 16 | 17 | ``` 18 | 19 |
20 | Or specify the exact version with integrity protection: 21 | 22 | ```html 23 | 28 | ``` 29 | 30 |
31 | 32 |
33 | You can use the minified version as well: 34 | 35 | ```html 36 | 41 | ``` 42 | 43 |
44 | 45 | ## Get Started (with template project) 46 | 47 | For new project without existing files, you can use the `data-template` cli to setup a simple project from template. 48 | 49 | ```bash 50 | npx data-template my-app 51 | cd my-app 52 | # then see the guides in the console output and README.md file 53 | ``` 54 | 55 | ## Features 56 | 57 | - [x] apply data into dom based on dataset (`data-*`) attributes 58 | - [x] auto repeat elements if the value is an array 59 | - [x] support fragments with [nested template](./template/public/examples/fragment.html#:L14) 60 | - [x] fetch and cache html template and api response with localStorage 61 | - [x] helper functions to do ajax and input format (date/time) 62 | - [x] lightweight, [1KB minified and gzipped](#size) 63 | 64 | **Supported `data-*` attributes**: 65 | 66 | | category | attributes | 67 | | -------- | -------------------------------------------- | 68 | | general | text, class, id, title | 69 | | link | href | 70 | | media | src, alt | 71 | | display | hidden, show, if | 72 | | input | value, checked, selected, disabled, readonly | 73 | | dialog | open | 74 | | form | action, onsubmit | 75 | | event | onclick | 76 | 77 | ## Quick Example with CDN 78 | 79 | (For script tag with exact version and integrity checksum, see [above section](#installation-with-cdn)) 80 | 81 | ```html 82 | 83 | 84 | 85 | 86 |
87 | loading articles... 88 |
89 | 90 | 100 | 101 | 124 | ``` 125 | 126 | More examples see [template/public](template/public) 127 | 128 | ## Functions 129 | 130 | **Render Functions**: 131 | 132 | ```javascript 133 | // render data-* attributes 134 | function renderData(container, values); 135 | 136 | // render template on specific host element 137 | function renderTemplate(hostElement, binds); 138 | 139 | // recursive scan for templates and render them 140 | function scanTemplates(rootElement, binds); 141 | 142 | // populate the form using values from the object 143 | function fillForm(form, object); 144 | ``` 145 | 146 | **Format Functions**: 147 | 148 | ```javascript 149 | // prepend '0' of the number is less than ten 150 | function d2(number); 151 | 152 | // convert to 'YYYY-MM-DD' format for input[type=date] 153 | function toInputDate(date_or_time_or_string); 154 | 155 | // convert to 'HH:mm' format for input[type=time] 156 | function toInputTime(date_or_time_or_string); 157 | ``` 158 | 159 | **AJAX Functions**: 160 | 161 | ```javascript 162 | // return promise of string, cached with localStorage 163 | function getText(url, options, callback); 164 | 165 | // return promise of json value, cached with localStorage 166 | function getJSON(url, options, callback); 167 | 168 | // submit form with ajax request in application/json 169 | function submitJSON(event_or_form): Promise 170 | 171 | // submit form with ajax request in application/x-www-form-urlencoded or url search parameters 172 | function submitForm(event_or_form): Promise 173 | 174 | // submit form with ajax request in multipart/form-data 175 | function uploadForm(event_or_form): Promise 176 | 177 | // send ajax request in application/json 178 | function postJSON(url, body): Promise 179 | function patchJSON(url, body): Promise 180 | function putJSON(url, body): Promise 181 | 182 | // send ajax request with DELETE method 183 | function del(url): Promise 184 | ``` 185 | 186 | For the `getText()` and `getJSON()` functions, the `options` and `cb` arguments are optional. 187 | 188 | The `options` object is the second argument passed to the `fetch` function. 189 | 190 | The `callback` function will be called with cached _and/or_ fetched data [(details)](#when-will-the-callback-function-be-called). 191 | 192 | If is recommended to provide `{ cache: 'reload' }` in the `options` or use callback function to receive the data if you want to avoid staled view. 193 | 194 | The returned promise can be used to do error handling. 195 | 196 | ### When will the callback function be called 197 | 198 | If the fetching data is already cached by url, the callback will be called immediately. 199 | Then the data will be fetched no matter cached or not. 200 | If the newly fetched data is different from the cached data, the callback will be called again. 201 | 202 | ## Size 203 | 204 | | Format | File Size | 205 | | -------------- | --------- | 206 | | base.js | 5.3 KB | 207 | | base.min.js | 2.9 KB | 208 | | base.min.js.gz | 1.4 KB | 209 | 210 | ## License 211 | 212 | This project is licensed with [BSD-2-Clause](./LICENSE) 213 | 214 | This is free, libre, and open-source software. It comes down to four essential freedoms [[ref]](https://seirdy.one/2021/01/27/whatsapp-and-the-domestication-of-users.html#fnref:2): 215 | 216 | - The freedom to run the program as you wish, for any purpose 217 | - The freedom to study how the program works, and change it so it does your computing as you wish 218 | - The freedom to redistribute copies so you can help others 219 | - The freedom to distribute copies of your modified versions to others 220 | -------------------------------------------------------------------------------- /cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { readFileSync, writeFileSync } from 'fs' 3 | import { copyTemplate, getDest, hasExec } from 'npm-init-helper' 4 | import { basename, join } from 'path' 5 | 6 | async function main() { 7 | let srcDir = join(__dirname, 'template') 8 | let dest = await getDest() 9 | await copyTemplate({ 10 | srcDir, 11 | dest, 12 | updatePackageJson: true, 13 | verbose: true, 14 | }) 15 | let name = basename(dest) 16 | if (name !== 'my-app') { 17 | // update project name in README.md 18 | let file = join(dest, 'README.md') 19 | let text = readFileSync(file) 20 | .toString() 21 | .replace(/my-app/g, name) 22 | writeFileSync(file, text) 23 | } 24 | console.log( 25 | ` 26 | Done. 27 | 28 | Get started by typing: 29 | 30 | cd ${dest} 31 | `.trim(), 32 | ) 33 | 34 | if (hasExec('pnpm')) { 35 | console.log(` pnpm i`) 36 | console.log(` npm run dev`) 37 | } else if (hasExec('yarn')) { 38 | console.log(` yarn install`) 39 | console.log(` yarn dev`) 40 | } else { 41 | console.log(` npm i`) 42 | console.log(` npm run dev`) 43 | } 44 | } 45 | main().catch(e => console.error(e)) 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "data-template", 3 | "version": "1.10.1", 4 | "description": "Lightweight and minimal HTML template helpers powered by native DOM", 5 | "main": "base.js", 6 | "bin": { 7 | "data-template": "cli.js" 8 | }, 9 | "scripts": { 10 | "test": "tsc --noEmit", 11 | "build": "run-p tsc base", 12 | "base": "bash scripts/size.sh && cp template/public/base.*js .", 13 | "tsc": "tsc -p ." 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/beenotung/data-template.git" 18 | }, 19 | "keywords": [ 20 | "html", 21 | "template", 22 | "native", 23 | "dom", 24 | "data", 25 | "dataset", 26 | "attribute", 27 | "render", 28 | "lightweight", 29 | "minimal", 30 | "ajax", 31 | "html-template", 32 | "data-template", 33 | "data-attribute", 34 | "data-binding" 35 | ], 36 | "author": "", 37 | "license": "BSD-2-Clause", 38 | "bugs": { 39 | "url": "https://github.com/beenotung/data-template/issues" 40 | }, 41 | "homepage": "https://github.com/beenotung/data-template#readme", 42 | "devDependencies": { 43 | "@types/node": "^16.18.52", 44 | "npm-run-all": "^4.1.5", 45 | "ts-node": "^10.9.1", 46 | "typescript": "^5.2.2" 47 | }, 48 | "dependencies": { 49 | "npm-init-helper": "^1.5.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /scripts/du.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // using readFile instead of stat to be compatible to MacOS 4 | let { readFileSync } = require('fs') 5 | let { argv } = process 6 | 7 | let files = argv.slice(2) 8 | 9 | let maxFileLength = Math.max(0, ...files.map(file => file.length)) 10 | 11 | for (let file of files) { 12 | let size = readFileSync(file).length 13 | let padding = ' '.repeat(maxFileLength - file.length) 14 | let line = file + padding + '\t' + size 15 | if (size >= 1024) { 16 | line += `\t(${(size / 1024).toFixed(1)} KB)` 17 | } 18 | console.log(line) 19 | } 20 | -------------------------------------------------------------------------------- /scripts/hash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # generate integrity attribute 3 | set -e 4 | set -o pipefail 5 | hash=$(shasum -b -a 384 $1 | awk '{print $1}' | xxd -r -p | base64) 6 | echo "" 11 | -------------------------------------------------------------------------------- /scripts/size.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -o pipefail 4 | 5 | cd template/public 6 | npx --yes esbuild base.js --minify > base.min.js 7 | gzip -f -k base.min.js 8 | 9 | ../../scripts/du.js base.* 10 | -------------------------------------------------------------------------------- /scripts/update-hash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -o pipefail 4 | ./scripts/size.sh 5 | cp template/public/base.js . 6 | cp template/public/base.min.js . 7 | ./scripts/hash.sh base.js >> README.md 8 | ./scripts/hash.sh base.min.js >> README.md 9 | -------------------------------------------------------------------------------- /template/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | *.tgz 6 | dist/ 7 | .env 8 | -------------------------------------------------------------------------------- /template/README.md: -------------------------------------------------------------------------------- 1 | # my-app 2 | 3 | Powered by [data-template](https://github.com/beenotung/data-template) 4 | 5 | ## Get Started 6 | 7 | ### Install dependencies 8 | 9 | Run: `npm install` 10 | 11 | Tips, you can also use below alternative installers: 12 | 13 | - `pnpm i` 14 | - `yarn install` 15 | - `slnpm` 16 | 17 | ### Start development server 18 | 19 | Run: `npm run dev` 20 | 21 | You will see output like below: 22 | 23 | ``` 24 | listening on http://localhost:8100 25 | listening on http://127.0.0.1:8100 (lo) 26 | listening on http://192.168.1.2:8100 (wlp3s0) 27 | ``` 28 | 29 | Then you can open http://localhost:8100 with a browser 30 | 31 | The port number may be changed by the `PORT` variable in the `.env` file 32 | 33 | ### Deploy production server 34 | 35 | 1. Run `npm run build` to compile the typescript project and minify the `base.js` 36 | 2. Run `npm start` to start the node.js server 37 | 38 | If you have installed pm2, you can start the server with: `pm2 start --name my-app dist/main.js` 39 | 40 | In the production mode, the server will enable compression (e.g. gzip) when the client supports it. 41 | 42 | It will also use the minified `base.min.js` when requested `base.min`, so you don't have to change the `src` attribute in the ` 16 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | Articles 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 43 | 58 | 59 | 60 | 61 | 62 | Cancel 63 | 64 | Add Article 65 | 66 | Submit 67 | 68 | 69 | 70 | 71 |
77 | 78 | 79 | Title 80 | 81 | 82 | 83 | Introduction 84 | 85 | 86 | 87 | Cover Image 88 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Alt Text 102 | 103 | 104 | 105 | Cover Title 106 | 107 | 108 | 109 | Archived 110 | 111 | 112 | 113 | Passed 114 | 115 | 116 | 117 | Highlight 118 | 119 | 120 | 121 |
122 |
123 |
124 | 125 | 126 | 127 | 128 | Close 129 | 130 | Details 131 | 132 | 133 | 134 |

135 |
136 | 143 |
144 |

alt text:

145 |

image title:

146 |
147 |
148 |

149 | Archived 150 | Passed 151 | Highlight 152 |
153 |
154 |
155 |
156 | 157 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /template/public/base.js: -------------------------------------------------------------------------------- 1 | ;(w => { 2 | // shortcuts to reduce minified size 3 | let t = 'template' 4 | 5 | let bindTemplate = (host, template, values) => { 6 | let node = template.content.cloneNode(true) 7 | let container = document.createElement('div') 8 | container.appendChild(node) 9 | renderData(container, values) 10 | while (container.childNodes.length) { 11 | host.appendChild(container.childNodes.item(0)) 12 | } 13 | } 14 | 15 | let parse = JSON.parse 16 | 17 | let useGet = use => (url, options, cb) => 18 | typeof options == 'function' 19 | ? use(url, {}, options) 20 | : use(url, options, cb) 21 | 22 | let toForm = (event_or_form = event) => 23 | event_or_form instanceof HTMLFormElement 24 | ? event_or_form 25 | : (event_or_form.preventDefault(), event_or_form.target) 26 | 27 | let fetchJSON = method => (url, body) => 28 | fetch(url, { 29 | method, 30 | headers: { 'Content-Type': 'application/json' }, 31 | body: JSON.stringify(body), 32 | }) 33 | 34 | w.renderData = (container, values) => { 35 | let apply = (attr, f) => { 36 | container.querySelectorAll(`[data-${attr}]`).forEach(e => { 37 | let key = e.dataset[attr], 38 | value = values[key], 39 | t = e.tagName == 'TEMPLATE', 40 | last = e 41 | if (t && value && (attr == 'show' || attr == 'if')) value = [1] 42 | if (!Array.isArray(value)) return f(e, value, key) 43 | value.forEach(value => { 44 | let node = (t ? e.content : e).cloneNode(true) 45 | f(node, value, key) 46 | value && typeof value == 'object' && renderData(node, value) 47 | if (!t) 48 | return last.insertAdjacentElement('afterend', node), last = node 49 | for (let child of node.childNodes) 50 | child.nodeType == Node.TEXT_NODE 51 | ? last.insertAdjacentText('afterend', child.textContent) 52 | : (last.insertAdjacentElement('afterend', child), last = child) 53 | }) 54 | e.remove() 55 | }) 56 | } 57 | apply('text', (e, v) => e.textContent = v) 58 | apply('class', (e, v, k) => v == true ? e.classList.add(k) : v && e.classList.add(...v.split(' '))) 59 | apply('show', (e, v) => e.hidden = !v) 60 | apply('if', (e, v) => v || e.remove()) 61 | apply('readonly', (e, v) => e.readOnly = !!v) 62 | for (let attr of ['open', 'checked', 'disabled', 'selected', 'hidden']) 63 | apply(attr, (e, v) => e[attr] = !!v) 64 | for (let attr of [ 65 | 'id', 66 | 'title', 67 | 'href', 68 | 'src', 69 | 'alt', 70 | 'value', 71 | 'action', 72 | 'onsubmit', 73 | 'onclick', 74 | ]) 75 | apply(attr, (e, v) => v && (e[attr] = v)) 76 | } 77 | 78 | w.renderTemplate = async (host, binds = {}) => { 79 | let name = host.dataset.template 80 | let values = binds[host.dataset.bind] || binds 81 | let template 82 | host.textContent = '' 83 | if (name.endsWith('.html')) { 84 | template = document.createElement(t) 85 | getText(name, html => { 86 | // deepcode ignore DOMXSS: the template is authored by the application developer, not from untrusted users 87 | template.innerHTML = html 88 | next() 89 | }) 90 | } else { 91 | template = document.querySelector(`${t}[data-name="${name}"]`) 92 | template ? next() : console.error(t, `not found:`, name) 93 | } 94 | function next() { 95 | Array.isArray(values) 96 | ? values.forEach(values => bindTemplate(host, template, values)) 97 | : bindTemplate(host, template, values) 98 | } 99 | } 100 | 101 | w.scanTemplates = (root = document.body, binds = {}) => 102 | root 103 | .querySelectorAll(`[data-${t}]`) 104 | .forEach(host => renderTemplate(host, binds)) 105 | 106 | w.fillForm = (form, o) => { 107 | let e 108 | for (let k in o) (e = form[k]) && (e.value = o[k]) 109 | } 110 | 111 | w.d2 = x => x < 10 ? '0' + x : x 112 | 113 | w.toInputDate = date => { 114 | let d = new Date(date) 115 | return d.getFullYear() + '-' + d2(d.getMonth() + 1) + '-' + d2(d.getDate()) 116 | } 117 | 118 | w.toInputTime = date => { 119 | let d = new Date(date) 120 | return d2(d.getHours()) + ':' + d2(d.getMinutes()) 121 | } 122 | 123 | w.getText = useGet(async (url, options, cb) => { 124 | let text = localStorage.getItem(url) 125 | let p = fetch(url, options).then(res => res.text()) 126 | let cache = options && options.cache 127 | let skipCache = cache && cache != 'force-cache' 128 | p.then(newText => { 129 | let diff = newText != text 130 | ;(skipCache || diff) && cb?.(newText) 131 | diff && localStorage.setItem(url, newText) 132 | }) 133 | return !skipCache && text ? (cb?.(text), text) : p 134 | }) 135 | 136 | w.getJSON = useGet((url, options, cb) => 137 | getText(url, options, cb && (text => cb(parse(text)))).then(parse), 138 | ) 139 | 140 | w.submitJSON = event_or_form => { 141 | let form = toForm(event_or_form) 142 | let body = {} 143 | for (let input of form.elements) 144 | if (input.name && (input.type != 'checkbox' || input.checked)) 145 | body[input.name] = input.value 146 | return fetchJSON(form.method)(form.action, body) 147 | } 148 | 149 | w.submitForm = event_or_form => { 150 | let form = toForm(event_or_form) 151 | let body = new URLSearchParams(new FormData(form)) 152 | let { method, action } = form 153 | return method == 'get' 154 | ? fetch(action + '?' + body) 155 | : fetch(action, { 156 | method, 157 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 158 | body, 159 | }) 160 | } 161 | 162 | w.uploadForm = event_or_form => { 163 | let form = toForm(event_or_form) 164 | return fetch(form.action, { 165 | method: form.method, 166 | body: new FormData(form), 167 | }) 168 | } 169 | 170 | w.postJSON = fetchJSON('POST') 171 | w.patchJSON = fetchJSON('PATCH') 172 | w.putJSON = fetchJSON('PUT') 173 | w.del = url => fetch(url, { method:'DELETE' }) 174 | })(window) 175 | -------------------------------------------------------------------------------- /template/public/examples/article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Article Detail 8 | 9 | 10 | 11 |
12 | 13 |
14 | loading article... 15 |
16 | Back to homepage 17 |
18 |
19 | 40 | 41 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /template/public/examples/bind.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | bind demo 8 | 13 | 14 | 15 |
16 |

renderData without bind

17 |
18 |
title:
19 |
20 |
21 |
22 |

renderTemplate with bind

23 |
24 |
25 |
26 |

renderTemplate without bind

27 |
28 |
29 | 32 | 33 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /template/public/examples/counter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |
16 |
17 | 21 |
22 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /template/public/examples/data-show.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | data-show examples 8 | 24 | 25 | 26 |
27 | 89 | 90 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /template/public/examples/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | View and Edit Form 7 | 19 | 20 | 21 | 22 | 23 |
30 | 57 | 60 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /template/public/examples/form.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 |
9 | 10 | 34 | 35 | 36 | 46 | -------------------------------------------------------------------------------- /template/public/examples/fragment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 32 | 33 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /template/public/examples/icon-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Icon List 7 | 14 | 15 | 16 |
17 | 23 | 24 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /template/public/examples/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Array Value Demo 8 | 13 | 14 | 15 |

Array Value Demo

16 |
17 | 24 | 25 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /template/public/examples/search.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 | 11 | -------------------------------------------------------------------------------- /template/public/footer.html: -------------------------------------------------------------------------------- 1 | this is footer (loaded from footer.html) -------------------------------------------------------------------------------- /template/public/header.html: -------------------------------------------------------------------------------- 1 | this is header (loaded form header.html) -------------------------------------------------------------------------------- /template/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | data-template Demo 8 | 9 | 53 | 54 | 55 |
56 | 57 | 66 |
67 | loading articles... 68 |
69 |
70 |
71 | 96 | 97 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /template/public/style.css: -------------------------------------------------------------------------------- 1 | @media (prefers-color-scheme: dark) { 2 | body { 3 | background-color: #222; 4 | color: #eee; 5 | } 6 | a { 7 | color: #cc0; 8 | } 9 | a:visited { 10 | color: #0c0; 11 | } 12 | button[disabled] { 13 | color: #000; 14 | } 15 | } 16 | 17 | [hidden] { 18 | display: none !important; 19 | } 20 | 21 | .error { 22 | border: 1px solid red; 23 | padding: 0.5rem; 24 | width: fit-content; 25 | } 26 | 27 | .highlight { 28 | font-weight: bold; 29 | } 30 | 31 | article img { 32 | width: 200px; 33 | height: 200px; 34 | } 35 | 36 | img.small { 37 | width: 64px; 38 | height: 64px; 39 | } 40 | 41 | ul.tags { 42 | padding: 0; 43 | } 44 | ul.tags li { 45 | list-style: none; 46 | display: inline-block; 47 | padding: 0.5rem; 48 | border-radius: 1.5rem; 49 | font-size: 0.8rem; 50 | } 51 | @media (prefers-color-scheme: dark) { 52 | ul.tags li { 53 | background-color: #555; 54 | } 55 | } 56 | @media (prefers-color-scheme: light) { 57 | ul.tags li { 58 | background-color: #ccc; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /template/server/api.ts: -------------------------------------------------------------------------------- 1 | import { checkbox, object, string } from 'cast.ts' 2 | import { Router } from 'express' 3 | 4 | export let api = Router() 5 | 6 | type Article = { 7 | id: number 8 | title: string 9 | tags: string[] 10 | intro: string 11 | cover_image: string 12 | cover_alt: string 13 | cover_title: string 14 | detail: string 15 | archived?: boolean 16 | passed?: boolean 17 | highlight?: boolean 18 | } 19 | let articles: Article[] = [ 20 | { 21 | id: 1, 22 | title: 'Hello World', 23 | tags: ['tag1', 'tag2', 'tag3'], 24 | intro: 'This is a sample article', 25 | cover_image: 'https://picsum.photos/seed/1/200', 26 | cover_alt: 'brown tune photo of bridge', 27 | cover_title: 'sample image 1', 28 | detail: '/article.html?id=1', 29 | }, 30 | { 31 | id: 2, 32 | title: 'Hello Blog', 33 | tags: ['tag1'], 34 | intro: 'This is the second sample article', 35 | cover_image: 'https://picsum.photos/seed/2/200', 36 | cover_alt: 37 | 'desk with laptop, keyboard, mouse, notebook and a pair of glasses', 38 | cover_title: 'sample image 2', 39 | detail: '/article.html?id=2', 40 | archived: true, 41 | }, 42 | { 43 | id: 3, 44 | title: 'Hello Article', 45 | tags: [], 46 | intro: 'This is text should be bolded', 47 | cover_image: 'https://picsum.photos/seed/3/200', 48 | cover_alt: 'a small waterfall in the forest', 49 | cover_title: 'sample image 3', 50 | detail: '/article.html?id=3', 51 | passed: true, 52 | highlight: true, 53 | }, 54 | ] 55 | 56 | api.get('/articles', (req, res) => { 57 | res.json(articles) 58 | }) 59 | 60 | api.get('/article', (req, res) => { 61 | let article = articles.find(article => article.id == +req.query.id!) 62 | if (!article) { 63 | res.status(404).json({ error: 'article not found' }) 64 | return 65 | } 66 | res.json(article) 67 | }) 68 | 69 | let postArticleRequestParser = object({ 70 | body: object({ 71 | title: string({ nonEmpty: true }), 72 | intro: string({ nonEmpty: true }), 73 | cover_image: string({ nonEmpty: true }), 74 | cover_alt: string({ nonEmpty: true }), 75 | cover_title: string({ nonEmpty: true }), 76 | archived: checkbox(), 77 | passed: checkbox(), 78 | highlight: checkbox(), 79 | }), 80 | }) 81 | api.post('/articles', (req, res) => { 82 | let { body } = postArticleRequestParser.parse(req) 83 | let id = articles.reduce((id, article) => Math.max(id, article.id), 0) + 1 84 | articles.push({ 85 | id, 86 | detail: '/article.html?id=' + id, 87 | tags: [], 88 | ...body, 89 | }) 90 | res.json({ id }) 91 | }) 92 | 93 | let counter = 0 94 | api.get('/counter', (req, res) => { 95 | res.json(counter) 96 | }) 97 | api.post('/counter/inc', (req, res) => { 98 | res.json(++counter) 99 | }) 100 | api.post('/counter/dec', (req, res) => { 101 | res.json(--counter) 102 | }) 103 | api.patch('/counter', (req, res) => { 104 | res.json((counter = req.body.value)) 105 | }) 106 | -------------------------------------------------------------------------------- /template/server/env.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv' 2 | import populateEnv from 'populate-env' 3 | 4 | config() 5 | 6 | export let env = { 7 | NODE_ENV: 'development', 8 | PORT: 8100, 9 | } 10 | 11 | populateEnv(env, { mode: 'halt' }) 12 | -------------------------------------------------------------------------------- /template/server/error.ts: -------------------------------------------------------------------------------- 1 | export class HttpError extends Error { 2 | status?: number 3 | constructor(public statusCode: number, message: string) { 4 | super(message) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /template/server/main.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { print } from 'listening-on' 3 | import { join } from 'path' 4 | import { env } from './env' 5 | import { readFileSync, readdirSync } from 'fs' 6 | import { api } from './api' 7 | import { HttpError } from './error' 8 | 9 | let app = express() 10 | 11 | if ( 12 | env.NODE_ENV == 'production' && 13 | readdirSync('public').includes('base.min.js') 14 | ) { 15 | let base_min_js = readFileSync(join('public', 'base.min.js')) 16 | app.get('/base.js', (req, res) => { 17 | res.contentType('text/javascript') 18 | res.end(base_min_js) 19 | }) 20 | } 21 | 22 | app.use(express.static('public')) 23 | app.use(express.static('public/examples')) 24 | app.use(express.json()) 25 | app.use(express.urlencoded({ extended: false })) 26 | 27 | app.use(api) 28 | 29 | let errorHandler: express.ErrorRequestHandler = ( 30 | error: HttpError, 31 | req, 32 | res, 33 | next, 34 | ) => { 35 | if (!(error instanceof HttpError)) { 36 | console.error(error) 37 | } 38 | res.status(error.status || error.statusCode || 500) 39 | if (req.header('Sec-Fetch-Mode') == 'navigate') { 40 | res.end(String(error)) 41 | } else { 42 | res.json({ error: String(error) }) 43 | } 44 | } 45 | app.use(errorHandler) 46 | 47 | let port = env.PORT 48 | app.listen(port, () => { 49 | print(port) 50 | }) 51 | -------------------------------------------------------------------------------- /template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "outDir": "dist", 10 | "incremental": true 11 | } 12 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "incremental": true 10 | }, 11 | "include": [ 12 | "cli.ts" 13 | ] 14 | } --------------------------------------------------------------------------------