├── .npmignore ├── .gitignore ├── test ├── mocha.opts ├── plugins.coffee ├── helpers │ └── index.coffee ├── text.coffee ├── heredocs.coffee ├── stack_trace.coffee ├── vars.coffee ├── benchmarks │ └── index.coffee ├── custom.coffee ├── selfclosing.coffee ├── attributes.coffee ├── crel.coffee ├── render.coffee ├── nesting.coffee ├── escaping.coffee ├── components.coffee ├── coffeekup_org-sample.coffee └── css_selectors.coffee ├── .travis.yml ├── LICENSE ├── package.json ├── README.md └── src └── teact.coffee /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib 3 | .idea 4 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers coffee:coffee-script/register 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | -------------------------------------------------------------------------------- /test/plugins.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | teact = require '../src/teact' 3 | 4 | describe 'plugins', -> 5 | it 'are applied via use', -> 6 | expect(teact.use).to.be.a 'function' 7 | -------------------------------------------------------------------------------- /test/helpers/index.coffee: -------------------------------------------------------------------------------- 1 | ReactDOM = require 'react-dom/server' 2 | 3 | module.exports = 4 | render: (template, args...) -> 5 | element = template(args...) 6 | if typeof element is 'string' then element 7 | else ReactDOM.renderToStaticMarkup(element) 8 | -------------------------------------------------------------------------------- /test/text.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {text, h1} = require '../src/teact' 3 | {render} = require './helpers' 4 | 5 | describe 'text', -> 6 | it 'renders text verbatim', -> 7 | expect(render -> text 'foobar').to.equal 'foobar' 8 | 9 | it 'renders numbers', -> 10 | expect(render -> text 1).to.equal '1' 11 | expect(render -> text 0).to.equal '0' 12 | -------------------------------------------------------------------------------- /test/heredocs.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {script} = require '../src/teact' 3 | {render} = require './helpers' 4 | 5 | describe 'HereDocs', -> 6 | it 'preserves line breaks', -> 7 | template = -> script """ 8 | $(document).ready(function(){ 9 | alert('test'); 10 | }); 11 | """ 12 | expect(render template).to.equal '' 13 | -------------------------------------------------------------------------------- /test/stack_trace.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {div, p} = require '../src/teact' 3 | {render} = require './helpers' 4 | 5 | describe 'stack trace', -> 6 | it 'should contain crel names', -> 7 | template = -> 8 | div -> 9 | p -> 10 | throw new Error() 11 | try 12 | render template 13 | catch error 14 | expect(error.stack).to.contain 'div' 15 | expect(error.stack).to.contain 'p' 16 | -------------------------------------------------------------------------------- /test/vars.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {h1} = require '../src/teact' 3 | {render} = require './helpers' 4 | 5 | describe 'Context data', -> 6 | it 'is an argument to the template function', -> 7 | template = ({foo}) -> h1 foo 8 | expect(render template, foo: 'bar').to.equal '
'
12 | it 'should throw when passed content', ->
13 | expect(-> render(-> img 'with some text')).to.throwException /must not have content/
14 | describe 'foobar
' 8 | 9 | it 'renders numbers', -> 10 | expect(render -> p 1).to.equal '1
' 11 | expect(render -> p 0).to.equal '0
' 12 | 13 | it "renders undefined as ''", -> 14 | expect(render -> p undefined).to.equal "" 15 | 16 | it 'renders empty tags', -> 17 | template = -> 18 | script src: 'js/app.js' 19 | expect(render template).to.equal('') 20 | 21 | it 'renders text tags as strings', -> 22 | expect(render -> crel.text "Foo").to.equal 'Foo' 23 | 24 | it 'throws on undefined element types', -> 25 | expect(-> crel undefined, className: 'foo').to.throwException /got: undefined/ 26 | -------------------------------------------------------------------------------- /test/render.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {raw, cede, div, p, strong, a} = require '../src/teact' 3 | {render} = require './helpers' 4 | 5 | describe 'render', -> 6 | describe 'nested in a template', -> 7 | it 'returns the nested template without clobbering the parent result', -> 8 | template = -> 9 | p dangerouslySetInnerHTML: __html: "This text could use #{render -> strong -> a href: '/', 'a link'}." 10 | expect(render template).to.equal 'This text could use a link.
' 11 | 12 | it 'doesn\'t modify the attributes object', -> 13 | d = { id: 'foobar', href: 'http://example.com' } 14 | template = -> 15 | p -> 16 | a '.first', d, "link 1" 17 | a d, "link 2" 18 | expect(render template).to.equal '' 19 | -------------------------------------------------------------------------------- /test/nesting.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {div, span, p, pureComponent} = require '../src/teact' 3 | {render} = require './helpers' 4 | 5 | describe 'nesting templates', -> 6 | user = 7 | first: 'Huevo' 8 | last: 'Bueno' 9 | 10 | it 'renders nested template in the same output', -> 11 | 12 | nameHelper = (user) -> 13 | p "#{user.first} #{user.last}" 14 | 15 | template = (user) -> 16 | div -> 17 | nameHelper user 18 | 19 | expect(render template, user).to.equal 'Huevo Bueno
'
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Teact
2 |
3 | It's [better than cjsx](#how-is-this-better-than-cjsx).
4 |
5 | Build React element trees by composing functions.
6 | You get full javascript control flow, and minimal boilerplate.
7 | It's also quite simple, just a thin wrapper around [React.createElement](https://facebook.github.io/react/docs/top-level-api.html#react.createelement) like JSX, making it [fast](#performance) and lightweight (2KB gzipped).
8 |
9 | [](https://travis-ci.org/hurrymaplelad/teact)
10 | [](https://www.npmjs.org/package/teact)
11 |
12 | ## Usage
13 | ```coffee
14 | {crel} = require 'teact'
15 |
16 | crel 'div', '#root.container', ->
17 | unless props.signedIn
18 | crel 'button', onClick: handleOnClick, 'Sign In'
19 | crel.text 'Welcome!'
20 | ```
21 |
22 | Transforms into:
23 |
24 | ```coffee
25 | React.createElement('div',
26 | {id: root, className: 'container'}, [
27 | (props.signedIn ? React.createElement('button',
28 | {onClick: handleOnClick}, 'Sign In'
29 | ) : null)
30 | 'Welcome!'
31 | ]
32 | )
33 | ```
34 |
35 | Use it from your component's render method:
36 | ```coffee
37 | {Component} = require 'react'
38 | {crel} = require 'teact'
39 |
40 | class Widget extends Component
41 | render: ->
42 | crel 'div', className: 'foo', =>
43 | crel 'div', 'bar'
44 | ```
45 |
46 | Or in a [stateless component](https://facebook.github.io/react/docs/reusable-components.html#stateless-functions):
47 |
48 | ```coffee
49 | module.exports = (props) ->
50 | crel 'div', className: 'foo', ->
51 | crel 'div', props.bar
52 | ```
53 |
54 | ### Nesting Components
55 |
56 | `crel` is just a thin wrapper around [React.createElement](https://facebook.github.io/react/docs/top-level-api.html#react.createelement),
57 | so you can pass it components instead of crel names:
58 |
59 | ```coffee
60 | class DooDad extends Component
61 | render: ->
62 | crel 'div', className: 'doodad', =>
63 | crel 'span', @props.children
64 |
65 | class Widget extends Component
66 | handleFiddle: =>
67 | # ...
68 |
69 | render: ->
70 | crel 'div', className: 'foo', =>
71 | crel DooDad, onFiddled: @handleFiddle, =>
72 | crel 'div', "I'm passed to DooDad.props.children"
73 |
74 | ```
75 |
76 | If you need to build up a tree of elements inside a component's render method, you can
77 | escape the element stack via the `pureComponent` helper:
78 |
79 | ```coffee
80 | {crel, pureComponent} = require 'teact'
81 |
82 | Teas = pureComponent (teas) ->
83 | teas.map (tea) ->
84 | # Without pureComponent, this would add teas to the element tree
85 | # in iteration order. With pureComponent, we just return the reversed list
86 | # of divs without adding the element tree. The caller may choose to add
87 | # the returned list.
88 | crel 'div', tea
89 | .reverse()
90 |
91 | class Widget extends Component
92 | render: ->
93 | crel 'div', Teas(@props.teas)
94 | ```
95 |
96 | ### Sugar Syntax
97 | Teact exports bound functions for elements, giving you options for
98 | terser syntax if you're into that:
99 |
100 | ```coffee
101 | T = require 'teact'
102 |
103 | T.div className: 'foo', ->
104 | T.text 'Blah!'
105 | ```
106 |
107 | or the Teacup / CoffeeCup signatures:
108 |
109 | ```coffee
110 | {div, text} = require 'teact'
111 |
112 | div '.foo', ->
113 | text 'Blah!'
114 | ```
115 |
116 | ## Performance
117 |
118 | A [super-basic performance test](test/benchmarks/index.coffee) suggests that teact has no discernible impact on React rendering performance:
119 |
120 | ```sh
121 | $ npm run benchmark
122 |
123 | > native x 5,197 ops/sec ±3.30% (76 runs sampled)
124 | > teact x 5,339 ops/sec ±2.23% (82 runs sampled)
125 | > Fastest is teact,native
126 | ```
127 |
128 | It's also lightweight, at 5KB minified, 2KB gzipped.
129 |
130 | ## How is this better than CJSX?
131 |
132 | - Familiar control flow with branching and loops. See examples above.
133 | - No transpiler to [maintain](https://github.com/jsdf/coffee-react/issues/28).
134 | - No [extraneous enclosing tags](https://babeljs.io/repl/#?experimental=false&evaluate=true&loose=false&spec=false&code=%3Cdiv%3E%3C%2Fdiv%3E%0A%3Cdiv%3E%3C%2Fdiv%3E) required:
135 |
136 | ```coffee
137 | renderHeader: ->
138 | unless @props.signedIn
139 | crel 'a', href: '...', 'Sign in'
140 | crel 'h1', 'Tea Shop'
141 | ```
142 |
143 | Just works.
144 | - [Nice syntax errors](https://github.com/jsdf/coffee-react/issues/32).
145 | - Half the lines of code. Those closing tags really add up.
146 |
147 | Other folks have [reached similar conclusions](https://slack-files.com/T024L9M0Y-F02HP4JM3-80d714). They were later [bit by using the React API directly](https://github.com/planningcenter/react-patterns#jsx) when the implementation changed. A thin wrapper like Teact should minimize this risk.
148 |
149 | ## Legacy
150 |
151 | [Markaby](http://github.com/markaby/markaby) begat [CoffeeKup](http://github.com/mauricemach/coffeekup) begat
152 | [CoffeeCup](http://github.com/gradus/coffeecup) and [DryKup](http://github.com/mark-hahn/drykup) which begat
153 | [Teacup](http://github.com/goodeggs/teacup) which begat **Teact**.
154 |
155 | ## Contributing
156 |
157 | ```sh
158 | $ git clone https://github.com/hurrymaplad/teact && cd teact
159 | $ npm install
160 | $ npm test
161 | ```
162 |
--------------------------------------------------------------------------------
/src/teact.coffee:
--------------------------------------------------------------------------------
1 | React = require 'react'
2 |
3 | elements =
4 | # Valid HTML 5 elements requiring a closing crel.
5 | # Note: the `var` element is out for obvious reasons, please use `crel 'var'`.
6 | regular: 'a abbr address article aside audio b bdi bdo blockquote body button
7 | canvas caption cite code colgroup datalist dd del details dfn div dl dt em
8 | fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup
9 | html i iframe ins kbd label legend li map mark menu meter nav noscript object
10 | ol optgroup option output p pre progress q rp rt ruby s samp script section
11 | select small span strong sub summary sup table tbody td textarea tfoot
12 | th thead time title tr u ul video'
13 |
14 | # Valid self-closing HTML 5 elements.
15 | void: 'area base br col command embed hr img input keygen link meta param
16 | source track wbr'
17 |
18 | obsolete: 'applet acronym bgsound dir frameset noframes isindex listing
19 | nextid noembed plaintext rb strike xmp big blink center font marquee multicol
20 | nobr spacer tt'
21 |
22 | obsolete_void: 'basefont frame'
23 |
24 | # Create a unique list of element names merging the desired groups.
25 | merge_elements = (args...) ->
26 | result = []
27 | for a in args
28 | for element in elements[a].split ' '
29 | result.push element unless element in result
30 | result
31 |
32 |
33 | class Teact
34 | constructor: ->
35 | @stack = null
36 |
37 | resetStack: (stack=null) ->
38 | previous = @stack
39 | @stack = stack
40 | return previous
41 |
42 | isSelector: (string) ->
43 | string.length > 1 and string.charAt(0) in ['#', '.']
44 |
45 | parseSelector: (selector) ->
46 | id = null
47 | classes = []
48 | for token in selector.split '.'
49 | token = token.trim()
50 | if id
51 | classes.push token
52 | else
53 | [klass, id] = token.split '#'
54 | classes.push token unless klass is ''
55 | return {id, classes}
56 |
57 | normalizeArgs: (args) ->
58 | attrs = {}
59 | selector = null
60 | contents = null
61 |
62 | for arg, index in args when arg?
63 | switch typeof arg
64 | when 'string'
65 | if index is 0 and @isSelector(arg)
66 | selector = arg
67 | parsedSelector = @parseSelector(arg)
68 | else
69 | contents = arg
70 | when 'function', 'number', 'boolean'
71 | contents = arg
72 | when 'object'
73 | if arg.constructor == Object and not React.isValidElement arg
74 | attrs = Object.keys(arg).reduce(
75 | (clone, key) -> clone[key] = arg[key]; clone
76 | {}
77 | )
78 | else
79 | contents = arg
80 | else
81 | contents = arg
82 |
83 | if parsedSelector?
84 | {id, classes} = parsedSelector
85 | attrs.id = id if id?
86 | if classes?.length
87 | if attrs.className
88 | classes.push attrs.className
89 | attrs.className = classes.join(' ')
90 |
91 | # Expand data attributes
92 | dataAttrs = attrs.data
93 | if typeof dataAttrs is 'object'
94 | delete attrs.data
95 | for k, v of dataAttrs
96 | attrs["data-#{k}"] = v
97 |
98 | return {attrs, contents, selector}
99 |
100 | crel: (tagName, args...) ->
101 | unless tagName?
102 | throw new Error "Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: #{tagName}"
103 | {attrs, contents} = @normalizeArgs args
104 |
105 | switch typeof contents
106 | when 'function'
107 | previous = @resetStack []
108 | contents()
109 | children = @resetStack previous
110 | else
111 | children = contents
112 |
113 | if children?.splice
114 | el = React.createElement tagName, attrs, children...
115 | else
116 | el = React.createElement tagName, attrs, children
117 |
118 | @stack?.push el
119 | return el
120 |
121 | pureComponent: (contents) ->
122 | teact = @
123 | return ->
124 | previous = teact.resetStack null
125 | children = contents.apply teact, arguments
126 | teact.resetStack previous
127 | return children
128 |
129 | selfClosingTag: (tagName, args...) ->
130 | {attrs, contents} = @normalizeArgs args
131 | if contents
132 | throw new Error "Teact: <#{tagName}/> must not have content. Attempted to nest #{contents}"
133 | @crel tagName, attrs
134 |
135 | text: (s) ->
136 | return s unless s?.toString
137 | @stack?.push(s.toString())
138 | return s.toString()
139 |
140 | #
141 | # Plugins
142 | #
143 | use: (plugin) ->
144 | plugin @
145 |
146 | #
147 | # Binding
148 | #
149 | tags: ->
150 | bound = {}
151 |
152 | boundMethodNames = [].concat(
153 | 'ie normalizeArgs script crel pureComponent text use'.split(' ')
154 | merge_elements 'regular', 'obsolete', 'void', 'obsolete_void'
155 | )
156 | for method in boundMethodNames
157 | do (method) =>
158 | bound[method] = (args...) => @[method].apply @, args
159 |
160 | bound.crel.text = bound.text
161 | return bound
162 |
163 | for tagName in merge_elements 'regular', 'obsolete'
164 | do (tagName) ->
165 | Teact::[tagName] = (args...) -> @crel tagName, args...
166 |
167 | for tagName in merge_elements 'void', 'obsolete_void'
168 | do (tagName) ->
169 | Teact::[tagName] = (args...) -> @selfClosingTag tagName, args...
170 |
171 | if module?.exports
172 | module.exports = new Teact().tags()
173 | module.exports.Teact = Teact
174 |
--------------------------------------------------------------------------------