├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── docs └── jsnox-specstring.png ├── jsnox.js ├── package.json └── tests ├── test_autokey.js ├── test_parsing.js └── test_trees.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.DS_Store 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint config 3 | // See http://www.jshint.com/options/ for full config documentation 4 | 5 | "browser" : true, // Standard browser globals e.g. `window`, `document`. 6 | "node" : true, // For testing if "module" exists 7 | "debug" : false, // Allow debugger statements 8 | "devel" : false, // Allow development statements (eg. console.log) 9 | 10 | // EcmaScript 5. 11 | "strict" : false, // Require `use strict` pragma in every file. 12 | "globalstrict" : false, // Allow global "use strict" (enables "strict"). 13 | 14 | // Enforcing Options 15 | // These options tell JSHint to be more strict towards your code. 16 | "bitwise" : true, // Prohibit bitwise operators (&, |, ^, etc.). 17 | "curly" : false, // Requires {} for every new block or scope. 18 | "eqeqeq" : true, // Requires triple equals i.e. `===`. 19 | "forin" : false, // Requires `for in` loops to be filtered with 20 | // `hasOwnPrototype`. 21 | "immed" : true, // Requires IIFEs to be wrapped in parens 22 | // eg. `( function(){}() );` 23 | "latedef" : false, // Prohibits variable use before definition. 24 | "newcap" : false, // Requires capitalization of all constructors 25 | "noarg" : true, // Prohibits use of `arguments.caller` and `callee`. 26 | "noempty" : true, // Prohibits use of empty blocks. 27 | "nonew" : false, // Prohibits use of constructors for side-effects. 28 | "plusplus" : false, // Prohibits use of `++` & `--`. 29 | "regexp" : true, // Prohibits `.` and `[^...]` in regular expressions. 30 | "trailing" : true, // Prohibits trailing whitespaces. 31 | "undef" : true, // Requires all non-globals be declared before use. 32 | "unused" : "vars", // Warns when you define unused vars (but not fn args) 33 | 34 | // Relaxing Options 35 | // These options allow you to suppress certain types of warnings. 36 | "asi" : true, // Allows statements without semicolons 37 | "boss" : false, // Tolerates assignments inside if, for & while. 38 | "eqnull" : true, // Tolerates use of `== null`. 39 | "evil" : false, // Tolerates use of `eval`. 40 | "expr" : true, // Tolerates `ExpressionStatement` as Programs. 41 | "laxbreak" : false, // Tolerates unsafe line breaks 42 | // eg. `return [\n] x` without semicolons. 43 | "loopfunc" : false, // Allows functions to be defined within loops. 44 | "regexdash" : false, // Tolerates unescaped last dash i.e. `[-...]`. 45 | "scripturl" : true, // Tolerates script-targeted URLs. 46 | "shadow" : false, // Allows re-defined variables later in code 47 | // eg. `var x=1; x=2;`. 48 | "sub" : true, // Tolerates use of subscript notation 49 | // (eg. obj['key'] instead of obj.key). 50 | "supernew" : true, // Tolerate `new function()` and `new Object;`. 51 | "lastsemic" : true // Tolerate missing ; for final statements in blocks 52 | } 53 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.0" 4 | - "0.12" 5 | - "0.10" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 Aaron Franks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/af/JSnoX.png)](http://travis-ci.org/af/JSnoX) 2 | 3 | # JSnoX 4 | 5 | Enjoy [React.js](http://facebook.github.io/react/), but not a fan of the JSX? 6 | JSnoX gives you a concise, expressive way to build ReactElement trees in pure JavaScript. 7 | 8 | 9 | ## Works with 10 | * React.js v0.12 and above 11 | * React Native 12 | 13 | 14 | ## Example 15 | 16 | ```js 17 | var React = require('react') 18 | var MyOtherComponent = require('./some/path.js') 19 | var d = require('jsnox')(React) 20 | 21 | var LoginForm = React.createClass({ 22 | submitLogin: function() { ... }, 23 | 24 | render: function() { 25 | return d('form[method=POST]', { onSubmit: this.submitLogin }, 26 | d('h1.form-header', 'Login'), 27 | d('input:email[name=email]'), 28 | d('input:password[name=pass]'), 29 | d(MyOtherComponent, { myProp: 'foo' }), 30 | d('button:submit', 'Login') 31 | ) 32 | } 33 | }) 34 | ``` 35 | 36 | 37 | ## API 38 | 39 | ```js 40 | // Create a function, d, that parses spec strings into React DOM: 41 | var React = require('react') 42 | var ReactDOM = require('react-dom') 43 | var d = require('jsnox')(React) 44 | 45 | // The function returned by JSnoX takes 3 arguments: 46 | // specString (required) - Specifies the tagName and (optionally) attributes 47 | // props (optional) - Additional props (can override output from specString) 48 | // children (optional) - String, or an array of ReactElements 49 | var myDom = d('div.foo', {}, 'hello') 50 | 51 | ReactDOM.render(myDom, myElement) // renders
hello
52 | ``` 53 | 54 | JSnoX's specStrings let you specify your components' HTML in a way resembling 55 | CSS selectors: 56 | 57 | ![spec strings](docs/jsnox-specstring.png) 58 | 59 | Each property referenced in the string is passed along in the props argument to 60 | `React.createElement()`. You can pass along additional props in the second argument 61 | (a JavaScript object). jsnox will merge the className attribute from both arguments 62 | automatically, useful if the element has a mix of static and dynamic classes. 63 | 64 | 65 | ## Bonus features 66 | 67 | * append a `^` to your specString to have a `key` prop automatically generated 68 | from the spec string. This can help when you have [dynamic 69 | children](https://facebook.github.io/react/docs/multiple-components.html#dynamic-children) 70 | where they all have unique specStrings, eg: 71 | 72 | ```js 73 | render() { 74 | return d('ul', 75 | // The ^ suffix below will give each
  • a unique key: 76 | categories.map(cat => d(`li.category.${cat.id}^`, cat.title)) 77 | ) 78 | } 79 | ``` 80 | 81 | * you can add '@foo' to a specString to point 82 | a [ref](http://facebook.github.io/react/docs/more-about-refs.html) named foo 83 | to that element: 84 | 85 | ```js 86 | // in render(): 87 | return d('input:email@emailAddr') 88 | 89 | // elsewhere in the component (after rendering): 90 | var email = this.refs.emailAddr.value 91 | ``` 92 | 93 | * You can pass a special `$renderIf` prop to your components or DOM elements. 94 | If it evaluates to false, the element won't be rendered: 95 | 96 | ```js 97 | // in render(): 98 | return d('div.debugOutput', { $renderIf: DEV_MODE }, 'hi') 99 | ``` 100 | 101 | 102 | ## Install 103 | 104 | ``` 105 | npm install jsnox 106 | ``` 107 | 108 | Npm is the recommended way to install. You can also include `jsnox.js` in your 109 | project directly and it will fall back to exporting a global variable as 110 | `window.jsnox`. 111 | 112 | 113 | ## Why this instead of JSX? 114 | 115 | * No weird XML dialect in the middle of your JavaScript 116 | * All your existing tooling (linter, minifier, editor, etc) works as it does 117 | with regular JavaScript 118 | * No forced build step 119 | 120 | 121 | ## Why this instead of plain JS with `React.DOM`? 122 | 123 | * More concise code; specify classes/ids/attributes in a way similar to CSS selectors 124 | * Use your custom ReactComponent instances on React 0.12+ without [needing 125 | to wrap them](https://gist.github.com/sebmarkbage/d7bce729f38730399d28) 126 | with `React.createFactory()` everywhere 127 | 128 | 129 | ## Notes/gotchas 130 | 131 | * Your top-level component should also be wrapped by the jsnox client, to 132 | prevent [warnings about `createFactory`](https://gist.github.com/sebmarkbage/ae327f2eda03bf165261). For example: 133 | 134 | ```js 135 | var d = require('jsnox')(React) 136 | 137 | // Good: 138 | React.render(d(MyTopLevelComponent, { prop1: 'foo'}), document.body) 139 | 140 | // Bad (will trigger a warning, and break in future React versions): 141 | React.render(MyTopLevelComponent({ prop1: 'foo'}), document.body) 142 | ``` 143 | 144 | * All attributes you specify should be the ones that React understands. So, for 145 | example, you want to type `'input[readOnly]'` (camel-cased), instead of 146 | `'readonly'` like you'd be used to with html. 147 | * JSnoX gives you a saner default `type` for `button` elements– unless you specify 148 | `'button:submit'` their type will be `"button"` (unintentionally form-submitting 149 | buttons is a personal pet peeve). 150 | 151 | 152 | ## See also 153 | 154 | * [react-hyperscript](https://github.com/mlmorg/react-hyperscript) is a similar 155 | module that converts [hyperscript](https://github.com/dominictarr/hyperscript) 156 | to ReactElements. 157 | * [react-no-jsx](https://github.com/jussi-kalliokoski/react-no-jsx) provides 158 | another way to write plain JS instead of JSX. 159 | * [r-dom](https://github.com/uber/r-dom) is a similar wrapper for `React.DOM` 160 | -------------------------------------------------------------------------------- /docs/jsnox-specstring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/af/JSnoX/95f26f9f5dc90fea3c55aa67a7d3f090b5cc430d/docs/jsnox-specstring.png -------------------------------------------------------------------------------- /jsnox.js: -------------------------------------------------------------------------------- 1 | ;(function(global) { // IIFE for legacy non-module usage 2 | 'use strict' 3 | 4 | var tagNameRegex = /^([a-z1-6]+)(?:\:([a-z]+))?/ // matches 'input' or 'input:text' 5 | var propsRegex = /((?:#|\.|@)[\w-]+)|(\[.*?\])/g // matches all further properties 6 | var attrRegex = /\[([\w-]+)(?:=([^\]]+))?\]/ // matches '[foo=bar]' or '[foo]' 7 | var autoKeyGenRegex = /\^$/ // matches 'anything^' 8 | var protoSlice = Array.prototype.slice 9 | 10 | // Error subclass to throw for parsing errors 11 | function ParseError(input) { 12 | this.message = input 13 | this.stack = new Error().stack 14 | } 15 | ParseError.prototype = Object.create(Error.prototype) 16 | ParseError.prototype.name = 'JSnoXParseError' 17 | 18 | 19 | // A simple module-level cache for parseTagSpec(). 20 | // Subsequent re-parsing of the same input string will be pulled 21 | // from this cache for an increase in performance. 22 | var specCache = {} 23 | 24 | // Convert a tag specification string into an object 25 | // eg. 'input:checkbox#foo.bar[name=asdf]' produces the output: 26 | // { 27 | // tagName: 'input', 28 | // props: { 29 | // type: 'checkbox', 30 | // id: 'foo', 31 | // className: 'bar', 32 | // name: 'asdf' 33 | // } 34 | // } 35 | function parseTagSpec(specString) { 36 | if (!specString || !specString.match) throw new ParseError(specString) 37 | if (specCache[specString]) return specCache[specString] 38 | 39 | // Parse tagName, and optional type attribute 40 | var tagMatch = specString.match(tagNameRegex) 41 | if (!tagMatch) { 42 | if (specString.match(propsRegex)) tagMatch = ['div', 'div'] // create
    if tagname omitted 43 | else throw new ParseError(specString) 44 | } 45 | var tagName = tagMatch[1] 46 | var props = {} 47 | var classes = [] 48 | 49 | if (specString.match(autoKeyGenRegex)) props.key = specString 50 | if (tagMatch[2]) props.type = tagMatch[2] 51 | else if (tagName === 'button') props.type = 'button' // Saner default for ') 62 | 63 | // Test combinations with other properties: 64 | t.equal(render(d('input:checkbox.foo[name=baz]')), 65 | '') 66 | t.equal(render(d('button:submit.foo')), '') 67 | t.end() 68 | }) 69 | 70 | test('Attributes with hyphens are passed through', function(t) { 71 | var expected = '
    hi
    ' 72 | t.equal(render(d('div.foo[data-bar=asdf]', 'hi')), expected) 73 | t.end() 74 | }) 75 | 76 | test('Parses combinations of properties', function(t) { 77 | t.equal(render(d('div#foo.bar.baz')), '
    ') 78 | t.equal(render(d('input#foo.bar.baz[readOnly]')), '') 79 | t.end() 80 | }) 81 | 82 | test('Combines parsed props and values from the props hash', function(t) { 83 | t.equal(render(d('div.foo', { id: 'bar' })), '
    ') 84 | 85 | // Classes should be combined from both sources: 86 | t.equal(render(d('div.foo', { className: 'bar' })), '
    ') 87 | t.equal(render(d('div', { className: 'foo' })), '
    ') 88 | t.equal(render(d('div.foo', {})), '
    ') 89 | 90 | // Edge cases for class combinations: 91 | t.equal(render(d('div.foo', { className: '' })), '
    ') 92 | t.equal(render(d('div.foo', { className: null })), '
    ') 93 | t.end() 94 | }) 95 | 96 | test('Props hash argument is optional', function(t) { 97 | t.equal(render(d('div.foo', 'Shazam')), '
    Shazam
    ') 98 | t.equal(render(d('div', [d('span^', 'hi')])), '
    hi
    ') 99 | t.end() 100 | }) 101 | 102 | test('button elements have default type="button"', function(t) { 103 | t.equal(render(d('button', 'hi')), '') 104 | t.equal(render(d('button.foo.bar', 'hi')), '') 105 | t.end() 106 | }) 107 | 108 | test('Data attributes get passed through as expected', function(t) { 109 | t.equal(render(d('div', {'data-foo': 'bar'}, 'hi')), '
    hi
    ') 110 | 111 | var dataHash = {'data-foo': 'bar', 'data-baz': 'boo'} 112 | t.equal(render(d('div', dataHash, 'hi')), '
    hi
    ') 113 | 114 | t.equal(render(d('div[data-foo=bar]', 'hi')), '
    hi
    ') 115 | t.equal(render(d('div[data-a=1][data-b=2]', 'hi')), '
    hi
    ') 116 | t.end() 117 | }) 118 | 119 | test('invalid input throws ParseError exceptions', function(t) { 120 | var errRegex = new RegExp(d.ParseError.prototype.name) 121 | t.throws(function() { d('-invalid') }, errRegex) 122 | t.throws(function() { d('') }, errRegex) 123 | t.throws(function() { d() }, errRegex) 124 | t.throws(function() { d(null) }, errRegex) 125 | t.throws(function() { d(false) }, errRegex) 126 | //t.throws(function() { d(14) }, errRegex) // This is one case where it won't throw 127 | t.end() 128 | }) 129 | 130 | test('$renderIf', function(t) { 131 | var conditionalTree = function(cond) { 132 | return d('div', d('div', { $renderIf: cond }, 'inside')) 133 | }; 134 | 135 | t.equal(render(conditionalTree(false)), '
    ') 136 | t.equal(render(conditionalTree(true)), '
    inside
    ') 137 | t.equal(render(conditionalTree(null)), '
    ') 138 | t.equal(render(conditionalTree(12)), '
    inside
    ') 139 | t.equal(render(conditionalTree({})), '
    inside
    ') 140 | t.equal(render(conditionalTree()), '
    ') // undefined should be treated as falsy 141 | t.end() 142 | }) 143 | 144 | test('$renderIf value does not pass through to components', function(t) { 145 | var PassThrough = React.createClass({ 146 | render: function() { return d('div', this.props.$renderIf) } 147 | }) 148 | t.equal(render(d('div', d(PassThrough, { $renderIf: 1 }))), '
    ') 149 | t.equal(render(d('div', d(PassThrough, { $renderIf: 0 }))), '
    ') 150 | t.end() 151 | }) 152 | -------------------------------------------------------------------------------- /tests/test_trees.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var React = require('react') 3 | var ReactDOM = require('react-dom/server') 4 | var d = require('..')(React) 5 | 6 | var render = function(domTree) { 7 | return ReactDOM.renderToStaticMarkup(domTree) 8 | } 9 | 10 | var Greeting = React.createClass({ 11 | displayName: 'Greeting', // Used to auto-generate a "key" prop 12 | render: function() { 13 | return React.DOM.div(null, this.props.text || 'hi') 14 | } 15 | }) 16 | 17 | 18 | test('trees of elements render correctly', function(t) { 19 | var tree = d('form.foo', 20 | d('input:email'), 21 | d('input:password'), 22 | d('button:submit', 'Submit') 23 | ) 24 | t.equal(render(tree), '
    ' + 25 | '' + 26 | '
    ') 27 | t.end() 28 | }) 29 | 30 | test('rendering custom components and text nodes', function(t) { 31 | var tree = d('section#stuff', 32 | d('span', 'a greeting:'), 33 | d(Greeting, { text: 'yo' }), 34 | d('span', '!') 35 | ) 36 | t.equal(render(tree), '
    a greeting:
    yo
    !
    ') 37 | t.end() 38 | }) 39 | 40 | test('second arg can be an array of ReactElements', function(t) { 41 | var items = ['one', 'two'] 42 | var tree = d('ul', items.map(function(item) { return d('li.' + item + '^') })) 43 | t.equal(render(tree), '') 44 | t.end() 45 | }) 46 | 47 | test('last arg can be number, will be treated as a string', function(t) { 48 | t.equal(render(d('div', null, 15)), '
    15
    ') 49 | t.equal(render(d('div', 15)), '
    15
    ') 50 | t.equal(render(d('div', -3)), '
    -3
    ') 51 | t.equal(render(d('div', 4.89)), '
    4.89
    ') 52 | t.end() 53 | }) 54 | 55 | test('second arg can be null', function(t) { 56 | var twoArgTree = d('div', null, d('span', 'hi')) 57 | t.equal(render(twoArgTree), '
    hi
    ') 58 | 59 | var threeArgTree = d('div', null, d('span', 'hi'), d('span', 'yo')) 60 | t.equal(render(threeArgTree), '
    hiyo
    ') 61 | 62 | var fourArgTree = d('div', null, d('span', 'hi'), d('span', 'yo'), d('span', 'hey')) 63 | t.equal(render(fourArgTree), '
    hiyohey
    ') 64 | t.end() 65 | }) 66 | 67 | test('second arg can be a ReactElement (no null or {} arg required)', function(t) { 68 | var twoArgTree = d('div', d('span', 'hi')) 69 | t.equal(render(twoArgTree), '
    hi
    ') 70 | 71 | var threeArgTree = d('div', d('span', 'hi'), d('span', 'yo')) 72 | t.equal(render(threeArgTree), '
    hiyo
    ') 73 | 74 | var fourArgTree = d('div', d('span', 'hi'), d('span', 'yo'), d('span', 'hey')) 75 | t.equal(render(fourArgTree), '
    hiyohey
    ') 76 | t.end() 77 | }) 78 | 79 | test('rendering custom components works correctly without props', function(t) { 80 | var tree = d('section#stuff', 81 | d(Greeting), 82 | d('span', '!') 83 | ) 84 | t.equal(render(tree), '
    hi
    !
    ') 85 | t.end() 86 | }) 87 | 88 | test('Readme example', function(t) { 89 | var noop = function() {} 90 | var tree = d('form[method=POST]', { onSubmit: noop }, 91 | d('h1.form-header', 'Login'), 92 | d('input:email[name=email]'), 93 | d('input:password[name=pass]'), 94 | d(Greeting, { text: 'yo' }), 95 | d('button:submit', 'Login') 96 | ) 97 | 98 | var expected = '

    Login

    yo
    ' 99 | t.equal(render(tree), expected) 100 | t.end() 101 | }) 102 | --------------------------------------------------------------------------------