├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── lib ├── blaze-react.jsx ├── context-proxy.js ├── create-from-blaze.js ├── fragment.jsx ├── inject.jsx ├── react-regex.js ├── react-template-compiler.js └── template-regex.js ├── package.js ├── plugin └── plugin.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # npm 21 | .npm 22 | 23 | # ========================= 24 | # Operating System Files 25 | # ========================= 26 | 27 | # OSX 28 | # ========================= 29 | 30 | .DS_Store 31 | .AppleDouble 32 | .LSOverride 33 | 34 | # Thumbnails 35 | ._* 36 | 37 | # Files that might appear on external disk 38 | .Spotlight-V100 39 | .Trashes 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ### v0.4.0 4 | 5 | > Project renamed to **Blaze React** (timbrandin:blaze-react), formerly known as **Sideburns** (timbrandin:sideburns) and React Templates (timbrandin:react-templates). 6 | 7 | * After a suggestion from @facespacey we're consolidating timbrandin:react-templates and timbrandin:sideburns to new package timbrandin:blaze-react. 8 | * Simplified code for easier maintainance. 9 | * Removed the need for users to remove the `ecmascript` package. 10 | 11 | ### v0.3.2 12 | 13 | * Correct helper contexts solving from (https://github.com/peerlibrary/meteor-blaze-components/blob/master/lookup.js#L94): 14 | * 2. look up a binding by traversing the lexical view hierarchy inside the current template 15 | 16 | ### v0.3.1 17 | 18 | * Blaze onCreated @TwinTailsX 19 | * Blaze onRendered @TwinTailsX 20 | * Blaze onDestroyed @TwinTailsX 21 | * Blaze autorun @TwinTailsX @facespacey 22 | * Blaze subscribe @TwinTailsX @facespacey 23 | 24 | ### v0.3.0 25 | 26 | * Using the new toolchain and transpiling `.html` and `.js` files instead of `.html.jsx` 27 | * Code for components can be split up in files instead as before in one file, closer to how Blaze works. 28 | 29 | ### v0.2.4 30 | 31 | * Now including React ES6 JSX files instead of compiling to ES5 JS files. 32 | 33 | ### v0.2.3 34 | 35 | * Improved variable checking for helpers. 36 | * Improved logging output on failure. 37 | 38 | ### v0.2.2 39 | 40 | * Implemented {{#if}}, {{#if}} {{else}}, {{#unless}} and {{#unless}} {{else}} 41 | * Fixed a bug with double linebreaks 42 | * Improved logging on error with line numbers. 43 | 44 | ### v0.2.1 45 | 46 | * Added possibility for custom events in event-map. 47 | * Fixed a bug with component inclusions. 48 | 49 | ### v0.2.0 50 | 51 | > Project renamed to **Sideburns** (timbrandin:sideburns), formerly known as **JSX Templates** (timbrandin:jsx-templates). 52 | 53 | * Blaze events 54 | * Spacebars {{helper}} (SafeString) 55 | * Spacebars {{{helper}}} (raw HTML) 56 | * Spacebars "{{helper}}" (SafeString – In Attribute Values) 57 | * Spacebars ={{helper}} (SafeString – Dynamic Attribute Value) 58 | * Spacebars {{#each in}} 59 | 60 | ### v0.1.3 61 | 62 | * .html.jsx templates 63 | * Blaze helpers 64 | * Blaze onCreated 65 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ======================================== 2 | Blaze React is licensed under the MIT License 3 | ======================================== 4 | 5 | Copyright (C) 2011--2015 Tim Brandin 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | 13 | 14 | ==================================================================== 15 | This license applies to all code in Blaze React that is not an externally 16 | maintained library. Externally maintained libraries have their own 17 | licenses, included in the LICENSES directory. 18 | ==================================================================== 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blaze React (devel) 2 | > **Blaze React** is a [Meteor](http://meteor.com) package which give you templates for React in a familiar [Blaze API](https://www.meteor.com/blaze) (giving you **helpers**, **events**, **onRendered**, **onCreated** etc) with a subset of [Spacebars](https://github.com/meteor/meteor/blob/devel/packages/spacebars/README.md) (aka Meteor flavored Handlebars). 3 | 4 | **Why React?** – Well it gives us faster pageloads, SEO without Spiderable, accessibility for users without JavaScript and general improvements in page rendering speed. 5 | 6 | **Why a Blaze/Spacebars API?** – Well it's way easier to learn and get started with, can't argue against that right (see examples below)? 7 | 8 | 9 | ## Installation 10 | 11 | ```bash 12 | meteor add timbrandin:blaze-react 13 | ``` 14 | 15 | ## Demo 16 | 17 | * **XXX** Add new demo here... I.e. Microscope. 18 | * http://spacedropcms.org (https://github.com/spacedrop/spacedrop) 19 | * http://timbrandin.com (https://github.com/timbrandin/timbrandin) 20 | 21 | ## Getting started 22 | 23 | > Currently this project is in flux until we have a preview-release, partly due to the announcement from MDG in developing a similar package that builds templates for React, but we think we can give this to you earlier than within a few months and with support for most of Blaze 1 features and Spacebars. 24 | 25 | > **XXX** Add example code to get started. 26 | 27 | ## Features 28 | 29 | - [x] .html to ReactTemplate 30 | - [x] Blaze helpers 31 | - [x] Blaze helper context (with the data from the context) 32 | - [x] Blaze onCreated @TwinTailsX 33 | - [x] Blaze events 34 | - [x] Blaze onRendered @TwinTailsX 35 | - [x] Blaze onDestroyed @TwinTailsX 36 | - [x] Blaze autorun @TwinTailsX @facespacey 37 | - [x] Blaze subscribe @TwinTailsX @facespacey 38 | - [x] Spacebars {{helper}} (SafeString) 39 | - [x] Spacebars {{helper ..args}} (SafeString) (with arguments) 40 | - [x] Spacebars {{{helper}}} (raw HTML) 41 | - [x] Spacebars {{{helper ..args}}} (raw HTML) (with arguments) 42 | - [x] Spacebars "{{helper}}" (SafeString – In Attribute Values) 43 | - [x] Spacebars ={{helper}} (SafeString – Dynamic Attribute Value) 44 | - [x] Spacebars {{#if}} 45 | - [x] Spacebars {{else}} {{/if}} 46 | - [x] Spacebars {{else}} {{/if}} (nested) 47 | - [x] Spacebars {{#each}} 48 | - [x] Spacebars {{#each in}} 49 | - [x] Spacebars {{else}} {{/each}} 50 | - [x] Spacebars {{else in}} {{/each}} 51 | - [x] Spacebars {{else}} {{/each}} (nested) 52 | - [x] Spacebars {{#with}} 53 | - [x] Spacebars {{else}} {{#with}} 54 | - [x] Spacebars {{else}} {{#with}} (nested) 55 | - [x] Spacebars {{#unless}} 56 | - [x] Spacebars {{else}} {{/unless}} 57 | - [x] Spacebars {{else}} {{/unless}} (nested) 58 | - [ ] Spacebars {{#helper}}} (Block helpers) 59 | - [ ] Spacebars {{#helper ...args}}} (Block helpers with arguments) 60 | - [ ] Spacebars {{/helper}}} (Block helpers) 61 | - [ ] Spacebars parentData() 62 | - [ ] Spacebars {{../../helper}} 63 | - [ ] Throw notice when the syntax is broken. 64 | - [ ] Transpilation as a Babel plugin, i.e. [babel-preset-react](https://github.com/babel/babel/tree/master/packages/babel-preset-react) 65 | -------------------------------------------------------------------------------- /lib/blaze-react.jsx: -------------------------------------------------------------------------------- 1 | /* Static Template methods */ 2 | 3 | Template = class { 4 | static _buildRootNode() { 5 | return ``; 6 | } 7 | 8 | static _getRootNode() { 9 | var rootNode = document.getElementById('react-root'); 10 | 11 | if(rootNode) { 12 | return rootNode; 13 | } else { 14 | var rootNodeHtml = Template._buildRootNode(); 15 | var body = document.getElementsByTagName('body')[0]; 16 | body.insertAdjacentHTML('beforeend', rootNodeHtml); 17 | rootNode = document.getElementById('react-root'); 18 | return rootNode; 19 | } 20 | } 21 | 22 | static registerHelper (name, func) { 23 | this._globalHelpers = this._globalHelpers || {}; 24 | 25 | this._globalHelpers[this.className] = func; 26 | } 27 | }; 28 | 29 | /* Blaze React Component */ 30 | 31 | BlazeReact = class extends React.Component { 32 | constructor(props, className) { 33 | super(props); 34 | this.className = className; 35 | this.init(); 36 | } 37 | 38 | /** 39 | * Initiates helpers, computations and sets the context. 40 | */ 41 | init() { 42 | const self = this; 43 | this.state = {}; 44 | this._comps = {}; 45 | this.data = {}; 46 | this.events = Template[this.className]._events || {}; 47 | this.helpers = Template[this.className]._helpers || {}; 48 | this._contexts = {}; 49 | 50 | _.extend(this.helpers, Template._globalHelpers); 51 | 52 | _.each(this.helpers, (fn, helper) => { 53 | // Define properties for all helpers. 54 | Object.defineProperty(this.data, helper, { 55 | get: function(...args) { 56 | args.push(self); 57 | 58 | if (typeof self._comps[helper] === 'undefined') { 59 | let state = {}, initial = true; 60 | // Create a computation for the helper. 61 | self._comps[helper] = Tracker.autorun(() => { 62 | state[helper] = fn.apply(this, args); 63 | // If helper returns a cursor, let's fetch the data for the state. 64 | if (state[helper] instanceof LocalCollection.Cursor) { 65 | state[helper] = state[helper].fetch(); 66 | } 67 | if (!initial) { 68 | // Set the state on the React Component. 69 | self.setState(state); 70 | } 71 | }); 72 | initial = false; 73 | } 74 | 75 | let value = fn.apply(this, args); 76 | // If helper returns a cursor, let's fetch the data for the state. 77 | if (value instanceof LocalCollection.Cursor) { 78 | value = value.fetch(); 79 | } 80 | return value; 81 | } 82 | }); 83 | }); 84 | } 85 | 86 | componentWillMount() { 87 | Template[this.className]._callbacks = Template[this.className]._callbacks || {}; 88 | Template[this.className]._callbacks.created = Template[this.className]._callbacks.created || []; 89 | 90 | // Call all registered 'onCreated' callbacks 91 | _.each(Template[this.className]._callbacks.created, (func) => { 92 | func.apply(this); 93 | }); 94 | } 95 | 96 | componentDidMount() { 97 | this.bindEvents(); 98 | 99 | Template[this.className]._callbacks = Template[this.className]._callbacks || {}; 100 | Template[this.className]._callbacks.rendered = Template[this.className]._callbacks.rendered || []; 101 | 102 | // Call all registered 'onRendered' callbacks 103 | _.each(Template[this.className]._callbacks.rendered, (func) => { 104 | func.apply(this); 105 | }); 106 | } 107 | 108 | componentDidUpdate() { 109 | this.bindEvents(); 110 | } 111 | 112 | componentWillUnmount() { 113 | // Prevent certain methods from being iniated in onDestroyed callback 114 | Template[this.className].isDestroyed = true; 115 | Template[this.className]._callbacks = Template[this.className]._callbacks || {}; 116 | Template[this.className]._callbacks.destroyed = Template[this.className]._callbacks.destroyed || []; 117 | 118 | _.each(this._comps, (comp) => { 119 | comp.stop(); 120 | }); 121 | // Call all registered 'onDestroyed' callbacks 122 | _.each(Template[this.className]._callbacks.destroyed, (func) => { 123 | func.apply(this); 124 | }); 125 | // Stop all template subscriptions 126 | _.each(this.subscriptionHandles, function (handle) { 127 | handle.stop(); 128 | }); 129 | } 130 | 131 | static helpers(helpers) { 132 | this._helpers = this._helpers || {}; 133 | _.extend(this._helpers, helpers); 134 | } 135 | 136 | static events(events) { 137 | this._events = this._events || {}; 138 | _.extend(this._events, events); 139 | } 140 | 141 | static onCreated(callback) { 142 | if (typeof callback !== "function") { 143 | throw new Error("onCreated callback must be a function"); 144 | } 145 | this._callbacks = this._callbacks || {}; 146 | this._callbacks.created = this._callbacks.created || []; 147 | // Add onCreated callback 148 | this._callbacks.created.push(callback); 149 | } 150 | 151 | static onRendered(callback) { 152 | if (typeof callback !== "function") { 153 | throw new Error("onRendered callback must be a function"); 154 | } 155 | this._callbacks = this._callbacks || {}; 156 | this._callbacks.rendered = this._callbacks.rendered || []; 157 | // Add onRendered callback 158 | this._callbacks.rendered.push(callback); 159 | } 160 | 161 | static onDestroyed(callback) { 162 | if (typeof callback !== "function") { 163 | throw new Error("onDestroyed callback must be a function"); 164 | } 165 | this._callbacks = this._callbacks || {}; 166 | this._callbacks.destroyed = this._callbacks.destroyed || []; 167 | // Add onDestroyed callback 168 | this._callbacks.destroyed.push(callback); 169 | } 170 | 171 | autorun(runFunc, onError) { 172 | this._trackers = this._trackers || []; 173 | 174 | if (Tracker.active) { 175 | throw new Error( 176 | "Can't call Template#autorun from a Tracker Computation;" 177 | + " try calling it from the created or rendered callback"); 178 | } 179 | // Give the autorun function a better name for debugging and profiling. 180 | // The `displayName` property is not part of the spec but browsers like Chrome 181 | // and Firefox prefer it in debuggers over the name function was declared by. 182 | //runFunc.displayName = 183 | // (self.name || 'anonymous') + ':' + (displayName || 'anonymous'); 184 | // Create autorun 185 | let comp = Tracker.autorun(runFunc, onError); 186 | 187 | // Track the tracker ;) 188 | this._trackers.push(comp); 189 | 190 | comp.onStop(() => { 191 | if (! this.isDestroyed) { 192 | let i = _.indexOf(this._trackers, comp); 193 | this._trackers.pull(i); 194 | } 195 | }); 196 | return comp; 197 | } 198 | 199 | subscribe(name, ...args) { 200 | if (this.isDestroyed) { 201 | throw new Error("Can't subscribe inside onDestroyed callback!"); 202 | } 203 | let subHandles = this._subscriptionHandles = this._subscriptionHandles || {}; 204 | // This dependency is used to identify state transitions in 205 | // _subscriptionHandles which could cause the result of 206 | // subscriptionsReady to change. Basically this is triggered 207 | // whenever a new subscription handle is added or when a subscription handle 208 | // is removed and they are not ready. 209 | this._allSubsReadyDep = this._allSubsReadyDep || new Tracker.Dependency(); 210 | this._allSubsReady = this._allSubsReady || false; 211 | // Duplicate logic from Meteor.subscribe 212 | let options = {}; 213 | if (args.length) { 214 | let lastParam = _.last(args); 215 | 216 | // Match pattern to check if the last arg is an options argument 217 | let lastParamOptionsPattern = { 218 | onReady: Match.Optional(Function), 219 | // XXX COMPAT WITH 1.0.3.1 onError used to exist, but now we use 220 | // onStop with an error callback instead. 221 | onError: Match.Optional(Function), 222 | onStop: Match.Optional(Function), 223 | connection: Match.Optional(Match.Any) 224 | }; 225 | 226 | if (_.isFunction(lastParam)) { 227 | options.onReady = args.pop(); 228 | } else if (lastParam && Match.test(lastParam, lastParamOptionsPattern)) { 229 | options = args.pop(); 230 | } 231 | } 232 | 233 | let subHandle; 234 | let oldStopped = options.onStop; 235 | options.onStop = (error) => { 236 | // When the subscription is stopped, remove it from the set of tracked 237 | // subscriptions to avoid this list growing without bound 238 | delete subHandles[subHandle.subscriptionId]; 239 | 240 | // Removing a subscription can only change the result of subscriptionsReady 241 | // if we are not ready (that subscription could be the one blocking us being 242 | // ready). 243 | if (! this._allSubsReady) { 244 | this._allSubsReadyDep.changed(); 245 | } 246 | 247 | if (oldStopped) { 248 | oldStopped(error); 249 | } 250 | }; 251 | 252 | let connection = options.connection; 253 | let callbacks = _.pick(options, ["onReady", "onError", "onStop"]); 254 | 255 | // The callbacks are passed as the last item in the arguments array passed to 256 | // View#subscribe 257 | args.push(callbacks); 258 | 259 | // Activate subscription 260 | if (connection) { 261 | subHandle = connection.subscribe.apply(connection, arguments); 262 | } else { 263 | subHandle = Meteor.subscribe.apply(Meteor, arguments); 264 | } 265 | 266 | if (! _.has(subHandles, subHandle.subscriptionId)) { 267 | subHandles[subHandle.subscriptionId] = subHandle; 268 | 269 | // Adding a new subscription will always cause us to transition from ready 270 | // to not ready, but if we are already not ready then this can't make us 271 | // ready. 272 | if (this._allSubsReady) { 273 | this._allSubsReadyDep.changed(); 274 | } 275 | } 276 | 277 | return subHandle; 278 | } 279 | 280 | subscriptionsReady() { 281 | // This dependency is used to identify state transitions in 282 | // _subscriptionHandles which could cause the result of 283 | // subscriptionsReady to change. Basically this is triggered 284 | // whenever a new subscription handle is added or when a subscription handle 285 | // is removed and they are not ready. 286 | this._allSubsReadyDep = this._allSubsReadyDep || new Tracker.Dependency(); 287 | this._allSubsReady = this._allSubsReady || false; 288 | this._subscriptionHandles = this._subscriptionHandles || {}; 289 | 290 | this._allSubsReadyDep.depend(); 291 | 292 | this._allSubsReady = _.all(this._subscriptionHandles, function (handle) { 293 | return handle.ready(); 294 | }); 295 | 296 | return this._allSubsReady; 297 | } 298 | 299 | /** 300 | * Helper to bind events to dom nodes once. 301 | */ 302 | bindEvents() { 303 | const self = this; 304 | const domNode = ReactDOM.findDOMNode(this); 305 | 306 | // https://facebook.github.io/react/tips/dom-event-listeners.html 307 | for (let key in this.events) { 308 | let hash = btoa(key).replace(/=+$/, ''); 309 | let selectors = key.split(','); 310 | for (let selector of selectors) { 311 | let [event, select] = selector.trim().split(/\s(.+)?/); 312 | let $el = $(select, domNode) 313 | $el.unbind(`${event}.${hash}`); 314 | $el.bind(`${event}.${hash}`, function(...args) { 315 | // Append component instance as last in the arguments. 316 | args.push(self); 317 | // Get the key for the closest occurance of a context. 318 | let contextKey = $(this).closest('[data-ctx]').data('reactid'); 319 | let context; 320 | if (contextKey) { 321 | context = {}; 322 | if (typeof self._contexts[contextKey] == 'function') { 323 | context = _.clone(self._contexts[contextKey]('', 'value')); 324 | }; 325 | } 326 | self.events[key].apply(context, args); 327 | }); 328 | } 329 | } 330 | } 331 | 332 | render() { 333 | return ReactTemplate[this.className].call(this, this.data); 334 | } 335 | }; 336 | -------------------------------------------------------------------------------- /lib/context-proxy.js: -------------------------------------------------------------------------------- 1 | // // Getting ready for ES6 Direct Proxies. 2 | // if (typeof Proxy !== 'undefined') { 3 | // let handler = { 4 | // get: function(target, name) { 5 | // if (typeof target[name] === 'object') { 6 | // return new Proxy(target[name], handler); 7 | // } 8 | // else if (typeof target[name] !== 'undefined') { 9 | // return target[name]; 10 | // } 11 | // else { 12 | // return ''; 13 | // } 14 | // } 15 | // }; 16 | // 17 | // context = new Proxy(context, handler); 18 | // } 19 | 20 | // XXX A temporary solution until ES6 Proxies are supported in major browsers. 21 | var helper = { 22 | get: function(target, name, type, key) { 23 | if (target) { 24 | if (type == 'array') { 25 | if (target[name] instanceof Array) { 26 | return _.map(target[name], function(_data) { 27 | let data = _data; 28 | // If we're in a "each in"-loop set the iterator key on the context. 29 | if (typeof key != 'undefined') { 30 | data = {}; 31 | data[key] = _data; 32 | } 33 | data.__proto__ = target; 34 | return new ContextProxy(data); 35 | }); 36 | } 37 | return []; 38 | } 39 | if (type == 'value') { 40 | return target[name]; 41 | } 42 | if (typeof target[name] === 'object') { 43 | let data = target[name]; 44 | data.__proto__ = target; 45 | return new ContextProxy(target[name]); 46 | } 47 | if (typeof target[name] !== 'undefined') { 48 | return target[name]; 49 | } 50 | } 51 | return ''; 52 | } 53 | } 54 | // Recursive lookup. 55 | ContextProxy = class ContextProxy { 56 | constructor(target) { 57 | return (dotObject, type, key) => { 58 | // Do not look any further if dotObject is "". 59 | if (dotObject.length == 0) { 60 | return helper.get([target], 0, type, key); 61 | } 62 | // Split out the current name from the dotObject. 63 | let [name, ...rest] = dotObject.split('.'); 64 | 65 | // Look for the value for the name in the target. 66 | let value = helper.get(target, name, type, key); 67 | // Continue recursive to next level ContextProxy. 68 | if (typeof value === 'function') { 69 | return value(rest.join('.'), type); 70 | } 71 | return value; 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/create-from-blaze.js: -------------------------------------------------------------------------------- 1 | ReactTemplate = {}; 2 | 3 | /** 4 | * Helper to create React Components for Blaze Templates. 5 | * @param {String} type Template or body. 6 | * @param {String} className The name of the template. 7 | * @param {Function} renderFunc Function used to render the template. 8 | * @return {void} 9 | */ 10 | React.Component.createFromBlaze = function(type, className, renderFunc) { 11 | // Assign the render function to ReactTemplate for later use and allow for overrides. 12 | ReactTemplate[className] = function(data) { 13 | // Pass the data through the context proxy to allow for unavailable 14 | // variables and attributes in the template. 15 | let context = new ContextProxy(data); 16 | const markup = (key) => { 17 | return {__html: context(key)} 18 | }; 19 | return renderFunc.call(this, context); 20 | }; 21 | 22 | // Create React Component based on the BlazeReact class. 23 | Template[className] = class Template extends BlazeReact { 24 | constructor(props) { 25 | super(props, className); 26 | } 27 | } 28 | 29 | // If type is body, we also want to render the component. 30 | if (type === 'body') { 31 | // If the app is using flow-router-ssr we can also get server side rendering, 32 | // here we setup so we don't get warnings of missing route for "/". 33 | if (Package['kadira:flow-router-ssr'] && Meteor.isClient) { 34 | // Disable warnings of missing "/" route. 35 | Package['kadira:flow-router-ssr'].FlowRouter.route('/'); 36 | } 37 | 38 | // Wait for DOM is loaded. 39 | Meteor.startup(function() { 40 | // Create and instanciate the React Component. 41 | let body = React.createElement(Template[className]); 42 | if (Meteor.isClient) { 43 | ReactDOM.render(body, Template._getRootNode()); 44 | } 45 | // If the app is using flow-router-ssr setup server side rendering using the "/" route. 46 | else if (Package['kadira:flow-router-ssr']) { 47 | // Enable fast page loads using flow-router-ssr. 48 | var FlowRouter = Package['kadira:flow-router-ssr'].FlowRouter; 49 | FlowRouter.route('/', { 50 | action: function() { 51 | var rootNodeHtml = Template._buildRootNode(); 52 | let elHtml = ReactDOMServer.renderToString(body); 53 | let html = rootNodeHtml.replace('', elHtml + ''); 54 | 55 | var ssrContext = FlowRouter.ssrContext.get(); 56 | ssrContext.setHtml(html); 57 | } 58 | }); 59 | } 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/fragment.jsx: -------------------------------------------------------------------------------- 1 | Fragment = class Fragment extends React.Component { 2 | // Lifecycle method to set the context for this fragment on the parent 3 | // component. 4 | componentWillMount() { 5 | this.setupContext(this.props); 6 | } 7 | 8 | // Lifecycle method to set the newly updated context for this fragment on the 9 | // parent component. 10 | componentWillUpdate(nextProps) { 11 | this.setupContext(nextProps); 12 | } 13 | 14 | // Helper to setup the current context on the parent component. 15 | setupContext(props) { 16 | if (props.component && props.context) { 17 | if (!props.component._contexts) { 18 | props.component._contexts = {}; 19 | } 20 | props.component._contexts[this._reactInternalInstance._rootNodeID] = props.context; 21 | } 22 | } 23 | 24 | // Render method that surrounds a list of elements or a string with a span and 25 | // marks if the fragment is redefining the context for the children. 26 | render(): ?ReactElement { 27 | let children = this.props.children; 28 | 29 | if (!children) { 30 | return null; 31 | } 32 | else if (children instanceof Array || typeof children == 'string') { 33 | if (this.props.context) { 34 | return ({children}); 35 | } 36 | return ({children}); 37 | } 38 | else { 39 | let child = children; 40 | if (this.props.context) { 41 | return React.cloneElement(child, {'data-ctx': ''}); 42 | } 43 | return React.Children.only(child); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/inject.jsx: -------------------------------------------------------------------------------- 1 | Inject = class Inject extends React.Component { 2 | render() { 3 | // A component exists. 4 | if (Template && Template[this.props.__template]) { 5 | return React.createElement(Template[this.props.__template], _.omit(this.props, '__template')); 6 | } 7 | // A template exists. 8 | else if (ReactTemplate[this.props.__template]) { 9 | return React.createElement(ReactTemplate[this.props.__template], _.omit(this.props, '__template')); 10 | } 11 | return ""; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/react-regex.js: -------------------------------------------------------------------------------- 1 | ReactRegex = {}; 2 | 3 | ReactRegex.BlockHelperLeft = [ 4 | // {{#each in }} 5 | { 6 | regex: /{{#each\s+([^\s]+)\s+in\s+([^}]+)\s*}}/gi, 7 | replace: "{context('$2', 'array').length > 0 ? context('$2', 'array', '$1').map((context, index) => {return (" 8 | }, 9 | // {{#each }} 10 | { 11 | regex: /{{#each\s+([^}]+)\s*}}/gi, 12 | replace: "{context('$1', 'array').length > 0 ? context('$1', 'array').map((context, index) => {return (" 13 | }, 14 | // {{#with }} 15 | { 16 | regex: /{{#with\s+([^}]+)\s*}}/gi, 17 | replace: "{context('$1') ? (" 18 | }, 19 | // {{#if boolean}} 20 | { 21 | regex: /{{#if\s+(true|false)\s*}}/gi, 22 | replace: "{$1 ? (" 23 | }, 24 | // {{#if }} 25 | { 26 | regex: /{{#if\s+([^}]*)\s*}}/gi, 27 | replace: function($0, $1) { 28 | const helperWithArguments = ReactCompiler.parseArguments(`${$1}`); 29 | return `{context(${helperWithArguments}) ? (`; 30 | } 31 | }, 32 | // {{#unless boolean}} 33 | { 34 | regex: /{{#unless\s+(true|false)\s*}}/gi, 35 | replace: "{!$1 ? (" 36 | }, 37 | // {{#unless }} 38 | { 39 | regex: /{{#unless\s+([^}]*)\s*}}/gi, 40 | replace: function($0, $1) { 41 | const helperWithArguments = ReactCompiler.parseArguments(`${$1}`); 42 | return `{!context(${helperWithArguments}) ? (`; 43 | } 44 | } 45 | ]; 46 | 47 | ReactRegex.BlockHelperRightElse = [ 48 | // {{else}} ... {{/each}} 49 | { 50 | regex: /{{\/each}}/gi, 51 | replace: ")}" 52 | }, 53 | // {{else}} ... {{/each}} {{/with}} {{/if}} {{unless}} 54 | { 55 | regex: /{{\/(with|if|unless)}}/gi, 56 | replace: ")}" 57 | } 58 | ]; 59 | 60 | ReactRegex.BlockHelperRight = [ 61 | // {{/each}} 62 | { 63 | regex: /{{\/each}}/gi, 64 | replace: ")}):''}" 65 | }, 66 | // {{/with}} {{/if}} {{unless}} 67 | { 68 | regex: /{{\/(with|if|unless)}}/gi, 69 | replace: "):''}" 70 | }, 71 | ]; 72 | 73 | ReactRegex.Between = [ 74 | // Remove all comment blocks. 75 | { 76 | regex: /(<\!\-\-[\w\W]*?\-\->)/g, 77 | replace: "" 78 | }, 79 | 80 | 81 | /* INLINE OR ATTRIBUTE HELPER REGEX. */ 82 | 83 | // Append child components when using {{> template/component}}. 84 | { 85 | regex: /({{>\s+([^}]+)}})/g, 86 | replace: "" 87 | }, 88 | 89 | // {{{helper}}} raw HTML 90 | { 91 | regex: /{{{([^}]*)}}}/g, 92 | replace: function($0, $1) { 93 | const helperWithArguments = ReactCompiler.parseArguments(`${$1}`); 94 | return ``; 95 | } 96 | }, 97 | // {{helper}} SafeString – Dynamic Attribute (class) 98 | { 99 | regex: /\sclass={{([^}]*)}}/g, 100 | replace: function($0, $1) { 101 | const helperWithArguments = ReactCompiler.parseArguments(`${$1}`); 102 | return ` className={classNames(${helperWithArguments})}`; 103 | } 104 | }, 105 | 106 | // {{helper}} SafeString – Dynamic Attribute (other) 107 | { 108 | regex: /={{([^}]*)}}/g, 109 | replace: function($0, $1) { 110 | const helperWithArguments = ReactCompiler.parseArguments(`${$1}`); 111 | return `={context(${helperWithArguments})}`; 112 | } 113 | }, 114 | 115 | // {{helper}} SafeString – In Attribute Values (class) 116 | { 117 | regex: /\sclass="([^\"{]*){{([^}]*)}}([^\"{]*)\"/g, 118 | replace: function($0, $1, $2, $3) { 119 | const helperWithArguments = ReactCompiler.parseArguments(`${$2}`); 120 | return ` className={'${$1}' + classNames(${helperWithArguments}) + '${$3}'}`; 121 | } 122 | }, 123 | // {{helper}} SafeString – In Attribute Values (other) 124 | { 125 | regex: /="([^\"{]*){{([^}]*)}}([^\"{]*)\"/g, 126 | replace: function($0, $1, $2, $3) { 127 | const helperWithArguments = ReactCompiler.parseArguments(`${$2}`); 128 | return `={'${$1}' + context(${helperWithArguments}) + '${$3}'}`; 129 | } 130 | }, 131 | // {{helper}} SafeString 132 | { 133 | regex: /{{((?!else)[^}]*)}}/g, 134 | replace: function($0, $1) { 135 | const helperWithArguments = ReactCompiler.parseArguments(`${$1}`); 136 | return `{context(${helperWithArguments})}`; 137 | } 138 | }, 139 | 140 | 141 | /* Clean-up. */ 142 | 143 | // ( ) Parathesises with only spaces in between. 144 | { 145 | regex: /\([\s\n\t\r]+\)/g, 146 | replace: "''" 147 | }, 148 | 149 | 150 | /* Replace all special React JSX CamelCase attributes. */ 151 | 152 | { 153 | regex: /\sclass=/g, 154 | replace: " className=" 155 | }, 156 | { 157 | regex: /\sfor=/g, 158 | replace: " htmlFor=" 159 | }, 160 | { 161 | regex: /\stabindex=/g, 162 | replace: " tabIndex=" 163 | } 164 | ]; 165 | -------------------------------------------------------------------------------- /lib/react-template-compiler.js: -------------------------------------------------------------------------------- 1 | var XRegExp = Npm.require('xregexp'); 2 | 3 | /** 4 | * Helper class to compile Blaze templates to React Components. 5 | */ 6 | ReactCompiler = class { 7 | /** 8 | * Parse templates to React Component. 9 | * @param {String} code Contents of the template file. 10 | * @return {String} Code that in runtime creates a React Component for the Template. 11 | */ 12 | static parse(code) { 13 | return ReactCompiler.parseTemplates(code); 14 | } 15 | 16 | /** 17 | * Parse template markup to React flavored jsx code (used by the template parser). 18 | * @param {String} className The name of the template. 19 | * @param {String} markup The markup of the template. 20 | * @return {String} Valid jsx markup. 21 | */ 22 | static parseMarkup(markup) { 23 | let result = ""; 24 | let isElse = false, isEach = false; 25 | 26 | var matches = XRegExp.matchRecursive(markup, '{{#[^}]+}}', '{{\\/[^}]+}}', 'gi', { 27 | valueNames: ['between', 'left', 'match', 'right'] 28 | }); 29 | 30 | for(let row of matches) { 31 | switch(row.name) { 32 | case 'left': 33 | isEach = /#each/i.test(row.value); 34 | ReactRegex.BlockHelperLeft.forEach(function (obj) { 35 | row.value = row.value.replace(obj.regex, obj.replace); 36 | }); 37 | result += row.value; 38 | break; 39 | 40 | case 'match': 41 | row.value = ReactCompiler.parseMarkup(row.value); 42 | isElse = /{{else}}/i.test(row.value); 43 | if (isEach) { 44 | result += row.value.replace(/{{else}}/i, ")}):("); 45 | } 46 | else { 47 | result += row.value.replace(/{{else}}/i, "):("); 48 | } 49 | break; 50 | 51 | case 'right': 52 | if (isElse) { 53 | ReactRegex.BlockHelperRightElse.forEach(function (obj) { 54 | row.value = row.value.replace(obj.regex, obj.replace); 55 | }); 56 | } 57 | else { 58 | ReactRegex.BlockHelperRight.forEach(function (obj) { 59 | row.value = row.value.replace(obj.regex, obj.replace); 60 | }); 61 | } 62 | result += row.value; 63 | break; 64 | 65 | case 'between': 66 | ReactRegex.Between.forEach(function (obj) { 67 | row.value = row.value.replace(obj.regex, obj.replace); 68 | }); 69 | result += row.value; 70 | break; 71 | } 72 | } 73 | 74 | return result; 75 | } 76 | 77 | /** 78 | * Find and parse templates in html files. 79 | * @param {String} content The content of the file. 80 | * @return {String} Valid jsx and code generating React Components for the Templates found in the content. 81 | */ 82 | static parseTemplates(content) { 83 | TemplateRegex.forEach(function (obj) { 84 | content = content.replace(obj.regex, obj.replace); 85 | }); 86 | 87 | return content; 88 | } 89 | 90 | static parseArguments(string) { 91 | let parts = string.split(/\s+/g); 92 | let stringWithArguments = `'${parts[0]}'`; 93 | 94 | // Wrap arguments. 95 | if (parts.length > 1) { 96 | // Add leading brace around the arguments. 97 | stringWithArguments += ', {' 98 | 99 | // Run through each argument passed. 100 | for (let i=1; i < parts.length; i++) { 101 | let [arg, value] = parts[i].split(/=/); 102 | // Assign a numeric key to the argument if not passed. 103 | if (!value) { 104 | value = arg; 105 | arg = i-1; 106 | } 107 | // Pass variables through the context. 108 | if (/^[^"']/.test(value)) { 109 | value = `context('${value}')`; 110 | } 111 | // Build each value. 112 | stringWithArguments += `${arg}: ${value}`; 113 | 114 | if (i < parts.length - 1) { 115 | stringWithArguments += ', '; 116 | } 117 | } 118 | 119 | // Add trailing brace around the arguments. 120 | stringWithArguments += '}'; 121 | } 122 | return stringWithArguments; 123 | } 124 | } 125 | 126 | /** 127 | * The compiler used by the toolchain plugin, initally parsing templates to 128 | * react components and overrideable template functions and finally transpiling 129 | * ES2015 to valid JavaScript. 130 | */ 131 | ReactTemplateCompiler = class extends BabelCompiler { 132 | constructor() { 133 | super({react: true}); 134 | } 135 | 136 | processFilesForTarget(inputFiles) { 137 | inputFiles.forEach((inputFile) => { 138 | let content = inputFile.getContentsAsString(); 139 | inputFile.getContentsAsString = function() { 140 | var markup = ReactCompiler.parse(content); 141 | 142 | if (process.env.DEBUG) { 143 | console.log('\n\n\n'); 144 | console.log(inputFile.getPathInPackage()); 145 | console.log('====================='); 146 | console.log(content); 147 | const lines = ("" + markup).split(/\n/g); 148 | _.each(lines, (line, i) => console.log((i+1) + ' ', line)); 149 | } 150 | 151 | return markup; 152 | }; 153 | }); 154 | 155 | // Pass content to BabelCompiler. 156 | super(inputFiles); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib/template-regex.js: -------------------------------------------------------------------------------- 1 | TemplateRegex = [ 2 | { 3 | // Replace the head with a dochead code. 4 | regex: /<(head)[^<>]*>([\w\W]*?(<\1[^<>]*>[\w\W]*?<\/\1>)*[\w\W]*?)<\/\1>/g, 5 | replace: function(match, className, code) { 6 | let markup = code; 7 | DocHeadRegex.forEach(function (obj) { 8 | markup = markup.replace(obj.regex, obj.replace); 9 | }); 10 | return markup; 11 | } 12 | }, 13 | { 14 | // Replace the body template with a React Component. 15 | regex: /<(body)[^<>]*>([\w\W]*?(<\1[^<>]*>[\w\W]*?<\/\1>)*[\w\W]*?)<\/\1>/g, 16 | replace: function(match, className, code) { 17 | // We need to pass template name to the template parser to enable injection of events defined in the app. 18 | const markup = ReactCompiler.parseMarkup(code || ''); 19 | return `React.Component.createFromBlaze("body", "${className}", function(context) {return (${markup})});`; 20 | } 21 | }, 22 | { 23 | // Replace templates with a React Component. 24 | regex: /<(template)\s+name="([^"]+)"[^<>]*>([\w\W]*?(<\1[^<>]*>[\w\W]*?<\/\1>)*[\w\W]*?)<\/\1>/g, 25 | replace: function(match, tag, className, code) { 26 | // We need to pass template name to the template parser to enable injection of events defined in the app. 27 | const markup = ReactCompiler.parseMarkup(code || ''); 28 | return `React.Component.createFromBlaze("template", "${className}", function(context) {return (${markup})});`; 29 | } 30 | } 31 | ]; 32 | 33 | DocHeadRegex = [ 34 | { 35 | regex: /\s*([^<>]+)<\/title>/g, 36 | replace: `DocHead.setTitle("$1");` 37 | } 38 | ]; 39 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'timbrandin:blaze-react', 3 | version: '0.4.0', 4 | summary: 'React templates for Meteor', 5 | git: 'https://github.com/timbrandin/blaze-react', 6 | documentation: 'README.md' 7 | }); 8 | 9 | Package.registerBuildPlugin({ 10 | name: 'blaze-react', 11 | use: [ 12 | 'ecmascript@0.1.6', 13 | 'babel-compiler@5.8.24_1', 14 | 'underscore@1.0.4', 15 | 'tracker@1.0.9', 16 | ], 17 | sources: [ 18 | // 'lib/xregexp.js', 19 | 'lib/template-regex.js', 20 | 'lib/react-regex.js', 21 | 'lib/react-template-compiler.js', 22 | 'plugin/plugin.js' 23 | ], 24 | npmDependencies: { 25 | 'xregexp': '3.0.0' 26 | } 27 | }); 28 | 29 | Package.onUse(function (api) { 30 | api.versionsFrom('1.2.1'); 31 | 32 | api.use([ 33 | 'ecmascript@0.1.6', 34 | 'underscore@1.0.4', 35 | 'isobuild:compiler-plugin@1.0.0', 36 | 'react-runtime@0.14.1_1', 37 | 'jsx@0.1.6', 38 | 'check', 39 | 'minimongo' 40 | ]); 41 | api.imply([ 42 | 'ecmascript@0.1.6', 43 | 'babel-runtime@0.1.4', 44 | 'react-runtime@0.14.1_1', 45 | 'kadira:dochead@1.3.2', 46 | 'timbrandin:safestring@0.0.1', 47 | 'timbrandin:classnames@0.0.1' 48 | ]); 49 | 50 | api.use('kadira:flow-router-ssr@3.5.0', ['client', 'server'], {weak: true}); 51 | 52 | api.addFiles([ 53 | 'lib/blaze-react.jsx', 54 | 'lib/create-from-blaze.js', 55 | 'lib/fragment.jsx', 56 | 'lib/inject.jsx', 57 | 'lib/context-proxy.js' 58 | ]); 59 | 60 | api.export(['ReactTemplate', 'Template', 'Fragment', 'Inject']); 61 | }); 62 | 63 | Npm.depends({'xregexp': '3.0.0'}); 64 | 65 | Package.onTest(function (api) { 66 | api.use([ 67 | 'ecmascript@0.1.6', 68 | 'babel-compiler@5.8.24_1', 69 | 'underscore@1.0.4', 70 | 'tracker@1.0.9', 71 | ]); 72 | 73 | api.addFiles([ 74 | 'lib/template-regex.js', 75 | 'lib/react-regex.js', 76 | 'lib/react-template-compiler.js' 77 | ]); 78 | 79 | api.use("tinytest"); 80 | api.use("timbrandin:blaze-react"); 81 | api.addFiles("test/test.js", "server"); 82 | }); 83 | -------------------------------------------------------------------------------- /plugin/plugin.js: -------------------------------------------------------------------------------- 1 | Plugin.registerCompiler({ 2 | extensions: ['html'], 3 | isTemplate: true 4 | }, () => new ReactTemplateCompiler() 5 | ); 6 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | Tinytest.add( 2 | "blaze-react - transpiling - {{#each docs}}", function (test, expect) { 3 | let str = ` 4 | <div> 5 | {{#each docs}} 6 | Hello world 7 | {{/each}} 8 | </div> 9 | `; 10 | test.equal(ReactCompiler.parseMarkup(str), ` 11 | <div> 12 | {context('docs', 'array').length > 0 ? context('docs', 'array').map((context, index) => {return (<Fragment key={index} context={context} component={this}> 13 | Hello world 14 | </Fragment>)}):''} 15 | </div> 16 | `); 17 | }); 18 | 19 | Tinytest.add( 20 | "blaze-react - transpiling - {{#each docs}} {{else}}", function (test, expect) { 21 | let str = ` 22 | <div> 23 | {{#each docs}} 24 | Hello world 25 | {{else}} 26 | No documents found. 27 | {{/each}} 28 | </div> 29 | `; 30 | test.equal(ReactCompiler.parseMarkup(str), ` 31 | <div> 32 | {context('docs', 'array').length > 0 ? context('docs', 'array').map((context, index) => {return (<Fragment key={index} context={context} component={this}> 33 | Hello world 34 | </Fragment>)}):(<Fragment> 35 | No documents found. 36 | </Fragment>)} 37 | </div> 38 | `); 39 | }); 40 | 41 | Tinytest.add( 42 | "blaze-react - transpiling - {{#each docs}} (nested)", function (test, expect) { 43 | let str = ` 44 | <div> 45 | {{#each docs}} 46 | {{#each docs}} 47 | Hello world 48 | {{/each}} 49 | {{/each}} 50 | </div> 51 | `; 52 | test.equal(ReactCompiler.parseMarkup(str), ` 53 | <div> 54 | {context('docs', 'array').length > 0 ? context('docs', 'array').map((context, index) => {return (<Fragment key={index} context={context} component={this}> 55 | {context('docs', 'array').length > 0 ? context('docs', 'array').map((context, index) => {return (<Fragment key={index} context={context} component={this}> 56 | Hello world 57 | </Fragment>)}):''} 58 | </Fragment>)}):''} 59 | </div> 60 | `); 61 | }); 62 | 63 | Tinytest.add( 64 | "blaze-react - transpiling - {{#each doc in docs}}", function (test, expect) { 65 | let str = ` 66 | <div> 67 | {{#each doc in docs}} 68 | Hello world 69 | {{/each}} 70 | </div> 71 | `; 72 | test.equal(ReactCompiler.parseMarkup(str), ` 73 | <div> 74 | {context('docs', 'array').length > 0 ? context('docs', 'array', 'doc').map((context, index) => {return (<Fragment key={index} context={context} component={this}> 75 | Hello world 76 | </Fragment>)}):''} 77 | </div> 78 | `); 79 | }); 80 | 81 | Tinytest.add( 82 | "blaze-react - transpiling - {{#each doc in docs}} (nested)", function (test, expect) { 83 | let str = ` 84 | <div> 85 | {{#each doc in docs}} 86 | {{#each doc in docs}} 87 | Hello world 88 | {{/each}} 89 | {{/each}} 90 | </div> 91 | `; 92 | test.equal(ReactCompiler.parseMarkup(str), ` 93 | <div> 94 | {context('docs', 'array').length > 0 ? context('docs', 'array', 'doc').map((context, index) => {return (<Fragment key={index} context={context} component={this}> 95 | {context('docs', 'array').length > 0 ? context('docs', 'array', 'doc').map((context, index) => {return (<Fragment key={index} context={context} component={this}> 96 | Hello world 97 | </Fragment>)}):''} 98 | </Fragment>)}):''} 99 | </div> 100 | `); 101 | }); 102 | 103 | Tinytest.add( 104 | "blaze-react - transpiling - {{#with doc}}", function (test, expect) { 105 | let str = ` 106 | <div> 107 | {{#with doc}} 108 | Hello world {{name}} 109 | {{/with}} 110 | </div> 111 | `; 112 | test.equal(ReactCompiler.parseMarkup(str), ` 113 | <div> 114 | {context('doc') ? (<Fragment context={context('doc')} component={this}> 115 | Hello world {context('name')} 116 | </Fragment>):''} 117 | </div> 118 | `); 119 | }); 120 | 121 | Tinytest.add( 122 | "blaze-react - transpiling - {{#with doc}} (nested)", function (test, expect) { 123 | let str = ` 124 | <div> 125 | {{#with doc}} 126 | {{#with doc}} 127 | Hello world {{name}} 128 | {{/with}} 129 | {{/with}} 130 | </div> 131 | `; 132 | test.equal(ReactCompiler.parseMarkup(str), ` 133 | <div> 134 | {context('doc') ? (<Fragment context={context('doc')} component={this}> 135 | {context('doc') ? (<Fragment context={context('doc')} component={this}> 136 | Hello world {context('name')} 137 | </Fragment>):''} 138 | </Fragment>):''} 139 | </div> 140 | `); 141 | }); 142 | 143 | Tinytest.add( 144 | "blaze-react - transpiling - {{#if boolean}}", function (test, expect) { 145 | let str = ` 146 | <div> 147 | {{#if true}} 148 | Hello world 149 | {{/if}} 150 | </div> 151 | `; 152 | test.equal(ReactCompiler.parseMarkup(str), ` 153 | <div> 154 | {true ? (<Fragment> 155 | Hello world 156 | </Fragment>):''} 157 | </div> 158 | `); 159 | }); 160 | 161 | Tinytest.add( 162 | "blaze-react - transpiling - {{#if helper}}", function (test, expect) { 163 | str = ` 164 | <div> 165 | {{#if docs}} 166 | Hello world 167 | {{/if}} 168 | </div> 169 | `; 170 | test.equal(ReactCompiler.parseMarkup(str), ` 171 | <div> 172 | {context('docs') ? (<Fragment> 173 | Hello world 174 | </Fragment>):''} 175 | </div> 176 | `); 177 | }); 178 | 179 | Tinytest.add( 180 | "blaze-react - transpiling - {{#if helper arg=value}}", function (test, expect) { 181 | str = ` 182 | <div> 183 | {{#if docs arg=value}} 184 | Hello world 185 | {{/unless}} 186 | </div> 187 | `; 188 | test.equal(ReactCompiler.parseMarkup(str), ` 189 | <div> 190 | {context('docs', {arg: context('value')}) ? (<Fragment> 191 | Hello world 192 | </Fragment>):''} 193 | </div> 194 | `); 195 | }); 196 | 197 | Tinytest.add( 198 | "blaze-react - transpiling - {{#if helper arg1=value arg2=value}}", function (test, expect) { 199 | str = ` 200 | <div> 201 | {{#if docs arg1=value arg2=value}} 202 | Hello world 203 | {{/unless}} 204 | </div> 205 | `; 206 | test.equal(ReactCompiler.parseMarkup(str), ` 207 | <div> 208 | {context('docs', {arg1: context('value'), arg2: context('value')}) ? (<Fragment> 209 | Hello world 210 | </Fragment>):''} 211 | </div> 212 | `); 213 | }); 214 | 215 | Tinytest.add( 216 | "blaze-react - transpiling - {{#if helper}} (nested)", function (test, expect) { 217 | str = ` 218 | <div> 219 | {{#if docs}} 220 | {{#if docs}} 221 | Hello world 222 | {{/if}} 223 | {{/if}} 224 | </div> 225 | `; 226 | test.equal(ReactCompiler.parseMarkup(str), ` 227 | <div> 228 | {context('docs') ? (<Fragment> 229 | {context('docs') ? (<Fragment> 230 | Hello world 231 | </Fragment>):''} 232 | </Fragment>):''} 233 | </div> 234 | `); 235 | }); 236 | 237 | Tinytest.add( 238 | "blaze-react - transpiling - {{#unless boolean}}", function (test, expect) { 239 | let str = ` 240 | <div> 241 | {{#unless true}} 242 | Hello world 243 | {{/unless}} 244 | </div> 245 | `; 246 | test.equal(ReactCompiler.parseMarkup(str), ` 247 | <div> 248 | {!true ? (<Fragment> 249 | Hello world 250 | </Fragment>):''} 251 | </div> 252 | `); 253 | }); 254 | 255 | Tinytest.add( 256 | "blaze-react - transpiling - {{#unless helper}}", function (test, expect) { 257 | str = ` 258 | <div> 259 | {{#unless docs}} 260 | Hello world 261 | {{/unless}} 262 | </div> 263 | `; 264 | test.equal(ReactCompiler.parseMarkup(str), ` 265 | <div> 266 | {!context('docs') ? (<Fragment> 267 | Hello world 268 | </Fragment>):''} 269 | </div> 270 | `); 271 | }); 272 | 273 | Tinytest.add( 274 | "blaze-react - transpiling - {{#unless helper arg=value}}", function (test, expect) { 275 | str = ` 276 | <div> 277 | {{#unless docs arg=value}} 278 | Hello world 279 | {{/unless}} 280 | </div> 281 | `; 282 | test.equal(ReactCompiler.parseMarkup(str), ` 283 | <div> 284 | {!context('docs', {arg: context('value')}) ? (<Fragment> 285 | Hello world 286 | </Fragment>):''} 287 | </div> 288 | `); 289 | }); 290 | 291 | Tinytest.add( 292 | "blaze-react - transpiling - {{#unless helper arg1=value arg2=value}}", function (test, expect) { 293 | str = ` 294 | <div> 295 | {{#unless docs arg1=value arg2=value}} 296 | Hello world 297 | {{/unless}} 298 | </div> 299 | `; 300 | test.equal(ReactCompiler.parseMarkup(str), ` 301 | <div> 302 | {!context('docs', {arg1: context('value'), arg2: context('value')}) ? (<Fragment> 303 | Hello world 304 | </Fragment>):''} 305 | </div> 306 | `); 307 | }); 308 | 309 | Tinytest.add( 310 | "blaze-react - transpiling - {{#unless helper}} (nested)", function (test, expect) { 311 | str = ` 312 | <div> 313 | {{#unless docs}} 314 | {{#unless docs}} 315 | Hello world 316 | {{/unless}} 317 | {{/unless}} 318 | </div> 319 | `; 320 | test.equal(ReactCompiler.parseMarkup(str), ` 321 | <div> 322 | {!context('docs') ? (<Fragment> 323 | {!context('docs') ? (<Fragment> 324 | Hello world 325 | </Fragment>):''} 326 | </Fragment>):''} 327 | </div> 328 | `); 329 | }); 330 | 331 | Tinytest.add( 332 | "blaze-react - transpiling - {{> template}}", function (test, expect) { 333 | let str = ` 334 | <div> 335 | {{> template}} 336 | </div> 337 | `; 338 | test.equal(ReactCompiler.parseMarkup(str), ` 339 | <div> 340 | <Inject __template='template' parent={this}/> 341 | </div> 342 | `); 343 | }); 344 | 345 | Tinytest.add( 346 | "blaze-react - transpiling - {{helper}}", function (test, expect) { 347 | let str = ` 348 | <div> 349 | {{helper}} 350 | </div> 351 | `; 352 | test.equal(ReactCompiler.parseMarkup(str), ` 353 | <div> 354 | {context('helper')} 355 | </div> 356 | `); 357 | }); 358 | 359 | Tinytest.add( 360 | "blaze-react - transpiling - {{helper arg=value}}", function (test, expect) { 361 | let str = ` 362 | <div> 363 | {{helper arg=value}} 364 | </div> 365 | `; 366 | test.equal(ReactCompiler.parseMarkup(str), ` 367 | <div> 368 | {context('helper', {arg: context('value')})} 369 | </div> 370 | `); 371 | }); 372 | 373 | Tinytest.add( 374 | "blaze-react - transpiling - {{helper arg1=value arg2=value}}", function (test, expect) { 375 | let str = ` 376 | <div> 377 | {{helper arg1=value arg2=value}} 378 | </div> 379 | `; 380 | test.equal(ReactCompiler.parseMarkup(str), ` 381 | <div> 382 | {context('helper', {arg1: context('value'), arg2: context('value')})} 383 | </div> 384 | `); 385 | }); 386 | 387 | Tinytest.add( 388 | "blaze-react - transpiling - {{helper \"arg1\" arg2=value arg3=\"arg3\"}}", function (test, expect) { 389 | let str = ` 390 | <div> 391 | {{helper "arg1" arg2=value arg3="arg3"}} 392 | </div> 393 | `; 394 | test.equal(ReactCompiler.parseMarkup(str), ` 395 | <div> 396 | {context('helper', {0: "arg1", arg2: context('value'), arg3: "arg3"})} 397 | </div> 398 | `); 399 | }); 400 | 401 | Tinytest.add( 402 | "blaze-react - transpiling - {{{helper}}} (raw HTML)", function (test, expect) { 403 | let str = ` 404 | <div> 405 | {{{helper}}} 406 | </div> 407 | `; 408 | test.equal(ReactCompiler.parseMarkup(str), ` 409 | <div> 410 | <span dangerouslySetInnerHTML={markup('helper')}></span> 411 | </div> 412 | `); 413 | }); 414 | 415 | Tinytest.add( 416 | "blaze-react - transpiling - {{{helper arg=value}}}} (raw HTML)", function (test, expect) { 417 | let str = ` 418 | <div> 419 | {{{helper arg=value}}} 420 | </div> 421 | `; 422 | test.equal(ReactCompiler.parseMarkup(str), ` 423 | <div> 424 | <span dangerouslySetInnerHTML={markup('helper', {arg: context('value')})}></span> 425 | </div> 426 | `); 427 | }); 428 | 429 | Tinytest.add( 430 | "blaze-react - transpiling - {{{helper arg1=value arg2=value}}}} (raw HTML)", function (test, expect) { 431 | let str = ` 432 | <div> 433 | {{{helper arg1=value arg2=value}}} 434 | </div> 435 | `; 436 | test.equal(ReactCompiler.parseMarkup(str), ` 437 | <div> 438 | <span dangerouslySetInnerHTML={markup('helper', {arg1: context('value'), arg2: context('value')})}></span> 439 | </div> 440 | `); 441 | }); 442 | 443 | Tinytest.add( 444 | "blaze-react - transpiling - {{{helper \"arg1\" arg2=value arg3=\"arg3\"}}} (raw HTML)", function (test, expect) { 445 | let str = ` 446 | <div> 447 | {{{helper "arg1" arg2=value arg3="arg3"}}} 448 | </div> 449 | `; 450 | test.equal(ReactCompiler.parseMarkup(str), ` 451 | <div> 452 | <span dangerouslySetInnerHTML={markup('helper', {0: "arg1", arg2: context('value'), arg3: "arg3"})}></span> 453 | </div> 454 | `); 455 | }); 456 | 457 | Tinytest.add( 458 | "blaze-react - transpiling - class={{helper}} – Dynamic Attribute (class)", function (test, expect) { 459 | let str = ` 460 | <div class={{helper}}> 461 | </div> 462 | `; 463 | test.equal(ReactCompiler.parseMarkup(str), ` 464 | <div className={classNames('helper')}> 465 | </div> 466 | `); 467 | }); 468 | 469 | Tinytest.add( 470 | "blaze-react - transpiling - class={{helper arg=value}} – Dynamic Attribute (class)", function (test, expect) { 471 | let str = ` 472 | <div class={{helper arg=value}}> 473 | </div> 474 | `; 475 | test.equal(ReactCompiler.parseMarkup(str), ` 476 | <div className={classNames('helper', {arg: context('value')})}> 477 | </div> 478 | `); 479 | }); 480 | 481 | Tinytest.add( 482 | "blaze-react - transpiling - class={{helper arg1=value arg2=value}} – Dynamic Attribute (class)", function (test, expect) { 483 | let str = ` 484 | <div class={{helper arg1=value arg2=value}}> 485 | </div> 486 | `; 487 | test.equal(ReactCompiler.parseMarkup(str), ` 488 | <div className={classNames('helper', {arg1: context('value'), arg2: context('value')})}> 489 | </div> 490 | `); 491 | }); 492 | 493 | Tinytest.add( 494 | "blaze-react - transpiling - class={{helper \"arg1\" arg2=value arg3=\"arg3\"}} – Dynamic Attribute (class)", function (test, expect) { 495 | let str = ` 496 | <div class={{helper "arg1" arg2=value arg3="arg3"}}> 497 | </div> 498 | `; 499 | test.equal(ReactCompiler.parseMarkup(str), ` 500 | <div className={classNames('helper', {0: "arg1", arg2: context('value'), arg3: "arg3"})}> 501 | </div> 502 | `); 503 | }); 504 | 505 | Tinytest.add( 506 | "blaze-react - transpiling - title={{helper}} – Dynamic Attribute (other)", function (test, expect) { 507 | let str = ` 508 | <div title={{helper}}> 509 | </div> 510 | `; 511 | test.equal(ReactCompiler.parseMarkup(str), ` 512 | <div title={context('helper')}> 513 | </div> 514 | `); 515 | }); 516 | 517 | Tinytest.add( 518 | "blaze-react - transpiling - class=\"{{helper}}\" – In Attribute Values (class)", function (test, expect) { 519 | let str = ` 520 | <div class="{{helper}}"> 521 | </div> 522 | `; 523 | test.equal(ReactCompiler.parseMarkup(str), ` 524 | <div className={'' + classNames('helper') + ''}> 525 | </div> 526 | `); 527 | }); 528 | 529 | Tinytest.add( 530 | "blaze-react - transpiling - class=\"{{helper arg=value}}\" – In Attribute Values (class)", function (test, expect) { 531 | let str = ` 532 | <div class="{{helper arg=value}}"> 533 | </div> 534 | `; 535 | test.equal(ReactCompiler.parseMarkup(str), ` 536 | <div className={'' + classNames('helper', {arg: context('value')}) + ''}> 537 | </div> 538 | `); 539 | }); 540 | 541 | Tinytest.add( 542 | "blaze-react - transpiling - class=\"{{helper arg1=value arg2=value}}\" – In Attribute Values (class)", function (test, expect) { 543 | let str = ` 544 | <div class="{{helper arg1=value arg2=value}}"> 545 | </div> 546 | `; 547 | test.equal(ReactCompiler.parseMarkup(str), ` 548 | <div className={'' + classNames('helper', {arg1: context('value'), arg2: context('value')}) + ''}> 549 | </div> 550 | `); 551 | }); 552 | 553 | Tinytest.add( 554 | "blaze-react - transpiling - class=\"{{helper \"arg1\" arg2=value arg3=\"arg3\"}}\" – In Attribute Values (class)", function (test, expect) { 555 | let str = ` 556 | <div class="{{helper "arg1" arg2=value arg3="arg3"}}"> 557 | </div> 558 | `; 559 | test.equal(ReactCompiler.parseMarkup(str), ` 560 | <div className={'' + classNames('helper', {0: "arg1", arg2: context('value'), arg3: "arg3"}) + ''}> 561 | </div> 562 | `); 563 | }); 564 | 565 | Tinytest.add( 566 | "blaze-react - transpiling - title=\"{{helper}}\" – In Attribute Values (other)", function (test, expect) { 567 | let str = ` 568 | <div title="{{helper}}"> 569 | </div> 570 | `; 571 | test.equal(ReactCompiler.parseMarkup(str), ` 572 | <div title={'' + context('helper') + ''}> 573 | </div> 574 | `); 575 | }); 576 | 577 | Tinytest.add( 578 | "blaze-react - transpiling - template", function (test, expect) { 579 | let str = ` 580 | <template name="template"> 581 | <div>Hello world</div> 582 | </template> 583 | `; 584 | test.equal(ReactCompiler.parse(str), ` 585 | React.Component.createFromBlaze("template", "template", function(context) {return (<Fragment context={context} component={this}> 586 | <div>Hello world</div> 587 | </Fragment>)}); 588 | `); 589 | }); 590 | 591 | Tinytest.add( 592 | "blaze-react - transpiling - body", function (test, expect) { 593 | let str = ` 594 | <body> 595 | <div>Hello world</div> 596 | </body> 597 | `; 598 | test.equal(ReactCompiler.parse(str), ` 599 | React.Component.createFromBlaze("body", "body", function(context) {return (<Fragment context={context} component={this}> 600 | <div>Hello world</div> 601 | </Fragment>)}); 602 | `); 603 | }); 604 | 605 | Tinytest.add( 606 | "blaze-react - transpiling - body {{> template}}", function (test, expect) { 607 | let str = ` 608 | <body> 609 | {{> hello}} 610 | </body> 611 | `; 612 | test.equal(ReactCompiler.parse(str), ` 613 | React.Component.createFromBlaze("body", "body", function(context) {return (<Fragment context={context} component={this}> 614 | <Inject __template='hello' parent={this}/> 615 | </Fragment>)}); 616 | `); 617 | }); 618 | 619 | Tinytest.add( 620 | "blaze-react - transpiling - head", function (test, expect) { 621 | let str = ` 622 | <head> 623 | <title>Hello world 624 | `; 625 | test.equal(ReactCompiler.parse(str), ` 626 | DocHead.setTitle("Hello world"); 627 | `); 628 | }); 629 | --------------------------------------------------------------------------------