Skip to content
This repository has been archived by the owner on Dec 20, 2023. It is now read-only.

Commit

Permalink
Merge pull request #3 from appcues/feature/string-ast
Browse files Browse the repository at this point in the history
Parse HTML strings into an AST, then convert to snabbdom VNodes.
  • Loading branch information
rayd committed May 4, 2016
2 parents 01481ee + ada304c commit 7a9c39b
Show file tree
Hide file tree
Showing 13 changed files with 365 additions and 150 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/index.js
/lib
node_modules
4 changes: 4 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
src
test
.travis.yml
.gitignore
.babelrc
43 changes: 36 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,53 @@ Require/import it.
import virtualize from 'snabbdom-virtualize';

// Require.
var virtualize = require('snabbdom-virtualize');
let virtualize = require('snabbdom-virtualize');
```

Pass it a set of DOM nodes or a string representing DOM nodes with one root node.

```javascript
// Actual DOM nodes
var topNode = document.createElement('div');
var textNode = document.createTextNode('Click ');
var linkNode = document.createElement('a');
let topNode = document.createElement('div');
let textNode = document.createTextNode('Click ');
let linkNode = document.createElement('a');
linkNode.setAttribute('href', 'http://example.com');
linkNode.textContent = 'here';
topNode.appendChild(textNode);
topNode.appendChild(linkNode);
var vnode = virtualize(topNode);
let vnode = virtualize(topNode);


// String
var vnode = virtualize('<div>Click <a href="http://example.com">here</a>');
let vnode = virtualize('<div>Click <a href="http://example.com">here</a>');
```

#### Using modules à la carte

If you'd prefer to import just the function for virtualizing DOM nodes or just
the function for virtualizing HTML strings, you're in luck. Just import
`snabbdom-virtualize/nodes` or `snabbdom-virtualize/strings` and use in the
same way:

```javascript
// DOM nodes.
import virtualize from 'snabbdom-virtualize/nodes';

let topNode = document.createElement('div');
let textNode = document.createTextNode('Click ');
let linkNode = document.createElement('a');
linkNode.setAttribute('href', 'http://example.com');
linkNode.textContent = 'here';
topNode.appendChild(textNode);
topNode.appendChild(linkNode);
let vnode = virtualize(topNode);


// HTML strings.
import virtualize from 'snabbdom-virtualize/strings';

let vnode = virtualize('<div>Click <a href="http://example.com">here</a>');

```

### Project setup
Expand All @@ -45,7 +74,7 @@ npm install
npm run build
```

This will output a compiled `index.js` file in the root directory.
This will output compiled files in the `lib` directory.

### Tests

Expand Down
1 change: 1 addition & 0 deletions nodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./lib/nodes');
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
},
"scripts": {
"prepublish": "npm run build",
"build": "webpack --config webpack.config.js",
"watch": "webpack --watch --config webpack.config.js",
"build": "babel src -d lib",
"watch": "npm run build -- --watch",
"test": "karma start test/karma.conf.js"
},
"main": "index.js",
"main": "lib/index.js",
"devDependencies": {
"babel-cli": "^6.3.17",
"babel-loader": "^6.2.2",
Expand All @@ -40,5 +40,8 @@
},
"peerDependencies": {
"snabbdom": "^0.3.0"
},
"dependencies": {
"html-parse-stringify": "^1.0.2"
}
}
121 changes: 8 additions & 113 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,118 +1,13 @@
import h from 'snabbdom/h';
import VNode from 'snabbdom/vnode';
import listeners from './event-listeners';
import virtualizeNode from './nodes';
import virtualizeString from './strings';

export default function snabbdomVirtualize(element) {
if (!element) {
return null;
export default function (el) {
if (typeof el === 'string') {
return virtualizeString(el);
}

// First thing to check is if a string is passed in.
if (typeof element === 'string') {
// General strategy here:
// Throw the string inside an element as innerHTML to get it parsed into
// DOM nodes. Then go and pull out the parsed nodes.
const el = document.createElement('div');
el.innerHTML = element;

// There should only be one top-level node in the string. Throw an error
// otherwise.
const childNodes = Array.prototype.slice.call(el.childNodes);
if (childNodes.length === 1) {
return snabbdomVirtualize(childNodes[0]);
}
else {
return childNodes.map((child) => { return snabbdomVirtualize(child); });
}
}

// If our node is a text node, then we only want to set the `text` part of
// the VNode.
if (element.nodeType === Node.TEXT_NODE) {
return VNode(undefined, undefined, undefined, element.textContent);
else {
return virtualizeNode(el);
}

// If not a text node, then build up a VNode based on the element's tag
// name, class and style attributes, and remaining attributes.

// Special values: style, class. We don't include these in the attrs hash
// of the VNode.
const data = {};
const classes = getClasses(element);
if (Object.keys(classes).length !== 0) {
data.class = classes;
}
const style = getStyle(element);
if (Object.keys(style).length !== 0) {
data.style = style;
}

// Build up set of attributes on the element.
const attributes = element.attributes;
for (let i = 0; i < attributes.length; i++) {
const attr = attributes.item(i);
const name = attr.name;
if (name !== 'style' && name !== 'class') {
if (!data.attrs) {
data.attrs = {};
}
data.attrs[name] = attr.value;
}
}

// Check for event listeners.
const on = {};
listeners.forEach((key) => {
if (element[key]) {
on[key.substring(2)] = element[key];
}
});
if (Object.keys(on).length > 0) {
data.on = on;
}

// Build up set of children.
let childNodes = null;
const children = element.childNodes;
if (children.length > 0) {
childNodes = [];
for (var i = 0; i < children.length; i++) {
childNodes.push(snabbdomVirtualize(children.item(i)));
}
}
return h(element.tagName.toLowerCase(), data, childNodes);
}

// Builds the class object for the VNode.
function getClasses(element) {
const className = element.className;
const classes = {};
if (className !== null && className.length > 0) {
className.split(' ').forEach((className) => {
classes[className] = true;
});
}
return classes;
}

// Builds the style object for the VNode.
function getStyle(element) {
const style = element.style;
const styles = {};
for (let i = 0; i < style.length; i++) {
const name = style.item(i);
const transformedName = transformName(name);
styles[transformedName] = style.getPropertyValue(name);
}
return styles;
}

function transformName(name) {
// Replace -a with A to help camel case style property names.
name = name.replace( /-(\w)/g, function _replace( $1, $2 ) {
return $2.toUpperCase();
});
// Handle properties that start with a -.
const firstChar = name.charAt(0).toLowerCase();
return `${firstChar}${name.substring(1)}`;
}
export { virtualizeNode, virtualizeString };
89 changes: 89 additions & 0 deletions src/nodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import h from 'snabbdom/h';
import { createTextVNode, transformName } from './utils';
import listeners from './event-listeners';

export default function snabbdomVirtualize(element) {
if (!element) {
return null;
}

// If our node is a text node, then we only want to set the `text` part of
// the VNode.
if (element.nodeType === Node.TEXT_NODE) {
return createTextVNode(element.textContent);
}

// If not a text node, then build up a VNode based on the element's tag
// name, class and style attributes, and remaining attributes.

// Special values: style, class. We don't include these in the attrs hash
// of the VNode.
const data = {};
const classes = getClasses(element);
if (Object.keys(classes).length !== 0) {
data.class = classes;
}
const style = getStyle(element);
if (Object.keys(style).length !== 0) {
data.style = style;
}

// Build up set of attributes on the element.
const attributes = element.attributes;
for (let i = 0; i < attributes.length; i++) {
const attr = attributes.item(i);
const name = attr.name;
if (name !== 'style' && name !== 'class') {
if (!data.attrs) {
data.attrs = {};
}
data.attrs[name] = attr.value;
}
}

// Check for event listeners.
const on = {};
listeners.forEach((key) => {
if (element[key]) {
on[key.substring(2)] = element[key];
}
});
if (Object.keys(on).length > 0) {
data.on = on;
}

// Build up set of children.
let childNodes = null;
const children = element.childNodes;
if (children.length > 0) {
childNodes = [];
for (var i = 0; i < children.length; i++) {
childNodes.push(snabbdomVirtualize(children.item(i)));
}
}
return h(element.tagName.toLowerCase(), data, childNodes);
}

// Builds the class object for the VNode.
function getClasses(element) {
const className = element.className;
const classes = {};
if (className !== null && className.length > 0) {
className.split(' ').forEach((className) => {
classes[className] = true;
});
}
return classes;
}

// Builds the style object for the VNode.
function getStyle(element) {
const style = element.style;
const styles = {};
for (let i = 0; i < style.length; i++) {
const name = style.item(i);
const transformedName = transformName(name);
styles[transformedName] = style.getPropertyValue(name);
}
return styles;
}
Loading

0 comments on commit 7a9c39b

Please sign in to comment.