simplifying javascript projects with reactjs
TRANSCRIPT
Simplifying JavaScript Projects with ReactJS and Friends
Kevin Dangoor, Sr. Computer Scientist, Adobe 1DevDay Detroit 2014
A modern, open source text editor that understands web design.
Brackets• MIT-licensed open source
• Sponsored by Adobe with hundreds of contributors and a number of non-Adobe committers
• 13th most starred project
• Hundreds of extensions are available
• 1.0 just released 10 days ago, with Extract for Brackets
function _documentSelectionFocusChange() { var curFile = EditorManager.getCurrentlyViewedPath(); if (curFile && _hasFileSelectionFocus()) {
function _documentSelectionFocusChange() { var curFile = EditorManager.getCurrentlyViewedPath(); if (curFile && _hasFileSelectionFocus()) { var nodeFound = $("#project-‐files-‐container li").is(function (index) { var $treeNode = $(this), entry = $treeNode.data("entry"); if (entry && entry.fullPath === curFile) {
function _documentSelectionFocusChange() { var curFile = EditorManager.getCurrentlyViewedPath(); if (curFile && _hasFileSelectionFocus()) { var nodeFound = $("#project-‐files-‐container li").is(function (index) { var $treeNode = $(this), entry = $treeNode.data("entry"); if (entry && entry.fullPath === curFile) { if (!_projectTree.jstree("is_selected", $treeNode)) { if ($treeNode.parents(".jstree-‐closed").length) { //don't auto-‐expand tree to show file -‐ but remember it if parent is manually expanded later _projectTree.jstree("deselect_all"); _lastSelected = $treeNode; } else {
if (!_projectTree.jstree("is_selected", $treeNode)) { if ($treeNode.parents(".jstree-‐closed").length) { //don't auto-‐expand tree to show file -‐ but remember it if parent is manually expanded later _projectTree.jstree("deselect_all"); _lastSelected = $treeNode; } else { //we don't want to trigger another selection change event, so manually deselect //and select without sending out notifications _projectTree.jstree("deselect_all"); _projectTree.jstree("select_node", $treeNode, false); // sets _lastSelected } }
If it’s hard to test, it won’t be tested.
http://www.infoq.com/presentations/Simple-Made-Easy
simple!
composed of one thing, not combined
complect!
interweave
function _documentSelectionFocusChange() { var curFile = EditorManager.getCurrentlyViewedPath(); if (curFile && _hasFileSelectionFocus()) { var nodeFound = $("#project-‐files-‐container li").is(function (index) { var $treeNode = $(this), entry = $treeNode.data("entry"); if (entry && entry.fullPath === curFile) { if (!_projectTree.jstree("is_selected", $treeNode)) { if ($treeNode.parents(".jstree-‐closed").length) { //don't auto-‐expand tree to show file -‐ but remember it if parent is manually expanded later _projectTree.jstree("deselect_all"); _lastSelected = $treeNode; } else {
Simple?
easy!
near at hand
React
https://www.destroyallsoftware.com/talks/boundaries
Functional core, Integration shell
function _documentSelectionFocusChange() { var curFile = EditorManager.getCurrentlyViewedPath(); if (curFile && _hasFileSelectionFocus()) { var nodeFound = $("#project-‐files-‐container li").is(function (index) { var $treeNode = $(this), entry = $treeNode.data("entry"); if (entry && entry.fullPath === curFile) { if (!_projectTree.jstree("is_selected", $treeNode)) { if ($treeNode.parents(".jstree-‐closed").length) { //don't auto-‐expand tree to show file -‐ but remember it if parent is manually expanded later _projectTree.jstree("deselect_all"); _lastSelected = $treeNode; } else {
function _documentSelectionFocusChange() { var curFullPath = MainViewManager.getCurrentlyViewedPath(MainViewManager.ACTIVE_PANE); if (curFullPath && _hasFileSelectionFocus()) { actionCreator.setSelected(curFullPath, true); } else { actionCreator.setSelected(null); } _fileViewControllerChange(); }
http://futurice.com/blog/reactive-mvc-and-the-virtual-dom/
ProjectModel
FileTreeViewModel
FileTreeView ActionCreator
React
function render(element, viewModel, projectRoot, actions, forceRender, platform) { if (!projectRoot) { return; } React.renderComponent(fileTreeView({ treeData: viewModel.treeData, selectionViewInfo: viewModel.selectionViewInfo, sortDirectoriesFirst: viewModel.sortDirectoriesFirst, parentPath: projectRoot.fullPath, actions: actions, extensions: _extensions, platform: platform, forceRender: forceRender }), element); }
var fileTreeView = React.createClass({ /** * Update for any change in the tree data or directory sorting preference. */ shouldComponentUpdate: function (nextProps, nextState) { return nextProps.forceRender || this.props.treeData !== nextProps.treeData || this.props.sortDirectoriesFirst !== nextProps.sortDirectoriesFirst || this.props.extensions !== nextProps.extensions || this.props.selectionViewInfo !== nextProps.selectionViewInfo; },
render: function () { var contents = directoryContents({ isRoot: true, parentPath: this.props.parentPath, sortDirectoriesFirst: this.props.sortDirectoriesFirst, contents: this.props.treeData, extensions: this.props.extensions, actions: this.props.actions, forceRender: this.props.forceRender, platform: this.props.platform }); return DOM.div( null, selectionBackground, contextBackground, extensionForSelection, extensionForContext, contents ); } });
directoryContents = React.createClass({ render: function () { var extensions = this.props.extensions, iconClass = extensions && extensions.get("icons") ? "jstree-‐icons" : "jstree-‐no-‐icons", ulProps = this.props.isRoot ? { className: "jstree-‐brackets jstree-‐no-‐dots " + iconClass } : null; var contents = this.props.contents, namesInOrder = _sortDirectoryContents(contents, this.props.sortDirectoriesFirst);
return DOM.ul(ulProps, namesInOrder.map(function (name) { var entry = contents.get(name); if (FileTreeViewModel.isFile(entry)) { return fileNode({ parentPath: this.props.parentPath, name: name, entry: entry, actions: this.props.actions, extensions: this.props.extensions, forceRender: this.props.forceRender, platform: this.props.platform, key: name }); } else { return directoryNode({
directoryNode = React.createClass({ mixins: [contextSettable, pathComputer, extendable], render: function () { var entry = this.props.entry, if (entry.get("rename")) { renameInput = directoryRenameInput({ actions: this.props.actions, entry: this.props.entry, name: this.props.name, parentPath: this.props.parentPath }); }
directoryNode = React.createClass({ render: function () { return DOM.li({ className: this.getClasses("jstree-‐" + nodeClass), onClick: this.handleClick, onMouseDown: this.handleMouseDown }, DOM.ins({ className: "jstree-‐icon" }, " "), renameInput, nameDisplay, childNodes); } });
handleClick: function (event) { var isOpen = this.props.entry.get("open"), setOpen = isOpen ? false : true; if (event.metaKey || event.ctrlKey) { // ctrl-‐alt-‐click toggles this directory and its children if (event.altKey) { if (setOpen) { // when opening, we only open the immediate children because // opening a whole subtree could be really slow (consider // a `node_modules` directory, for example). this.props.actions.toggleSubdirectories(this.myPath(), setOpen); this.props.actions.setDirectoryOpen(this.myPath(), setOpen); } else { // When closing, we recursively close the whole subtree. this.props.actions.closeSubtree(this.myPath()); } } else {
ActionCreator.prototype.toggleSubdirectories = function (path, openOrClose) { this.model.toggleSubdirectories(path, openOrClose).then(_saveTreeState); };
ProjectModel.prototype.toggleSubdirectories = function (path, openOrClose) { var self = this, d = new $.Deferred(); this.setDirectoryOpen(path, true).then(function () { var projectRelativePath = self.makeProjectRelativeIfPossible(path), childNodes = self._viewModel.getChildDirectories(projectRelativePath); Async.doInParallel(childNodes, function (node) { return self.setDirectoryOpen(path + node, openOrClose); }, true).then(function () { d.resolve(); }, function (err) { d.reject(err); }); }); return d.promise(); };
ProjectModel.prototype.setDirectoryOpen = function (path, open) { var projectRelative = this.makeProjectRelativeIfPossible(path), needsLoading = !this._viewModel.isPathLoaded(projectRelative), d = new $.Deferred(), self = this; if (open && needsLoading) { var parentDirectory = FileUtils.getDirectoryPath(FileUtils.stripTrailingSlash(path)); this.setDirectoryOpen(parentDirectory, true).then(function () { self._getDirectoryContents(path).then(onSuccess).fail(function (err) { d.reject(err); }); }, function (err) { d.reject(err); }); } else { onSuccess(); }
function onSuccess(contents) { // Update the view model if (contents) { self._viewModel.setDirectoryContents(projectRelative, contents); } if (open) { self._viewModel.openPath(projectRelative); if (self._focused) { var currentPathInProject = self.makeProjectRelativeIfPossible(self._currentPath); if (self._viewModel.isFilePathVisible(currentPathInProject)) { self.setSelected(self._currentPath, true); } else { self.setSelected(null); } } } else {
branches to test
FileTreeViewModel.prototype.openPath = function (path) { this._commit(_openPath(this._treeData, path)); };
{ "subdir": { open: true, children: { "afile.js": {}, "subsubdir": { children: { "thirdsub": { children: {} } } } } } }
subdir/subsubdir/thirdsub/
{ "subdir": { open: true, children: { "afile.js": {}, "subsubdir": { children: { "thirdsub": { children: {} } } } } } }
subdir/subsubdir/thirdsub/
treeData.subdir.subsubdir.open = true; treeData.subdir.subsubdir.thirdsub.open = true;
thirdsub = treeData.subdir.subsubdir.thirdsub; newThirdSub = thirdsub.set("open", true); thirdsub !== newThirdSub; // true treeData.subdir.subsubdir.thirdsub !== newThirdSub // true treeData.subdir.subsubdir.thirdsub.open === undefined; // true
Attack of the clones
https://www.flickr.com/photos/hjmediastudios/7910348016/
thirdsub = treeData.subdir.subsubdir.thirdsub; newThirdSub = thirdsub.set("open", true); thirdsub !== newThirdSub; // true treeData.subdir.subsubdir.thirdsub !== newThirdSub // true treeData.subdir.subsubdir.thirdsub.open === undefined; // true
thirdsub = treeData.subdir.subsubdir.thirdsub; newThirdSub = thirdsub.set("open", true); newSubSubDir = subsubdir.set("thirdsub", newThirdSub); newSubDir = subdir.set("subsubdir", newSubSubDir); treeData = treeData.set("subdir", newSubDir);
function _openPath(treeData, path) { var objectPath = _filePathToObjectPath(treeData, path); function setOpen(node) { return node.set("open", true); } while (objectPath && objectPath.length) { var node = treeData.getIn(objectPath); if (isFile(node)) { objectPath.pop(); } else { if (!node.get("open")) { treeData = treeData.updateIn(objectPath, setOpen); } objectPath.pop(); if (objectPath.length) { objectPath.pop(); } } } return treeData; }
mutable?
FileTreeViewModel.prototype.openPath = function (path) { this._commit(_openPath(this._treeData, path)); };
FileTreeViewModel.prototype._commit = function (treeData, selectionViewInfo) { var changed = false; if (treeData && treeData !== this._treeData) { this._treeData = treeData; changed = true; } if (selectionViewInfo && selectionViewInfo !== this._selectionViewInfo) { this._selectionViewInfo = selectionViewInfo; changed = true; } if (changed) { $(this).trigger(EVENT_CHANGE); } };
–C.A.R. Hoare
“There are two ways of constructing a software design: One way is to make it so simple that there are obviously no
deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far
more difficult.”
Simple?
simple!
composed of one thing, not combined
http://www.shaffner.us/cs/papers/tarpit.pdf
Simple Architecture
• Each part does one thing with side effects in few, known places
• React lets you generate a whole UI functionally
• Immutable-JS allows you to control when data updates occur
• Every part of your application can have a consistent state
• Object identity tells you when something has changed
A modern, open source text editor that understands web design.
http://brackets.io