├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── changelog.md ├── components.ts ├── index.d.ts ├── jsx-control-statements.flow.js ├── package.json ├── spec ├── fixtures │ ├── basic │ │ └── without-any-control-statements.jsx │ ├── choose │ │ ├── choose-empty.jsx │ │ ├── choose-with-otherwise.jsx │ │ ├── choose.jsx │ │ ├── chooses-within-if.jsx │ │ ├── nested-choose-no-inner-component.jsx │ │ └── nested-choose.jsx │ ├── errors │ │ ├── choose-with-multiple-otherwise.jsx │ │ ├── choose-with-no-children.jsx │ │ ├── choose-with-no-when.jsx │ │ ├── choose-with-otherwise-not-last.jsx │ │ ├── choose-with-wrong-children.jsx │ │ ├── else-with-no-children.jsx │ │ ├── for-with-no-attributes.jsx │ │ ├── for-with-no-of.jsx │ │ ├── for-with-non-expression-of.jsx │ │ ├── for-with-non-string-each.jsx │ │ ├── for-with-non-string-index.jsx │ │ ├── if-with-no-condition.jsx │ │ ├── if-with-non-expression-condition.jsx │ │ ├── when-with-no-condition.jsx │ │ └── when-with-non-expression-condition.jsx │ ├── extension │ │ ├── choose-with-multiple-children.jsx │ │ ├── for-with-expression-container.jsx │ │ ├── for-with-multiple-children.jsx │ │ ├── if-with-expression-container.jsx │ │ ├── if-with-multiple-children-nested.jsx │ │ ├── if-with-multiple-children.jsx │ │ └── if-with-string-literal.jsx │ ├── for │ │ ├── for-backwards-attributes.jsx │ │ ├── for-empty.jsx │ │ ├── for-tsx-syntax.jsx │ │ ├── for-with-index-without-each.jsx │ │ ├── for-with-index.jsx │ │ ├── for-without-each.jsx │ │ ├── for.jsx │ │ ├── nested-for-with-indexes.jsx │ │ └── nested-for.jsx │ ├── if │ │ ├── if-empty.jsx │ │ ├── if-with-else.jsx │ │ ├── if.jsx │ │ └── nested-if.jsx │ ├── mixed │ │ ├── for-inside-if.jsx │ │ └── if-inside-for.jsx │ └── with │ │ ├── with-element-child.jsx │ │ ├── with-empty-content.jsx │ │ ├── with-expression-attribute.jsx │ │ ├── with-multiple-attributes.jsx │ │ ├── with-multiple-children.jsx │ │ ├── with-nested-shadowed-restored.jsx │ │ ├── with-nested-shadowed.jsx │ │ ├── with-nested.jsx │ │ ├── with-no-attributes.jsx │ │ ├── with-outer-shadowed-restored.jsx │ │ ├── with-outer-shadowed.jsx │ │ ├── with-outer-this.jsx │ │ ├── with-outer.jsx │ │ ├── with-single-attribute.jsx │ │ ├── with-string-attribute.jsx │ │ ├── with-text-child.jsx │ │ ├── with-toplevel-component.jsx │ │ └── with-unused-attribute.jsx ├── test │ ├── basic.js │ ├── choose.js │ ├── error.js │ ├── extension.js │ ├── for.js │ ├── if.js │ ├── mixed.js │ └── with.js ├── testUtil.js └── tests.js ├── src ├── chooseStatement.js ├── forStatement.js ├── ifStatement.js ├── index.js ├── util │ ├── ast.js │ ├── conditional.js │ └── error.js └── withStatement.js ├── wallaby.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | [*] 7 | 8 | # Change these settings to your own preference 9 | indent_style = space 10 | indent_size = 2 11 | 12 | # We recommend you to keep these unchanged 13 | end_of_line = lf 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | 18 | 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb/legacy", "plugin:react/recommended", "plugin:jsx-control-statements/recommended"], 3 | "plugins": [ 4 | "react", 5 | "jsx-control-statements" 6 | ], 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | } 12 | }, 13 | "rules": { 14 | "brace-style": [2, "stroustrup"], 15 | "quotes": [2, "double"], 16 | "jsx-quotes": [2, "prefer-double"], 17 | "new-cap": 0, 18 | "func-names": 0, 19 | "no-param-reassign": 0, 20 | "space-before-function-paren": [2, "never"], 21 | "max-len": [1, 120], 22 | "consistent-return": 0, 23 | "vars-on-top": 0, 24 | "object-curly-spacing": 0, 25 | "global-require": 0, 26 | "react/display-name": 0, 27 | "react/prop-types": 0, 28 | "no-else-return": 0, 29 | "no-plusplus": 0, 30 | "no-multi-assign": 0, 31 | "operator-linebreak": 0 32 | }, 33 | "env": { 34 | "mocha": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 31 | 32 | *.iml 33 | 34 | ## Directory-based project format: 35 | .idea/ 36 | .vscode/ 37 | # if you remove the above rule, at least ignore the following: 38 | 39 | # User-specific stuff: 40 | # .idea/workspace.xml 41 | # .idea/tasks.xml 42 | # .idea/dictionaries 43 | 44 | # Sensitive or high-churn files: 45 | # .idea/dataSources.ids 46 | # .idea/dataSources.xml 47 | # .idea/sqlDataSources.xml 48 | # .idea/dynamic.xml 49 | # .idea/uiDesigner.xml 50 | 51 | # Gradle: 52 | # .idea/gradle.xml 53 | # .idea/libraries 54 | 55 | # Mongo Explorer plugin: 56 | # .idea/mongoSettings.xml 57 | 58 | ## File-based project format: 59 | *.ipr 60 | *.iws 61 | 62 | ## Plugin-specific files: 63 | 64 | # IntelliJ 65 | out/ 66 | 67 | # mpeltonen/sbt-idea plugin 68 | .idea_modules/ 69 | 70 | # JIRA plugin 71 | atlassian-ide-plugin.xml 72 | 73 | # Crashlytics plugin (for Android Studio and IntelliJ) 74 | com_crashlytics_export_strings.xml 75 | crashlytics.properties 76 | crashlytics-build.properties -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | spec 2 | .editorconfig 3 | .travis.yml 4 | CONTRIBUTING.md 5 | 6 | ### GITIGNORE FOLLOWS ### 7 | 8 | # Logs 9 | logs 10 | *.log 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | # Commenting this out is preferred by some people, see 31 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 32 | node_modules 33 | 34 | # Users Environment Variables 35 | .lock-wscript 36 | 37 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 38 | 39 | *.iml 40 | 41 | ## Directory-based project format: 42 | .idea/ 43 | # if you remove the above rule, at least ignore the following: 44 | 45 | # User-specific stuff: 46 | # .idea/workspace.xml 47 | # .idea/tasks.xml 48 | # .idea/dictionaries 49 | 50 | # Sensitive or high-churn files: 51 | # .idea/dataSources.ids 52 | # .idea/dataSources.xml 53 | # .idea/sqlDataSources.xml 54 | # .idea/dynamic.xml 55 | # .idea/uiDesigner.xml 56 | 57 | # Gradle: 58 | # .idea/gradle.xml 59 | # .idea/libraries 60 | 61 | # Mongo Explorer plugin: 62 | # .idea/mongoSettings.xml 63 | 64 | ## File-based project format: 65 | *.ipr 66 | *.iws 67 | 68 | ## Plugin-specific files: 69 | 70 | # IntelliJ 71 | out/ 72 | 73 | # mpeltonen/sbt-idea plugin 74 | .idea_modules/ 75 | 76 | # JIRA plugin 77 | atlassian-ide-plugin.xml 78 | 79 | # Crashlytics plugin (for Android Studio and IntelliJ) 80 | com_crashlytics_export_strings.xml 81 | crashlytics.properties 82 | crashlytics-build.properties 83 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: stable 3 | script: 4 | - npm run lint && npm run cover 5 | after_script: "cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js" 6 | notifications: 7 | email: 8 | on_success: never 9 | on_failure: change 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to JSX Control Statements 2 | 3 | JSX Control Statements loves pull requests, and it's benefited from the collaborative effort of several different individuals so far. Remember - the quickest way to get your feature request done or bug fixed is to do it yourself and submit a PR :). 4 | 5 | ## Building 6 | 7 | Should be as easy as: 8 | ``` 9 | git clone git@github.com:AlexGilleran/jsx-control-statements.git 10 | cd jsx-control-statements 11 | npm install 12 | ``` 13 | 14 | ## Testing 15 | ``` 16 | # To Run Tests: 17 | npm run test 18 | 19 | # To Run Coverage: 20 | npm run cover 21 | ``` 22 | 23 | ## Using Your Local Version With Your Own React Project 24 | It's recommended, particularly if you've included a new feature, that you check that it works the way you expect against an actual React project. To use the version of jsx-control-statements that you're working on instead of the one in npm: 25 | 26 | ``` 27 | # (in jsx-control-statements directory) 28 | npm link 29 | 30 | # (in the root of your project, at the level of package.json) 31 | npm link jsx-control-statements 32 | ``` 33 | 34 | ## PR Submission Checklist 35 | Before submitting a PR, make sure that you: 36 | - Raise an issue for whatever bug or feature you're working on if none exists already - there might be extra detail we can give you on how to implement/fix it, or it might be better if it works slightly differently etc. etc. - it's better that we have a discussion *before* you waste your time writing code. 37 | - Leave a comment to say that you're working on it, so multiple people aren't working on it at the same time. If the issue is already assigned to someone else, pick another one :). 38 | - WRITE TESTS. Unless there's some really good reason why your PR doesn't need tests (like you've fixed a spelling mistake), tests are super super mandatory. 39 | - When you submit your PR, make sure you leave a comment explaining what you did, and tag the issue # that it relates to. 40 | 41 | Happy Hacking :). 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Alex Gilleran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSX Control Statements 2 | 3 | [![Build Status](https://travis-ci.org/AlexGilleran/jsx-control-statements.svg?branch=master)](https://travis-ci.org/AlexGilleran/jsx-control-statements) [![Coverage Status](https://coveralls.io/repos/AlexGilleran/jsx-control-statements/badge.svg?branch=master&service=github)](https://coveralls.io/github/AlexGilleran/jsx-control-statements?branch=master) [![npm version](https://img.shields.io/npm/v/babel-plugin-jsx-control-statements.svg?style=flat)](https://www.npmjs.com/package/babel-plugin-jsx-control-statements) 4 | 5 | _JSX-Control-Statements_ is a Babel plugin that extends JSX to add basic control statements: **conditionals** and **loops**. 6 | It does so by transforming component-like control statements to their JavaScript counterparts - e.g. `Hello World!` becomes `condition() ? 'Hello World!' : null`. 7 | 8 | Developers coming to React from using JavaScript templating libraries like Handlebars are often surprised that there's no built-in looping or conditional syntax. This is by design - JSX is not a templating library, it's declarative syntactic sugar over functional JavaScript expressions. JSX Control Statements follows the same principle - it provides a component-like syntax that keeps your `render` functions neat and readable, but desugars into clean, readable JavaScript. 9 | 10 | The only dependency _JSX-Control-Statements_ relies upon is _Babel_. It is compatible with React and React Native. 11 | 12 | :skull_and_crossbones: Beware: This is a Babel plugin. It changes your code to other code - this means that some tooling that looks at your code (e.g. static analysis, typescript) is likely to not work. This plugin dates back to when JSX was daring and Javascript was more like playdough than meccano - if you want to stay on the well-trodden path stick with writing `&&` and `map`. 13 | 14 | ## Table of Contents 15 | 16 | - [A Note on Transformation and Alternative Solutions](#a-note-on-transformation-and-alternative-solutions) 17 | - [Installation](#installation) 18 | - [Syntax](#syntax) 19 | - [If Tag](#if-tag) 20 | - [``](#if) 21 | - [`` (deprecated)](#else) 22 | - [Transformation](#transformation-1) 23 | - [Choose Tag](#choose-tag) 24 | - [``](#choose) 25 | - [``](#when) 26 | - [``](#otherwise>) 27 | - [Transformation](#transformation-2) 28 | - [For Tag](#for-tag) 29 | - [Transformation](#transformation-3) 30 | - [With Tag](#with-tag) 31 | - [Transformation](#transformation-4) 32 | - [Linting](#linting) 33 | - [ESLint](#eslint) 34 | - [FlowType](#flowtype) 35 | - [Alternative Solutions](#alternative-solutions) 36 | - [Pure JavaScript](#pure-javascript) 37 | - [Conditionals](#conditionals) 38 | - [Loops](#loops) 39 | - [Comparison](#comparison) 40 | - [React Components](#react-components) 41 | - [What about Typescript](#what-about-typescript) 42 | - [Major Versions](#major-versions) 43 | - [I Want to Contribute!](#i-want-to-contribute) 44 | 45 | ### A Note on Transformation and Alternative Solutions 46 | 47 | It appears to be pretty easy to implement **conditionals as React component**, which is underlined by the amount 48 | of libraries which have taken this approach. However, all of them suffer from the same major caveat: A React component 49 | will always evaluate all of its properties including the component body. Hence the following example will fail for 50 | those libraries: 51 | 52 | ```javascript 53 | {item.title} 54 | ``` 55 | 56 | The error will be "Cannot read property 'title' of undefined", because React will evaluate the body of the custom 57 | component and pass it as "children" property to it. The only workaround is to force React into lazy evaluation by 58 | wrapping the statement in a function. 59 | 60 | This is the reason why conditionals must be implemented in pure JS. _JSX-Control-Statements_ only adds the 61 | syntactic sugar to write conditionals as component, while it transforms this "component" to a pure JS expression. 62 | 63 | See [Alternative Solutions](#alternative-solutions) for a more detailed comparison and pure JS solutions. 64 | 65 | ## Installation 66 | 67 | As a prerequisite you need to have [Babel](https://github.com/babel/babel) installed and configured in your project. 68 | 69 | Install via npm: 70 | 71 | ``` 72 | npm install --save-dev babel-plugin-jsx-control-statements 73 | ``` 74 | 75 | Then you only need to specify _JSX-Control-Statements_ as Babel plugin, which you would typically do in your `.babelrc`. 76 | 77 | ``` 78 | { 79 | ... 80 | "plugins": ["jsx-control-statements"] 81 | } 82 | ``` 83 | 84 | If you use the `transform-react-inline-elements` plugin, place it _after_ `jsx-control-statements`: 85 | 86 | ``` 87 | { 88 | ... 89 | "plugins": ["jsx-control-statements", "transform-react-inline-elements"] 90 | } 91 | ``` 92 | 93 | Babel can be used and configured in many different ways, so 94 | [use this guide](https://github.com/AlexGilleran/jsx-control-statements/wiki/Installation) to pick a configuration 95 | which fits your setup. 96 | 97 | ## Syntax 98 | 99 | ### If Tag 100 | 101 | Used to express the most simple conditional logic. 102 | 103 | ```javascript 104 | // simple 105 | 106 | IfBlock 107 | 108 | 109 | // using multiple child elements and / or expressions 110 | 111 | one 112 | { "two" } 113 | three 114 | four 115 | 116 | ``` 117 | 118 | #### <If> 119 | 120 | The body of the if statement only gets evaluated if `condition` is true. 121 | 122 | | Prop Name | Prop Type | Required | 123 | | --------- | --------- | ------------------ | 124 | | condition | boolean | :white_check_mark: | 125 | 126 | #### _<Else /> (deprecated)_ 127 | 128 | The else element has no properties and demarcates the `else` branch. 129 | 130 | This element is deprecated, since it's bad JSX/XML semantics and breaks auto-formatting. 131 | Please use `` instead. 132 | 133 | #### Transformation 134 | 135 | If statements transform to the _ternary operator_: 136 | 137 | ```javascript 138 | // before transformation 139 | 140 | Truth 141 | ; 142 | 143 | // after transformation 144 | { 145 | test ? Truth : null; 146 | } 147 | ``` 148 | 149 | ### Choose Tag 150 | 151 | This is an alternative syntax for more complex conditional statements. Its a equivalent of `switch` statement for jsx. The syntax itself is XMLish and conforms by and 152 | large to JSTL or XSLT (the attribute is called `condition` instead of `test`): 153 | 154 | ```javascript 155 | 156 | 157 | IfBlock 158 | 159 | 160 | ElseIfBlock 161 | Another ElseIfBlock 162 | ... 163 | 164 | 165 | ElseBlock 166 | 167 | 168 | 169 | // default block is optional; minimal example: 170 | 171 | 172 | IfBlock 173 | 174 | 175 | ``` 176 | 177 | #### <Choose> 178 | 179 | Acts as a simple container and only allows for `` and `` as children. 180 | Each `` statement requires at least one `` block but may contain as many as desired. 181 | The `` block is optional. 182 | 183 | #### <When> 184 | 185 | Analog to ``. 186 | 187 | | Prop Name | Prop Type | Required | 188 | | --------- | --------- | ------------------ | 189 | | condition | boolean | :white_check_mark: | 190 | 191 | #### <Otherwise> 192 | 193 | `` has no attributes and demarcates the else branch of the conditional. 194 | 195 | #### Transformation 196 | 197 | This syntax desugars into a (sequence of) ternary operator(s). 198 | 199 | ```javascript 200 | // Before transformation 201 | 202 | 203 | IfBlock1 204 | 205 | 206 | IfBlock2 207 | 208 | 209 | ElseBlock 210 | 211 | ; 212 | 213 | // After transformation 214 | { 215 | test1 ? ( 216 | IfBlock1 217 | ) : test2 ? ( 218 | IfBlock2 219 | ) : ( 220 | ElseBlock 221 | ); 222 | } 223 | ``` 224 | 225 | ### For Tag 226 | 227 | Define `` like so: 228 | 229 | ```javascript 230 | // you must provide the key attribute yourself 231 | 232 | { item.title } 233 | 234 | 235 | // using the index as key attribute is not stable if the array changes 236 | 237 | { item } 238 | Static Text 239 | 240 | ``` 241 | 242 | | Prop Name | Prop Type | Required | description | 243 | | --------- | ------------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | 244 | | of | array or collection(Immutable) | :white_check_mark: | the array to iterate over. This can also be a collection (Immutable.js) or anything on which a function with the name `map` can be called | 245 | | each | string | | a reference to the current item of the array which can be used within the body as variable | 246 | | index | string | | a reference to the index of the current item which can be used within the body as variable | 247 | 248 | Note that a `` _cannot_ be at the root of a `render()` function in a React component, because then you'd 249 | potentially have multiple components without a parent to group them which isn't allowed. As with ``, the same rules 250 | as using `Array.map()` apply - each element inside the loop should have a `key` attribute that uniquely identifies it. 251 | 252 | #### For Tag - Alternative Syntax 253 | 254 | For those using Typescript, the previous syntax introduces several issues with undefined variables. To deal with this issue, we introduce a following syntax, inspired by [tsx-control-statements](https://www.npmjs.com/package/tsx-control-statements). 255 | 256 | ```javascript 257 | // before transformation 258 | ( 261 | 262 | {index}. {item.title} 263 | 264 | )} 265 | />; 266 | 267 | // after transformation 268 | { 269 | items.map(function(item, index) { 270 | 271 | {index}. {item.title} 272 | ; 273 | }); 274 | } 275 | ``` 276 | 277 | | Prop Name | Prop Type | Required | description | 278 | | --------- | ------------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | 279 | | of | array or collection(Immutable) | :white_check_mark: | the array to iterate over. This can also be a collection (Immutable.js) or anything on which a function with the name `map` can be called | 280 | | body | map expression | | expression of the map statement | 281 | 282 | ### With Tag 283 | 284 | Used to assign values to local variables: 285 | 286 | ```javascript 287 | // simple 288 | 289 | { foo } 290 | { bar } 291 | 292 | 293 | // nested 294 | 295 | 296 | { foo } 297 | { bar } 298 | 299 | 300 | ``` 301 | 302 | | Prop Name | Prop Type | Required | description | 303 | | --------- | --------- | -------- | -------------------------------------------------------- | 304 | | any name | any type | | assign prop value to a local variable named by prop name | 305 | 306 | You may assign multiple variables with a single `` statement. The defined variable is 307 | available only within the `` block. 308 | 309 | #### Transformation 310 | 311 | `` statements transform to immediately-invoked function expressions: 312 | 313 | ```javascript 314 | // before transformation 315 | 316 | {foo} 317 | ; 318 | 319 | // after transformation 320 | { 321 | (function(foo) { 322 | return {foo}; 323 | }.call(this, 47)); 324 | } 325 | ``` 326 | 327 | ## Linting 328 | 329 | ### ESLint 330 | 331 | Since all control statements are transformed via Babel, no `require` or `import` calls are needed. This in turn 332 | (well, and some more cases) would lead to warnings or errors by ESLint about undefined variables. 333 | 334 | But fortunately you can use this 335 | [ESLint plugin for _JSX-Control-Statements_](https://github.com/vkbansal/eslint-plugin-jsx-control-statements) 336 | to lint your code. 337 | 338 | ### FlowType 339 | 340 | There's still not a perfect solution for FlowType given that it doesn't provide a lot of plugin functionality 341 | (at least not yet). Flow definitions are available in `jsx-control-statements.latest.flow.js` for Flow >= 0.53, or `jsx-control-statements.flow.js` (deprecated) for Flow < 0.53 - you can pick which file to use [like this](https://github.com/AlexGilleran/jsx-control-statements/pull/68#issuecomment-323562980). These will stop the 342 | type checker complaining about statements being undeclared. As of now there's no neat way to make the Flow checker 343 | recognise `each` attributes in `` loops as a variable - the best workaround for now is something like: 344 | 345 | ```javascript 346 | render() { 347 | declare var eachVariable: string; 348 | 349 | return ( 350 | 351 | {eachVariable} 352 | 353 | ); 354 | } 355 | ``` 356 | 357 | If you know of a better way to work around this please let us know! 358 | 359 | ## Alternative Solutions 360 | 361 | ### Pure JavaScript 362 | 363 | Since everything will be compiled to JavaScript anyway, you might prefer to stick to pure JavaScript solutions. 364 | 365 | #### Conditionals 366 | 367 | Probably the most common way for simple conditionals is the use of the && operator: 368 | 369 | ```javascript 370 | // simple if 371 | { 372 | test && true; 373 | } 374 | 375 | // additionally the else branch 376 | { 377 | !test && false; 378 | } 379 | ``` 380 | 381 | The ternary operator is probably more elegant for if / else conditionals: 382 | 383 | ```javascript 384 | // simple 385 | { 386 | test ? true : false; 387 | } 388 | 389 | // with multiple children 390 | { 391 | test ? ( 392 | [one, two] 393 | ) : ( 394 | false 395 | ); 396 | } 397 | ``` 398 | 399 | Another approach is to refactor your conditional into a function: 400 | 401 | ```javascript 402 | testFunc(condition){ 403 | if(condition) { 404 | return true; 405 | } 406 | else { 407 | return false 408 | } 409 | } 410 | 411 | render() { 412 | return ( 413 |
{ testFunc(test) }
414 | ) 415 | } 416 | ``` 417 | 418 | #### Loops 419 | 420 | Not many options here: 421 | 422 | ```javascript 423 | { 424 | items.map(function(item) { 425 | {item.title}; 426 | }); 427 | } 428 | ``` 429 | 430 | #### Comparison 431 | 432 | Arguments pro _JSX-Control-Statements_ in comparison to pure JS solutions: 433 | 434 | - More intuitive and easier to handle for designers and people with non-heavy JS background 435 | - JSX does not get fragmented by JS statements 436 | - Better readability and neatness, but that probably depends on you 437 | 438 | Cons: 439 | 440 | - Penalty on build-time performance 441 | - Depends on Babel 6 442 | - Some Babel configuration 443 | 444 | ### React Components 445 | 446 | There are a reasonable amount of React components for conditionals (e.g. [react-if](https://github.com/romac/react-if), which inspired this in the first place), _JSX-Control-Statements_ is the only approach we know of that avoids execution of all branches (see the [intro section](#a-note-on-transformation-and-alternative-solutions)), and there seems to be no other component-based solution to looping - while it would be possible to make a component that renders everything in `props.children` for every element of an array, you'd have to access the members of the array in that component instead of the one that uses it. 447 | 448 | For more discussion on `If` in React by the react team, have a look at https://github.com/reactjs/react-future/issues/35. 449 | 450 | To sum up: 451 | 452 | - Conditionals don't execute invalid paths 453 | - Loops with variable references to each element and index are made possible 454 | - No penalty on runtime performance 455 | - No import / require statements needed to use control statements 456 | - It works exactly as JSX is supposed to work: Plain syntactic sugar 457 | 458 | Cons: 459 | 460 | - Depends on Babel 6 461 | - Some Babel configuration 462 | - Slightly longer build times 463 | - Requires an extra plugin to work with ESLint 464 | 465 | ## For macros 466 | If you are looking to use macros with it, take a look at [jsx-control-statements-macros](https://github.com/akilansengottaiyan/jsx-control-statements) which is basically the extension of jsx-control-statements with macros support. 467 | 468 | ## What about Typescript? 469 | 470 | [There's a version for that by @KonstantinSimeonov!](https://github.com/KonstantinSimeonov/tsx-control-statements) 471 | 472 | 473 | ## Major Versions 474 | 475 | - 4.x.x is a pure Babel plugin supporting Babel >= 7. 476 | - 3.x.x is a pure Babel plugin supporting Babel >= 6. 477 | - 2.x.x was a Babel plugin supporting Babel >= 6, and a set of JSTransform visitors. 478 | - 1.x.x was a Babel plugin supporting Babel <= 5, and a set of JSTransform visitors. 479 | 480 | This used to support both JSTransform and Babel, but as JSTransform is no longer maintained support was dropped. You can 481 | find the code for the JSTransform version at https://github.com/AlexGilleran/jsx-control-statements-jstransform. 482 | 483 | ## I Want to Contribute! 484 | 485 | Yay! Please read the [Contributor's Guide](https://github.com/AlexGilleran/jsx-control-statements/blob/master/CONTRIBUTING.md). 486 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | - 1.0.0 [2015/02/26]: First release 2 | - 1.0.1 [2015/02/27]: Fixed a typo in readme.md 3 | - 1.0.2 [2015/03/08]: Fixed nested control statements completely breaking. 4 | - 1.0.3 [2015/05/12]: Added babel plugin 5 | - 1.0.4 [2015/05/12]: Added instructions for use with browserify. 6 | - 1.1.0 [2015/06/08]: 7 | - Added "index" attribute to statement (thanks helarqjsc!) 8 | - fixed working-but-noncompliant code generation through JSTransform 9 | - Added support for JSXElement and XJSElement in JSTransform (thanks justafish!) 10 | - 1.1.1 [2015/07/06]: Added example of looping over an object to docs. 11 | - 2.0.0 [2015/12/01]: Updated for Babel 6, moved repo to personal Github. 12 | - 2.0.1 [2015/12/07]: Updated readme for greater clarity. 13 | - 2.0.2 [2016/01/04]: Fixed `` in `` returning `` when condition false (thanks texttechne!) 14 | - 2.0.3 [2016/01/05]: Added travis / coveralls badges to readme.md. 15 | - 3.0.0 [2016/01/07]: Removed JSTransform support, added support for JSXExpressionContainer as child. 16 | - 3.1.0 [2016/01/31]: Removed lodash (#33), supported multiple elements in control statements (#2), added support for "choose" syntax 17 | - 3.1.1 [2016/04/12]: Corrected branches of evaluating in reverse order (thanks @RacingTadpole!) 18 | - 3.1.2 [2016/04/15]: Fixed combination of spread operator and multiple children leading to parser error, big changes to readme file with clearer documentation. 19 | - 3.1.3 [2016/08/30]: 20 | - Fixed key warnings for nested statements. 21 | - Fixed `index` attribute on `` tag only working if `each` was also set. 22 | - Added basic flowtype definitions. 23 | - 3.1.4 [2016/08/31]: Fixed key on `` definition with a string in `` or `` causing a compile error. 24 | - 3.1.5 [2016/09/18]: Fixed key generation for control statements inside control statements often generating `[Object]`. 25 | - 3.2.5 [2017/06/05]: Added `` statement (thanks @martinmacko47!) 26 | - 3.2.6 [2017/08/20]: Added FlowType 0.53-compatible statements (thanks @mwiencek!) 27 | - 3.2.7 [2017/10/02]: Created `babel-plugin-jsx-control-statements`, deprecated `jsx-control-statements` on npm 28 | - 3.2.8 [2017/10/02]: Added table of contents to readme. 29 | - 4.0.0 [2018/10/19]: Changed dependencies over to babel 7 (thanks @spoldman!), moved new Flowtype statements into the main flow file. 30 | -------------------------------------------------------------------------------- /components.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Stub definitions that will be replaced by the transformer. 3 | * They're used to provide type definitions for the typescript 4 | * compiler and various static analysis tools. 5 | * 6 | * From https://github.com/KonstantinSimeonov/tsx-control-statements/blob/master/transformer/components.ts 7 | */ 8 | 9 | export function Choose(props: { children?: any }) { 10 | return props.children; 11 | } 12 | 13 | export function When(props: { children?: any; condition: boolean }) { 14 | return props.children; 15 | } 16 | 17 | export function If(props: { children?: any; condition: boolean }) { 18 | return props.children; 19 | } 20 | 21 | type NoBody = { 22 | children?: any; 23 | each: string; 24 | of: Iterable; 25 | index?: string; 26 | }; 27 | type WithBody = { 28 | children?: any; 29 | of: Iterable; 30 | body: (x: T, index: number) => any; 31 | }; 32 | export function For(props: NoBody | WithBody) { 33 | return props.children; 34 | } 35 | 36 | export function Otherwise(props: { children?: any }) { 37 | return props.children; 38 | } 39 | 40 | export function With(props: { [id: string]: any }) { 41 | return props.children; 42 | } 43 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare function Choose(props: { children: React.ReactNode }): any; 2 | declare function When(props: { condition: boolean, children: React.ReactNode }): any; 3 | declare function Otherwise(props: { children: React.ReactNode }): any; 4 | declare function If(props: { condition: boolean, children: React.ReactNode }): any; 5 | declare function For(props: { 6 | each: string; 7 | of: Iterable; 8 | index?: string; 9 | children: React.ReactNode 10 | }): any; 11 | declare function For(props: { 12 | of: Iterable; 13 | body: (item: T, index?: number) => React.ReactNode; 14 | }): any; 15 | declare function With(props: { [id: string]: any, children: React.ReactNode }): any; 16 | -------------------------------------------------------------------------------- /jsx-control-statements.flow.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var If: React$ComponentType<{condition: boolean}>; 4 | declare var For: React$ComponentType<{each: string, index: string, of: Array}>; 5 | declare var Choose: React$ComponentType<{}>; 6 | declare var When: React$ComponentType<{condition: boolean}>; 7 | declare var Otherwise: React$ComponentType<{}>; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-jsx-control-statements", 3 | "version": "4.1.2", 4 | "description": "Neater control statements (if/for) for jsx", 5 | "author": { 6 | "name": "Alex Gilleran", 7 | "email": "alex@alexgilleran.com" 8 | }, 9 | "main": "src/index.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/AlexGilleran/jsx-control-statements" 13 | }, 14 | "types": "index.d.ts", 15 | "bugs": { 16 | "url": "https://github.com/AlexGilleran/jsx-control-statements/issues" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "jsx", 21 | "if", 22 | "else", 23 | "for", 24 | "each", 25 | "loop", 26 | "babel", 27 | "react-component" 28 | ], 29 | "license": "MIT", 30 | "scripts": { 31 | "lint": "eslint src spec", 32 | "test": "mocha spec/tests.js", 33 | "cover": "istanbul cover -x \"**/spec/**\" node_modules/mocha/bin/_mocha spec/*.js" 34 | }, 35 | "dependencies": { 36 | "@babel/core": "^7.1.2" 37 | }, 38 | "devDependencies": { 39 | "@babel/preset-react": "^7.0.0", 40 | "@babel/register": "^7.0.0", 41 | "babel-eslint": "^10.0.1", 42 | "chai": "^4.2.0", 43 | "chai-spies": "^1.0.0", 44 | "coveralls": "^3.0.9", 45 | "eslint": "^6.8.0", 46 | "eslint-config-airbnb": "^18.1.0", 47 | "eslint-plugin-jsx-control-statements": "^2.1.0", 48 | "eslint-plugin-react": "^7.19.0", 49 | "istanbul": "^0.4.1", 50 | "mocha": "^7.1.0", 51 | "react": "^16.13.0", 52 | "react-dom": "^16.13.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /spec/fixtures/basic/without-any-control-statements.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | class Iff extends React.Component { 4 | render() { 5 | return (Test); 6 | } 7 | } 8 | 9 | module.exports = class extends React.Component { 10 | render() { 11 | return ( 12 |
13 | 14 |
15 | ); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /spec/fixtures/choose/choose-empty.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 | // Can"t have "If" as the root because if the condition isn"t true then render returns undefined. 7 | // Note that this means that this fixture also tests if behaviour when the If tag is not the root of render(). 8 |
9 | 10 | 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/choose/choose-with-otherwise.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | WhenBlock1 9 | 10 | 11 | OtherwiseBlock 12 | 13 | 14 | ); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /spec/fixtures/choose/choose.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 | // Can"t have "If" as the root because if the condition isn"t true then render returns undefined. 7 | // Note that this means that this fixture also tests if behaviour when the If tag is not the root of render(). 8 |
9 | 10 | 11 | WhenBlock1 12 | 13 | 14 | WhenBlock2 15 | 16 | 17 |
18 | ); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /spec/fixtures/choose/chooses-within-if.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | 10 | 11 | Blah A 12 | 13 | 14 | 15 | 16 | Blah B 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Blah C 25 | 26 | 27 | 28 | 29 | Blah D 30 | 31 | 32 | 33 | 34 |
35 | ); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /spec/fixtures/choose/nested-choose-no-inner-component.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | 10 | 11 | When-When 12 | 13 | 14 | When-Otherwise 15 | 16 | 17 | 18 | 19 | Otherwise 20 | 21 | 22 |
23 | ); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /spec/fixtures/choose/nested-choose.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | test 10 | 11 | 12 | When-When 13 | 14 | 15 | When-Otherwise 16 | 17 | 18 | 19 | 20 | test 21 | 22 | 23 | Otherwise-When 24 | 25 | 26 | Otherwise-Otherwise 27 | 28 | 29 | 30 | 31 |
32 | ); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /spec/fixtures/errors/choose-with-multiple-otherwise.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | 10 | Fails here! 11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /spec/fixtures/errors/choose-with-no-children.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /spec/fixtures/errors/choose-with-no-when.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | Fails here! 10 | 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/errors/choose-with-otherwise-not-last.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | Fails here! 10 | 11 | 12 | 13 |
14 | ); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /spec/fixtures/errors/choose-with-wrong-children.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 |
not allowed!
10 |
11 |
12 | ); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /spec/fixtures/errors/else-with-no-children.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 | 7 | IfBlock 8 | 9 | 10 | ); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /spec/fixtures/errors/for-with-no-attributes.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | this.test = "test"; 6 | 7 | return ( 8 |
9 | 10 | {blah + this.test} 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/errors/for-with-no-of.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | this.test = "test"; 6 | 7 | return ( 8 |
9 | 10 | {blah + this.test} 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/errors/for-with-non-expression-of.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | Fails 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/errors/for-with-non-string-each.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | Fails 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/errors/for-with-non-string-index.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | var x = "i"; 6 | return ( 7 |
8 | 9 | Fails 10 | 11 |
12 | ); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /spec/fixtures/errors/if-with-no-condition.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | IfBlock 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/errors/if-with-non-expression-condition.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | IfBlock 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/errors/when-with-no-condition.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | WhenBlock 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/errors/when-with-non-expression-condition.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | IfBlock 10 | 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/extension/choose-with-multiple-children.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | const {when, ...otherProps} = this.props; 6 | 7 | return ( 8 |
9 | 10 | 11 | When1 12 | When2 13 | 14 | 15 | Other1 16 | Other2 17 | 18 | 19 |
20 | ); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /spec/fixtures/extension/for-with-expression-container.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | this.test = "test"; 6 | 7 | return ( 8 |
9 | 10 | {item} 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/extension/for-with-multiple-children.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | {item} 9 | {item + "test"} 10 | 11 |
12 | ); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /spec/fixtures/extension/if-with-expression-container.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | {"if rendered"} 9 | 10 | {"else rendered"} 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/extension/if-with-multiple-children-nested.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | {"outer1"} 9 | outer2 10 | 11 | inner1 12 | inner2 13 | inner3 14 | 15 | outer3 16 | outer4 17 | 18 |
19 | ); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /spec/fixtures/extension/if-with-multiple-children.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | const {condition, ...passdown} = this.props; 6 | 7 | return ( 8 |
9 | 10 | {"if rendered"} 11 | test 12 | 13 | {"else rendered"} 14 | test 15 | 16 |
17 | ); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /spec/fixtures/extension/if-with-string-literal.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | if rendered 9 | 10 | else rendered 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/for/for-backwards-attributes.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | this.test = "test"; 6 | 7 | return ( 8 |
9 | 10 | {item + this.test} 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/for/for-empty.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/for/for-tsx-syntax.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | this.test = "test"; 6 | 7 | return ( 8 |
9 | {item + this.test}} 12 | /> 13 |
14 | ); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /spec/fixtures/for/for-with-index-without-each.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | {$index} 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/for/for-with-index.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | this.test = "test"; 6 | 7 | return ( 8 |
9 | 10 | {item + this.test + $index} 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/for/for-without-each.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | ABC 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/for/for.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | this.test = "test"; 6 | 7 | return ( 8 |
9 | 10 | {item + this.test} 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/for/nested-for-with-indexes.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | {otherItem + item + $index2 + $index1} 10 | 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/for/nested-for.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | {otherItem + item} 10 | 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/if/if-empty.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 | // Can"t have "If" as the root because if the condition isn"t true then render returns undefined. 7 | // Note that this means that this fixture also tests if behaviour when the If tag is not the root of render(). 8 |
9 | 10 | 11 |
12 | ); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /spec/fixtures/if/if-with-else.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 | 7 | IfBlock 8 | 9 | ElseBlock 10 | 11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/if/if.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 | // Can"t have "If" as the root because if the condition isn"t true then render returns undefined. 7 | // Note that this means that this fixture also tests if behaviour when the If tag is not the root of render(). 8 |
9 | 10 | IfBlock 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/if/nested-if.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | test 9 | 10 | If-If 11 | 12 | If-Else 13 | 14 | 15 | test2 16 | 17 | Else-If 18 | 19 | Else-Else 20 | 21 | 22 |
23 | ); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /spec/fixtures/mixed/for-inside-if.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | {blah} 10 | 11 | 12 | 13 | {otherBlah} 14 | 15 | 16 |
17 | ); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /spec/fixtures/mixed/if-inside-for.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | {blah} 10 | 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-element-child.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | {attr} 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-empty-content.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-expression-attribute.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | "expr" + "ession"}> 8 | {attr()} 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-multiple-attributes.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | {attr1 + attr2 + attr3} 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-multiple-children.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | {attr} 9 | {attr} 10 | {attr} 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-nested-shadowed-restored.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | {attr} 9 | 10 | {attr} 11 | 12 | {attr} 13 | 14 |
15 | ); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-nested-shadowed.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | {attr} 10 | 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-nested.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 9 | {attr1 + attr2} 10 | 11 | 12 |
13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-no-attributes.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | let test = "test" 6 | return ( 7 |
8 | 9 | {test} 10 | 11 |
12 | ); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-outer-shadowed-restored.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | var foo = "variable" 6 | return ( 7 |
8 | {foo} 9 | 10 | {foo} 11 | 12 | {foo} 13 |
14 | ); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-outer-shadowed.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | var foo = "variable" 6 | return ( 7 |
8 | 9 | {foo} 10 | 11 |
12 | ); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-outer-this.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | this.foo = "outer" 6 | return ( 7 |
8 | 9 | {foo + this.foo} 10 | 11 |
12 | ); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-outer.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | var foo = "variable" 6 | return ( 7 |
8 | 9 | {foo + bar} 10 | 11 |
12 | ); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-single-attribute.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | {attr} 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-string-attribute.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | {attr} 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-text-child.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | text child {attr} 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-toplevel-component.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 | 7 | {attr} 8 | 9 | ); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /spec/fixtures/with/with-unused-attribute.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | module.exports = class extends React.Component { 4 | render() { 5 | return ( 6 |
7 | 8 | {attr1} 9 | 10 |
11 | ); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/test/basic.js: -------------------------------------------------------------------------------- 1 | var util = require("../testUtil"); 2 | var expect = require("chai").expect; 3 | 4 | describe("Other JSXElements should not be affected", function() { 5 | var Fixture = require("../fixtures/basic/without-any-control-statements"); 6 | 7 | it("should not affect other JSXElements", function() { 8 | var rendered = util.render(Fixture); 9 | expect(rendered).to.match(/]*>]*>Test<\/span><\/div>/); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /spec/test/choose.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | var spies = require("chai-spies"); 3 | var util = require("../testUtil"); 4 | 5 | chai.use(spies); 6 | var expect = chai.expect; 7 | 8 | 9 | describe("requiring in component with empty when", function() { 10 | var Fixture = require("../fixtures/choose/choose-empty.jsx"); 11 | 12 | it("should render nothing when condition true, but when is empty", function() { 13 | var rendered = util.render(Fixture); 14 | expect(rendered).to.match(util.matchEmptyDiv()); 15 | }); 16 | }); 17 | 18 | describe("requiring in component with choose", function() { 19 | var Fixture = require("../fixtures/choose/choose.jsx"); 20 | 21 | it("should render first when block when condition true", function() { 22 | var rendered = util.render(Fixture, {when1: true}); 23 | expect(rendered).to.match(util.matchTextWithinSpanWithinDiv("WhenBlock1")); 24 | }); 25 | 26 | it("should render first when block when both conditions true", function() { 27 | var rendered = util.render(Fixture, {when1: true, when2: true}); 28 | expect(rendered).to.match(util.matchTextWithinSpanWithinDiv("WhenBlock1")); 29 | }); 30 | 31 | it("should render second when block when condition true", function() { 32 | var rendered = util.render(Fixture, {when2: true}); 33 | expect(rendered).to.match(util.matchTextWithinSpanWithinDiv("WhenBlock2")); 34 | }); 35 | 36 | it("should render nothing when condition false", function() { 37 | var rendered = util.render(Fixture); 38 | expect(rendered).to.match(util.matchEmptyDiv()); 39 | }); 40 | }); 41 | 42 | describe("requiring in component with choose/otherwise", function() { 43 | var Fixture = require("../fixtures/choose/choose-with-otherwise.jsx"); 44 | 45 | it("should render choose block when condition true", function() { 46 | var rendered = util.render(Fixture, {when1: true}); 47 | expect(rendered).to.match(util.matchTextWithinSpan("WhenBlock1")); 48 | }); 49 | 50 | it("should render else block when condition false", function() { 51 | var rendered = util.render(Fixture); 52 | expect(rendered).to.match(util.matchTextWithinSpan("OtherwiseBlock")); 53 | }); 54 | }); 55 | 56 | describe("requiring in component with nested choose", function() { 57 | var Fixture = require("../fixtures/choose/nested-choose.jsx"); 58 | var consoleSpy; 59 | 60 | beforeEach(function() { 61 | consoleSpy = chai.spy.on(console, "error"); 62 | }); 63 | 64 | afterEach(function() { 65 | chai.spy.restore(console, "error"); 66 | }); 67 | 68 | it("should render when-when block when both conditions true", function() { 69 | var rendered = util.render(Fixture, {outerWhen: true, innerWhen: true}); 70 | expect(rendered).to.match(util.matchTextWithinSpansWithinDiv("test", "When-When")); 71 | expect(consoleSpy).to.not.have.been.called(); 72 | }); 73 | 74 | it("should render when-otherwise block when outer condition true, inner false", function() { 75 | var rendered = util.render(Fixture, {outerWhen: true}); 76 | expect(rendered).to.match(util.matchTextWithinSpansWithinDiv("test", "When-Otherwise")); 77 | expect(consoleSpy).to.not.have.been.called(); 78 | }); 79 | 80 | it("should render otherwise-when block when outer condition false, inner true", function() { 81 | var rendered = util.render(Fixture, {innerWhen: true}); 82 | expect(rendered).to.match(util.matchTextWithinSpansWithinDiv("test", "Otherwise-When")); 83 | expect(consoleSpy).to.not.have.been.called(); 84 | }); 85 | 86 | it("should render otherwise-otherwise block when both conditions false", function() { 87 | var rendered = util.render(Fixture); 88 | expect(rendered).to.match(util.matchTextWithinSpansWithinDiv("test", "Otherwise-Otherwise")); 89 | expect(consoleSpy).to.not.have.been.called(); 90 | }); 91 | }); 92 | 93 | describe("requiring in component with nested choose and a key (issue #52)", function() { 94 | // This is to guard against against a specific case that actually breaks compilation - the fact that this code even 95 | // runs probably means it's OK but we'll do a proper test because why not. 96 | 97 | var Fixture = require("../fixtures/choose/nested-choose-no-inner-component.jsx"); 98 | var consoleSpy; 99 | 100 | beforeEach(function() { 101 | consoleSpy = chai.spy.on(console, "error"); 102 | }); 103 | 104 | afterEach(function() { 105 | chai.spy.restore(console, "error"); 106 | }); 107 | 108 | it("should render when-when block when both conditions true", function() { 109 | var rendered = util.render(Fixture, {outerWhen: true, innerWhen: true}); 110 | expect(rendered).to.match(util.matchTextWithinSpanWithinDiv("When-When")); 111 | expect(consoleSpy).to.not.have.been.called(); 112 | }); 113 | }); 114 | 115 | describe("requiring in component with adjacent chooses within if", function() { 116 | var Fixture = require("../fixtures/choose/chooses-within-if.jsx"); 117 | 118 | it("a", function() { 119 | var rendered = util.render(Fixture, {type: "a"}); 120 | expect(rendered).to.match(util.matchTextWithinSpansWithinDiv("Blah A", "Blah C")); 121 | }); 122 | 123 | it("b", function() { 124 | var rendered = util.render(Fixture, {type: "b"}); 125 | expect(rendered).to.match(util.matchTextWithinSpansWithinDiv("Blah B", "Blah D")); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /spec/test/error.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | 3 | var errorUtil = require("../../src/util/error"); 4 | var renderError = errorUtil.renderErrorMessage; 5 | var errors = errorUtil.ERRORS; 6 | 7 | describe("when encountering errors", function() { 8 | it("should fail for an with no condition", function() { 9 | expect(function() { 10 | require("../fixtures/errors/if-with-no-condition.jsx"); 11 | }).to.throw(Error, renderError(errors.NO_ATTRIBUTE, {attribute: "condition", element: "If"})); 12 | }); 13 | 14 | it("should fail for a without children", function() { 15 | expect(function() { 16 | require("../fixtures/errors/choose-with-no-children.jsx"); 17 | }).to.throw(Error, renderError(errors.CHOOSE_WITHOUT_WHEN)); 18 | }); 19 | 20 | it("should fail for a with no ", function() { 21 | expect(function() { 22 | require("../fixtures/errors/choose-with-no-when.jsx"); 23 | }).to.throw(Error, renderError(errors.CHOOSE_WITHOUT_WHEN)); 24 | }); 25 | 26 | it("should fail for a with not as last element", function() { 27 | expect(function() { 28 | require("../fixtures/errors/choose-with-otherwise-not-last.jsx"); 29 | }).to.throw(Error, renderError(errors.CHOOSE_OTHERWISE_NOT_LAST)); 30 | }); 31 | 32 | it("should fail for a with multiple ", function() { 33 | expect(function() { 34 | require("../fixtures/errors/choose-with-multiple-otherwise.jsx"); 35 | }).to.throw(Error, renderError(errors.CHOOSE_WITH_MULTIPLE_OTHERWISE)); 36 | }); 37 | 38 | it("should fail for a with wrong children", function() { 39 | expect(function() { 40 | require("../fixtures/errors/choose-with-wrong-children.jsx"); 41 | }).to.throw(Error, renderError(errors.CHOOSE_WITH_WRONG_CHILDREN)); 42 | }); 43 | 44 | it("should fail for a with no condition", function() { 45 | expect(function() { 46 | require("../fixtures/errors/when-with-no-condition.jsx"); 47 | }).to.throw(Error, renderError(errors.NO_ATTRIBUTE, {attribute: "condition", element: "When"})); 48 | }); 49 | 50 | it("should fail for a with no of", function() { 51 | expect(function() { 52 | require("../fixtures/errors/for-with-no-of.jsx"); 53 | }).to.throw(Error, renderError(errors.NO_ATTRIBUTE, {attribute: "of", element: "For"})); 54 | }); 55 | 56 | it("should give location of errors", function() { 57 | expect(function() { 58 | require("../fixtures/errors/if-with-no-condition.jsx"); 59 | }).to.throw(Error, /.*7,8.*/); 60 | }); 61 | }); 62 | 63 | describe("when encountering the wrong data type", function() { 64 | it("should fail for a with a non expression \"condition\" attribute", function() { 65 | expect(function() { 66 | require("../fixtures/errors/if-with-non-expression-condition.jsx"); 67 | }).to.throw(Error, renderError(errors.NOT_EXPRESSION_TYPE, {element: "If", attribute: "condition"})); 68 | }); 69 | 70 | it("should fail for a with a non expression \"condition\" attribute", function() { 71 | expect(function() { 72 | require("../fixtures/errors/when-with-non-expression-condition.jsx"); 73 | }).to.throw(Error, renderError(errors.NOT_EXPRESSION_TYPE, {element: "When", attribute: "condition"})); 74 | }); 75 | 76 | it("should fail for a with a non string \"each\" attribute", function() { 77 | expect(function() { 78 | require("../fixtures/errors/for-with-non-string-each.jsx"); 79 | }).to.throw(Error, renderError(errors.NOT_STRING_TYPE, {element: "For", attribute: "each"})); 80 | }); 81 | 82 | it("should fail for a with non string \"index\" attribute", function() { 83 | expect(function() { 84 | require("../fixtures/errors/for-with-non-string-index.jsx"); 85 | }).to.throw(Error, renderError(errors.NOT_STRING_TYPE, {element: "For", attribute: "index"})); 86 | }); 87 | 88 | it("should fail for a with non ExpressionContainer \"of\" attribute", function() { 89 | expect(function() { 90 | require("../fixtures/errors/for-with-non-expression-of.jsx"); 91 | }).to.throw(Error, renderError(errors.NOT_EXPRESSION_TYPE, {element: "For", attribute: "of"})); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /spec/test/extension.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | var spies = require("chai-spies"); 3 | var util = require("../testUtil"); 4 | 5 | chai.use(spies); 6 | var expect = chai.expect; 7 | 8 | describe("extension: data type handling", function() { 9 | var IfStringLiteral = require("../fixtures/extension/if-with-string-literal.jsx"); 10 | var IfExpressionContainer = require("../fixtures/extension/if-with-expression-container.jsx"); 11 | var ForExpressionContainer = require("../fixtures/extension/for-with-expression-container.jsx"); 12 | 13 | it("should handle string literals within if tag", function() { 14 | var rendered = util.render(IfStringLiteral, {condition: true}); 15 | expect(rendered).to.match(util.matchTextWithinDiv("if rendered")); 16 | }); 17 | 18 | it("should handle string literals within else tag", function() { 19 | var rendered = util.render(IfStringLiteral, {condition: false}); 20 | expect(rendered).to.match(util.matchTextWithinDiv("else rendered")); 21 | }); 22 | 23 | it("should handle expression containers within if tag", function() { 24 | var rendered = util.render(IfExpressionContainer, {condition: true}); 25 | expect(rendered).to.match(util.matchTextWithinDiv("if rendered")); 26 | }); 27 | 28 | it("should handle expression containers within else tag", function() { 29 | var rendered = util.render(IfExpressionContainer, {condition: false}); 30 | expect(rendered).to.match(util.matchTextWithinDiv("else rendered")); 31 | }); 32 | 33 | it("should handle expression containers within for tag", function() { 34 | var rendered = util.render(ForExpressionContainer, {items: ["test1", "test2", "test3"]}); 35 | 36 | expect(rendered).to.match( 37 | util.createDivMatcher() 38 | .addReactText("test1") 39 | .addReactText("test2") 40 | .addReactText("test3") 41 | .build() 42 | ); 43 | }); 44 | }); 45 | 46 | describe("extension: multiple children", function() { 47 | var FixtureIf = require("../fixtures/extension/if-with-multiple-children.jsx"); 48 | var FixtureIfNested = require("../fixtures/extension/if-with-multiple-children-nested.jsx"); 49 | var FixtureChoose = require("../fixtures/extension/choose-with-multiple-children.jsx"); 50 | var FixtureFor = require("../fixtures/extension/for-with-multiple-children.jsx"); 51 | 52 | var consoleSpy; 53 | 54 | beforeEach(function() { 55 | consoleSpy = chai.spy.on(console, "error"); 56 | }); 57 | 58 | afterEach(function() { 59 | chai.spy.restore(console, "error"); 60 | }); 61 | 62 | it("should allow for multiple children within ", function() { 63 | var rendered = util.render(FixtureIf, {condition: true}); 64 | 65 | expect(rendered).to.contain("if rendered"); 66 | expect(rendered).to.contain("test"); 67 | expect(consoleSpy).to.not.have.been.called(); 68 | }); 69 | 70 | it("should allow for multiple children within nested s", function() { 71 | var rendered = util.render(FixtureIfNested, {conditionInner: true}); 72 | 73 | expect(rendered).to.match( 74 | util.createDivMatcher() 75 | .addReactText("outer1") 76 | .addSpan("outer2") 77 | .addSpan("inner1") 78 | .addSpan("inner2") 79 | .addSpan("inner3") 80 | .addSpan("outer3") 81 | .addSpan("outer4") 82 | .build() 83 | ); 84 | expect(consoleSpy).to.not.have.been.called(); 85 | }); 86 | 87 | it("should allow for multiple children within ", function() { 88 | var rendered = util.render(FixtureIf); 89 | 90 | expect(rendered).to.match( 91 | util.createDivMatcher() 92 | .addReactText("else rendered") 93 | .addSpan("test") 94 | .build() 95 | ); 96 | expect(consoleSpy).to.not.have.been.called(); 97 | }); 98 | 99 | it("should allow for multiple children within ", function() { 100 | var rendered = util.render(FixtureChoose, {when: true}); 101 | 102 | expect(rendered).to.match( 103 | util.createDivMatcher() 104 | .addSpan("When1") 105 | .addSpan("When2") 106 | .build() 107 | ); 108 | expect(consoleSpy).to.not.have.been.called(); 109 | }); 110 | 111 | it("should allow for multiple children within ", function() { 112 | var rendered = util.render(FixtureChoose); 113 | 114 | expect(rendered).to.match( 115 | util.createDivMatcher() 116 | .addSpan("Other1") 117 | .addSpan("Other2") 118 | .build() 119 | ); 120 | expect(consoleSpy).to.not.have.been.called(); 121 | }); 122 | 123 | it("should allow for multiple children within ", function() { 124 | var rendered = util.render(FixtureFor, {items: [1, 2, 3]}); 125 | 126 | expect(rendered).to.match( 127 | util.createDivMatcher() 128 | .addSpan("1") 129 | .addSpan("1test") 130 | .addSpan("2") 131 | .addSpan("2test") 132 | .addSpan("3") 133 | .addSpan("3test") 134 | .build() 135 | ); 136 | expect(consoleSpy).to.not.have.been.called(); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /spec/test/for.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var util = require("../testUtil"); 3 | 4 | describe("requiring in component with minimalistic for", function() { 5 | var FixtureEmpty = require("../fixtures/for/for-empty.jsx"); 6 | var FixtureNoEach = require("../fixtures/for/for-without-each.jsx"); 7 | 8 | describe("should render nothing if loop is empty", function() { 9 | var rendered = util.render(FixtureEmpty); 10 | expect(rendered).to.match(/]*><\/div>/); 11 | }); 12 | 13 | describe("should simply iterate without each", function() { 14 | var rendered = util.render(FixtureNoEach); 15 | expect(rendered).to.match( 16 | /]*>(ABC()?){3}<\/div>/ 17 | ); 18 | }); 19 | }); 20 | 21 | describe("requiring in component with for", function() { 22 | var ForView = require("../fixtures/for/for.jsx"); 23 | var ForViewRevAttrs = require("../fixtures/for/for-backwards-attributes.jsx"); 24 | 25 | function runForTests(ComponentDefinition) { 26 | it("should render list of items", function() { 27 | var rendered = util.render(ComponentDefinition, { 28 | items: ["item1", "item2", "item3"] 29 | }); 30 | expect(rendered).to.match( 31 | /.*span.*item1test..*span.*span.*item2test.*span.*item3test.*span.*/ 32 | ); 33 | }); 34 | 35 | it("should render empty list of items as blank", function() { 36 | var rendered = util.render(ComponentDefinition, { items: [] }); 37 | expect(rendered).to.match(/<\/div>/); 38 | }); 39 | } 40 | 41 | describe("when attributes in normal order", runForTests.bind(this, ForView)); 42 | describe( 43 | "when attributes in reverse order", 44 | runForTests.bind(this, ForViewRevAttrs) 45 | ); 46 | }); 47 | 48 | describe("using tsx syntax with for", function() { 49 | var ForView = require("../fixtures/for/for-tsx-syntax.jsx"); 50 | 51 | function runForTests(ComponentDefinition) { 52 | it("should render list of items", function() { 53 | var rendered = util.render(ComponentDefinition, { 54 | items: ["item1", "item2", "item3"] 55 | }); 56 | expect(rendered).to.match( 57 | /.*span.*item1test..*span.*span.*item2test.*span.*item3test.*span.*/ 58 | ); 59 | }); 60 | 61 | it("should render empty list of items as blank", function() { 62 | var rendered = util.render(ComponentDefinition, { items: [] }); 63 | expect(rendered).to.match(/<\/div>/); 64 | }); 65 | } 66 | 67 | describe("when attributes in normal order", runForTests.bind(this, ForView)); 68 | }); 69 | 70 | describe("requiring in component with for with index", function() { 71 | var ForWithIndex = require("../fixtures/for/for-with-index.jsx"); 72 | var ForWithIndexWithoutEach = require("../fixtures/for/for-with-index-without-each.jsx"); 73 | 74 | it("should render list of items", function() { 75 | var rendered = util.render(ForWithIndex, { 76 | items: ["item1", "item2", "item3"] 77 | }); 78 | expect(rendered).to.match( 79 | /.*span.*item1test0..*span.*span.*item2test1.*span.*item3test2.*span.*/ 80 | ); 81 | }); 82 | 83 | it("should render empty list of items as blank", function() { 84 | var rendered = util.render(ForWithIndex, { items: [] }); 85 | expect(rendered).to.match(/<\/div>/); 86 | }); 87 | 88 | it("should render indices without values", function() { 89 | var rendered = util.render(ForWithIndexWithoutEach, { 90 | items: ["one", "two", "three"] 91 | }); 92 | expect(rendered).to.match(/.*span.*0.*1.*2.*/); 93 | }); 94 | }); 95 | 96 | describe("nesting for within for", function() { 97 | var ForInsideFor = require("../fixtures/for/nested-for.jsx"); 98 | 99 | it("should render only the first loop when if condition is true", function() { 100 | var rendered = util.render(ForInsideFor, { 101 | items: ["item1", "item2", "item3"], 102 | otherItems: ["1hlab", "2hlab", "3hlab"], 103 | test: true 104 | }); 105 | 106 | expect(rendered).to.contain("1hlabitem1"); 107 | expect(rendered).to.contain("1hlabitem2"); 108 | expect(rendered).to.contain("1hlabitem3"); 109 | expect(rendered).to.contain("2hlabitem1"); 110 | expect(rendered).to.contain("2hlabitem2"); 111 | expect(rendered).to.contain("2hlabitem3"); 112 | expect(rendered).to.contain("3hlabitem1"); 113 | expect(rendered).to.contain("3hlabitem2"); 114 | expect(rendered).to.contain("3hlabitem3"); 115 | }); 116 | }); 117 | 118 | describe("nesting for within for with indexes", function() { 119 | var ForInsideFor = require("../fixtures/for/nested-for-with-indexes.jsx"); 120 | 121 | it("should render a list of items using indexes from both Fors", function() { 122 | var rendered = util.render(ForInsideFor, { 123 | items: ["item1", "item2", "item3"], 124 | otherItems: ["1hlab", "2hlab", "3hlab"], 125 | test: true 126 | }); 127 | 128 | expect(rendered).to.contain("1hlabitem100"); 129 | expect(rendered).to.contain("1hlabitem210"); 130 | expect(rendered).to.contain("1hlabitem320"); 131 | expect(rendered).to.contain("2hlabitem101"); 132 | expect(rendered).to.contain("2hlabitem211"); 133 | expect(rendered).to.contain("2hlabitem321"); 134 | expect(rendered).to.contain("3hlabitem102"); 135 | expect(rendered).to.contain("3hlabitem212"); 136 | expect(rendered).to.contain("3hlabitem322"); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /spec/test/if.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | var spies = require("chai-spies"); 3 | var util = require("../testUtil"); 4 | 5 | chai.use(spies); 6 | var expect = chai.expect; 7 | 8 | 9 | describe("requiring in component with empty if", function() { 10 | var Fixture = require("../fixtures/if/if-empty.jsx"); 11 | 12 | it("should render nothing when condition true, but if is empty", function() { 13 | var rendered = util.render(Fixture); 14 | expect(rendered).to.match(/^]*><\/div>$/); 15 | }); 16 | }); 17 | 18 | describe("requiring in component with if", function() { 19 | var IfWithoutElse = require("../fixtures/if/if.jsx"); 20 | 21 | it("should render if block when condition true", function() { 22 | var rendered = util.render(IfWithoutElse, {condition: "blah"}); 23 | expect(rendered).to.contain("If-If<"); 68 | expect(rendered).not.to.contain("Else"); 69 | expect(consoleSpy).to.not.have.been.called(); 70 | }); 71 | 72 | it("should render if-else block when outer condition true, inner false", function() { 73 | var rendered = util.render(Fixture, {ifCondition: true}); 74 | expect(rendered).to.contain(">If-Else<"); 75 | expect(rendered).not.to.contain("If<"); 76 | expect(rendered).not.to.contain(">Else"); 77 | expect(consoleSpy).to.not.have.been.called(); 78 | }); 79 | 80 | it("should render else-if block when outer condition false, inner true", function() { 81 | var rendered = util.render(Fixture, {nestedIfCondition: "other"}); 82 | expect(rendered).to.contain(">Else-If<"); 83 | expect(rendered).not.to.contain("If-"); 84 | expect(rendered).not.to.contain("-Else"); 85 | expect(consoleSpy).to.not.have.been.called(); 86 | }); 87 | 88 | it("should render else-else block when both conditions false", function() { 89 | var rendered = util.render(Fixture); 90 | expect(rendered).to.contain("Else-Else"); 91 | expect(rendered).not.to.contain("If"); 92 | expect(consoleSpy).to.not.have.been.called(); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /spec/test/mixed.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var util = require("../testUtil"); 3 | 4 | describe("nesting if within for", function() { 5 | var IfInsideFor = require("../fixtures/mixed/if-inside-for.jsx"); 6 | 7 | it("should render only the item where if condition is true", function() { 8 | var rendered = util.render(IfInsideFor, {blahs: ["blah1", "blah2", "blah3"]}); 9 | expect(rendered).to.contain("blah1"); 10 | expect(rendered).to.not.contain("blah2"); 11 | }); 12 | 13 | it("should render nothing if condition is false", function() { 14 | var rendered = util.render(IfInsideFor, {blahs: ["nope", "nope2", "nope3"]}); 15 | expect(rendered).to.not.contain("Not Rendered"); 16 | expect(rendered).to.not.contain("]*>]*>test<\/span><\/div>$/); 10 | }); 11 | 12 | it("should render with a single attribute", function() { 13 | var Fixture = require("../fixtures/with/with-single-attribute.jsx"); 14 | var rendered = util.render(Fixture); 15 | expect(rendered).to.match(/^]*>]*>value<\/span><\/div>$/); 16 | }); 17 | 18 | it("should render with multiple attributes", function() { 19 | var Fixture = require("../fixtures/with/with-multiple-attributes.jsx"); 20 | var rendered = util.render(Fixture); 21 | expect(rendered).to.match(/^]*>]*>value1value2value3<\/span><\/div>$/); 22 | }); 23 | 24 | it("should render with an unused attribute", function() { 25 | var Fixture = require("../fixtures/with/with-unused-attribute.jsx"); 26 | var rendered = util.render(Fixture); 27 | expect(rendered).to.match(/^]*>]*>used<\/span><\/div>$/); 28 | }); 29 | 30 | it("should render a string attribute", function() { 31 | var Fixture = require("../fixtures/with/with-string-attribute.jsx"); 32 | var rendered = util.render(Fixture); 33 | expect(rendered).to.match(/^]*>]*>string<\/span><\/div>$/); 34 | }); 35 | 36 | it("should render an expression attribute", function() { 37 | var Fixture = require("../fixtures/with/with-expression-attribute.jsx"); 38 | var rendered = util.render(Fixture); 39 | expect(rendered).to.match(/^]*>]*>expression<\/span><\/div>$/); 40 | }); 41 | 42 | it("should inherit nested attributes", function() { 43 | var Fixture = require("../fixtures/with/with-nested.jsx"); 44 | var rendered = util.render(Fixture); 45 | expect(rendered).to.match(/^]*>]*>outerinner<\/span><\/div>$/); 46 | }); 47 | 48 | it("should shadow nested attributes", function() { 49 | var Fixture = require("../fixtures/with/with-nested-shadowed.jsx"); 50 | var rendered = util.render(Fixture); 51 | expect(rendered).to.match(/^]*>]*>inner<\/span><\/div>$/); 52 | }); 53 | 54 | it("should restore shadowed nested attributes", function() { 55 | var Fixture = require("../fixtures/with/with-nested-shadowed-restored.jsx"); 56 | var rendered = util.render(Fixture); 57 | expect(rendered).to.match( 58 | /^]*>]*>outer<\/span>]*>inner<\/span>]*>outer<\/span><\/div>$/ 59 | ); 60 | }); 61 | 62 | it("should preserve access to outer variables", function() { 63 | var Fixture = require("../fixtures/with/with-outer.jsx"); 64 | var rendered = util.render(Fixture); 65 | expect(rendered).to.match(/^]*>]*>variableattribute<\/span><\/div>$/); 66 | }); 67 | 68 | it("should shadow outer variables", function() { 69 | var Fixture = require("../fixtures/with/with-outer-shadowed.jsx"); 70 | var rendered = util.render(Fixture); 71 | expect(rendered).to.match(/^]*>]*>attribute<\/span><\/div>$/); 72 | }); 73 | 74 | it("should restore shadowed outer variables", function() { 75 | var Fixture = require("../fixtures/with/with-outer-shadowed-restored.jsx"); 76 | var rendered = util.render(Fixture); 77 | expect(rendered).to.match( 78 | /^]*>]*>variable<\/span>]*>attribute<\/span>]*>variable<\/span><\/div>$/ 79 | ); 80 | }); 81 | 82 | it("should preserve access to outer this", function() { 83 | var Fixture = require("../fixtures/with/with-outer-this.jsx"); 84 | var rendered = util.render(Fixture); 85 | expect(rendered).to.match(/^]*>]*>attributeouter<\/span><\/div>$/); 86 | }); 87 | 88 | it("should render nothing if content is empty", function() { 89 | var Fixture = require("../fixtures/with/with-empty-content.jsx"); 90 | var rendered = util.render(Fixture); 91 | expect(rendered).to.match(/^]*><\/div>$/); 92 | }); 93 | 94 | it("should render a text child", function() { 95 | var Fixture = require("../fixtures/with/with-text-child.jsx"); 96 | var rendered = util.render(Fixture); 97 | expect(rendered).to.match(/^]*>text child value<\/div>$/); 98 | }); 99 | 100 | it("should render a single element child", function() { 101 | var Fixture = require("../fixtures/with/with-element-child.jsx"); 102 | var rendered = util.render(Fixture); 103 | expect(rendered).to.match(/^]*>]*>value<\/span><\/div>$/); 104 | }); 105 | 106 | it("should render multiple children", function() { 107 | var Fixture = require("../fixtures/with/with-multiple-children.jsx"); 108 | var rendered = util.render(Fixture); 109 | expect(rendered).to.match( 110 | /^]*>]*>value<\/span>]*>value<\/span>]*>value<\/span><\/div>$/ 111 | ); 112 | }); 113 | 114 | it("should render as toplevel component", function() { 115 | var Fixture = require("../fixtures/with/with-toplevel-component.jsx"); 116 | var rendered = util.render(Fixture); 117 | expect(rendered).to.match(/^]*>value<\/span>$/); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /spec/testUtil.js: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var ReactDOMServer = require("react-dom/server"); 3 | 4 | exports.render = function(Fixture, args) { 5 | var fixture = React.createElement(Fixture, args); 6 | return ReactDOMServer.renderToString(fixture); 7 | }; 8 | 9 | function getSpan(content) { 10 | return "]*>" + content + "
"; 11 | } 12 | 13 | function getDiv(content) { 14 | return "]*>" + content + ""; 15 | } 16 | 17 | function buildRegExp(matcher) { 18 | return new RegExp("^" + matcher + "$"); 19 | } 20 | 21 | exports.matchTextWithinSpan = function(text) { 22 | return buildRegExp(getSpan(text)); 23 | }; 24 | 25 | exports.matchTextWithinDiv = function(text) { 26 | return buildRegExp(getDiv(text)); 27 | }; 28 | 29 | exports.matchTextWithinSpanWithinDiv = function(text) { 30 | return buildRegExp(getDiv(getSpan(text))); 31 | }; 32 | 33 | exports.matchTextWithinSpansWithinDiv = function(text1, text2) { 34 | return buildRegExp(getDiv(getSpan(text1) + getSpan(text2))); 35 | }; 36 | 37 | exports.matchEmptyDiv = function() { 38 | return buildRegExp(getDiv("")); 39 | }; 40 | 41 | var Builder = function(type) { 42 | var items = []; 43 | 44 | return { 45 | addReactText: function(content) { 46 | items.push({ text: content, type: "react-text" }); 47 | return this; 48 | }, 49 | addSpan: function(content) { 50 | items.push({ text: getSpan(content) }); 51 | return this; 52 | }, 53 | addDiv: function(content) { 54 | items.push({ text: getDiv(content) }); 55 | return this; 56 | }, 57 | build: function() { 58 | var result = ""; 59 | 60 | for (var i = 0; i < items.length; i++) { 61 | result += items[i].text; 62 | var nextI = i + 1; 63 | 64 | if ( 65 | nextI < items.length && 66 | items[i].type === "react-text" && 67 | items[nextI].type === "react-text" 68 | ) { 69 | result += ""; 70 | } 71 | } 72 | 73 | return buildRegExp(type === "div" ? getDiv(result) : getSpan(result)); 74 | } 75 | }; 76 | }; 77 | 78 | exports.createDivMatcher = function() { 79 | return new Builder("div"); 80 | }; 81 | -------------------------------------------------------------------------------- /spec/tests.js: -------------------------------------------------------------------------------- 1 | var plugin = require("../src/index"); 2 | 3 | require("@babel/register")({ 4 | presets: ["@babel/preset-react"], 5 | plugins: [plugin], 6 | cache: false 7 | }); 8 | 9 | require("./test/basic"); 10 | require("./test/if"); 11 | require("./test/choose"); 12 | require("./test/for"); 13 | require("./test/with"); 14 | require("./test/mixed"); 15 | require("./test/extension"); 16 | require("./test/error"); 17 | -------------------------------------------------------------------------------- /src/chooseStatement.js: -------------------------------------------------------------------------------- 1 | var astUtil = require("./util/ast"); 2 | var conditionalUtil = require("./util/conditional"); 3 | var errorUtil = require("./util/error"); 4 | 5 | var ELEMENTS = { 6 | CHOOSE: "Choose", 7 | WHEN: "When", 8 | OTHERWISE: "Otherwise" 9 | }; 10 | 11 | 12 | function getBlocks(types, children, errorInfos, key) { 13 | var childNodes; 14 | var startResult = {}; 15 | startResult[ELEMENTS.WHEN] = []; 16 | 17 | var result = children.reduceRight(function(resultSoFar, child) { 18 | if (astUtil.isTag(child, ELEMENTS.OTHERWISE)) { 19 | childNodes = astUtil.getChildren(types, child); 20 | errorInfos.element = ELEMENTS.OTHERWISE; 21 | errorInfos.node = child; 22 | 23 | if (resultSoFar[ELEMENTS.WHEN].length) { 24 | errorUtil.throwChooseOtherwiseNotLast(errorInfos); 25 | } 26 | else if (resultSoFar[ELEMENTS.OTHERWISE]) { 27 | errorUtil.throwChooseWithMultipleOtherwise(errorInfos); 28 | } 29 | 30 | resultSoFar[ELEMENTS.OTHERWISE] = astUtil.getSanitizedExpressionForContent(types, childNodes, key); 31 | } 32 | else if (astUtil.isTag(child, ELEMENTS.WHEN)) { 33 | childNodes = astUtil.getChildren(types, child); 34 | errorInfos.element = ELEMENTS.WHEN; 35 | errorInfos.node = child; 36 | 37 | resultSoFar[ELEMENTS.WHEN].push({ 38 | condition: conditionalUtil.getConditionExpression(child, errorInfos), 39 | children: astUtil.getSanitizedExpressionForContent(types, childNodes, key) 40 | }); 41 | } 42 | else { 43 | errorInfos.element = ELEMENTS.CHOOSE; 44 | errorInfos.node = child; 45 | errorUtil.throwChooseWithWrongChildren(errorInfos); 46 | } 47 | 48 | return resultSoFar; 49 | }, startResult); 50 | 51 | if (!result[ELEMENTS.OTHERWISE]) { 52 | result[ELEMENTS.OTHERWISE] = types.NullLiteral(); 53 | } 54 | 55 | return result; 56 | } 57 | 58 | module.exports = function(babel) { 59 | var types = babel.types; 60 | 61 | return function(node, file) { 62 | var errorInfos = { node: node, file: file, element: ELEMENTS.CHOOSE }; 63 | var children = astUtil.getChildren(types, node); 64 | var key = astUtil.getKey(node); 65 | var blocks = getBlocks(types, children, errorInfos, key); 66 | var ternaryExpression = blocks[ELEMENTS.OTHERWISE]; 67 | 68 | if (!blocks[ELEMENTS.WHEN].length) { 69 | errorInfos.node = node; 70 | errorUtil.throwChooseWithoutWhen(errorInfos); 71 | } 72 | 73 | blocks[ELEMENTS.WHEN].forEach(function(whenBlock) { 74 | ternaryExpression = types.ConditionalExpression(whenBlock.condition, whenBlock.children, ternaryExpression); 75 | }); 76 | 77 | return ternaryExpression; 78 | }; 79 | }; 80 | -------------------------------------------------------------------------------- /src/forStatement.js: -------------------------------------------------------------------------------- 1 | var astUtil = require("./util/ast"); 2 | var errorUtil = require("./util/error"); 3 | 4 | var ELEMENTS = { 5 | FOR: "For" 6 | }; 7 | var ATTRIBUTES = { 8 | EACH: "each", 9 | OF: "of", 10 | INDEX: "index", 11 | BODY: "body" 12 | }; 13 | 14 | function addMapParam(types, params, attributes, attributeKey) { 15 | var attribute = attributes[attributeKey]; 16 | if (attribute && attribute.value) { 17 | params.push(types.Identifier(attribute.value.value)); 18 | } 19 | else { 20 | params.push(types.Identifier(attributeKey)); 21 | } 22 | } 23 | 24 | function checkForString(attributes, name, errorInfos) { 25 | if (attributes[name] && !astUtil.isStringLiteral(attributes[name])) { 26 | errorUtil.throwNotStringType(name, errorInfos); 27 | } 28 | } 29 | 30 | function checkForExpression(attributes, name, errorInfos) { 31 | if (attributes[name] && !astUtil.isExpressionContainer(attributes[name])) { 32 | errorUtil.throwNotExpressionType(name, errorInfos); 33 | } 34 | } 35 | 36 | module.exports = function(babel) { 37 | var types = babel.types; 38 | 39 | return function(node, file) { 40 | var mapParams = []; 41 | var errorInfos = { node: node, file: file, element: ELEMENTS.FOR }; 42 | var attributes = astUtil.getAttributeMap(node); 43 | var children = astUtil.getChildren(types, node); 44 | var returnExpression = astUtil.getSanitizedExpressionForContent( 45 | types, 46 | children 47 | ); 48 | 49 | // required attribute 50 | if (!attributes[ATTRIBUTES.OF]) { 51 | errorUtil.throwNoAttribute(ATTRIBUTES.OF, errorInfos); 52 | } 53 | 54 | if (attributes[ATTRIBUTES.BODY]) { 55 | return types.callExpression( 56 | types.memberExpression( 57 | attributes[ATTRIBUTES.OF].value.expression, 58 | types.identifier("map") 59 | ), 60 | [attributes[ATTRIBUTES.BODY].value.expression, types.identifier("this")] 61 | ); 62 | } 63 | // check for correct data types, as far as possible 64 | checkForExpression(attributes, ATTRIBUTES.OF, errorInfos); 65 | checkForString(attributes, ATTRIBUTES.EACH, errorInfos); 66 | checkForString(attributes, ATTRIBUTES.INDEX, errorInfos); 67 | 68 | // simply return without any child nodes 69 | if (!children.length) { 70 | return returnExpression; 71 | } 72 | 73 | addMapParam(types, mapParams, attributes, ATTRIBUTES.EACH); 74 | addMapParam(types, mapParams, attributes, ATTRIBUTES.INDEX); 75 | 76 | return types.callExpression( 77 | types.memberExpression( 78 | attributes[ATTRIBUTES.OF].value.expression, 79 | types.identifier("map") 80 | ), 81 | [ 82 | types.functionExpression( 83 | null, 84 | mapParams, 85 | types.blockStatement([types.returnStatement(returnExpression)]) 86 | ), 87 | types.identifier("this") 88 | ] 89 | ); 90 | }; 91 | }; 92 | -------------------------------------------------------------------------------- /src/ifStatement.js: -------------------------------------------------------------------------------- 1 | var astUtil = require("./util/ast"); 2 | var conditionalUtil = require("./util/conditional"); 3 | 4 | var ELEMENTS = { 5 | IF: "If", 6 | ELSE: "Else" 7 | }; 8 | 9 | function getBlocks(nodes) { 10 | var result = { 11 | ifBlock: [], 12 | elseBlock: [] 13 | }; 14 | var currentBlock = result.ifBlock; 15 | 16 | nodes.forEach(function(node) { 17 | if (astUtil.isTag(node, ELEMENTS.ELSE)) { 18 | currentBlock = result.elseBlock; 19 | } 20 | else { 21 | currentBlock.push(node); 22 | } 23 | }); 24 | 25 | return result; 26 | } 27 | 28 | module.exports = function ifStatement(babel) { 29 | var types = babel.types; 30 | 31 | return function(node, file) { 32 | var ifBlock; 33 | var elseBlock; 34 | var errorInfos = {node: node, file: file, element: ELEMENTS.IF}; 35 | var condition = conditionalUtil.getConditionExpression(node, errorInfos); 36 | var key = astUtil.getKey(node); 37 | var children = astUtil.getChildren(types, node); 38 | var blocks = getBlocks(children); 39 | 40 | ifBlock = astUtil.getSanitizedExpressionForContent(types, blocks.ifBlock, key); 41 | elseBlock = astUtil.getSanitizedExpressionForContent(types, blocks.elseBlock, key); 42 | 43 | return types.ConditionalExpression(condition, ifBlock, elseBlock); 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var transformFor = require("./forStatement"); 2 | var transformIf = require("./ifStatement"); 3 | var transformChoose = require("./chooseStatement"); 4 | var transformWith = require("./withStatement"); 5 | 6 | 7 | module.exports = function jcsPlugin(babel) { 8 | var nodeHandlers = { 9 | For: transformFor(babel), 10 | If: transformIf(babel), 11 | Choose: transformChoose(babel), 12 | With: transformWith(babel) 13 | }; 14 | 15 | var visitor = { 16 | JSXElement: function(path) { 17 | var nodeName = path.node.openingElement.name.name; 18 | var handler = nodeHandlers[nodeName]; 19 | 20 | if (handler) { 21 | path.replaceWith(handler(path.node, path.hub.file)); 22 | } 23 | } 24 | }; 25 | 26 | return { 27 | visitor: visitor 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /src/util/ast.js: -------------------------------------------------------------------------------- 1 | var TYPES = { 2 | ELEMENT: "JSXElement", 3 | EXPRESSION_CONTAINER: "JSXExpressionContainer", 4 | STRING_LITERAL: "StringLiteral" 5 | }; 6 | 7 | function getTagName(node) { 8 | return node.openingElement.name.name; 9 | } 10 | 11 | /** 12 | * Test if this is a custom JSX element with the given name. 13 | * 14 | * @param {object} node - Current node to test 15 | * @param {string} tagName - Name of element 16 | * @returns {boolean} whether the searched for element was found 17 | */ 18 | exports.isTag = function(node, tagName) { 19 | return node.type === TYPES.ELEMENT && getTagName(node) === tagName; 20 | }; 21 | 22 | 23 | /** 24 | * Tests whether this is an JSXExpressionContainer and returns it if true. 25 | * 26 | * @param {object} attribute - The attribute the value of which is tested 27 | * @returns {boolean} 28 | */ 29 | exports.isExpressionContainer = function(attribute) { 30 | return attribute && attribute.value.type === TYPES.EXPRESSION_CONTAINER; 31 | }; 32 | 33 | /** 34 | * Get expression from given attribute. 35 | * 36 | * @param {JSXAttribute} attribute 37 | * @returns {Expression} 38 | */ 39 | exports.getExpression = function(attribute) { 40 | return attribute.value.expression; 41 | }; 42 | 43 | /** 44 | * Tests whether this is an StringLiteral and returns it if true. 45 | * 46 | * @param {object} attribute - The attribute the value of which is tested 47 | * @returns {boolean} 48 | */ 49 | exports.isStringLiteral = function(attribute) { 50 | return attribute && attribute.value.type === TYPES.STRING_LITERAL; 51 | }; 52 | 53 | /** 54 | * Get all attributes from given element. 55 | * 56 | * @param {JSXElement} node - Current node from which attributes are gathered 57 | * @returns {object} Map of all attributes with their name as key 58 | */ 59 | exports.getAttributeMap = function(node) { 60 | return node.openingElement.attributes.reduce(function(result, attr) { 61 | result[attr.name.name] = attr; 62 | return result; 63 | }, {}); 64 | }; 65 | 66 | /** 67 | * Get the string value of a node's key attribute if present. 68 | * 69 | * @param {JSXElement} node - Node to get attributes from 70 | * @returns {object} The string value of the key attribute of this node if present, otherwise undefined. 71 | */ 72 | exports.getKey = function(node) { 73 | var key = exports.getAttributeMap(node).key; 74 | return key ? key.value.value : undefined; 75 | }; 76 | 77 | /** 78 | * Get all children from given element. Normalizes JSXText and JSXExpressionContainer to expressions. 79 | * 80 | * @param {object} babelTypes - Babel lib 81 | * @param {JSXElement} node - Current node from which children are gathered 82 | * @returns {array} List of all children 83 | */ 84 | exports.getChildren = function(babelTypes, node) { 85 | return babelTypes.react.buildChildren(node); 86 | }; 87 | 88 | /** 89 | * Adds attribute "key" to given node, if not already preesent. 90 | * 91 | * @param {object} babelTypes - Babel lib 92 | * @param {JSXElement} node - Current node to which the new attribute is added 93 | * @param {string} keyValue - Value of the key 94 | */ 95 | var addKeyAttribute = function(babelTypes, node, keyValue) { 96 | var keyFound = false; 97 | 98 | node.openingElement.attributes.forEach(function(attrib) { 99 | if (babelTypes.isJSXAttribute(attrib) && attrib.name.name === "key") { 100 | keyFound = true; 101 | return false; 102 | } 103 | }); 104 | 105 | if (!keyFound) { 106 | var keyAttrib = babelTypes.jSXAttribute(babelTypes.jSXIdentifier("key"), babelTypes.stringLiteral("" + keyValue)); 107 | node.openingElement.attributes.push(keyAttrib); 108 | } 109 | }; 110 | exports.addKeyAttribute = addKeyAttribute; 111 | 112 | /** 113 | * Return either a NullLiteral (if no content is available) or 114 | * the single expression (if there is only one) or an ArrayExpression. 115 | * 116 | * @param babelTypes - Babel lib 117 | * @param blocks - the content blocks 118 | * @param keyPrefix - a prefix to use when automatically generating keys 119 | * @returns {NullLiteral|Expression|ArrayExpression} 120 | */ 121 | exports.getSanitizedExpressionForContent = function(babelTypes, blocks, keyPrefix) { 122 | if (!blocks.length) { 123 | return babelTypes.NullLiteral(); 124 | } 125 | else if (blocks.length === 1) { 126 | var firstBlock = blocks[0]; 127 | 128 | if (keyPrefix && firstBlock.openingElement) { 129 | addKeyAttribute(babelTypes, firstBlock, keyPrefix); 130 | } 131 | 132 | return firstBlock; 133 | } 134 | 135 | for (var i = 0; i < blocks.length; i++) { 136 | var thisBlock = blocks[i]; 137 | if (babelTypes.isJSXElement(thisBlock)) { 138 | var key = keyPrefix ? keyPrefix + "-" + i : i; 139 | addKeyAttribute(babelTypes, thisBlock, key); 140 | } 141 | } 142 | 143 | return babelTypes.arrayExpression(blocks); 144 | }; 145 | -------------------------------------------------------------------------------- /src/util/conditional.js: -------------------------------------------------------------------------------- 1 | var astUtil = require("./ast"); 2 | var errorUtil = require("./error"); 3 | 4 | 5 | var ATTRIBUTES = { 6 | CONDITION: "condition" 7 | }; 8 | 9 | exports.getConditionExpression = function(node, errorInfos) { 10 | var condition = astUtil.getAttributeMap(node)[ATTRIBUTES.CONDITION]; 11 | 12 | if (!condition) { 13 | errorUtil.throwNoAttribute(ATTRIBUTES.CONDITION, errorInfos); 14 | } 15 | if (!astUtil.isExpressionContainer(condition)) { 16 | errorUtil.throwNotExpressionType(ATTRIBUTES.CONDITION, errorInfos); 17 | } 18 | 19 | return astUtil.getExpression(condition); 20 | }; 21 | -------------------------------------------------------------------------------- /src/util/error.js: -------------------------------------------------------------------------------- 1 | // http://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#answer-4673436 2 | function formatString(format) { 3 | var args = Array.prototype.slice.call(arguments, 1); 4 | return format.replace(/{(\d+)}/g, function(match, number) { 5 | return typeof args[number] !== "undefined" ? args[number] : match; 6 | }); 7 | } 8 | 9 | function throwError(errorMsg, infos) { 10 | throw new Error( 11 | [ 12 | exports.renderErrorMessage(errorMsg, infos), 13 | " at ", 14 | infos.file.opts.filename, 15 | ": ", 16 | infos.node.loc.start.line, 17 | ",", 18 | infos.node.loc.start.column 19 | ].join("") 20 | ); 21 | } 22 | 23 | var ERRORS = exports.ERRORS = { 24 | NO_ATTRIBUTE: "Attribute \"{0}\" is required for <{1}>, but missing!", 25 | NOT_EXPRESSION_TYPE: "Attribute \"{0}\" of <{1}> tag must be an expression, e.g. \"{0}={ ... }\"", 26 | NOT_STRING_TYPE: "Attribute \"{0}\" of <{1}> tag must be of type String, e.g. {0}=\"...\"", 27 | CHOOSE_WITHOUT_WHEN: " statement requires at least one element!", 28 | CHOOSE_OTHERWISE_NOT_LAST: " must be the last element within a statement!", 29 | CHOOSE_WITH_MULTIPLE_OTHERWISE: " statement allows only for one block!", 30 | CHOOSE_WITH_WRONG_CHILDREN: "Only and are allowed child elements for !" 31 | }; 32 | 33 | exports.renderErrorMessage = function(errorMsg, infos) { 34 | var args = []; 35 | if (infos) { 36 | args.push(infos.attribute); 37 | args.push(infos.element); 38 | } 39 | return formatString(errorMsg, args); 40 | }; 41 | 42 | exports.throwNoAttribute = function(attributeName, infos) { 43 | infos.attribute = attributeName; 44 | throwError(ERRORS.NO_ATTRIBUTE, infos); 45 | }; 46 | 47 | exports.throwNotExpressionType = function(attributeName, infos) { 48 | infos.attribute = attributeName; 49 | throwError(ERRORS.NOT_EXPRESSION_TYPE, infos); 50 | }; 51 | 52 | exports.throwNotStringType = function(attributeName, infos) { 53 | infos.attribute = attributeName; 54 | throwError(ERRORS.NOT_STRING_TYPE, infos); 55 | }; 56 | 57 | exports.throwChooseWithoutWhen = function(infos) { 58 | throwError(ERRORS.CHOOSE_WITHOUT_WHEN, infos); 59 | }; 60 | 61 | exports.throwChooseOtherwiseNotLast = function(infos) { 62 | throwError(ERRORS.CHOOSE_OTHERWISE_NOT_LAST, infos); 63 | }; 64 | 65 | exports.throwChooseWithMultipleOtherwise = function(infos) { 66 | throwError(ERRORS.CHOOSE_WITH_MULTIPLE_OTHERWISE, infos); 67 | }; 68 | 69 | exports.throwChooseWithWrongChildren = function(infos) { 70 | throwError(ERRORS.CHOOSE_WITH_WRONG_CHILDREN, infos); 71 | }; 72 | -------------------------------------------------------------------------------- /src/withStatement.js: -------------------------------------------------------------------------------- 1 | var astUtil = require("./util/ast"); 2 | 3 | module.exports = function(babel) { 4 | var types = babel.types; 5 | 6 | return function(node) { 7 | var params = []; 8 | var values = []; 9 | var key = astUtil.getKey(node); 10 | var attributes = astUtil.getAttributeMap(node); 11 | var children = astUtil.getChildren(types, node); 12 | 13 | Object.keys(attributes).forEach(function(attribute) { 14 | params.push(types.identifier(attribute)); 15 | if (astUtil.isExpressionContainer(attributes[attribute])) { 16 | values.push(attributes[attribute].value.expression); 17 | } 18 | else { 19 | values.push(attributes[attribute].value); 20 | } 21 | }); 22 | 23 | values.unshift(types.identifier("this")); 24 | 25 | return types.callExpression( 26 | types.memberExpression( 27 | types.functionExpression( 28 | null, 29 | params, 30 | types.blockStatement([ 31 | types.returnStatement( 32 | astUtil.getSanitizedExpressionForContent(types, children, key) 33 | ) 34 | ]) 35 | ), 36 | types.identifier("call") 37 | ), 38 | values 39 | ); 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return { 3 | files: [ 4 | "src/**/*.js", 5 | "spec/testUtil.js", 6 | { 7 | pattern: "spec/**/*.jsx", 8 | instrument: false, 9 | load: false, 10 | ignore: false 11 | } 12 | ], 13 | 14 | tests: ["spec/**/*.js", "!spec/tests.js", "!spec/testUtil.js"], 15 | 16 | env: { 17 | type: "node", 18 | runner: "node" 19 | }, 20 | 21 | setup: function(wallaby) { 22 | var plugin = require("./src/index"); 23 | require("@babel/register")({ 24 | presets: ["@babel/preset-react"], 25 | plugins: [plugin], 26 | cache: false 27 | }); 28 | } 29 | }; 30 | }; 31 | --------------------------------------------------------------------------------