Skip to content

Instantly share code, notes, and snippets.

@craigmaslowski
Last active August 29, 2015 14:20
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 craigmaslowski/51e3f96f61d4ef625d9b to your computer and use it in GitHub Desktop.
Save craigmaslowski/51e3f96f61d4ef625d9b to your computer and use it in GitHub Desktop.
Mithril Blog

I recently discovered Mithril.js and wanted to give it a shot with an application that's slightly more complicated than the standard ToDo example. I decided on making a simple blogging software. The requirements for the software were as follows:

  • A listing of all blog posts as the front page of the app
  • The ability to add, edit, and remove posts.
  • A combined Add/Edit Form for managing posts
  • The usage of localstorage
  • Accessing localstorage in a faux asynchronous manner to simulate REST type requests

What follows is the result.

I plan to continue expanding and refining this prototype to continue learning about Mithril, and eventually turn it into a series of blog posts. In the meantime, here is the code I've come up with. It's broken down into several pieces.

  • App Router - A file designating the app's routes.
  • Posts - A namespace containing the Post model and functions to persist it.
  • PostForm - A Mithril module containing the view and controller for the Post Form.
  • PostList - A Mithril module containing the view and controller for the Post List.
  • Utils - Some helper functions I use in various places.
  • Polyfills - A few additions to the Array prototype.

I doubt my organization and patterns are close to solid yet, but I feel I'm on the right track.

m.route.mode = 'hash';
m.route(document.querySelector('#app'), '/', {
'/': PostList,
'/add': PostForm,
'/edit/:id': PostForm
});
<html>
<head>
<title>Mithril Blog</title>
<link rel="stylesheet" href="/app.css" />
</head>
<body>
<header><h1><a href="/">Mithril Blog</a></h1></header>
<div id="app"></div>
<script src="mithril.js"></script>
<script src="polyfill.js"></script>
<script src="utils.js"></script>
<script src="posts.js"></script>
<script src="post.form.module.js"></script>
<script src="post.list.module.js"></script>
<script src="app.router.js"></script>
</body>
</html>
if (!Array.prototype.findIndex) {
Array.prototype.findIndex = function (predicate) {
if (this == null) {
throw new TypeError('Array.prototype.findIndex called on null or undefined');
}
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
var list = Object(this);
var length = list.length >>> 0;
var thisArg = arguments[1];
var value;
for (var i = 0; i < length; i++) {
value = list[i];
if (predicate.call(thisArg, value, i, list)) {
return i;
}
}
return -1;
};
}
if (!Array.prototype.without) {
Array.prototype.without = function (predicate) {
if (this == null) {
throw new TypeError('Array.prototype.findIndex called on null or undefined');
}
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
return this.reduce(function (acc, item) {
if (predicate(item)) return acc;
acc.push(item);
return acc;
}, []);
};
}
var PostFormController = function () {
var ctrl = this,
id = m.route.param('id');
ctrl.post = m.prop({});
if (id)
Posts.load(id).then(ctrl.post);
else
ctrl.post(new Posts.Model());
ctrl.save = function () {
Posts.save(ctrl.post).then(function () { m.route('/'); });
};
ctrl.remove = function () {
Posts.remove(ctrl.post.id()).then(function () { m.route('/'); });;
};
};
var PostFormView = function (ctrl) {
var post = ctrl.post();
var postFields = [
m('label', 'Title'),
m('input[type=text][placeholder=Title]',
{ value: post.title(), oninput: m.withAttr('value', post.title) }),
m('label', 'Body'),
m('textarea[placeholder=Body]',
{value: post.body(), oninput: m.withAttr('value', post.body)}),
m('button[type=button]', { onclick: ctrl.save, href: '#' }, 'Save')
];
if (post.id() != -1) {
postFields.push(m('button[type=button]', { onclick: ctrl.remove, href: '#' }, 'Remove'));
}
return m('form.post-form', [ m('fieldset', postFields) ]);
};
var PostForm = {
controller: PostFormController,
view: PostFormView
};
var PostListController = function () {
var ctrl = this;
ctrl.posts = m.prop([]);
Posts.load().then(ctrl.posts);
};
var PostListView = function (ctrl) {
var posts = ctrl.posts();
var innerEls = posts.map(function (post, index) {
return m('.post', [
m('h2.title', post.title()),
m('div.body', post.body()),
m('a[href=/edit/' + post.id() + ']', {config: m.route}, 'Edit Post')
]);
});
innerEls.push(m('a[href="/add"]', { config: m.route }, 'Add Post'));
return m('.posts', innerEls);
};
var PostList = {
controller: PostListController,
view: PostListView
};
var Posts = {};
Posts.Model = function (data) {
this.id = m.prop(getPropertyOrDefault('id', data, -1));
this.title = m.prop(getPropertyOrDefault('title', data));
this.body = m.prop(getPropertyOrDefault('body', data));
this.toJSON = toJSON;
};
Posts.load = function (id) {
m.startComputation();
var deferred = m.deferred(),
posts = getArrayFromLocalStorage('mblog.posts');
setTimeout(function () {
if (id) {
var index = posts.findIndex(function (p) {
return p.id == id;
});
deferred.resolve(new Posts.Model(posts[index]));
} else {
deferred.resolve(posts.map(function (post) {
return new Posts.Model(post);
}));
}
m.endComputation();
}, 200);
return deferred.promise;
};
Posts.save = function (post) {
m.startComputation();
var deferred = m.deferred(),
posts = getArrayFromLocalStorage('mblog.posts');;
setTimeout(function () {
// add or update post
if (post.id() === -1) {
// set post id
post.id(posts.reduce(function (acc, p) {
return Math.max(acc, p.id);
}, 0) + 1);
// add post
posts.push(post.toJSON());
} else {
var index = posts.findIndex(function (p) {
return p.id == post.id();
});
posts[index] = post.toJSON();
}
localStorage['mblog.posts'] = JSON.stringify(posts);
deferred.resolve(post);
m.endComputation();
}, 200);
return deferred.promise;
};
Posts.remove = function (id) {
m.startComputation();
var deferred = m.deferred(),
posts = getArrayFromLocalStorage('mblog.posts');
setTimeout(function () {
localStorage['mblog.posts'] = JSON.stringify(posts.without(function (p) {
return p.id == id;
}));
deferred.resolve();
m.endComputation();
}, 200);
return deferred.promise;
};
function getPropertyOrDefault (prop, data, defaultValue) {
defaultValue = defaultValue || '';
return data && data[prop] ? data[prop] : defaultValue;
};
function getArrayFromLocalStorage (key) {
return localStorage[key] != 'undefined'
? JSON.parse(localStorage[key])
: [];
};
function toJSON () {
var self = this;
return Object.keys(self).reduce(function (acc, key) {
if (key != 'toJSON')
acc[key] = self[key]();
return acc;
}, {});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment