UNPKG

armo-breadboard

Version:

Edit a live React component's source in real time.

434 lines (348 loc) 15.9 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = undefined; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _class, _temp, _initialiseProps; var _exenv = require('exenv'); var _exenv2 = _interopRequireDefault(_exenv); var _hatt = require('hatt'); var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _reactDom = require('react-dom'); var _reactDom2 = _interopRequireDefault(_reactDom); var _server = require('react-dom/server'); var _server2 = _interopRequireDefault(_server); var _ConsoleController = require('./ConsoleController'); var _ConsoleController2 = _interopRequireDefault(_ConsoleController); var _FakeWindow = require('./FakeWindow'); var _FakeWindow2 = _interopRequireDefault(_FakeWindow); var _util = require('./util'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } function defaultBreadboardRequire(name) { if (name === 'react') { return _react2.default; } } function defaultRenderToString(source, require, window, props) { try { var execute; var exports = {}; var module = { exports: exports }; eval('execute = function execute(module, exports, require, window, console) { ' + source + ' }'); execute(module, exports, require, window, window.console); var component = exports.default; return _server2.default.renderToString(_react2.default.createElement(component, props)); } catch (err) { return err; } } function defaultPrepare(source, require, window) { try { var exports = {}; var module = { exports: exports }; var execute = new Function('window', 'setTimeout', 'setInterval', 'requestAnimationFrame', 'fetch', 'History', 'console', 'module', 'exports', 'require', source); execute(window, window.setTimeout, window.setInterval, window.requestAnimationFrame, window.fetch, window.History, window.console, module, exports, require); var component = exports.default; return function (mount) { var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (component) { try { _reactDom2.default.render(_react2.default.createElement(component, props), mount); } catch (err) { return err; } } }; } catch (err) { return function () { return err; }; } } var Breadboard = (_temp = _class = function (_Component) { _inherits(Breadboard, _Component); function Breadboard(props) { _classCallCheck(this, Breadboard); var _this = _possibleConstructorReturn(this, (Breadboard.__proto__ || Object.getPrototypeOf(Breadboard)).call(this, props)); _initialiseProps.call(_this); var source = props.defaultSource.replace(/^\n|\n$/g, ''); _this.consoleController = (0, _hatt.createController)(_ConsoleController2.default); _this.consoleController.thaw(); _this.fakeWindow = new _FakeWindow2.default(_this.consoleController.get().actions); _this.debouncedChangeSource = (0, _util.debounce)(_this.changeSource, 100); _this.viewController = props.viewController; var modes = _this.props.modes; _this.state = { consoleMessages: [], source: source, editorSource: source, value: null, modes: modes, transformedSource: null, executableSource: null, transformError: null, renderer: null, executionError: null }; if (_exenv2.default.canUseDOM && props.viewController) { props.viewController.subscribe(_this.handleViewUpdate); } var execute = modes.view || modes.console; if (modes.transformed || execute) { var _this$props$transform = _this.props.transform(source), transformedSource = _this$props$transform.transformedSource, executableSource = _this$props$transform.executableSource, error = _this$props$transform.error; _this.state.transformedSource = transformedSource; _this.state.executableSource = executableSource; _this.state.transformError = error; if (execute && executableSource) { if (props.renderToString) { _this.state.string = props.renderToString(executableSource, props.require, _this.fakeWindow.actions, props.viewController && props.viewController.get()); } if (_exenv2.default.canUseDOM) { _this.fakeWindow.reset(); _this.state.renderer = props.prepare(executableSource, props.require, _this.fakeWindow.actions); } } } _this.state.consoleMessages = _this.consoleController.get().messages; return _this; } _createClass(Breadboard, [{ key: 'componentDidMount', value: function componentDidMount() { this.consoleController.subscribe(this.handleConsoleChange); // Use this instead of the `modes` on state, as if the above // manageDimensions call has caused a change, it may not have // propagated through to `this.state` yet. if (this.props.modes.view) { var viewController = this.props.viewController; this.execute(viewController && viewController.get()); } } }, { key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { if (nextProps.viewController !== this.viewController) { console.warn('Breadboard does not currently support changes to the `viewController` prop!'); } if (nextProps.modes !== this.props.modes) { this.handleModesChange(nextProps.modes); } if (nextProps.transform !== this.props.transform || nextProps.prepare !== this.props.prepare || nextProps.require !== this.props.require) { this.setState(this.transformAndPrepare(this.state.source, nextProps, nextProps.modes) || {}); } } }, { key: 'componentDidUpdate', value: function componentDidUpdate(prevProps, prevState) { var modes = this.state.modes; if ((modes.view || modes.console) && (this.state.renderer !== prevState.renderer || !(prevState.modes.view || prevState.modes.console))) { try { _reactDom2.default.unmountComponentAtNode(this.refs.mount); } catch (e) {} var viewController = this.viewController; this.execute(viewController && viewController.get()); } } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { this.consoleController.destroy(); this.fakeWindow.destroy(); try { _reactDom2.default.unmountComponentAtNode(this.refs.mount); } catch (e) {} } // Used so to create debouncedChangeSource. This is separate to the event handler // as React doesn't like us keeping the event objects around for the completion of // the timeout. }, { key: 'render', value: function render() { // Generate the mount elememnt here to ensure that the ref attaches to // this component instance this.mountElement = _exenv2.default.canUseDOM ? _react2.default.createElement('div', { ref: 'mount' }) : _react2.default.createElement('div', { ref: 'mount', dangerouslySetInnerHTML: { __html: this.state.string } }); var rootElement = this.props.theme({ consoleMessages: this.state.consoleMessages, transformedSource: this.state.transformedSource, transformError: this.state.transformError, executionError: this.state.executionError, renderEditorElement: this.renderEditorElement, renderMountElement: this.renderMountElement, modes: this.state.modes, modeActions: this.state.modes }); return _react2.default.cloneElement(rootElement, { ref: this.setRootElement }); } }, { key: 'transformAndPrepare', value: function transformAndPrepare(source, props, modes) { var state = this.state; var execute = modes.view || modes.console; if (execute || modes.transformed) { var _props$transform = props.transform(source), transformedSource = _props$transform.transformedSource, executableSource = _props$transform.executableSource, error = _props$transform.error; this.fakeWindow.reset(); if (transformedSource !== state.transformedSource || executableSource !== state.executableSource || error !== state.transformError) { var result = { transformError: error, transformedSource: transformedSource, executableSource: executableSource }; if (execute && executableSource) { result.executionError = null; result.renderer = props.prepare(executableSource, props.require, this.fakeWindow.actions); } return result; } } } }, { key: 'execute', value: function execute(viewProps) { if (this.state.renderer) { var executionError = this.state.renderer(this.refs.mount, viewProps || {}); if (executionError) { this.setState({ executionError: executionError }); } } } }]); return Breadboard; }(_react.Component), _class.propTypes = { /** * A string containing the original source. Updates to the source will * be stored in component state. Updates to `defaultSource` will not be * reflected once the source has undergone any change. */ defaultSource: _react.PropTypes.string.isRequired, /** * A Controller output that keeps track of the current visible modes. * Breadboard will only compile and/or execute code when it is required. */ modes: _react.PropTypes.object.isRequired, /** * A function that takes the transformed source and returns a function * that can be used to render a value from the controller to the mount. */ prepare: _react.PropTypes.func.isRequired, /** * A controller whose state will be injected into the preview element's * props. If non-existent, we'll assume that our source calls render * manually. */ viewController: _react.PropTypes.object, /** * Allows you to configure the editor component. Accepts a function that * takes a `{ layout, value, onChange }`, and returns an editor element. */ renderEditorElement: _react.PropTypes.func.isRequired, /** * An optional function that renders the source with a given controller * state to a string suitable for use with server side rendering. */ renderToString: _react.PropTypes.func, /** * The function that will be used to handle CommonJS `require()` calls * within the evaluated code. Defaults to a function that only provides * the `react` module. */ require: _react.PropTypes.func, /** * A function that renders the breadboard given a set of state and * event handlers. */ theme: _react.PropTypes.func.isRequired, /** * A function that transforms the source before evaluating it. * * Transform functions are often pretty heavy, so we don't include anything * by default. */ transform: _react.PropTypes.func }, _class.defaultProps = { prepare: defaultPrepare, renderToString: defaultRenderToString, require: defaultBreadboardRequire }, _initialiseProps = function _initialiseProps() { var _this2 = this; this.handleModesChange = function (modes) { var prevModes = _this2.state.modes; var prevExecute = prevModes.view || prevModes.console; var nextExecute = modes.view || modes.console; var updates = { modes: modes }; if (!prevModes.transformed && modes.transformed || !prevExecute && nextExecute) { Object.assign(updates, _this2.transformAndPrepare(_this2.state.source, _this2.props, modes)); } _this2.setState(updates); }; this.changeSource = function (source) { if (source !== _this2.state.source) { _this2.setState(_extends({ source: source }, _this2.transformAndPrepare(source, _this2.props, _this2.props.modes))); } }; this.handleChangeSource = function (e) { var source = typeof e === 'string' ? e : e && e.target && e.target.value; _this2.setState({ editorSource: source }); _this2.debouncedChangeSource(source); }; this.handleConsoleChange = function (_ref) { var messages = _ref.messages; _this2.setState({ consoleMessages: messages }); }; this.handleViewUpdate = function (viewProps) { if (_this2.state.modes.view || _this2.state.modes.console) { _this2.execute(viewProps); } }; this.renderEditorElement = function () { var themeableProps = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; if (process.env.NODE_ENV !== 'production') { // Editor components are complicated beings, and probably will feel the // same way about being "styled" as a dog feels about taking a bath. // // If you want to theme your editor, you'll need to do so by passing in // an already themed editor. The only condition is that it accepts // layout styles via `style`, a `value` with the current source, and an // `onChange` callback that notifies us of a new value. (0, _util.verifyThemePropTypes)(themeableProps, { layout: true }); } return _this2.props.renderEditorElement({ layout: themeableProps.layout, value: _this2.state.editorSource, onChange: _this2.handleChangeSource }); }; this.renderMountElement = function () { var themeableProps = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; if (process.env.NODE_ENV !== 'production') { (0, _util.verifyMissingProps)(themeableProps, ['children', 'style']); } var layout = themeableProps.layout, other = _objectWithoutProperties(themeableProps, ['layout']); return _react2.default.cloneElement(_this2.mountElement, _extends({}, other, { style: layout })); }; this.setRootElement = function (el) { _this2.rootElement = el; }; }, _temp); exports.default = Breadboard;