armo-breadboard
Version:
Edit a live React component's source in real time.
434 lines (348 loc) • 15.9 kB
JavaScript
'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;