A simple way of writing views with s-expressions, and meant to be used with a virtual DOM library.
Because plain JavaScript is simpler to write and build than JSX or HTML string literals, and it's great to write views in a way that is independent of the virtual DOM library being used. It's also nice to use convenient features even if the underlying virtual DOM library does not support them.
Instead of writing this in JSX:
<div id="home">
<span className="instruction">Enter your name:</span>
<input type="text" id="username" name="username" size="10"/>
{isMessage && <div className={"message" + (isError ? " error" : "")}>{message}</div>}
</div>
Or even this in hyperscript:
h('div', { id: 'home' }, [
h('span', { className: 'instruction' }, 'Enter your name:'),
h('input', { type: 'text', id: 'username', name: 'username', size: 10 }),
isMessage && h('div', { className: 'message' + (isError ? ' error' : '') }, message)
])
You can write this with seview
:
['div#home',
['span.instruction', 'Enter your name:'],
['input:text#username[name=username][size=10]'],
isMessage && ['div.message', { class: { 'error': isError } }, message]
]
Besides the conveniences of the syntax, you also don't have to write h
at every element. To
switch from one virtual DOM library to another, you only need to make changes in one place.
All your view code can remain the same.
If you are using the Meiosis pattern, seview
is a great way to further
decouple your code from specific libraries. Your views become independent of the underlying
virtual DOM library API.
Using Node.js:
npm i seview
With a script tag:
<script src="http://unpkg.com/seview"></script>
Out of the box, seview
supports 3 view libraries:
Using a different library is not difficult. See Using a different view library.
When using seview
with built-in support, we assume writing views with the following attributes:
class
for the HTMLclass
attribute - converted toclassName
for Reactfor
for the HTMLfor
attribute - converted tohtmlFor
for ReactinnerHTML
for using unescaped HTML - converted appropriately for React, Preact, and MithrilonClick
,onChange
, etc. for DOM events - converted to lowercase for Mithril
By writing views with the conventions above, you can switch between React, Preact, or Mithril without changing any of your view code! You can see this in action in the Meiosis Realworld Example, where switching can be achieving just by editing one file.
To use seview
with React:
import { h } from 'seview/react';
import { createRoot } from 'react-dom/client';
const rootView = (...) =>
['div.container',
[...]
];
const root = createRoot(document.getElementById('app'));
root.render(h(rootView(...)));
To use seview
with Preact:
import { h } from 'seview/preact';
import { render } from 'preact';
const rootView = (...) =>
['div.container',
[...]
];
const element = document.getElementById('app');
render(h(rootView(...)), element);
To use seview
with Mithril:
import { h } from 'seview/mithril';
import m from 'mithril';
const rootView = (...) =>
['div.container',
[...]
];
m.mount(document.getElementById('app'), {
view: () => h(rootView(...))
});
seview
supports CSS-style selectors in tag names, { class: boolean }
for toggling classes, using
an array or varags for children, flattening of nested arrays, and removal of null/empty elements.
An element is an array:
[tag, attrs, children]
or a string (text node):
'this is a text node'
The tag
can be a string, or something that your virtual DOM library understands; for example,
a Component
in React. For the latter, seview
just returns the selector as-is.
When the tag is a string, it is assumed to be a tag name, possibly with CSS-style selectors:
'div'
,'span'
,'h1'
,'input'
, etc.'div.highlighted'
,'button.btn.btn-default'
for classes'div#home'
forid
'input:text'
for<input type="text">
. There can only be one type, so additional types are ignored.'input:password:text'
would result in<input type="password">
.'input[name=username][required]'
results in<input name="username" required="true">
- if you need spaces, just use them:
'input[placeholder=Enter your name here]'
- default tag is
'div'
, so you can write''
,'.highlighted'
,'#home'
, etc. - these features can all be used together, for example
'input:password#duck.quack.yellow[name=pwd][required]'
results in<input type="password" id="duck" class="quack yellow" name="pwd" required="true">
If the second item is an object, it is considered to be the attributes for the element.
Of course, for everything that you can do with a CSS-style selector in a tag as shown in the previous section, you can also use attributes:
['input', { type: 'password', name: 'password', placeholder: 'Enter your password here' }]
You can also mix selectors and attributes. If you specify something in both places, the attribute overwrites the selector.
['input:password[name=password]', { placeholder: 'Enter your password here' }]
<input type="password" name="password" placeholder="Enter password name here">
['input:password[name=username]', { type: 'text', placeholder: 'Enter your username here' }]
<input type="text" name="username" placeholder="Enter your username here">
Classes can be specified in the tag as a selector (as shown above), and/or in attributes using
class
:
['button.btn.info', { class: 'btn-default special' }]
<button class="btn info btn-default special">
If you specify an object instead of a string for class
, the keys are classes and the values
indicate whether or not to include the class. The class is only included if the value is truthy.
// isDefault is true
// isError is false
['button.btn', { class: { 'btn-default': isDefault, 'error': isError } }]
<button class="btn btn-default">
The last item(s), (starting with the second if there are no attributes, and starting with the third if attributes are present), are the children. The children can be:
- an array, or
- varargs.
You can specify children as an array:
['div', [
['span', ['Hello']],
['b', ['World']]
]
<div>
<span>Hello</span>
<b>World</b>
</div>
You can specify children as varargs:
['div',
['span', 'Hello'],
['b', 'World']
]
<div>
<span>Hello</span>
<b>World</b>
</div>
The problem with supporting varargs is, how do you differentiate a single element from two text nodes?
For example:
['div', ['b', 'hello']]
vs
['div', ['hello', 'there']]
For the second case, varargs must be used:
['div', 'hello', 'there']
Whether using an array of children or varargs, nested arrays are automatically flattened:
['div', [
['div', 'one'],
[
['div', 'two'],
[
['div', 'three']
]
]
]]
or
['div',
['div', 'one'],
[
['div', 'two'],
[
['div', 'three']
]
]
]
Both result in
<div>
<div>one</div>
<div>two</div>
<div>three</div>
</div>
The following elements are ignored and not included in the output:
undefined
null
false
''
[]
This makes it simple to conditionally include an element by writing:
condition && ['div', 'message']
If condition
is falsy, the div
will not be included in the output. Because it is completely
excluded, this will work even if the virtual DOM library that you are using does not handle
false
, null
, or undefined
.
The following elements will be converted to a string:
true
- numbers
NaN
Infinity
seview
exports a single function, seview
, that you use to create an h
function that works with
the view library of your choice. Calling h(view)
, where view
is the view expressed as arrays as
we have seen above, produces the final result suitable for your view library.
To set up your view library, call seview
and pass it a function that gets called for every node in
the view. Each node has the following structure:
{
tag: 'button',
attrs: { id: 'save', class: 'btn btn-default', ... }
children: [ ... ]
}
The function that you write needs to convert the structure above to what is expected by the view
library that you are using. Note that your function will also be called for each element in
children
.
import { seview } from 'seview';
import { myViewLibrary } from 'my-view-library';
export const h = seview((node) => {
const tag = processTagAsNecessary(node.tag);
const attrs = processAttrsAsNecessary(node.tag);
return myViewLibrary(tag, attrs, node.children || []);
});
Then you can use h
with your view library in a similar way as we saw in the Usage
section.
seview
is inspired by the following. Credit goes to the authors and their communities - thank you
for your excellent work!
seview is developed by foxdonut (@foxdonut00) and is released under the MIT license.