var dom = Bloop.dom; // components var App = Bloop.createClass({ componentDidRender: function() { var state = this.state; // A hack because we don't have proper lifecycle methods if(state.settingsOpen) { var anchor = document.querySelector('.toolbar'); var rect = anchor.getBoundingClientRect(); var node = document.querySelector('.settings'); var nodeRect = node.getBoundingClientRect(); node.style.top = rect.bottom + 16 + 'px'; node.style.left = rect.right - nodeRect.width - 10 + 'px'; $('.settings input').focus(); } else if(state.feed.items.length > 1) { $('.feed input').focus(); } }, handleMessage: function(e) { if(e.keyCode === 13) { this.submitMessage(); } else { this.state.newMessage = e.target.value; } }, submitMessage: function() { var state = this.state; if(state.newMessage) { document.querySelector('.app input').value = ''; state.feed.items.unshift({ id: Math.random() * 10000 | 0, author: state.username, text: state.newMessage, starred: false }); state.newMessage = ''; } }, openItem: function(id) { var msg = getMessage(parseInt(id)); if(msg) { this.state.selectedMessage = msg; } else { this.state.selectedMessage = null; } }, updateSettings: function(settings) { this.state.username = settings.username; }, toggleSettings: function() { this.state.settingsOpen = !this.state.settingsOpen; }, toggleStarred: function(id) { var msg = getMessage(parseInt(id)); msg.starred = !msg.starred; }, render: function() { var state = this.state; var section; if(state.selectedMessage) { section = dom.div( dom.h1('Message'), dom.p(state.selectedMessage.text), dom.a({ href: '#', onClick: this.openItem.bind(null, null) }, 'Back') ); } else { section = dom.div( { className: 'feed' }, state.feed.items.length ? dom.div({ className: 'feed-count' }, state.feed.items.length + ' message(s)') : null, dom.input({ onKeyUp: this.handleMessage.bind(this) }, ''), Feed({ items: state.feed.items, onOpenItem: this.openItem, onStar: this.toggleStarred }) ); } return dom.div( { className: 'app' }, Toolbar({ username: state.username, onSettings: this.toggleSettings }), dom.div( { className: 'content' }, section ), state.settingsOpen ? Settings({ username: state.username, onSave: this.updateSettings, onClose: this.toggleSettings }) : null ); } }); var Toolbar = Bloop.createClass({ render: function() { return dom.div( { className: 'toolbar' }, dom.em('Logged in as ' + this.props.username), dom.button({ onClick: this.props.onSettings }, 'settings'), dom.button({ onClick: undo }, 'undo') ); } }); var Feed = Bloop.createClass({ render: function() { return dom.div( this.props.items.map(function(msg) { return dom.div( { className: 'message', onClick: this.props.onOpenItem.bind(null, msg.id) }, dom.h4(msg.author), dom.div(msg.text), dom.a({ href: '#', onClick: function(e) { e.stopPropagation(); this.props.onStar(msg.id); }.bind(this), className: 'star' }, dom.img({ src: msg.starred ? 'star.png' : 'star-outline.png' })) ); }, this) ); } }); var Settings = Bloop.createClass({ getInitialState: function() { return { username: this.props.username }; }, save: function(e) { if(e) { e.preventDefault(); } this.props.onSave({ username: this.state.username }); this.props.onClose(); }, handleUsername: function(e) { this.state.username = e.target.value; }, render: function() { return dom.div( null, dom.div( { className: 'dismiss', onClick: this.props.onClose } ), dom.div( { className: 'settings' }, dom.form( { onSubmit: this.save }, 'Username: ', dom.input({ value: this.state.username, onChange: this.handleUsername }), dom.div( { className: 'submit' }, dom.button({ type: 'submit', onClick: this.save }, 'save') ) ) ) ); } }); var appState = { username: 'James', feed: { items: [ { id: Math.random() * 10000 | 0, author: 'James', text: 'This is your initial message. Love it, embrace it.', starred: false } ] }, selectedMessage: null, settingsOpen: false }; var app = App({state: appState}); // store function getMessage(id) { return _.find(appState.feed.items, { id: id }); } // render var prevStates = [JSON.stringify(appState)]; function undo() { while(1) { var state = JSON.parse(prevStates.pop()); if(!prevStates.length) { // This is the initial app state, so unconditionally apply it // and push it back onto the history so we don't lose it appState.feed = state.feed; prevStates.push(JSON.stringify(state)); break; } else if(JSON.stringify(appState.feed) !== JSON.stringify(state.feed)) { // We found a state where the feed has changed, so apply it appState.feed = state.feed; break; } } } function render() { app.state = appState; var changed = Bloop.renderComponent(app, document.body); if(changed) { prevStates.push(JSON.stringify(appState)); } requestAnimationFrame(render); } render();