diff --git a/README.md b/README.md
index 5c7b96f..bb70e9a 100644
--- a/README.md
+++ b/README.md
@@ -10,15 +10,6 @@ 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.
-## Convenient Features
-
-- CSS-style selectors such as `"input:text.form-control[name=username]"`
-- Removal of `null`, `undefined`, and `false`, so that you can write
- `isMessage && ["div", "message"]`
-- CSS classes included or excluded using flags, so that you can write
- `["div", { className: { "error": isError } }, "message"]`
-- Express children as an array or as varargs
-
## Example
Instead of writing this in JSX:
@@ -41,7 +32,7 @@ h("div", { id: "home" }, [
])
```
-You write this with `seview`:
+You can write this with `seview`:
```javascript
["div#home",
@@ -55,26 +46,142 @@ Besides the conveniences of the syntax, you also don't have to write `h` at ever
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.
-## Installation
+## Features
-Using Node.js:
+`seview` supports CSS-style selectors in tag names, `{ className: boolean }` for toggling classes,
+using an array or varags for children, flattening of nested arrays, and removal of null/empty elements.
+
+### Element
+
+An element is an array:
```
-npm i -S seview
+[tag, attrs, children]
```
-With a script tag:
+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.
+
+### Tag
+
+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"` for `id`
+- `"input:text"` for ` `. There can only be one type, so additional types are
+ignored. `"input:password:text"` would result in ` `.
+- `"input[name=username][required]"` results in ` `
+- 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
+ ` `
+
+### Attributes
+
+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:
+
+```javascript
+["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.
+
+```javascript
+["input:password[name=password]", { placeholder: "Enter your password here" }]
+```
```html
-
+
+```
+
+```javascript
+["input:password[name=username]", { type: "text", placeholder: "Enter your username here" }]
+```
+```html
+
```
+### Classes
-## More Content to come
+Classes can be specified in the tag as a selector (as shown above), and/or in attributes using
+`className`:
-_This README is a work-in-progress, more content is forthcoming._
+```javascript
+["button.btn.info", { className: "btn-default special" }]
+```
+```html
+
+```
-## Varargs and text nodes
+If you specify an object instead of a string for `className`, 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.
+
+```javascript
+// isDefault is true
+// isError is false
+["button.btn", { className: { "btn-default": isDefault, "error": isError } }]
+```
+```html
+
+```
+
+Note that `className` is the default key, but this can be configured to be something else, such
+as `class`.
+
+### Children (array or varags)
+
+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.
+
+#### Using an array
+
+You can specify children as an array:
+
+```javascript
+["div", [
+ ["span", ["Hello"]],
+ ["b", ["World"]]
+]
+```
+```html
+
+ Hello
+ World
+
+```
+
+#### Using varargs
+
+You can specify children as varargs:
+
+```javascript
+["div",
+ ["span", "Hello"],
+ ["b", "World"]
+]
+```
+```html
+
+ Hello
+ World
+
+```
+
+### Varargs and text nodes
The problem with supporting varargs is, how do you differentiate a single element from two text nodes?
@@ -90,15 +197,341 @@ vs
["div", ["hello", "there"]]
```
-For the second case, varargs MUST be used:
+For the second case, varargs **must** be used:
```js
["div", "hello", "there"]
```
+### Flattened arrays
+
+Whether using an array of children or varargs, nested arrays are automatically flattened:
+
+```javascript
+["div", [
+ ["div", "one"],
+ [
+ ["div", "two"],
+ [
+ ["div", "three"]
+ ]
+ ]
+]]
+```
+
+or
+
+```javascript
+["div",
+ ["div", "one"],
+ [
+ ["div", "two"],
+ [
+ ["div", "three"]
+ ]
+ ]
+]
+```
+
+Both result in
+
+```html
+
+```
+
+### Ignored elements
+
+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:
+
+```javascript
+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`.
+
+### Elements converted to a string
+
+The following elements will be converted to a string:
+
+- `true`
+- numbers
+- `NaN`
+- `Infinity`
+
+## Installation
+
+Using Node.js:
+
+```
+npm i -S seview
+```
+
+With a script tag:
+
+```html
+
+```
+
+## Usage
+
+`seview` exports a single function, `sv`, that you use to obtain a function which you can name
+as you wish; in the examples, I name this function `h`. Calling `h(view)`, where `view` is the view
+expressed as arrays as we have seen above, produces the final result suitable for your virtual DOM
+library.
+
+When you call `sv`, you pass it a function that gets called for every node in the view. Each
+node has the following structure:
+
+```javascript
+{
+ tag: "button",
+ attrs: { id: "save", className: "btn btn-default", ... }
+ children: [ ... ]
+}
+```
+
+The function that you write needs to convert the structure above to what is expected by the
+virtual DOM library that you are using. Note that your function will also be called for each
+element in `children`.
+
+You can optionally pass a second parameter to `sv` to indicate something other than `className`
+as the property to use for CSS classes. For example:
+
+```javascript
+const h = sv(func, { className: "class" })
+```
+
+This would use the `class` property in the `attrs` to indicate the CSS classes.
+
+So you need to write a snippet of code that you pass to `sv` to wire up `seview` with the virtual
+DOM library that you are using. Below, you will find examples for 8 libraries. Using a different
+library is not difficult; you should get a pretty good idea of what to do from the examples below.
+
+Also, please note that the snippets below are just examples; feel free to change and adapt
+according to your specific needs. In fact, this is why these snippets are not included in
+`seview` or even as separate libraries. They are just a handful of code, and you might like
+to tweak the code to your preference.
+
+## [React](https://reactjs.org/)
+
+```javascript
+import React from "react";
+import { sv } from "seview";
+
+const h = sv(node => {
+ if (typeof node === "string") {
+ return node;
+ }
+ const attrs = node.attrs || {};
+ if (attrs.innerHTML) {
+ attrs.dangerouslySetInnerHTML = { __html: attrs.innerHTML };
+ delete attrs.innerHTML;
+ }
+ const args = [node.tag, node.attrs || {}];
+ if (node.children) {
+ node.children.forEach(child => args.push(child))
+ }
+ return React.createElement.apply(null, args);
+});
+```
+
+## [Preact](https://preactjs.com/)
+
+```javascript
+import preact from "preact";
+import { sv } from "seview";
+
+const h = sv(node => {
+ if (typeof node === "string") {
+ return node;
+ }
+ const attrs = node.attrs || {};
+ if (attrs.innerHTML) {
+ attrs.dangerouslySetInnerHTML = { __html: attrs.innerHTML };
+ delete attrs.innerHTML;
+ }
+ return preact.h(node.tag, node.attrs || {}, node.children || []);
+});
+```
+
+## [Inferno](https://infernojs.org/)
+
+```javascript
+import { h as hyper } from "inferno-hyperscript";
+import { sv } from "seview";
+
+const processAttrs = (attrs = {}) => {
+ Object.keys(attrs).forEach(key => {
+ if (key === "htmlFor") {
+ const value = attrs[key];
+ delete attrs[key];
+ attrs["for"] = value;
+ }
+ })
+ return attrs;
+};
+
+const h = sv(node =>
+ (typeof node === "string")
+ ? node
+ : hyper(node.tag, processAttrs(node.attrs), node.children || [])
+);
+```
+
+## [Mithril](http://mithril.js.org/)
+
+```javascript
+import m from "mithril";
+import { sv } from "seview";
+
+const processAttrs = (attrs = {}) => {
+ Object.keys(attrs).forEach(key => {
+ if (key.startsWith("on")) {
+ const value = attrs[key];
+ delete attrs[key];
+ attrs[key.toLowerCase()] = value;
+ }
+ })
+ return attrs;
+};
+
+const h = sv(node =>
+ (typeof node === "string")
+ ? { tag: "#", children: node }
+ : node.attrs && node.attrs.innerHTML
+ ? m(node.tag, m.trust(node.attrs.innerHTML))
+ : m(node.tag, processAttrs(node.attrs), node.children || [])
+);
+```
+
+## [Snabbdom](https://github.com/snabbdom/snabbdom/)
+
+```javascript
+import { html } from "snabbdom-jsx";
+import { sv } from "seview";
+
+const processAttrs = (attrs = {}) => {
+ Object.keys(attrs).forEach(key => {
+ if (key.startsWith("on")) {
+ const value = attrs[key];
+ delete attrs[key];
+ attrs["on-" + key.toLowerCase().substring(2)] = value;
+ }
+ })
+ return attrs;
+};
+
+const h = sv(node =>
+ (typeof node === "string")
+ ? node
+ : html(node.tag, processAttrs(node.attrs), node.children || [])
+);
+```
+
+## [domvm](https://github.com/leeoniya/domvm)
+
+```javascript
+import { defineElement } from "domvm";
+import { sv } from "seview";
+
+const attrMappings = {
+ "className": "class"
+};
+
+const processAttrs = (attrs = {}) => {
+ Object.keys(attrs).forEach(key => {
+ if (key.startsWith("on")) {
+ const value = attrs[key];
+ delete attrs[key];
+ attrs[key.toLowerCase()] = value;
+ }
+ else {
+ const to = attrMappings[key];
+ if (to) {
+ const value = attrs[key];
+ delete attrs[key];
+ attrs[to] = value;
+ }
+ }
+ });
+ return attrs;
+};
+
+const h = sv(node =>
+ (typeof node === "string")
+ ? node
+ : defineElement(node.tag, processAttrs(node.attrs), node.children || [])
+);
+```
+
+## [petit-dom](https://github.com/yelouafi/petit-dom)
+
+```javascript
+import { h as hyper } from "petit-dom";
+import { sv } from "seview";
+
+const attrMappings = {
+ "htmlFor": "for",
+ "className": "class"
+};
+
+const processAttrs = (attrs = {}) => {
+ Object.keys(attrs).forEach(key => {
+ if (key.startsWith("on")) {
+ const value = attrs[key];
+ delete attrs[key];
+ attrs[key.toLowerCase()] = value;
+ }
+ else {
+ const to = attrMappings[key];
+ if (to) {
+ const value = attrs[key];
+ delete attrs[key];
+ attrs[to] = value;
+ }
+ }
+ });
+ return attrs;
+};
+
+const h = sv(node =>
+ (typeof node === "string")
+ ? node
+ : hyper(node.tag, processAttrs(node.attrs), node.children || [])
+);
+```
+
+## [DIO](https://dio.js.org/)
+
+```javascript
+import { h as hyper } from "dio.js";
+import { sv } from "seview";
+
+const h = sv(node =>
+ (typeof node === "string")
+ ? node
+ : hyper(node.tag, node.attrs || {}, node.children || [])
+);
+```
+
## Credits
-`seview` is inspired by the following. Credit goes to the authors and their communities.
+`seview` is inspired by the following. Credit goes to the authors and their communities - thank you
+for your excellent work!
- [How to UI in 2018](https://medium.com/@thi.ng/how-to-ui-in-2018-ac2ae02acdf3)
- [ijk](https://github.com/lukejacksonn/ijk)
diff --git a/src/index.js b/src/index.js
index c6e4389..23deabe 100644
--- a/src/index.js
+++ b/src/index.js
@@ -11,7 +11,7 @@ const transformNodeDef = (transform, def) => {
return transform(def)
}
-export const sv = transform => node => {
- const def = nodeDef(node)
+export const sv = (transform, options) => node => {
+ const def = nodeDef(node, options)
return transformNodeDef(transform, def)
}
diff --git a/src/util.js b/src/util.js
index fb99a70..01bbb76 100644
--- a/src/util.js
+++ b/src/util.js
@@ -63,7 +63,7 @@ returns tag properties: for example, "input:password#duck.quack.yellow[name=pwd]
attrs: { type: "password", id: "duck", name: "pwd", required: true }
}
*/
-export const getTagProperties = selector => {
+export const getTagProperties = (selector, className = "className") => {
const result = {}
let tagType = selector.match(tagTypeRegex)
@@ -100,7 +100,7 @@ export const getTagProperties = selector => {
})
if (classes.length > 0) {
- set(result, ["attrs", "className"], classes.join(" "))
+ set(result, ["attrs", className], classes.join(" "))
}
}
@@ -136,14 +136,14 @@ const processChildren = (rest, result = []) => {
return result
}
-export const nodeDef = node => {
+export const nodeDef = (node, options = { className: "className" }) => {
// Tag
let rest = node[2]
let varArgsLimit = 3
// Process tag
const result = isString(node[0])
- ? getTagProperties(node[0])
+ ? getTagProperties(node[0], options.className)
: { tag: node[0] }
// Process attrs
@@ -151,9 +151,9 @@ export const nodeDef = node => {
const attrs = node[1]
// Process className
- if (attrs["className"] !== undefined) {
- const classAttr = attrs["className"]
- delete attrs["className"]
+ if (attrs[options.className] !== undefined) {
+ const classAttr = attrs[options.className]
+ delete attrs[options.className]
let addClasses = []
if (isString(classAttr)) {
@@ -167,9 +167,9 @@ export const nodeDef = node => {
})
}
if (addClasses.length > 0) {
- const existingClassName = get(result, ["attrs", "className"])
+ const existingClassName = get(result, ["attrs", options.className])
const addClassName = addClasses.join(" ")
- set(result, ["attrs", "className"],
+ set(result, ["attrs", options.className],
(existingClassName ? existingClassName + " " : "")
+ addClassName
)
diff --git a/test/index.test.js b/test/index.test.js
index 5655f63..5d3e9f1 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -15,6 +15,7 @@ const transform = node => {
}
const h = sv(transform)
+const k = sv(transform, { className: "class" })
const p = sv(node => {
if (isString(node)) {
@@ -91,6 +92,34 @@ export default {
] }
}
],
+ combineClassName: [
+ h(["button.btn", { className: "btn-default other" }]),
+ {
+ type: "button",
+ props: { className: "btn btn-default other" }
+ }
+ ],
+ combineClassNameWithDifferentProp: [
+ k(["button.btn", { class: "btn-default other" }], { className: "class" }),
+ {
+ type: "button",
+ props: { class: "btn btn-default other" }
+ }
+ ],
+ classNameToggles: [
+ h(["button.btn", { className: { "btn-primary": true, "btn-default": false }}]),
+ {
+ type: "button",
+ props: { className: "btn btn-primary" }
+ }
+ ],
+ classNameTogglesFalsy: [
+ h(["button.btn", { className: { "btn-primary": true, "one": null, "two": undefined, "three": 0 }}]),
+ {
+ type: "button",
+ props: { className: "btn btn-primary" }
+ }
+ ],
basicVarArgs: [
p(["div", {},
["div", "test1"],
diff --git a/test/util.test.js b/test/util.test.js
index 38ea897..8aa1d73 100644
--- a/test/util.test.js
+++ b/test/util.test.js
@@ -234,6 +234,12 @@ export default {
]
},
getTagProperties: {
+ divByDefault: [
+ getTagProperties(""),
+ {
+ tag: "div"
+ }
+ ],
divByDefaultWithClass: [
getTagProperties(".btn"),
{
@@ -255,12 +261,26 @@ export default {
attrs: { type: "password", id: "duck", className: "quack yellow", name: "pwd", required: true }
}
],
+ valueWithSpaces: [
+ getTagProperties("input[placeholder=Enter your name here]"),
+ {
+ tag: "input",
+ attrs: { placeholder: "Enter your name here" }
+ }
+ ],
extraTypesIgnored: [
getTagProperties("input:text:password.form-input"),
{
tag: "input",
attrs: { type: "text", className: "form-input" }
}
+ ],
+ optionForClassName: [
+ getTagProperties("input.form-input", "class"),
+ {
+ tag: "input",
+ attrs: { class: "form-input" }
+ }
]
},
nodeDef: {
@@ -407,6 +427,13 @@ export default {
attrs: { className: "btn btn-default other" }
}
],
+ combineClassNameWithDifferentProp: [
+ nodeDef(["button.btn", { class: "btn-default other" }], { className: "class" }),
+ {
+ tag: "button",
+ attrs: { class: "btn btn-default other" }
+ }
+ ],
classNameToggles: [
nodeDef(["button.btn", { className: { "btn-primary": true, "btn-default": false }}]),
{