Last active
January 27, 2017 06:48
-
-
Save smolinari/b8bbbd1f174908f0f18d5d184322da46 to your computer and use it in GitHub Desktop.
Why (I think) Vue is better than React
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Reason 1 - Vue is more pragmatic | |
// Reason 2 - Code using Vue ends up more elegant | |
// Reason 3 - Vue performs better in most cases | |
// Reason 4 - Vue is smaller, yet covers practically everything React does | |
// Reason 5 - Vue is easier to reason about or rather JSX is harder to reason about | |
// Reason 6 - JSX is for JS experts, Vue speaks to a much larger crowd i.e. beginners and intermediates | |
// Reason 6.1 - Vue is easier to learn | |
// Reason 7 - Vue's templating system allows for better static analysis | |
// Reason 8 - If you must do JSX, Vue can do it too | |
// Reason 9 - JSX with React isn't necessary, but ugh, then it gets even uglier | |
// Reason 10 - TODO MVC 484 LOC with React to 169 LOC with Vue :-O | |
// TODO MVC from Vue | |
//app.js | |
(function (exports) { | |
'use strict'; | |
var filters = { | |
all: function (todos) { | |
return todos; | |
}, | |
active: function (todos) { | |
return todos.filter(function (todo) { | |
return !todo.completed; | |
}); | |
}, | |
completed: function (todos) { | |
return todos.filter(function (todo) { | |
return todo.completed; | |
}); | |
} | |
}; | |
exports.app = new Vue({ | |
// the root element that will be compiled | |
el: '.todoapp', | |
// app initial state | |
data: { | |
todos: todoStorage.fetch(), | |
newTodo: '', | |
editedTodo: null, | |
visibility: 'all' | |
}, | |
// watch todos change for localStorage persistence | |
watch: { | |
todos: { | |
deep: true, | |
handler: todoStorage.save | |
} | |
}, | |
// computed properties | |
// http://vuejs.org/guide/computed.html | |
computed: { | |
filteredTodos: function () { | |
return filters[this.visibility](this.todos); | |
}, | |
remaining: function () { | |
return filters.active(this.todos).length; | |
}, | |
allDone: { | |
get: function () { | |
return this.remaining === 0; | |
}, | |
set: function (value) { | |
this.todos.forEach(function (todo) { | |
todo.completed = value; | |
}); | |
} | |
} | |
}, | |
// methods that implement data logic. | |
// note there's no DOM manipulation here at all. | |
methods: { | |
pluralize: function (word, count) { | |
return word + (count === 1 ? '' : 's'); | |
}, | |
addTodo: function () { | |
var value = this.newTodo && this.newTodo.trim(); | |
if (!value) { | |
return; | |
} | |
this.todos.push({ title: value, completed: false }); | |
this.newTodo = ''; | |
}, | |
removeTodo: function (todo) { | |
var index = this.todos.indexOf(todo); | |
this.todos.splice(index, 1); | |
}, | |
editTodo: function (todo) { | |
this.beforeEditCache = todo.title; | |
this.editedTodo = todo; | |
}, | |
doneEdit: function (todo) { | |
if (!this.editedTodo) { | |
return; | |
} | |
this.editedTodo = null; | |
todo.title = todo.title.trim(); | |
if (!todo.title) { | |
this.removeTodo(todo); | |
} | |
}, | |
cancelEdit: function (todo) { | |
this.editedTodo = null; | |
todo.title = this.beforeEditCache; | |
}, | |
removeCompleted: function () { | |
this.todos = filters.active(this.todos); | |
} | |
}, | |
// a custom directive to wait for the DOM to be updated | |
// before focusing on the input field. | |
// http://vuejs.org/guide/custom-directive.html | |
directives: { | |
'todo-focus': function (el, binding) { | |
if (binding.value) { | |
el.focus(); | |
} | |
} | |
} | |
}); | |
})(window); | |
// routes.js | |
(function (app, Router) { | |
'use strict'; | |
var router = new Router(); | |
['all', 'active', 'completed'].forEach(function (visibility) { | |
router.on(visibility, function () { | |
app.visibility = visibility; | |
}); | |
}); | |
router.configure({ | |
notfound: function () { | |
window.location.hash = ''; | |
app.visibility = 'all'; | |
} | |
}); | |
router.init(); | |
})(app, Router); | |
// store.js | |
(function (exports) { | |
'use strict'; | |
var STORAGE_KEY = 'todos-vuejs'; | |
exports.todoStorage = { | |
fetch: function () { | |
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); | |
}, | |
save: function (todos) { | |
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); | |
} | |
}; | |
})(window); | |
/////////////////////////////////// | |
// TODO MVC from React | |
////////////////////////////////// | |
// app.jsx | |
var app = app || {}; | |
(function () { | |
'use strict'; | |
app.ALL_TODOS = 'all'; | |
app.ACTIVE_TODOS = 'active'; | |
app.COMPLETED_TODOS = 'completed'; | |
var TodoFooter = app.TodoFooter; | |
var TodoItem = app.TodoItem; | |
var ENTER_KEY = 13; | |
var TodoApp = React.createClass({ | |
getInitialState: function () { | |
return { | |
nowShowing: app.ALL_TODOS, | |
editing: null, | |
newTodo: '' | |
}; | |
}, | |
componentDidMount: function () { | |
var setState = this.setState; | |
var router = Router({ | |
'/': setState.bind(this, {nowShowing: app.ALL_TODOS}), | |
'/active': setState.bind(this, {nowShowing: app.ACTIVE_TODOS}), | |
'/completed': setState.bind(this, {nowShowing: app.COMPLETED_TODOS}) | |
}); | |
router.init('/'); | |
}, | |
handleChange: function (event) { | |
this.setState({newTodo: event.target.value}); | |
}, | |
handleNewTodoKeyDown: function (event) { | |
if (event.keyCode !== ENTER_KEY) { | |
return; | |
} | |
event.preventDefault(); | |
var val = this.state.newTodo.trim(); | |
if (val) { | |
this.props.model.addTodo(val); | |
this.setState({newTodo: ''}); | |
} | |
}, | |
toggleAll: function (event) { | |
var checked = event.target.checked; | |
this.props.model.toggleAll(checked); | |
}, | |
toggle: function (todoToToggle) { | |
this.props.model.toggle(todoToToggle); | |
}, | |
destroy: function (todo) { | |
this.props.model.destroy(todo); | |
}, | |
edit: function (todo) { | |
this.setState({editing: todo.id}); | |
}, | |
save: function (todoToSave, text) { | |
this.props.model.save(todoToSave, text); | |
this.setState({editing: null}); | |
}, | |
cancel: function () { | |
this.setState({editing: null}); | |
}, | |
clearCompleted: function () { | |
this.props.model.clearCompleted(); | |
}, | |
render: function () { | |
var footer; | |
var main; | |
var todos = this.props.model.todos; | |
var shownTodos = todos.filter(function (todo) { | |
switch (this.state.nowShowing) { | |
case app.ACTIVE_TODOS: | |
return !todo.completed; | |
case app.COMPLETED_TODOS: | |
return todo.completed; | |
default: | |
return true; | |
} | |
}, this); | |
var todoItems = shownTodos.map(function (todo) { | |
return ( | |
<TodoItem | |
key={todo.id} | |
todo={todo} | |
onToggle={this.toggle.bind(this, todo)} | |
onDestroy={this.destroy.bind(this, todo)} | |
onEdit={this.edit.bind(this, todo)} | |
editing={this.state.editing === todo.id} | |
onSave={this.save.bind(this, todo)} | |
onCancel={this.cancel} | |
/> | |
); | |
}, this); | |
var activeTodoCount = todos.reduce(function (accum, todo) { | |
return todo.completed ? accum : accum + 1; | |
}, 0); | |
var completedCount = todos.length - activeTodoCount; | |
if (activeTodoCount || completedCount) { | |
footer = | |
<TodoFooter | |
count={activeTodoCount} | |
completedCount={completedCount} | |
nowShowing={this.state.nowShowing} | |
onClearCompleted={this.clearCompleted} | |
/>; | |
} | |
if (todos.length) { | |
main = ( | |
<section className="main"> | |
<input | |
className="toggle-all" | |
type="checkbox" | |
onChange={this.toggleAll} | |
checked={activeTodoCount === 0} | |
/> | |
<ul className="todo-list"> | |
{todoItems} | |
</ul> | |
</section> | |
); | |
} | |
return ( | |
<div> | |
<header className="header"> | |
<h1>todos</h1> | |
<input | |
className="new-todo" | |
placeholder="What needs to be done?" | |
value={this.state.newTodo} | |
onKeyDown={this.handleNewTodoKeyDown} | |
onChange={this.handleChange} | |
autoFocus={true} | |
/> | |
</header> | |
{main} | |
{footer} | |
</div> | |
); | |
} | |
}); | |
var model = new app.TodoModel('react-todos'); | |
function render() { | |
React.render( | |
<TodoApp model={model}/>, | |
document.getElementsByClassName('todoapp')[0] | |
); | |
} | |
model.subscribe(render); | |
render(); | |
})(); | |
// footer.jsx | |
var app = app || {}; | |
(function () { | |
'use strict'; | |
app.TodoFooter = React.createClass({ | |
render: function () { | |
var activeTodoWord = app.Utils.pluralize(this.props.count, 'item'); | |
var clearButton = null; | |
if (this.props.completedCount > 0) { | |
clearButton = ( | |
<button | |
className="clear-completed" | |
onClick={this.props.onClearCompleted}> | |
Clear completed | |
</button> | |
); | |
} | |
var nowShowing = this.props.nowShowing; | |
return ( | |
<footer className="footer"> | |
<span className="todo-count"> | |
<strong>{this.props.count}</strong> {activeTodoWord} left | |
</span> | |
<ul className="filters"> | |
<li> | |
<a | |
href="#/" | |
className={classNames({selected: nowShowing === app.ALL_TODOS})}> | |
All | |
</a> | |
</li> | |
{' '} | |
<li> | |
<a | |
href="#/active" | |
className={classNames({selected: nowShowing === app.ACTIVE_TODOS})}> | |
Active | |
</a> | |
</li> | |
{' '} | |
<li> | |
<a | |
href="#/completed" | |
className={classNames({selected: nowShowing === app.COMPLETED_TODOS})}> | |
Completed | |
</a> | |
</li> | |
</ul> | |
{clearButton} | |
</footer> | |
); | |
} | |
}); | |
})(); | |
// todoItem.jsx | |
var app = app || {}; | |
(function () { | |
'use strict'; | |
var ESCAPE_KEY = 27; | |
var ENTER_KEY = 13; | |
app.TodoItem = React.createClass({ | |
handleSubmit: function (event) { | |
var val = this.state.editText.trim(); | |
if (val) { | |
this.props.onSave(val); | |
this.setState({editText: val}); | |
} else { | |
this.props.onDestroy(); | |
} | |
}, | |
handleEdit: function () { | |
this.props.onEdit(); | |
this.setState({editText: this.props.todo.title}); | |
}, | |
handleKeyDown: function (event) { | |
if (event.which === ESCAPE_KEY) { | |
this.setState({editText: this.props.todo.title}); | |
this.props.onCancel(event); | |
} else if (event.which === ENTER_KEY) { | |
this.handleSubmit(event); | |
} | |
}, | |
handleChange: function (event) { | |
if (this.props.editing) { | |
this.setState({editText: event.target.value}); | |
} | |
}, | |
getInitialState: function () { | |
return {editText: this.props.todo.title}; | |
}, | |
/** | |
* This is a completely optional performance enhancement that you can | |
* implement on any React component. If you were to delete this method | |
* the app would still work correctly (and still be very performant!), we | |
* just use it as an example of how little code it takes to get an order | |
* of magnitude performance improvement. | |
*/ | |
shouldComponentUpdate: function (nextProps, nextState) { | |
return ( | |
nextProps.todo !== this.props.todo || | |
nextProps.editing !== this.props.editing || | |
nextState.editText !== this.state.editText | |
); | |
}, | |
/** | |
* Safely manipulate the DOM after updating the state when invoking | |
* `this.props.onEdit()` in the `handleEdit` method above. | |
* For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate | |
* and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate | |
*/ | |
componentDidUpdate: function (prevProps) { | |
if (!prevProps.editing && this.props.editing) { | |
var node = React.findDOMNode(this.refs.editField); | |
node.focus(); | |
node.setSelectionRange(node.value.length, node.value.length); | |
} | |
}, | |
render: function () { | |
return ( | |
<li className={classNames({ | |
completed: this.props.todo.completed, | |
editing: this.props.editing | |
})}> | |
<div className="view"> | |
<input | |
className="toggle" | |
type="checkbox" | |
checked={this.props.todo.completed} | |
onChange={this.props.onToggle} | |
/> | |
<label onDoubleClick={this.handleEdit}> | |
{this.props.todo.title} | |
</label> | |
<button className="destroy" onClick={this.props.onDestroy} /> | |
</div> | |
<input | |
ref="editField" | |
className="edit" | |
value={this.state.editText} | |
onBlur={this.handleSubmit} | |
onChange={this.handleChange} | |
onKeyDown={this.handleKeyDown} | |
/> | |
</li> | |
); | |
} | |
}); | |
})(); | |
// todoModel.jsx | |
var app = app || {}; | |
(function () { | |
'use strict'; | |
var Utils = app.Utils; | |
// Generic "model" object. You can use whatever | |
// framework you want. For this application it | |
// may not even be worth separating this logic | |
// out, but we do this to demonstrate one way to | |
// separate out parts of your application. | |
app.TodoModel = function (key) { | |
this.key = key; | |
this.todos = Utils.store(key); | |
this.onChanges = []; | |
}; | |
app.TodoModel.prototype.subscribe = function (onChange) { | |
this.onChanges.push(onChange); | |
}; | |
app.TodoModel.prototype.inform = function () { | |
Utils.store(this.key, this.todos); | |
this.onChanges.forEach(function (cb) { cb(); }); | |
}; | |
app.TodoModel.prototype.addTodo = function (title) { | |
this.todos = this.todos.concat({ | |
id: Utils.uuid(), | |
title: title, | |
completed: false | |
}); | |
this.inform(); | |
}; | |
app.TodoModel.prototype.toggleAll = function (checked) { | |
// Note: it's usually better to use immutable data structures since they're | |
// easier to reason about and React works very well with them. That's why | |
// we use map() and filter() everywhere instead of mutating the array or | |
// todo items themselves. | |
this.todos = this.todos.map(function (todo) { | |
return Utils.extend({}, todo, {completed: checked}); | |
}); | |
this.inform(); | |
}; | |
app.TodoModel.prototype.toggle = function (todoToToggle) { | |
this.todos = this.todos.map(function (todo) { | |
return todo !== todoToToggle ? | |
todo : | |
Utils.extend({}, todo, {completed: !todo.completed}); | |
}); | |
this.inform(); | |
}; | |
app.TodoModel.prototype.destroy = function (todo) { | |
this.todos = this.todos.filter(function (candidate) { | |
return candidate !== todo; | |
}); | |
this.inform(); | |
}; | |
app.TodoModel.prototype.save = function (todoToSave, text) { | |
this.todos = this.todos.map(function (todo) { | |
return todo !== todoToSave ? todo : Utils.extend({}, todo, {title: text}); | |
}); | |
this.inform(); | |
}; | |
app.TodoModel.prototype.clearCompleted = function () { | |
this.todos = this.todos.filter(function (todo) { | |
return !todo.completed; | |
}); | |
this.inform(); | |
}; | |
})(); | |
// utils.jsx | |
var app = app || {}; | |
(function () { | |
'use strict'; | |
app.Utils = { | |
uuid: function () { | |
/*jshint bitwise:false */ | |
var i, random; | |
var uuid = ''; | |
for (i = 0; i < 32; i++) { | |
random = Math.random() * 16 | 0; | |
if (i === 8 || i === 12 || i === 16 || i === 20) { | |
uuid += '-'; | |
} | |
uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)) | |
.toString(16); | |
} | |
return uuid; | |
}, | |
pluralize: function (count, word) { | |
return count === 1 ? word : word + 's'; | |
}, | |
store: function (namespace, data) { | |
if (data) { | |
return localStorage.setItem(namespace, JSON.stringify(data)); | |
} | |
var store = localStorage.getItem(namespace); | |
return (store && JSON.parse(store)) || []; | |
}, | |
extend: function () { | |
var newObj = {}; | |
for (var i = 0; i < arguments.length; i++) { | |
var obj = arguments[i]; | |
for (var key in obj) { | |
if (obj.hasOwnProperty(key)) { | |
newObj[key] = obj[key]; | |
} | |
} | |
} | |
return newObj; | |
} | |
}; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment