├── .eslintrc.js ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── babel.config.json ├── jestconfig.json ├── mithril ├── index.js └── index.mjs ├── package-lock.json ├── package.json ├── preact ├── index.js └── index.mjs ├── react ├── index.js └── index.mjs ├── rollup.config.mjs ├── src ├── index.js └── util.js └── tests ├── index.test.js └── util.test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['foxdonut'] 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # seview 64 | dist 65 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | .gitignore 3 | .vscode 4 | babel.config.json 5 | jestconfig.json 6 | rollup.config.mjs 7 | tests 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Fred Daoud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # seview: S-Expression View 2 | 3 | A simple way of writing views with [s-expressions](https://en.wikipedia.org/wiki/S-expression), 4 | and meant to be used with a virtual DOM library. 5 | 6 | ## Why? 7 | 8 | Because plain JavaScript is simpler to write and build than JSX or HTML string literals, and it's 9 | great to write views in a way that is independent of the virtual DOM library being used. It's also 10 | nice to use convenient features even if the underlying virtual DOM library does not support them. 11 | 12 | ## Example 13 | 14 | Instead of writing this in JSX: 15 | 16 | ```jsx 17 |
18 | Enter your name: 19 | 20 | {isMessage &&
{message}
} 21 |
22 | ``` 23 | 24 | Or even this in hyperscript: 25 | 26 | ```js 27 | h('div', { id: 'home' }, [ 28 | h('span', { className: 'instruction' }, 'Enter your name:'), 29 | h('input', { type: 'text', id: 'username', name: 'username', size: 10 }), 30 | isMessage && h('div', { className: 'message' + (isError ? ' error' : '') }, message) 31 | ]) 32 | ``` 33 | 34 | You can write this with `seview`: 35 | 36 | ```js 37 | ['div#home', 38 | ['span.instruction', 'Enter your name:'], 39 | ['input:text#username[name=username][size=10]'], 40 | isMessage && ['div.message', { class: { 'error': isError } }, message] 41 | ] 42 | ``` 43 | 44 | Besides the conveniences of the syntax, you also don't have to write `h` at every element. To 45 | switch from one virtual DOM library to another, you only need to make changes in **one** place. 46 | All your view code can remain the same. 47 | 48 | If you are using the [Meiosis pattern](http://meiosis.js.org), `seview` is a great way to further 49 | decouple your code from specific libraries. Your views become independent of the underlying 50 | virtual DOM library API. 51 | 52 | ## Installation 53 | 54 | Using Node.js: 55 | 56 | ``` 57 | npm i seview 58 | ``` 59 | 60 | With a script tag: 61 | 62 | ```html 63 | 64 | ``` 65 | 66 | ## Usage 67 | 68 | Out of the box, `seview` supports 3 view libraries: 69 | 70 | - [React](https://react.dev) 71 | - [Preact](https://preactjs.com) 72 | - [Mithril](https://mithril.js.org) 73 | 74 | Using a different library is not difficult. See 75 | [Using a different view library](#using-a-different-view-library). 76 | 77 | When using `seview` with built-in support, we assume writing views with the following attributes: 78 | 79 | - `class` for the HTML `class` attribute - converted to `className` for React 80 | - `for` for the HTML `for` attribute - converted to `htmlFor` for React 81 | - `innerHTML` for using unescaped HTML - converted appropriately for React, Preact, and Mithril 82 | - `onClick`, `onChange`, etc. for DOM events - converted to lowercase for Mithril 83 | 84 | > By writing views with the conventions above, you can switch between React, Preact, or Mithril 85 | without changing any of your view code! You can see this in action in the 86 | [Meiosis Realworld Example](https://meiosis.js.org/examples/realworld/index.html), 87 | where switching can be achieving just by editing 88 | [one file](https://github.com/foxdonut/meiosis-examples/blob/master/examples/realworld/src/util/view.js). 89 | 90 | ## React 91 | 92 | To use `seview` with [React](https://react.dev): 93 | 94 | ```js 95 | import { h } from 'seview/react'; 96 | import { createRoot } from 'react-dom/client'; 97 | 98 | const rootView = (...) => 99 | ['div.container', 100 | [...] 101 | ]; 102 | 103 | const root = createRoot(document.getElementById('app')); 104 | root.render(h(rootView(...))); 105 | ``` 106 | 107 | > [Click here for a live example: seview + React](https://flems.io/#0=N4IgtglgJlA2CmIBcA2FA6ALARgDQgGd4EBjAF3imRAEMAHO9AKwJHwDMIFWkBtUAHY0wiJLQboAFmTCw2IEgHsBFFdQA8UCADcABNAC8AHXF0TAPnUB6LdvMgAvrkHDRp5q3xKV8NWIhgdIoATmS6wLqSug667MGKYLoA5ETaEPAA7gACAEzoAAzo2FbB8DTkVhACUPAAHuhgLEkA3EYCbd4EYeVkEMoEugbhbbr6AiSlIipIugAUJMSwuLrCigCuKgCUg+Yjo7oLsLDoa3RQNBSzERSB8MEXa6Uzs7XbBua6tboA1Ctg6ypops2g5Wu1xv0wgAVeC3e5kR7wABq6QygzmhyWB0kXCgpQEb12AlGvCStiSuD2JKS6AotTIAFoCGsSAsCAQKckYXCHqUUZkkgBdSnE-akgh0GgCTlJbl0O68+AzTmY9BdC7wWmw+XwxHCqm6cWS6XLCJVAR3AASUIAsgAZGYmABkNQA5s0AMImaL60XU8ki-ZipIAIzWZDIynQIbIAmjsYZdGCARowQAnhSDUGIsoPbAICQANbPQkrch9AQEdBVCaw3xkeaLZbYbZOZIASXGk3rQsDQcNofDkbjMZHCZq7Boa1gZAasAZ2Ezfv7OYEeYLxbmpZ6FarNe7KkbR2WC9byySABF4LWpmQhQbfUGSDjYHjfHtBWCOrAaOzdAAlMpyH5NE6lUKABkAnp0A9BIggtQFgD2ToyGCFlI2CWYk0UOgCG2JDl2ZHUsPiXDNjBUYHD2fEakw-CDVKBFgmJWYs10TQdAOH92QAOVcYxwEZHILDY0YOL0EhuIIPiRAEulGWZVl4HZCwoOA1FrFsIl+32cTzAABV-IgoDGXRtBoWA1iVcIyBxKtsNw9BzMs+AHE0nRtJ0my7PQByq2fXF8So5cxJsDyDXIvZguCjpIRWBh0QI0YqggXoLJmJL9huHVFRmHJ8iivs0kyZ5MUJA1SXQbwyBoKo7iXHTSUkTAZVSVEfgAoCwjqYQ6AQXs2LlBUmORVEjyxUkA2SWCfEBSV2UoXRI10IbdT5VEhU2Psg14NSyBA00zIsqyZkwHJomWSadBlGbVDCebjKWxROp6EChUFB8QS-CFKzCTEBiGG14D6AgICrIgEToK54roIFvpQ3R4kUMIhj2i8AHkbSq0oNX-RRkdmKBFBINZb3QV14DIABRBBbwAITTdsoFmJJ6DoJJNki8ZFirMB6FmcbS0ypHZxou5ZkkWY2acsays5sEHC5+QlECLg7moEMaBDYhHGFEB8wEQseH4EAhBEagcfIdAam0YgcLJlh5EeOQxGkMhcKQKwrA2OhC1dKqEhKLqsmwAAOdA8nyb2wCgIPoJtu26AdzwQDINN5WoAgJggOgyEcZxTdcC2uoZImwGt+Bbdge36w8J3ghdkA3Y9r2fb9gOwDj8hS4SEPw8j6PY8txky4rqua5UOv8DTjOxCz5Nc-zlxzbnyvUSnkBneoZuCE972BF9-2VasNrMlyAp0AAZjCroT7XzIN5ntx55zvOnGXtwRBBsGmUp0568bjvPebcj6By-ooUGBBf6QyyBgbARQrDgMgdA04j907P2zovd+hcV4gBDPjMgXR7iMEgHGEg7IAHbwjC3RBNBagkCgCOAhRD6AMLIYHfByMWF0CsJgC+UdyEECsJwwhqF6ANCqFVCh090GZzTggXW+AzbP3vmiPajggA) 108 | 109 | ## Preact 110 | 111 | To use `seview` with [Preact](https://preactjs.com): 112 | 113 | ```js 114 | import { h } from 'seview/preact'; 115 | import { render } from 'preact'; 116 | 117 | const rootView = (...) => 118 | ['div.container', 119 | [...] 120 | ]; 121 | 122 | const element = document.getElementById('app'); 123 | render(h(rootView(...)), element); 124 | ``` 125 | 126 | > [Click here for a live example: seview + Preact](https://flems.io/#0=N4IgtglgJlA2CmIBcA2FA6ALARgDQgGd4EBjAF3imRAEMAHO9AKwJHwDMIFWkBtUAHY0wiJLQboAFmTCw2IEgHsBFFdQA8UCADcABNAC8AHXF0TAPnUB6LdvMgAvrkHDRp5q3xKV8NWKsAVLoAAiwAHrp0AE7wNORSugFWRgIQYHSKUWS6wLqSug667FGKYLoA5ETaEPAA7sEATOgADOjYVtGx5FYQAlDwYehgLOUA3Ckp3gTZcWQQygS6BjkpuvoCJDEiKki6ABQkxLC4usKKAK4qAJRL5qtruoewsOjndFA0FHu5FOnwUZ9zjFdnswjcDOZdBEANSnMAXFQFK4pBzjASTBbZAAq8D+ALIQPgADUarUlvsnsdHpIuFAYgJwXcBGteOVbOVcPcWeV0BQwmQALQEc4kQ4EAgcio4vGAmIkurlAC6nOZD1ZBDoNAEkvK0ro-1l8F2ksp6Gmn3gvNx+vxhOVXN06s12pOuV6An+AAksQBZAAyuxMADJ+gBzUYAYRMBXtqu57JVDzV5QARucyGRlOgU2QBNncwLomkaFEAJ4ch1J3LKCOwCAkADWIMZp3I8wEBHQvU2uN8ZAORxO2BuTgqAEkNls+0rE0nHan05m8znlwX+uwaOdYGQhrABdgK3G59WBLX6039i3Zu3O92pyoB88TvuRydygAReA97ZkJUO2NJiQNKwHSvj3IqaKTLANDiroAAKMSzPKZIDKoUCLJ0szoBGpQZB6iLAPcUxkFEIqZlEezRIodAEDchFHsKNqUSUNFXGiawOPc9L9BRdEOjEBJRMyeyVromg6I80HisY4CCg0FiiWs4l6CQUkEDJfKCsKorwOKFgIV0ZDIdYthMnODzKeYcEwUQUDrLo2g0LA5xGjkZA0p2VE0egjnOfADgmToZnmW5HnoF5nZAbS9KcUeSk2EFDpsfcsWxRiHYzAw5L0WsvQQHMTm7DlDy-Dahq7A0zQpbO1R1CClKMg6rLoN4ZA0L0-yHuZrKSJgOpVKSuiwgZsy6AMwh0AgM6iXqBqCcSpKPlSrIJhUOE+IimripQuiZros22nKpJKlcs5JrwI3kMhroOU5Lm7JgDQFCcK06Dq62qNkW22btijwYhV3HYqir-iikEbJijxHIsyw+vA8wEBAnZEASdDfKcWUOMl6IQxlY0ID+5JQIoJDnD+6ChvAZAAKIE32ABCpZjlAezlPQdDlNjlKdmA9B7EtLbFZh8Tcf8eySHs7M+YtDWnfjvbXGiWNovISjpFw-zUCmNApsQ8hEKQcwLNQ2BIM0jjKiAdYCA2PD8CAQgiNQws7mTUAePIQJyGI0hkDRSBWFYlx0A2oYtaUHQA2QwTYK02AAMxtAl0yR4ZrxgO7LDyGQpb6tQBCbBAdBkI4zgO64+fwLVtQe-gXvUL7-uB8Hofh2AVgDXUjQtOg8fJ2QHdV6StcgDnediAXURFyXTguE7YgiAjSNClTbye1E3sgI3BAB0HAgh2HatWIviiIwQK+o8EGDYEnJ9nxfbwj2PbiT9Ppdz24KaKIoZDTACjCQDzCQcU69N7b13rzMIJAoDLm-r-Ei9BoFAIjl-H+f96BWEwD3ZoVhgEECsKg+B-8hi9BaiA-Az9845wQBbfAjsX5DzqP9QyjggA) 127 | 128 | ## Mithril 129 | 130 | To use `seview` with [Mithril](https://mithril.js.org): 131 | 132 | ```js 133 | import { h } from 'seview/mithril'; 134 | import m from 'mithril'; 135 | 136 | const rootView = (...) => 137 | ['div.container', 138 | [...] 139 | ]; 140 | 141 | m.mount(document.getElementById('app'), { 142 | view: () => h(rootView(...)) 143 | }); 144 | ``` 145 | 146 | > [Click here for a live example: seview + Mithril](https://flems.io/#0=N4IgtglgJlA2CmIBcA2FA6ALARgDQgGd4EBjAF3imRAEMAHO9AKwJHwDMIFWkBtUAHY0wiJLQboAFmTCw2IEgHsBFFdQA8UCADcABNAC8AHXF0TAPnUB6LdvMgAvrkHDRp5q3xKV8NWIhgdIoATmS6wLqSug667MGKYLoA5ETaEPAA7gACAEzoAAzo2FaQZJLBXFYQAlDwAB7oYCxJANxGAu3eBGE05BDKBLoG4e26+gIkwfAiKki6ABQkxLC4usKKAK4qAJRD5qNjukuwsOgbdFA0FPMRFIHwwVcbU3PzdbsG5rp1ugDUa2BNipott2g42h0JgMwgAVaZ0B5PKYANXSGSGC2OKyOki4UCmAg++wEY14SVsSVwB1JSXQFDqZAAtAQNiQlgQCJTknD7o8yM94KjMkkALpUkmHMkEOg0ARcpI8hF8gVzLlY9DdK7wOnwxH8qZi6m6KUyuWrCLVAQPAASMIAsgAZOYmABktQA5i0AMImaKGiU0iniw6SpIAIw2ZDIynQYbIAlj8cZdAqYBowQAnpSjSGIsovbAICQANavIlrPoDdDVSbTXxkRbLVbYXZOZIASQmUxmZFFwZDxvDkejCbjo6TtXYNA2sDIjVgjOw2YDA7zAgLRdLC3LvTI-QEBGrXbrKkbJ1Wi9bqySABF4LWe6Kjf6QyRcbB8b4DiKIZ1obo7QgMoKlgIV0WGYADjSTJXgiK4yGCAhVjfPECRBPYjTAeZaTAJkcmXAddCw2l6SZFk2XgDl5UA4CuDApJtn7ENiKDZIAAUaA5Shxl0bQaFgDZ4FVVZ4MQ9A+IE+BGJzHFUK-FdQQEBx2j-A8egYDFIIlaogIgfi5i0kM7iVJEhN0HJ8gOJwoLRV4sSJI0yXQbwyBoaoHgIgcyUkTB5VSNE-gAoDyi4XR6mEOgED7GTFT1AUwLPbEyVYpIvWUVQwhlLioF0aNdFi5UUTRUVpJXUkaJC0C0XNXj+MEuZMByaJVmSnR5TSnxgSyogcryiqQPokURWfMFfyhNSjmWQZhjteB+gICBDyIfk6BuNYNIcbYxrARogQbKBFBIDYe3Qd14DIABRBAewAIQzdsoGw+g6AY80bJg7c9kieZnvEtFEoIeZtmBsEtpUiYpsaeh5iBr6dqmfEaAyIGwYEeQlECLgHmoMMaDDYhHDFEBCwEYseH4EAhBEahSkqjx5GeOQxGkMg6AIJArCsLY6GLd1nISEpgpA3J0DyHJBdo04WHkMgMwRagCEmCA6DIRxnEp1wFfgaCMnp-BGeoFm2Y5rmBB5vmMasfzMhFwoAGYbEWsgre1tE9ZAWX5bERWKhVtWXGpsQRHmxbmXO84GeCJmQCN9nOe53n+bAEo5sUBaCDDlasgwbAihTkOM+W853c9twfeV1WnADtww0URQyG6R5GEgBMSA5SPo9jk20zqEgoFHOuG4Q+g+9bgXa-rxv6CsTACgKKw24IKwJ6HpvGmqZz2-wUuFdlhBCfwKmy9dzIgslxwgA) 147 | 148 | ## Features 149 | 150 | `seview` supports CSS-style selectors in tag names, `{ class: boolean }` for toggling classes, using 151 | an array or varags for children, flattening of nested arrays, and removal of null/empty elements. 152 | 153 | ### Element 154 | 155 | An element is an array: 156 | 157 | ``` 158 | [tag, attrs, children] 159 | ``` 160 | 161 | or a string (text node): 162 | 163 | ``` 164 | 'this is a text node' 165 | ``` 166 | 167 | The `tag` can be a string, or something that your virtual DOM library understands; for example, 168 | a `Component` in React. For the latter, `seview` just returns the selector as-is. 169 | 170 | ### Tag 171 | 172 | When the tag is a string, it is assumed to be a tag name, possibly with CSS-style selectors: 173 | 174 | - `'div'`, `'span'`, `'h1'`, `'input'`, etc. 175 | - `'div.highlighted'`, `'button.btn.btn-default'` for classes 176 | - `'div#home'` for `id` 177 | - `'input:text'` for ``. There can only be one type, so additional types are 178 | ignored. `'input:password:text'` would result in ``. 179 | - `'input[name=username][required]'` results in `` 180 | - if you need spaces, just use them: `'input[placeholder=Enter your name here]'` 181 | - default tag is `'div'`, so you can write `''`, `'.highlighted'`, `'#home'`, etc. 182 | - these features can all be used together, for example 183 | `'input:password#duck.quack.yellow[name=pwd][required]'` results in 184 | `` 185 | 186 | ### Attributes 187 | 188 | If the second item is an object, it is considered to be the attributes for the element. 189 | 190 | Of course, for everything that you can do with a CSS-style selector in a tag as shown in the 191 | previous section, you can also use attributes: 192 | 193 | ```js 194 | ['input', { type: 'password', name: 'password', placeholder: 'Enter your password here' }] 195 | ``` 196 | 197 | You can also mix selectors and attributes. If you specify something in both places, the attribute 198 | overwrites the selector. 199 | 200 | ```js 201 | ['input:password[name=password]', { placeholder: 'Enter your password here' }] 202 | ``` 203 | ```html 204 | 205 | ``` 206 | 207 | ```js 208 | ['input:password[name=username]', { type: 'text', placeholder: 'Enter your username here' }] 209 | ``` 210 | ```html 211 | 212 | ``` 213 | 214 | ### Classes 215 | 216 | Classes can be specified in the tag as a selector (as shown above), and/or in attributes using 217 | `class`: 218 | 219 | ```js 220 | ['button.btn.info', { class: 'btn-default special' }] 221 | ``` 222 | ```html 223 |