From fa52102b7783739f78d5e0bae66b383ce106f83c Mon Sep 17 00:00:00 2001
From: Ray Schamp <ray@scratch.mit.edu>
Date: Sun, 16 Oct 2016 20:18:22 -0400
Subject: [PATCH] Container/presentation refactor

Move behavior-related components to "containers", leave presentation components in "components".
---
 src/components/blocks.js          | 117 ++--------------------------
 src/components/green-flag.js      |  21 ++---
 src/components/gui.js             |  54 +------------
 src/components/sprite-selector.js |  41 +++-------
 src/components/stage.js           |  90 +---------------------
 src/components/stop-all.js        |  20 ++---
 src/containers/blocks.js          | 124 ++++++++++++++++++++++++++++++
 src/containers/green-flag.js      |  24 ++++++
 src/containers/gui.js             |  63 +++++++++++++++
 src/containers/sprite-selector.js |  45 +++++++++++
 src/containers/stage.js           |  84 ++++++++++++++++++++
 src/containers/stop-all.js        |  29 +++++++
 src/index.js                      |   2 +-
 13 files changed, 402 insertions(+), 312 deletions(-)
 create mode 100644 src/containers/blocks.js
 create mode 100644 src/containers/green-flag.js
 create mode 100644 src/containers/gui.js
 create mode 100644 src/containers/sprite-selector.js
 create mode 100644 src/containers/stage.js
 create mode 100644 src/containers/stop-all.js

diff --git a/src/components/blocks.js b/src/components/blocks.js
index 56c30596c..fdd6f1467 100644
--- a/src/components/blocks.js
+++ b/src/components/blocks.js
@@ -1,78 +1,11 @@
-const bindAll = require('lodash.bindall');
-const defaultsDeep = require('lodash.defaultsdeep');
 const React = require('react');
-const ScratchBlocks = require('scratch-blocks');
 
-class Blocks extends React.Component {
-    constructor (props) {
-        super(props);
-        bindAll(this, [
-            'attachVM',
-            'detachVM',
-            'onStackGlowOn',
-            'onStackGlowOff',
-            'onBlockGlowOn',
-            'onBlockGlowOff',
-            'onVisualReport',
-            'onWorkspaceUpdate'
-        ]);
-    }
-    componentDidMount () {
-        let workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options);
-        this.workspace = ScratchBlocks.inject(this.refs.scratchBlocks, workspaceConfig);
-        this.attachVM();
-    }
-    componentWillUnmount () {
-        this.detachVM();
-        this.workspace.dispose();
-    }
-    attachVM () {
-        this.workspace.addChangeListener(this.props.vm.blockListener);
-        this.workspace.getFlyout().getWorkspace().addChangeListener(
-            this.props.vm.flyoutBlockListener
-        );
-        this.props.vm.on('STACK_GLOW_ON', this.onStackGlowOn);
-        this.props.vm.on('STACK_GLOW_OFF', this.onStackGlowOff);
-        this.props.vm.on('BLOCK_GLOW_ON', this.onBlockGlowOn);
-        this.props.vm.on('BLOCK_GLOW_OFF', this.onBlockGlowOff);
-        this.props.vm.on('VISUAL_REPORT', this.onVisualReport);
-        this.props.vm.on('workspaceUpdate', this.onWorkspaceUpdate);
-    }
-    detachVM () {
-        this.props.vm.off('STACK_GLOW_ON', this.onStackGlowOn);
-        this.props.vm.off('STACK_GLOW_OFF', this.onStackGlowOff);
-        this.props.vm.off('BLOCK_GLOW_ON', this.onBlockGlowOn);
-        this.props.vm.off('BLOCK_GLOW_OFF', this.onBlockGlowOff);
-        this.props.vm.off('VISUAL_REPORT', this.onVisualReport);
-        this.props.vm.off('workspaceUpdate', this.onWorkspaceUpdate);
-    }
-    onStackGlowOn (data) {
-        this.workspace.glowStack(data.id, true);
-    }
-    onStackGlowOff (data) {
-        this.workspace.glowStack(data.id, false);
-    }
-    onBlockGlowOn (data) {
-        this.workspace.glowBlock(data.id, true);
-    }
-    onBlockGlowOff (data) {
-        this.workspace.glowBlock(data.id, false);
-    }
-    onVisualReport (data) {
-        this.workspace.reportValue(data.id, data.value);
-    }
-    onWorkspaceUpdate (data) {
-        ScratchBlocks.Events.disable();
-        this.workspace.clear();
-        let dom = ScratchBlocks.Xml.textToDom(data.xml);
-        ScratchBlocks.Xml.domToWorkspace(dom, this.workspace);
-        ScratchBlocks.Events.enable();
-    }
+class BlocksComponent extends React.Component {
     render () {
         return (
             <div
+                ref={this.props.componentRef}
                 className="scratch-blocks"
-                ref="scratchBlocks"
                 style={{
                     position: 'absolute',
                     top: 0,
@@ -85,48 +18,8 @@ class Blocks extends React.Component {
     }
 }
 
-Blocks.propTypes = {
-    options: React.PropTypes.shape({
-        media: React.PropTypes.string,
-        zoom: React.PropTypes.shape({
-            controls: React.PropTypes.boolean,
-            wheel: React.PropTypes.boolean,
-            startScale: React.PropTypes.number
-        }),
-        colours: React.PropTypes.shape({
-            workspace: React.PropTypes.string,
-            flyout: React.PropTypes.string,
-            scrollbar: React.PropTypes.string,
-            scrollbarHover: React.PropTypes.string,
-            insertionMarker: React.PropTypes.string,
-            insertionMarkerOpacity: React.PropTypes.number,
-            fieldShadow: React.PropTypes.string,
-            dragShadowOpacity: React.PropTypes.number
-        })
-    }),
-    vm: React.PropTypes.object
-};
-
-Blocks.defaultOptions = {
-    zoom: {
-        controls: true,
-        wheel: true,
-        startScale: 0.75
-    },
-    colours: {
-        workspace: '#334771',
-        flyout: '#283856',
-        scrollbar: '#24324D',
-        scrollbarHover: '#0C111A',
-        insertionMarker: '#FFFFFF',
-        insertionMarkerOpacity: 0.3,
-        fieldShadow: 'rgba(255, 255, 255, 0.3)',
-        dragShadowOpacity: 0.6
-    }
-};
-
-Blocks.defaultProps = {
-    options: Blocks.defaultOptions
+BlocksComponent.propTypes = {
+    componentRef: React.PropTypes.func
 };
 
-module.exports = Blocks;
+module.exports = BlocksComponent;
diff --git a/src/components/green-flag.js b/src/components/green-flag.js
index 03d3f7d9b..20be45505 100644
--- a/src/components/green-flag.js
+++ b/src/components/green-flag.js
@@ -1,15 +1,6 @@
-const bindAll = require('lodash.bindall');
 const React = require('react');
 
-class GreenFlag extends React.Component {
-    constructor (props) {
-        super(props);
-        bindAll(this, ['onClick']);
-    }
-    onClick (e) {
-        e.preventDefault();
-        this.props.vm.greenFlag();
-    }
+class GreenFlagComponent extends React.Component {
     render () {
         return (
             <div
@@ -21,19 +12,19 @@ class GreenFlag extends React.Component {
                     width: 50
                 }}
             >
-                <button onClick={this.onClick}>{this.props.title}</button>
+                <button onClick={this.props.onClick}>{this.props.title}</button>
             </div>
         );
     }
 }
 
-GreenFlag.propTypes = {
+GreenFlagComponent.propTypes = {
+    onClick: React.PropTypes.func,
     title: React.PropTypes.string,
-    vm: React.PropTypes.object
 };
 
-GreenFlag.defaultProps = {
+GreenFlagComponent.defaultProps = {
     title: 'Go'
 };
 
-module.exports = GreenFlag;
+module.exports = GreenFlagComponent;
diff --git a/src/components/gui.js b/src/components/gui.js
index b21acc0cc..a6dc55238 100644
--- a/src/components/gui.js
+++ b/src/components/gui.js
@@ -1,34 +1,6 @@
 const React = require('react');
 
-const Blocks = require('./blocks');
-const GreenFlag = require('./green-flag');
-const SpriteSelector = require('./sprite-selector');
-const Stage = require('./stage');
-const StopAll = require('./stop-all');
-const VM = require('scratch-vm');
-const VMManager = require('../lib/vm-manager');
-
-class GUI extends React.Component {
-    constructor (props) {
-        super(props);
-        this.vmManager = new VMManager(this.props.vm);
-    }
-    componentDidMount () {
-        this.vmManager.attachKeyboardEvents();
-        this.props.vm.loadProject(this.props.projectData);
-        this.props.vm.start();
-        this.vmManager.startAnimation();
-    }
-    componentWillUnmount () {
-        this.vmManager.detachKeyboardEvents();
-        this.props.vm.stopAll();
-        this.vmManager.stopAnimation();
-    }
-    componentWillReceiveProps (nextProps) {
-        if (this.props.projectData !== nextProps.projectData) {
-            this.props.vm.loadProject(nextProps.projectData);
-        }
-    }
+class GUIComponent extends React.Component {
     render () {
         return (
             <div
@@ -41,30 +13,10 @@ class GUI extends React.Component {
                     left: 0
                 }}
             >
-                <GreenFlag vm={this.props.vm} />
-                <StopAll vm={this.props.vm} />
-                <Stage vm={this.props.vm} />
-                <SpriteSelector vm={this.props.vm} />
-                <Blocks
-                    options={{
-                        media: this.props.basePath + 'static/blocks-media/'
-                    }}
-                    vm={this.props.vm}
-                />
+                {this.props.children}
             </div>
         );
     }
 }
 
-GUI.propTypes = {
-    basePath: React.PropTypes.string,
-    projectData: React.PropTypes.string,
-    vm: React.PropTypes.object,
-};
-
-GUI.defaultProps = {
-    basePath: '/',
-    vm: new VM()
-};
-
-module.exports = GUI;
+module.exports = GUIComponent;
diff --git a/src/components/sprite-selector.js b/src/components/sprite-selector.js
index 2a6117517..65c0ff2bd 100644
--- a/src/components/sprite-selector.js
+++ b/src/components/sprite-selector.js
@@ -1,25 +1,6 @@
-const bindAll = require('lodash.bindall');
 const React = require('react');
 
-class SpriteSelector extends React.Component {
-    constructor (props) {
-        super(props);
-        bindAll(this, ['onChange', 'targetsUpdate']);
-        this.state = {
-            targets: {
-                targetList: []
-            }
-        };
-    }
-    componentDidMount () {
-        this.props.vm.on('targetsUpdate', this.targetsUpdate);
-    }
-    onChange (event) {
-        this.props.vm.setEditingTarget(event.target.value);
-    }
-    targetsUpdate (data) {
-        this.setState({targets: data});
-    }
+class SpriteSelectorComponent extends React.Component {
     render () {
         return (
             <div
@@ -31,22 +12,18 @@ class SpriteSelector extends React.Component {
             >
                 <select
                     multiple
-                    value={[this.state.targets.editingTarget]}
-                    onChange={this.onChange}
-
+                    value={this.props.value}
+                    onChange={this.props.onChange}
                 >
-                    {this.state.targets.targetList.map(
-                        target => <option value={target[0]} key={target[0]}>{target[1]}</option>
-                    )}
+                    {this.props.sprites.map(sprite => (
+                        <option value={sprite.id} key={sprite.id}>
+                            {sprite.name}
+                        </option>
+                    ))}
                 </select>
             </div>
-
         );
     }
 }
 
-SpriteSelector.propTypes = {
-    vm: React.PropTypes.object.isRequired
-};
-
-module.exports = SpriteSelector;
+module.exports = SpriteSelectorComponent;
diff --git a/src/components/stage.js b/src/components/stage.js
index 40601d2fa..4cdbc7895 100644
--- a/src/components/stage.js
+++ b/src/components/stage.js
@@ -1,88 +1,6 @@
-const bindAll = require('lodash.bindall');
 const React = require('react');
-const Renderer = require('scratch-render');
 
-class Stage extends React.Component {
-    constructor (props) {
-        super(props);
-        bindAll(this, [
-            'attachMouseEvents',
-            'detachMouseEvents',
-            'onMouseUp',
-            'onMouseMove',
-            'onMouseDown'
-        ]);
-    }
-    componentDidMount () {
-        this.renderer = new Renderer(this.canvas);
-        this.props.vm.attachRenderer(this.renderer);
-        this.attachMouseEvents(this.canvas);
-    }
-    componentWillUnmount () {
-        this.detachMouseEvents(this.canvas);
-    }
-    attachMouseEvents (canvas) {
-        document.addEventListener('mousemove', this.onMouseMove);
-        canvas.addEventListener('mouseup', this.onMouseUp);
-        canvas.addEventListener('mousedown', this.onMouseDown);
-    }
-    detachMouseEvents (canvas) {
-        document.removeEventListener('mousemove', this.onMouseMove);
-        canvas.removeEventListener('mouseup', this.onMouseUp);
-        canvas.removeEventListener('mousedown', this.onMouseDown);
-    }
-    onMouseMove (e) {
-        let rect = this.canvas.getBoundingClientRect();
-        let coordinates = {
-            x: e.clientX - rect.left,
-            y: e.clientY - rect.top,
-            canvasWidth: rect.width,
-            canvasHeight: rect.height
-        };
-        this.props.vm.postIOData('mouse', coordinates);
-    }
-    onMouseUp (e) {
-        let rect = this.canvas.getBoundingClientRect();
-        let data = {
-            isDown: false,
-            x: e.clientX - rect.left,
-            y: e.clientY - rect.top,
-            canvasWidth: rect.width,
-            canvasHeight: rect.height
-        };
-        this.props.vm.postIOData('mouse', data);
-        e.preventDefault();
-    }
-    onMouseDown (e) {
-        let rect = this.canvas.getBoundingClientRect();
-        let data = {
-            isDown: true,
-            x: e.clientX - rect.left,
-            y: e.clientY - rect.top,
-            canvasWidth: rect.width,
-            canvasHeight: rect.height
-        };
-        this.props.vm.postIOData('mouse', data);
-        e.preventDefault();
-    }
-    render () {
-        return (
-            <StageCanvas
-                {... this.props}
-                canvasRef={canvas => this.canvas = canvas}
-            />
-        );
-    }
-}
-
-Stage.propTypes = {
-    vm: React.PropTypes.shape({
-        attachRenderer: React.PropTypes.func,
-        postIOData: React.PropTypes.func
-    }).isRequired
-};
-
-class StageCanvas extends React.Component {
+class StageComponent extends React.Component {
     render () {
         return (
             <canvas
@@ -100,16 +18,16 @@ class StageCanvas extends React.Component {
     }
 }
 
-StageCanvas.propTypes = {
+StageComponent.propTypes = {
     canvasRef: React.PropTypes.func,
     width: React.PropTypes.number,
     height: React.PropTypes.number
 };
 
-StageCanvas.defaultProps = {
+StageComponent.defaultProps = {
     canvasRef: function () {},
     width: 480,
     height: 360
 };
 
-module.exports = Stage;
+module.exports = StageComponent;
diff --git a/src/components/stop-all.js b/src/components/stop-all.js
index 321478f9b..123b00a3d 100644
--- a/src/components/stop-all.js
+++ b/src/components/stop-all.js
@@ -1,15 +1,6 @@
-const bindAll = require('lodash.bindall');
 const React = require('react');
 
-class StopAll extends React.Component {
-    constructor (props) {
-        super(props);
-        bindAll(this, ['onClick']);
-    }
-    onClick (e) {
-        e.preventDefault();
-        this.props.vm.stopAll();
-    }
+class StopAllComponent extends React.Component {
     render () {
         return (
             <div
@@ -21,19 +12,18 @@ class StopAll extends React.Component {
                     width: 50
                 }}
             >
-                <button onClick={this.onClick}>{this.props.title}</button>
+                <button onClick={this.props.onClick}>{this.props.title}</button>
             </div>
         );
     }
 }
 
-StopAll.propTypes = {
+StopAllComponent.propTypes = {
     title: React.PropTypes.string,
-    vm: React.PropTypes.object
 };
 
-StopAll.defaultProps = {
+StopAllComponent.defaultProps = {
     title: 'Stop'
 };
 
-module.exports = StopAll;
+module.exports = StopAllComponent;
diff --git a/src/containers/blocks.js b/src/containers/blocks.js
new file mode 100644
index 000000000..5a6993c43
--- /dev/null
+++ b/src/containers/blocks.js
@@ -0,0 +1,124 @@
+const bindAll = require('lodash.bindall');
+const defaultsDeep = require('lodash.defaultsdeep');
+const React = require('react');
+const ScratchBlocks = require('scratch-blocks');
+
+const BlocksComponent = require('../components/blocks');
+
+class Blocks extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'attachVM',
+            'detachVM',
+            'onStackGlowOn',
+            'onStackGlowOff',
+            'onBlockGlowOn',
+            'onBlockGlowOff',
+            'onVisualReport',
+            'onWorkspaceUpdate'
+        ]);
+    }
+    componentDidMount () {
+        let workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options);
+        this.workspace = ScratchBlocks.inject(this.blocks, workspaceConfig);
+        this.attachVM();
+    }
+    componentWillUnmount () {
+        this.detachVM();
+        this.workspace.dispose();
+    }
+    attachVM () {
+        this.workspace.addChangeListener(this.props.vm.blockListener);
+        this.workspace.getFlyout().getWorkspace().addChangeListener(
+            this.props.vm.flyoutBlockListener
+        );
+        this.props.vm.on('STACK_GLOW_ON', this.onStackGlowOn);
+        this.props.vm.on('STACK_GLOW_OFF', this.onStackGlowOff);
+        this.props.vm.on('BLOCK_GLOW_ON', this.onBlockGlowOn);
+        this.props.vm.on('BLOCK_GLOW_OFF', this.onBlockGlowOff);
+        this.props.vm.on('VISUAL_REPORT', this.onVisualReport);
+        this.props.vm.on('workspaceUpdate', this.onWorkspaceUpdate);
+    }
+    detachVM () {
+        this.props.vm.off('STACK_GLOW_ON', this.onStackGlowOn);
+        this.props.vm.off('STACK_GLOW_OFF', this.onStackGlowOff);
+        this.props.vm.off('BLOCK_GLOW_ON', this.onBlockGlowOn);
+        this.props.vm.off('BLOCK_GLOW_OFF', this.onBlockGlowOff);
+        this.props.vm.off('VISUAL_REPORT', this.onVisualReport);
+        this.props.vm.off('workspaceUpdate', this.onWorkspaceUpdate);
+    }
+    onStackGlowOn (data) {
+        this.workspace.glowStack(data.id, true);
+    }
+    onStackGlowOff (data) {
+        this.workspace.glowStack(data.id, false);
+    }
+    onBlockGlowOn (data) {
+        this.workspace.glowBlock(data.id, true);
+    }
+    onBlockGlowOff (data) {
+        this.workspace.glowBlock(data.id, false);
+    }
+    onVisualReport (data) {
+        this.workspace.reportValue(data.id, data.value);
+    }
+    onWorkspaceUpdate (data) {
+        ScratchBlocks.Events.disable();
+        this.workspace.clear();
+        let dom = ScratchBlocks.Xml.textToDom(data.xml);
+        ScratchBlocks.Xml.domToWorkspace(dom, this.workspace);
+        ScratchBlocks.Events.enable();
+    }
+    render () {
+        return (
+            <BlocksComponent componentRef={c => this.blocks = c} />
+        );
+    }
+}
+
+Blocks.propTypes = {
+    options: React.PropTypes.shape({
+        media: React.PropTypes.string,
+        zoom: React.PropTypes.shape({
+            controls: React.PropTypes.boolean,
+            wheel: React.PropTypes.boolean,
+            startScale: React.PropTypes.number
+        }),
+        colours: React.PropTypes.shape({
+            workspace: React.PropTypes.string,
+            flyout: React.PropTypes.string,
+            scrollbar: React.PropTypes.string,
+            scrollbarHover: React.PropTypes.string,
+            insertionMarker: React.PropTypes.string,
+            insertionMarkerOpacity: React.PropTypes.number,
+            fieldShadow: React.PropTypes.string,
+            dragShadowOpacity: React.PropTypes.number
+        })
+    }),
+    vm: React.PropTypes.object
+};
+
+Blocks.defaultOptions = {
+    zoom: {
+        controls: true,
+        wheel: true,
+        startScale: 0.75
+    },
+    colours: {
+        workspace: '#334771',
+        flyout: '#283856',
+        scrollbar: '#24324D',
+        scrollbarHover: '#0C111A',
+        insertionMarker: '#FFFFFF',
+        insertionMarkerOpacity: 0.3,
+        fieldShadow: 'rgba(255, 255, 255, 0.3)',
+        dragShadowOpacity: 0.6
+    }
+};
+
+Blocks.defaultProps = {
+    options: Blocks.defaultOptions
+};
+
+module.exports = Blocks;
diff --git a/src/containers/green-flag.js b/src/containers/green-flag.js
new file mode 100644
index 000000000..da97dff71
--- /dev/null
+++ b/src/containers/green-flag.js
@@ -0,0 +1,24 @@
+const bindAll = require('lodash.bindall');
+const React = require('react');
+
+const GreenFlagComponent = require('../components/green-flag');
+
+class GreenFlag extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, ['onClick']);
+    }
+    onClick (e) {
+        e.preventDefault();
+        this.props.vm.greenFlag();
+    }
+    render () {
+        return <GreenFlagComponent onClick={this.onClick} />;
+    }
+}
+
+GreenFlag.propTypes = {
+    vm: React.PropTypes.object
+};
+
+module.exports = GreenFlag;
diff --git a/src/containers/gui.js b/src/containers/gui.js
new file mode 100644
index 000000000..ba398cdd6
--- /dev/null
+++ b/src/containers/gui.js
@@ -0,0 +1,63 @@
+const React = require('react');
+const VM = require('scratch-vm');
+
+const VMManager = require('../lib/vm-manager');
+
+const Blocks = require('./blocks');
+const GUIComponent = require('../components/gui');
+const GreenFlag = require('./green-flag');
+const SpriteSelector = require('./sprite-selector');
+const Stage = require('./stage');
+const StopAll = require('./stop-all');
+
+class GUI extends React.Component {
+    constructor (props) {
+        super(props);
+        this.vmManager = new VMManager(this.props.vm);
+    }
+    componentDidMount () {
+        this.vmManager.attachKeyboardEvents();
+        this.props.vm.loadProject(this.props.projectData);
+        this.props.vm.start();
+        this.vmManager.startAnimation();
+    }
+    componentWillUnmount () {
+        this.vmManager.detachKeyboardEvents();
+        this.props.vm.stopAll();
+        this.vmManager.stopAnimation();
+    }
+    componentWillReceiveProps (nextProps) {
+        if (this.props.projectData !== nextProps.projectData) {
+            this.props.vm.loadProject(nextProps.projectData);
+        }
+    }
+    render () {
+        return (
+            <GUIComponent>
+                <GreenFlag vm={this.props.vm} />
+                <StopAll vm={this.props.vm} />
+                <Stage vm={this.props.vm} />
+                <SpriteSelector vm={this.props.vm} />
+                <Blocks
+                    options={{
+                        media: this.props.basePath + 'static/blocks-media/'
+                    }}
+                    vm={this.props.vm}
+                />
+            </GUIComponent>
+        );
+    }
+}
+
+GUI.propTypes = {
+    basePath: React.PropTypes.string,
+    projectData: React.PropTypes.string,
+    vm: React.PropTypes.object,
+};
+
+GUI.defaultProps = {
+    basePath: '/',
+    vm: new VM()
+};
+
+module.exports = GUI;
diff --git a/src/containers/sprite-selector.js b/src/containers/sprite-selector.js
new file mode 100644
index 000000000..173717960
--- /dev/null
+++ b/src/containers/sprite-selector.js
@@ -0,0 +1,45 @@
+const bindAll = require('lodash.bindall');
+const React = require('react');
+
+const SpriteSelectorComponent = require('../components/sprite-selector');
+
+class SpriteSelector extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, ['onChange', 'targetsUpdate']);
+        this.state = {
+            targets: {
+                targetList: []
+            }
+        };
+    }
+    componentDidMount () {
+        this.props.vm.on('targetsUpdate', this.targetsUpdate);
+    }
+    onChange (event) {
+        this.props.vm.setEditingTarget(event.target.value);
+    }
+    targetsUpdate (data) {
+        this.setState({targets: data});
+    }
+    render () {
+        return (
+            <SpriteSelectorComponent
+                value={[this.state.targets.editingTarget]}
+                onChange={this.onChange}
+                sprites={this.state.targets.targetList.map(target => (
+                    {
+                        id: target[0],
+                        name: target[1]
+                    }
+                ))}
+            />
+        );
+    }
+}
+
+SpriteSelector.propTypes = {
+    vm: React.PropTypes.object.isRequired
+};
+
+module.exports = SpriteSelector;
diff --git a/src/containers/stage.js b/src/containers/stage.js
new file mode 100644
index 000000000..aafe19892
--- /dev/null
+++ b/src/containers/stage.js
@@ -0,0 +1,84 @@
+const bindAll = require('lodash.bindall');
+const React = require('react');
+const Renderer = require('scratch-render');
+
+const StageComponent = require('../components/stage');
+
+class Stage extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'attachMouseEvents',
+            'detachMouseEvents',
+            'onMouseUp',
+            'onMouseMove',
+            'onMouseDown'
+        ]);
+    }
+    componentDidMount () {
+        this.renderer = new Renderer(this.canvas);
+        this.props.vm.attachRenderer(this.renderer);
+        this.attachMouseEvents(this.canvas);
+    }
+    componentWillUnmount () {
+        this.detachMouseEvents(this.canvas);
+    }
+    attachMouseEvents (canvas) {
+        document.addEventListener('mousemove', this.onMouseMove);
+        canvas.addEventListener('mouseup', this.onMouseUp);
+        canvas.addEventListener('mousedown', this.onMouseDown);
+    }
+    detachMouseEvents (canvas) {
+        document.removeEventListener('mousemove', this.onMouseMove);
+        canvas.removeEventListener('mouseup', this.onMouseUp);
+        canvas.removeEventListener('mousedown', this.onMouseDown);
+    }
+    onMouseMove (e) {
+        let rect = this.canvas.getBoundingClientRect();
+        let coordinates = {
+            x: e.clientX - rect.left,
+            y: e.clientY - rect.top,
+            canvasWidth: rect.width,
+            canvasHeight: rect.height
+        };
+        this.props.vm.postIOData('mouse', coordinates);
+    }
+    onMouseUp (e) {
+        let rect = this.canvas.getBoundingClientRect();
+        let data = {
+            isDown: false,
+            x: e.clientX - rect.left,
+            y: e.clientY - rect.top,
+            canvasWidth: rect.width,
+            canvasHeight: rect.height
+        };
+        this.props.vm.postIOData('mouse', data);
+        e.preventDefault();
+    }
+    onMouseDown (e) {
+        let rect = this.canvas.getBoundingClientRect();
+        let data = {
+            isDown: true,
+            x: e.clientX - rect.left,
+            y: e.clientY - rect.top,
+            canvasWidth: rect.width,
+            canvasHeight: rect.height
+        };
+        this.props.vm.postIOData('mouse', data);
+        e.preventDefault();
+    }
+    render () {
+        return (
+            <StageComponent canvasRef={canvas => this.canvas = canvas} />
+        );
+    }
+}
+
+Stage.propTypes = {
+    vm: React.PropTypes.shape({
+        attachRenderer: React.PropTypes.func,
+        postIOData: React.PropTypes.func
+    }).isRequired
+};
+
+module.exports = Stage;
diff --git a/src/containers/stop-all.js b/src/containers/stop-all.js
new file mode 100644
index 000000000..9e08aa16a
--- /dev/null
+++ b/src/containers/stop-all.js
@@ -0,0 +1,29 @@
+const bindAll = require('lodash.bindall');
+const React = require('react');
+
+const StopAllComponent = require('../components/stop-all');
+
+class StopAll extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, ['onClick']);
+    }
+    onClick (e) {
+        e.preventDefault();
+        this.props.vm.stopAll();
+    }
+    render () {
+        return (
+            <StopAllComponent
+                onClick={this.onClick}
+                {... this.props}
+            />
+        );
+    }
+}
+
+StopAll.propTypes = {
+    vm: React.PropTypes.object
+};
+
+module.exports = StopAll;
diff --git a/src/index.js b/src/index.js
index f2a708cc7..573f3a983 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,6 @@
 const React = require('react');
 const ReactDOM = require('react-dom');
-const GUI = require('./components/gui');
+const GUI = require('./containers/gui');
 const log = require('./lib/log');
 const ProjectLoader = require('./lib/project-loader');
 
-- 
GitLab