├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── src └── adapter.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sumul Shah 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Adapter for Fractal 2 | 3 | This adapter lets you use React as a template engine in [Fractal](http://fractal.build). It's based on Fractal's [Handlebars adapter](https://github.com/frctl/handlebars). This adapter aims to maintain a React flavor rather than achieve complete feature parity with the Handlebars adapter. The goal is to facilitate writing React components that can easily be used in other projects. 4 | 5 | ## Installation 6 | 7 | Install the adapter via NPM: 8 | 9 | ``` 10 | npm i --save fractal-react-adapter 11 | ``` 12 | 13 | Plug it into your `fractal.js` file like so: 14 | 15 | ```javascript 16 | const reactAdapter = require('fractal-react-adapter')(); 17 | fractal.components.engine(reactAdapter); 18 | fractal.components.set('ext', '.jsx'); 19 | ``` 20 | 21 | The adapter uses [Babel](https://babeljs.io) to compile React components via [babel-register](https://babeljs.io/docs/usage/babel-register/) (which hooks into `require` or `import` and automatically routes those files through Babel). By default, `babel-register` is configured to compile `.jsx` files and use the `react` and `es2015` Babel presets, but you can override these with any valid `babel-register` config (see Configuration below). 22 | 23 | ```javascript 24 | // Default babel-register config 25 | { 26 | extensions: [".jsx"], 27 | presets: ['es2015', 'react'], 28 | } 29 | ``` 30 | 31 | The adapter also uses `babel-plugin-module-resolver` to expose Fractal's component handles as node module names. This allows you to move a component around in the file system without worrying about rewriting your imports. 32 | 33 | ```javascript 34 | import Button from '@button'; 35 | ``` 36 | 37 | ```javascript 38 | const Button = require('@button'); 39 | ``` 40 | 41 | ## Configuration 42 | 43 | These options can be overridden when the adapter is set up. 44 | 45 | * `babelConfig`: any valid configuration options for [babel-register](https://babeljs.io/docs/usage/babel-register/) 46 | * `renderMethod`: `'renderToStaticMarkup'` or `'renderToString'` (see [ReactDOMServer](https://facebook.github.io/react/docs/react-dom-server.html)) 47 | 48 | ```javascript 49 | const reactAdapter = require('fractal-react-adapter')({ 50 | babelConfig: { 51 | presets: ['es2015', 'react', 'stage-0'] 52 | }, 53 | renderMethod: 'renderToString', 54 | }); 55 | ``` 56 | 57 | ## Example components 58 | 59 | ### A simple button component 60 | 61 | ```javascript 62 | import React from 'react'; 63 | 64 | class Button extends React.Component { 65 | render() { 66 | return 67 | } 68 | } 69 | 70 | module.exports = Button; 71 | ``` 72 | 73 | ### A card component that uses the button 74 | 75 | ```javascript 76 | import React from 'react'; 77 | import Button from '@button' 78 | 79 | class Card extends React.Component { 80 | render() { 81 | return ( 82 |
83 |

{this.props.title}

84 |

{this.props.description}

85 | 86 |
87 | ); 88 | } 89 | } 90 | 91 | module.exports = Card; 92 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./src/adapter'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fractal-react-adapter", 3 | "version": "1.0.2", 4 | "description": "Use React as the template engine for Fractal components.", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/sumul/fractal-react-adapter.git" 9 | }, 10 | "keywords": [ 11 | "fractal", 12 | "react" 13 | ], 14 | "author": "Sumul Shah ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/sumul/fractal-react-adapter/issues" 18 | }, 19 | "homepage": "https://github.com/sumul/fractal-react-adapter#readme", 20 | "dependencies": { 21 | "@frctl/fractal": "^1.1.3", 22 | "babel-plugin-module-resolver": "^2.7.0", 23 | "babel-preset-es2015": "^6.24.1", 24 | "babel-preset-react": "^6.24.1", 25 | "babel-register": "^6.24.1", 26 | "bluebird": "^3.5.0", 27 | "html": "^1.0.0", 28 | "lodash": "^4.17.4", 29 | "react": "^15.4.2", 30 | "react-dom": "^15.4.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/adapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const Promise = require('bluebird'); 5 | const Adapter = require('@frctl/fractal').Adapter; 6 | const React = require('react'); 7 | const ReactDOM = require('react-dom/server'); 8 | const prettyPrint = require('html').prettyPrint; 9 | const babelReg = require('babel-register'); 10 | 11 | 12 | /* 13 | * Adpater options 14 | * --------------- 15 | * These options can be overridden when the adapter is set up. 16 | * Syntax: require('./react-adapter')({ options }) 17 | * 18 | * - babelConfig: any configuration options for babel-register 19 | * https://babeljs.io/docs/usage/babel-register/ 20 | * 21 | * - renderMethod: 'renderToStaticMarkup' or 'renderToString' 22 | * https://facebook.github.io/react/docs/react-dom-server.html 23 | */ 24 | const DEFAULT_OPTIONS = { 25 | babelConfig: { 26 | extensions: [".jsx"], 27 | presets: ['es2015', 'react'], 28 | }, 29 | renderMethod: 'renderToStaticMarkup', 30 | }; 31 | 32 | 33 | /* 34 | * React Adapter 35 | * ------------- 36 | */ 37 | class ReactAdapter extends Adapter { 38 | 39 | constructor(source, app, options) { 40 | super(null, source); 41 | this._app = app; 42 | 43 | if(options.renderMethod == 'renderToString') { 44 | this._renderMethod = ReactDOM.renderToString; 45 | } 46 | else { 47 | this._renderMethod = ReactDOM.renderToStaticMarkup; 48 | } 49 | } 50 | 51 | render(path, str, context, meta) { 52 | meta = meta || {}; 53 | 54 | setEnv('_self', meta.self, context); 55 | setEnv('_target', meta.target, context); 56 | setEnv('_env', meta.env, context); 57 | setEnv('_config', this._app.config(), context); 58 | 59 | delete require.cache[path]; 60 | const component = require(path); 61 | const element = React.createElement(component, context); 62 | const renderedHtml = this._renderMethod(element); 63 | const prettyHtml = prettyPrint(renderedHtml); 64 | 65 | return Promise.resolve(prettyHtml); 66 | } 67 | } 68 | 69 | function setEnv(key, value, context) { 70 | if (_.isUndefined(context[key]) && ! _.isUndefined(value)) { 71 | context[key] = value; 72 | } 73 | } 74 | 75 | 76 | /* 77 | * Register Babel 78 | * -------------- 79 | * This hooks into import/require statements and tells Babel 80 | * to compile according to user-configurable options. 81 | * 82 | * Call this whenever components have been parsed so we can 83 | * register all Fractal handles as import module aliases. 84 | * 85 | * Makes these possible: 86 | * 87 | * import Button from '@button'; 88 | * const Button = require('@button'); 89 | */ 90 | function registerBabel(app, config) { 91 | // Extract module aliases (e.g. '@button': '/path/to/button.jsx') 92 | var aliases = {}; 93 | app.components.items().forEach(function(item) { 94 | aliases['@' + item.handle] = item.viewPath; 95 | }); 96 | 97 | // Add resolver plugin aliases to babel config 98 | // https://github.com/tleunen/babel-plugin-module-resolver 99 | _.assign(config, { 100 | plugins: [ 101 | ["module-resolver", { 102 | "alias": aliases 103 | }] 104 | ] 105 | }); 106 | 107 | // Hook up that babel 108 | babelReg(config); 109 | } 110 | 111 | 112 | /* 113 | * Adapter registration 114 | * -------------------- 115 | */ 116 | module.exports = function(config = {}) { 117 | const options = _.assign({}, DEFAULT_OPTIONS, config); 118 | 119 | return { 120 | register(source, app) { 121 | const componentsReady = () => { registerBabel(app, options.babelConfig) }; 122 | app.components.on('loaded', componentsReady); 123 | app.components.on('updated', componentsReady); 124 | 125 | const adapter = new ReactAdapter(source, app, options); 126 | return adapter; 127 | } 128 | } 129 | }; 130 | --------------------------------------------------------------------------------