├── .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 |