├── .babelrc ├── .gitignore ├── .npmignore ├── README.md ├── package.json ├── source ├── Breadboard.js ├── BreadboardBuild.js ├── ComponentBreadboard.js ├── ConsoleController.js ├── FakeWindow.js ├── Injectors.js ├── MDXBreadboard.js ├── RawBreadboard.js ├── ResponsiveDualModeController.js ├── compose.js ├── index.js └── util.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "latest"], 3 | "plugins": ["transform-class-properties", "transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | source 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | armo-breadboard 2 | =============== 3 | 4 | [![npm version](https://img.shields.io/npm/v/armo-breadboard.svg)](https://www.npmjs.com/package/armo-breadboard) 5 | 6 | A themeable React component. Use it to edit a live React component's source in real time. 7 | 8 | Used on [reactarmory.com](http://reactarmory.com). **Only use this component to display your own code -- it is not safe for use with publicly submitted code. For public code, use a service like [codepen.io](http://codepen.io).** 9 | 10 | Installation 11 | ------------ 12 | 13 | ```bash 14 | yarn add armo-breadboard 15 | ``` 16 | 17 | Usage 18 | ----- 19 | 20 | There are currently three Breadboard components: 21 | 22 | - `RawBreadboard` 23 | 24 | Expects that the code will call `ReactDOM.render()` itself, with the result placed in `document.getElementById('app')`. Provides global `React` and `ReactDOM` objects. 25 | 26 | - `ComponentBreadboard` 27 | 28 | Expects that a script will import `React`, and export a default Component. Imports and renders the component. 29 | 30 | - `MDXBreadboard` 31 | 32 | Expects a Markdown document and compiles it to a React Component with [mdxc](https://github.com/jamesknelson/mdxc). 33 | 34 | ### Props 35 | 36 | Here is an overview of the props available for all Breadboard components. For full details on each component's available props, see the `propTypes` definition in the source. 37 | 38 | - `defaultSource` ***required*** 39 | 40 | The source to execute. Breadboards are uncontrolled; they currently do not emit events when the code updates/renders. PRs to add this functionality would be welcome. 41 | 42 | - `theme` ***required*** 43 | 44 | The theme object that actually renders the Breadboard. This is required as the Breadboard components themselves do not generate any HTML. For an example of a Breadboard theme, see the [theme](#themes) section. 45 | 46 | - `require` 47 | 48 | The `require` function that will be used when executing the Breadboard's source. Use this to configure how `import` statements work. 49 | 50 | By default, Breadboard provides a `require` function that just makes `react` available. 51 | 52 | - `defaultMode` 53 | 54 | Specifies the mode that the Breadboard will be in when loaded. Available options are: 55 | 56 | * `source` 57 | * `transformed` 58 | * `view` 59 | * `console` 60 | 61 | - `defaultSecondary` 62 | 63 | If your Breadboard element has enough space, it will split the view into two panels. In this case, the `source` panel will always be displayed. This option chooses a default for the second panel. All options from above are available -- except `source`. 64 | 65 | ### Themes 66 | 67 | Different websites call for default themes. Of course, CSS isn't always sufficient to make the theming changes that you'd like. Because of this, Breadboards do not generate *any* Markup themselves. Instead, they leave the markup generation to you, via *theme objects*. 68 | 69 | For an example of how theming can be used in practice, see [MDXC Playground](dump.jamesknelson.com/mdxc-playground.html). This page uses two Breadboard themes: 70 | 71 | - The "fullscreen" theme renders the document's source on the left, and the full document on the right. *([source](https://github.com/jamesknelson/mdxc-playground/blob/8924c21913ed568fbef8867463af3c10f6230422/source/fullscreenMDXBreadboardTheme.js))* 72 | - The "default" theme is used for embedded examples within the right pane. *([source](https://github.com/jamesknelson/mdxc-playground/blob/8924c21913ed568fbef8867463af3c10f6230422/source/defaultMDXBreadboardTheme.js))* 73 | 74 | The actual options available on a theme object differ between breadboards. For details, you'll currently need to view the source. 75 | 76 | #### Example 77 | 78 | This is an example of a theme for `RawBreadboard` and `ComponentBreadboard` that renders the editor using CodeMirror. This is used on [reactarmory.com](https://reactarmory.com) 79 | 80 | ```jsx 81 | import './defaultBreadboardTheme.less' 82 | import React, { Component, PropTypes } from 'react' 83 | import debounce from 'lodash.debounce' 84 | import codeMirror from 'codemirror' 85 | import createClassNamePrefixer from '../utils/createClassNamePrefixer' 86 | 87 | require("codemirror/mode/jsx/jsx") 88 | 89 | 90 | const cx = createClassNamePrefixer('defaultBreadboardTheme') 91 | 92 | 93 | export default { 94 | maxSinglePaneWidth: 800, 95 | 96 | renderBreadboard: function(props) { 97 | const { 98 | consoleMessages, 99 | transformedSource, 100 | transformError, 101 | executionError, 102 | 103 | renderEditorElement, 104 | renderMountElement, 105 | 106 | modes, 107 | modeActions, 108 | 109 | reactVersion, 110 | appId 111 | } = props 112 | 113 | const activeModeCount = Object.values(modes).reduce((acc, x) => acc + x || 0, 0) 114 | 115 | const sourceLayout = { 116 | position: 'relative', 117 | flexBasis: 600, 118 | flexGrow: 0, 119 | flexShrink: 0, 120 | } 121 | if (activeModeCount === 1) { 122 | sourceLayout.flexShrink = 1 123 | } 124 | 125 | const secondaryLayout = { 126 | position: 'relative', 127 | flexBasis: 600, 128 | flexGrow: 0, 129 | flexShrink: 1, 130 | overflow: 'auto', 131 | } 132 | 133 | return ( 134 |
135 | { (consoleMessages.length || activeModeCount == 1) && 136 | 145 | } 146 | { modes.source && 147 | renderEditorElement({ layout: sourceLayout }) 148 | } 149 | { // Always render the preview element, as the user's code may depend 150 | // on it being available. Hide it if it isn't selected. 151 |
152 | {renderMountElement()} 153 |
154 | } 155 | { modes.console && !transformError && !executionError && 156 | 161 | } 162 | { (transformError || executionError) && 163 |
164 |
165 |               Failed to Compile
166 |               {(transformError || executionError).toString()}
167 |             
168 |
169 | } 170 |
171 | ) 172 | }, 173 | 174 | renderEditor: function({ layout, value, onChange }) { 175 | return ( 176 | 182 | ) 183 | }, 184 | } 185 | 186 | const getType = function (el) { 187 | let t = typeof el; 188 | 189 | if (Array.isArray(el)) { 190 | t = "array"; 191 | } else if (el === null) { 192 | t = "null"; 193 | } 194 | 195 | return t; 196 | }; 197 | 198 | // Based on react-playground by Formidable Labs 199 | // See: https://github.com/FormidableLabs/component-playground/blob/master/src/components/es6-preview.jsx 200 | const wrapMap = { 201 | wrapnumber(num) { 202 | return {num}; 203 | }, 204 | 205 | wrapstring(str) { 206 | return {"'" + str + "'"}; 207 | }, 208 | 209 | wrapboolean(bool) { 210 | return {bool ? "true" : "false"}; 211 | }, 212 | 213 | wraparray(arr) { 214 | return ( 215 | 216 | {"["} 217 | {arr.map((entry, i) => { 218 | return ( 219 | 220 | {wrapMap["wrap" + getType(entry)](entry)} 221 | {i !== arr.length - 1 ? ", " : ""} 222 | 223 | ); 224 | })} 225 | {"]"} 226 | 227 | ); 228 | }, 229 | 230 | wrapobject(obj) { 231 | const pairs = []; 232 | let first = true; 233 | 234 | for (const key in obj) { 235 | pairs.push( 236 | 237 | 238 | {(first ? "" : ", ") + key} 239 | 240 | {": "} 241 | {wrapMap["wrap" + getType(obj[key])](obj[key])} 242 | 243 | ); 244 | 245 | first = false; 246 | } 247 | 248 | return {"Object {"}{pairs}{"}"}; 249 | }, 250 | 251 | wrapfunction() { 252 | return {"function"}; 253 | }, 254 | 255 | wrapnull() { 256 | return {"null"}; 257 | }, 258 | 259 | wrapundefined() { 260 | return {"undefined"}; 261 | } 262 | } 263 | 264 | 265 | function BreadboardConsole({ className, messages, style }) { 266 | return ( 267 |
268 | {messages.map(({ type, args }, i) => 269 |
270 | {args.map((arg, i) => 271 |
{wrapMap["wrap" + getType(arg)](arg)}
272 | )} 273 |
274 | )} 275 |
276 | ) 277 | } 278 | 279 | 280 | function normalizeLineEndings (str) { 281 | if (!str) return str; 282 | return str.replace(/\r\n|\r/g, '\n'); 283 | } 284 | 285 | // Based on these two files: 286 | // https://github.com/JedWatson/react-codemirror/blob/master/src/Codemirror.js 287 | // https://github.com/FormidableLabs/component-playground/blob/master/src/components/editor.jsx 288 | class JSXEditor extends Component { 289 | static propTypes = { 290 | theme: PropTypes.string, 291 | readOnly: PropTypes.bool, 292 | value: PropTypes.string, 293 | selectedLines: PropTypes.array, 294 | onChange: PropTypes.func, 295 | style: PropTypes.object, 296 | className: PropTypes.string 297 | } 298 | 299 | static defaultProps = { 300 | theme: "monokai", 301 | } 302 | 303 | state = { 304 | isFocused: false, 305 | } 306 | 307 | constructor(props) { 308 | super(props) 309 | 310 | this.componentWillReceiveProps = debounce(this.componentWillReceiveProps, 0) 311 | } 312 | 313 | componentDidMount() { 314 | const textareaNode = ReactDOM.findDOMNode(this.refs.textarea); 315 | const options = { 316 | mode: "jsx", 317 | lineNumbers: false, 318 | lineWrapping: false, 319 | smartIndent: false, 320 | matchBrackets: true, 321 | theme: this.props.theme, 322 | readOnly: this.props.readOnly, 323 | viewportMargin: Infinity, 324 | } 325 | 326 | this.codeMirror = codeMirror.fromTextArea(textareaNode, options); 327 | this.codeMirror.on('change', this.handleChange); 328 | this.codeMirror.on('focus', this.handleFocus.bind(this, true)); 329 | this.codeMirror.on('blur', this.handleFocus.bind(this, false)); 330 | this.codeMirror.on('scroll', this.handleScroll); 331 | this.codeMirror.setValue(this.props.defaultValue || this.props.value || ''); 332 | } 333 | 334 | componentWillReceiveProps(nextProps) { 335 | if (this.codeMirror && nextProps.value !== undefined && normalizeLineEndings(this.codeMirror.getValue()) !== normalizeLineEndings(nextProps.value)) { 336 | if (this.props.preserveScrollPosition) { 337 | var prevScrollPosition = this.codeMirror.getScrollInfo(); 338 | this.codeMirror.setValue(nextProps.value); 339 | this.codeMirror.scrollTo(prevScrollPosition.left, prevScrollPosition.top); 340 | } else { 341 | this.codeMirror.setValue(nextProps.value); 342 | } 343 | } 344 | } 345 | 346 | componentWillUnmount() { 347 | // is there a lighter-weight way to remove the cm instance? 348 | if (this.codeMirror) { 349 | this.codeMirror.toTextArea(); 350 | } 351 | } 352 | 353 | highlightSelectedLines = () => { 354 | if (Array.isArray(this.props.selectedLines)) { 355 | this.props.selectedLines.forEach(lineNumber => 356 | this.codeMirror.addLineClass(lineNumber, "wrap", "CodeMirror-activeline-background")) 357 | } 358 | } 359 | 360 | focus() { 361 | if (this.codeMirror) { 362 | this.codeMirror.focus() 363 | } 364 | } 365 | 366 | render() { 367 | return ( 368 |
369 |