Skip to content

Instantly share code, notes, and snippets.

@smolinari
Last active January 27, 2017 06:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save smolinari/b8bbbd1f174908f0f18d5d184322da46 to your computer and use it in GitHub Desktop.
Save smolinari/b8bbbd1f174908f0f18d5d184322da46 to your computer and use it in GitHub Desktop.
Why (I think) Vue is better than React
// 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