├── .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 '

bar

' 9 | 10 | describe 'Local vars', -> 11 | it 'are in the template function closure', -> 12 | obj = {foo: 'bar'} 13 | template = -> h1 "dynamic: #{obj.foo}" 14 | expect(render template).to.equal '

dynamic: bar

' 15 | obj.foo = 'baz' 16 | expect(render template).to.equal '

dynamic: baz

' 17 | -------------------------------------------------------------------------------- /test/benchmarks/index.coffee: -------------------------------------------------------------------------------- 1 | {Suite} = require 'benchmark' 2 | React = require 'react' 3 | {crel} = require '../../src/teact' 4 | {render} = require '../helpers' 5 | 6 | new Suite() 7 | .add 'native', -> 8 | render -> 9 | React.createElement('div', {className: 'foo'}, 10 | React.createElement 'div', {className: 'bar'} 11 | ) 12 | 13 | .add 'teact', -> 14 | render -> 15 | crel 'div', '.foo', -> 16 | crel 'div', '.bar' 17 | 18 | .on 'cycle', (event) -> 19 | console.log String event.target 20 | 21 | .on 'complete', -> 22 | console.log "Fastest is #{@filter('fastest').pluck('name')}" 23 | 24 | .run async: true 25 | -------------------------------------------------------------------------------- /test/custom.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {crel, input, normalizeArgs} = require '../src/teact' 3 | {render} = require './helpers' 4 | 5 | describe 'custom crel', -> 6 | it 'should render', -> 7 | template = -> crel 'custom' 8 | expect(render template).to.equal '' 9 | it 'should render empty given null content', -> 10 | template = -> crel 'custom', null 11 | expect(render template).to.equal '' 12 | it 'should render with attributes', -> 13 | template = -> crel 'custom', id: 'bar' 14 | expect(render template).to.equal '' 15 | it 'should render with attributes and content', -> 16 | template = -> crel 'custom', id: 'bar', 'zag' 17 | expect(render template).to.equal 'zag' 18 | -------------------------------------------------------------------------------- /test/selfclosing.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {img, br, link} = require '../src/teact' 3 | {render} = require './helpers' 4 | 5 | describe 'Self Closing Tags', -> 6 | describe '', -> 7 | it 'should render', -> 8 | expect(render img).to.equal '' 9 | it 'should render with attributes', -> 10 | expect(render -> img src: 'http://foo.jpg.to') 11 | .to.equal '' 12 | it 'should throw when passed content', -> 13 | expect(-> render(-> img 'with some text')).to.throwException /must not have content/ 14 | describe '
', -> 15 | it 'should render', -> 16 | expect(render br).to.equal '
' 17 | describe '', -> 18 | it 'should render with attributes', -> 19 | expect(render -> link href: '/foo.css', rel: 'stylesheet') 20 | .to.equal '' 21 | -------------------------------------------------------------------------------- /test/attributes.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {a, br, div} = require '../src/teact' 3 | {render} = require './helpers' 4 | 5 | describe 'Attributes', -> 6 | 7 | describe 'with a hash', -> 8 | it 'renders the corresponding HTML attributes', -> 9 | template = -> a href: '/', title: 'Home' 10 | expect(render template).to.equal '' 11 | 12 | describe 'data attribute', -> 13 | it 'expands attributes', -> 14 | template = -> br data: { name: 'Name', value: 'Value' } 15 | expect(render template).to.equal '
' 16 | 17 | describe 'nested hyphenated attribute', -> 18 | it 'renders', -> 19 | template = -> 20 | div 'data-on-x': 'beep', -> 21 | div 'data-on-y': 'boop' 22 | expect(render template).to.equal '
' 23 | -------------------------------------------------------------------------------- /test/crel.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {crel, p, div, script} = require '../src/teact' 3 | {render} = require './helpers' 4 | 5 | describe 'crel', -> 6 | it 'renders text verbatim', -> 7 | expect(render -> p 'foobar').to.equal '

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 '

link 1link 2

' 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

' 20 | 21 | describe 'pureComponent', -> 22 | it 'returns components without adding them to the parent stack', -> 23 | 24 | nameHelper = pureComponent (user) -> 25 | [ 26 | span user.first 27 | span user.last 28 | ].reverse() 29 | 30 | template = (user) -> 31 | div nameHelper(user) 32 | 33 | expect(render template, user).to.equal '
BuenoHuevo
' 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012 Good Eggs, Inc. 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 | 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teact", 3 | "version": "1.3.0", 4 | "description": "Generate React elements with CoffeeScript functions", 5 | "homepage": "http://github.com/hurrymaplelad/teact", 6 | "license": "MIT", 7 | "main": "lib/teact", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/hurrymaplelad/teact.git" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "cjsx", 15 | "coffeescript", 16 | "coffee-script", 17 | "teacup", 18 | "coffee", 19 | "template", 20 | "render", 21 | "view", 22 | "html", 23 | "coffeekup", 24 | "coffeecup", 25 | "drykup", 26 | "express" 27 | ], 28 | "devDependencies": { 29 | "benchmark": "^1.0.0", 30 | "coffee-script": ">=1.8.0", 31 | "expect.js": "^0.3.1", 32 | "mocha": "^2.3.3", 33 | "react-dom": "^15.4.1", 34 | "react": "^15.4.1" 35 | }, 36 | "scripts": { 37 | "pretest": "npm run compile", 38 | "prepublish": "npm run compile", 39 | "test": "mocha", 40 | "compile": "coffee --compile --output lib/ src/", 41 | "benchmark": "coffee test/benchmarks" 42 | }, 43 | "peerDependencies": { 44 | "react": "^15.4.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/escaping.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {raw, script, escape, h1, input} = require '../src/teact' 3 | {render} = require './helpers' 4 | 5 | describe 'Auto escaping', -> 6 | describe 'a script crel', -> 7 | it "adds HTML entities for sensitive characters", -> 8 | template = -> h1 "" 9 | expect(render template).to.equal "

<script>alert('"owned" by c&a &copy;')</script>

" 10 | 11 | it 'escapes crel attributes', -> 12 | template = -> input name: '"pwned' 13 | expect(render template).to.equal '' 14 | 15 | it 'escapea single quotes in crel attributes', -> 16 | template = -> input name: "'pwned" 17 | expect(render template).to.equal '' 18 | 19 | describe 'script crel', -> 20 | it 'escapes /', -> 21 | user = name: '' 22 | template = -> 23 | script "window.user = #{JSON.stringify user}" 24 | 25 | expect(render template).to.equal '' 26 | -------------------------------------------------------------------------------- /test/components.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {crel, p, div, script} = require '../src/teact' 3 | {render} = require './helpers' 4 | {Component} = require 'react' 5 | 6 | class DooDad extends Component 7 | render: -> 8 | props = @props 9 | crel 'div', className: 'doodad', -> 10 | crel.text props.label 11 | crel 'span', props.children 12 | 13 | class Widget extends Component 14 | render: -> 15 | crel 'div', className: 'foo', -> 16 | crel DooDad, label: 'Doo', -> 17 | crel.text "I'm passed to DooDad.props.children" 18 | 19 | describe 'components', -> 20 | it 'render with crel', -> 21 | expect(render -> 22 | crel DooDad, label: 'Boo' 23 | ).to.equal '
Boo
' 24 | 25 | describe 'nesting components', -> 26 | it 'supports a single child', -> 27 | expect(render -> 28 | crel Widget 29 | ).to.equal '
DooI'm passed to DooDad.props.children
' 30 | 31 | it 'supports a multipl children', -> 32 | expect(render -> 33 | crel DooDad, label: 'A', -> 34 | crel DooDad, label: 'B' 35 | crel DooDad, label: 'C' 36 | ).to.equal '
A
B
C
' 37 | -------------------------------------------------------------------------------- /test/coffeekup_org-sample.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {html, head, meta, link, style, title, script, body, 3 | coffeescript, header, section, nav, footer, h1, h2, ul, li, a, p} = require '../src/teact' 4 | {render} = require './helpers' 5 | 6 | describe 'coffeekup.org example', -> 7 | 8 | it 'works', -> 9 | x = title: 'Foo' 10 | path = '/zig' 11 | user = {} 12 | max = 12 13 | shoutify = (s) -> s.toUpperCase() + '!' 14 | 15 | template = -> 16 | html -> 17 | head -> 18 | meta charSet: 'utf-8' 19 | title "#{x.title or 'Untitled'} | My awesome website" 20 | meta(name: 'description', content: desc) if desc? 21 | link rel: 'stylesheet', href: '/stylesheets/app.css' 22 | script src: '/javascripts/jquery.js' 23 | body -> 24 | header -> 25 | h1 x.title or 'Untitled' 26 | nav -> 27 | ul -> 28 | (li -> a href: '/', 'Home') unless path is '/' 29 | li -> a href: '/chunky', 'Bacon!' 30 | switch user.role 31 | when 'owner', 'admin' 32 | li -> a href: '/admin', 'Secret Stuff' 33 | when 'vip' 34 | li -> a href: '/vip', 'Exclusive Stuff' 35 | else 36 | li -> a href: '/commoners', 'Just Stuff' 37 | section -> 38 | h2 "Let's count to #{max}:" 39 | p i for i in [1..max] 40 | footer -> 41 | p shoutify('bye') 42 | 43 | expect(render template).to.contain 'Just Stuff' 44 | -------------------------------------------------------------------------------- /test/css_selectors.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | {div, img} = require '../src/teact' 3 | {render} = require './helpers' 4 | 5 | describe 'CSS Selectors', -> 6 | describe 'id selector', -> 7 | it 'sets the id attribute', -> 8 | template = -> div '#myid', 'foo' 9 | expect(render template).to.equal '
foo
' 10 | 11 | it 'must be greater than length 1', -> 12 | template = -> div '#' 13 | expect(render template).to.equal '
#
' 14 | 15 | describe 'one class selector', -> 16 | it 'adds an html class', -> 17 | template = -> div '.myclass', 'foo' 18 | expect(render template).to.equal '
foo
' 19 | 20 | describe 'and a class attribute', -> 21 | it 'prepends the selector class', -> 22 | template = -> div '.myclass', 'className': 'myattrclass', 'foo' 23 | expect(render template).to.equal '
foo
' 24 | 25 | describe 'multi-class selector', -> 26 | it 'adds all the classes', -> 27 | template = -> div '.myclass.myclass2.myclass3', 'foo' 28 | expect(render template).to.equal '
foo
' 29 | 30 | describe 'with an id and classes, separated by spaces', -> 31 | it 'adds ids and classes with minimal whitespace', -> 32 | template = -> div '#myid.myclass1 .myclass2 ' 33 | expect(render template).to.equal '
' 34 | 35 | describe 'without contents', -> 36 | it 'still adds attributes', -> 37 | template = -> img '#myid.myclass', src: '/pic.png' 38 | expect(render template).to.equal '' 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 | [![Build Status](http://img.shields.io/travis/hurrymaplelad/teact.svg?style=flat-square)](https://travis-ci.org/hurrymaplelad/teact) 10 | [![NPM version](http://img.shields.io/npm/v/teact.svg?style=flat-square)](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 | --------------------------------------------------------------------------------