Skip to content
jsguy edited this page Mar 24, 2015 · 22 revisions

Models, views, controllers

When creating a route, you must assign a controller and a view to it - this is achieved by creating a file in the /mvc directory - by convention, you should name it as per the path you want, (see the routing section for details).

Here is a minimal example using the sugartags, and getting a parameter:

var m = require('mithril'),
	miso = require('../server/miso.util.js'),
	sugartags = require('../server/mithril.sugartags.node.js')(m);

module.exports.index = {
	controller: function(params) {
		this.who = miso.getParam('who', params, 'world');
		return this;
	},
	view: function(ctrl){
		with(sugartags) {
			return DIV('Hello ' + ctrl.who);
		}
	}
};

Save this into a file /mvc/hello.js, and open http://localhost/hellos, this will show "Hello world". Note the 's' on the end - this is due to how the route by convention works.

Now open /cfg/routes.json, and add the following routes:

	"/hello": { "method": "get", "name": "hello", "action": "index" },
	"/hello/:who": { "method": "get", "name": "hello", "action": "index" }

Save the file, and go back to the browser, and you'll see an error! This is because we have now overridden the automatic route. Open http://localhost/hello, and you'll see our action. Now open http://localhost/hello/YOURNAME, and you'll see it getting the first parameter, and greeting you!

Routing

The routing can be defined in one of two ways

Route by convention

You can use a naming convention as follows:

Action Method URL Description
index GET [controller] + 's' List the items
edit GET [controller]/[id] Display a form to edit the item
new GET [controller] + 's' + '/new' Display a form to add a new item

Say you have a mvc file named "user.js", and you define an action like so:

module.exports.index = {...

Miso will automatically map a "GET" to "/users".
Now say you have a mvc file named "user.js", and you define an action like so:

module.exports.edit = {...

Miso will automatically map a "GET" to "/user/:user_id", so that users can access via a route such as "/user/27" for use with ID of 27. Note: You can get the user_id using a miso utility: var userId = miso.getParam('user_id', params);.

Route by configuration

By using /cfg/routes.json config file:

{
	"[Pattern]": { "method": "[Method]", "name": "[Route name]", "action": "[Action]" }
}

Where:

  • Pattern - the route pattern we want, including any parameters
  • Method - one of 'GET', 'POST', 'PUT', 'DELETE'
  • Route name - name of your route file from /mvc
  • Action - name of the action to call on your route file from /mvc

Example

{
	"/": { "method": "get", "name": "home", "action": "index" }
}

This will map a "GET" to the root of the URL for the index action in home.js

Note: The routing config will override any automatically defined routes, so if you need multiple routes to point to the same action, you must manually define them. For example, if you have a mvc file named "term.js", and you define an action like so:

module.exports.index = {...

Miso will automatically map a "GET" to "/terms". Now, if you want to map it also to "/AGB", you will need to add two entries in the routes config:

{
	"/terms": { "method": "get", "name": "terms", "action": "index" },
	"/AGB": { "method": "get", "name": "terms", "action": "index" }
}

This is because Miso assumes that if you override the defaulted routes, you actually want to replace them, not just override them. Note: this is correct behaviour, as it minority case is when you want more than one route pointing to the same action.

Routing patterns

Type Example
Path "/abcd" - match paths starting with /abcd
Path Pattern "/abc?d" - match paths starting with /abcd and /abd
Path Pattern "/ab+cd" - match paths starting with /abcd, /abbcd, /abbbbbcd and so on
Path Pattern "/ab*cd" - match paths starting with /abcd, /abxcd, /abFOOcd, /abbArcd and so on
Path Pattern "/a(bc)?d" - will match paths starting with /ad and /abcd
Regular Expression //abc|/xyz/ - will match paths starting with /abc and /xyz
Array ["/abcd", "/xyza", //lmn|/pqr/] - match paths starting with /abcd, /xyza, /lmn, and /pqr

Links

When you create links, in order to get the app to work as an SPA, you must pass in m.route as a config, so that the history will be updated correctly, for example:

A({href:"/users/new", config: m.route}, "Add new user")

This will correctly work as a SPA. If you leave out config: m.route, the app will still work, but the page will reload every time the link is followed.

Note: if you are planning to manually route, ie: use m.route, be sure to use the name of the route, not a URL. Ie: if you have a route "/account", using m.route("http://p1.io/account") won't match, mithril is expecting m.route("/account") instead of the full URL.

Data models

Data models are progressively enhanced mithril models - you simply create your model as usual, then add validation and type information as it becomes pertinent. For example, say you have a model like so:

var userModel = function(data){
	this.name = m.p(data.name||"");
	this.email = m.p(data.email||"");
	this.id = m.p(data._id||"");
	return this;
}

In order to make it validatable, add the validator module:

var validate = require('validator.modelbinder');

Then add a isValid validation method to your model, with any declarations based on node validator:

var userModel = function(data){
	this.name = m.p(data.name||"");
	this.email = m.p(data.email||"");
	this.id = m.p(data._id||"");

	//	Validate the model		
	this.isValid = validate.bind(this, {
		name: {
			isRequired: "You must enter a name"
		},
		email: {
			isRequired: "You must enter an email address",
			isEmail: "Must be a valid email address"
		}
	});

	return this;
};

This creates a method that the miso database api can use to validate your model. You get full access to the validation info as well, so you can show an error message near your field, for example:

user.isValid('email')

Will return true if the email property of your user model is valid, or a list of errors messages if it is invalid:

["You must enter an email address", "Must be a valid email address"]

So you can for example add a class name to a div surrounding your field like so:

DIV({class: (ctrl.user.isValid('email') == true? "valid": "invalid")}, [...

And show the error messages like so:

SPAN(ctrl.user.isValid('email') == true? "": ctrl.user.isValid('email').join(", "))

Database api and model interaction

Miso uses the model definitions that you declare in your mvc file to build up a set of models that the API can use, the model definitions work like this:

  • On the models attribute of the mvc, we define a standard mithril data model, (ie: a javascript object where properties can be either standard javascript data types, or a function that works as a getter/setter, eg: m.prop)
  • On server startup, miso reads this and creates a cache of the model objects, including the name space of the model, eg: "hello.edit.hello"
  • Models can optionally include data validation information, and the database api will get access to this.

Assuming we have a model in the hello.models object like so:

hello: function(data){
    this.who = m.prop(data.who);
	this.isValid = validate.bind(this, {
		who: {
			isRequired: "You must know who you are talking to"
		}
	});
}

The API works like this:

  • We create an endpoint at /api where each we load whatever api is configured in /cfg/server.json, and expose each method. For example /api/save is available for the default flatfiledb api.
  • Next we create a set of API files - one for client, (/system/api.client.js), and one for server (/system/api.server.js) - each have the same methods, but do vastly different things:
  • api.client.js is a thin wrapper that uses mithril's m.request to create an ajax request to the server API, it simply passes messages back and forth (in JSON RPC 2.0 format).
  • api.server.js calls the database api methods, which in turn handles models and validation so for example when a request is made and a type and model is included, we can re-construct the data model based on this info, for example you might send: {type: 'hello.edit.hello', model: {who: 'Dave'}}, this can then be cast back into a model that we can call the isValid method on.

Now, the important bit: The reason for all this functionality is that mithril internally delays rendering to the DOM whilst a request is going on, so we need to handle this within miso - in order to be able to render things on the server - so we have a binding system that delays rendering whilst an async request is still being executed. That means mithril-like code like this:

controller: function(){
	var ctrl = this;
	api.find({type: 'hello.index.hello'}).then(function(data) {
		var list = Object.keys(data.result).map(function(key) {
			var myHello = data.result[key];
			return new self.models.hello(myHello);
		});
		ctrl.model = new ctrl.vm.todoList(list);
	});
	return ctrl;
}

Will still work. Note: the magic here is that there is absolutely nothing in the code above that runs a callback to let mithril know the data is ready - this is a design feature of mithril to delay rendering automatically whilst an m.request is in progress, so we cater for this to have the ability to render the page server-side first, so that SEO works out of the box.

Client vs server code

In miso, you include files using the standard nodejs require function. When you need to do something that works differently in the client than the server, there are a few ways you can achieve it:

  • The recommended way is to create and require a file in the modules/ directory, and then create the same file with a ".client" before the extension, and miso will automatically load that file for you on the client side instead. For example if you have /modules/something.js, if you create /modules/something.client.js, miso will automatically use that on the client.
  • Another option is to use miso.util - you can use miso.util.isServer() to test if you're on the server or not, though it is better practice to use the ".client" method mentioned above - only use isServer if you absolutely have no other option.

First page load

When a new user enters your site via a URL, and miso loads the first page, a number of things happen:

  • The server generates the page, including any data the user might have access to. This is mainly for SEO purposes, but also to make the perceptible loading time less, plus provide beautiful urls out of the box.
  • Once the page has loaded, mithril kicks in and creates a XHR (ajax) request to retreive the data, and setup any events and the virtual DOM, etc.

Now you might be thinking: we don't really need that 2nd request for data - it's already in the page, right? Well, sort of - you see miso does not make any assumptions about the structure of your data, or how you want to use it in your models, so there is no way for us to re-use that data, as it could be any structure. Another key feature of miso is the fact that all actions can be bookmarkable - for example the /users app - click on a user, and see the url change - we didn't do another server round-trip, but rather just a XHR request that returned the data we required - the UI was completely rendered client side - so it's really on that first time we load the page that you end up loading the data twice.

So that is the reason the architecture works the way it does, and has that seemingly redundant 2nd request for the data - it is a small price to pay for SEO, and perceptibly quick loading pages and as mentioned, it only ever happens on the first page load.

Of course you could implement caching of the data yourself, if the 2nd request is an issue - after all you might be loading quite a bit of data. One way to do this would be like so (warning: rather contrived example follows):

var m = require('mithril'),
	miso = require('../modules/miso.util.js'),
	sugartags = require('mithril.sugartags')(m),
	db = require('../system/api/flatfiledb/api.server.js')(m);

var edit = module.exports.edit = {
	models: {
		hello: function(data){
			this.who = m.prop(data.who);
		}
	},
	controller: function(params) {
		var ctrl = this,
			who = miso.getParam('hello_id', params);

		//	Check if our data is available, if so: use it.
		if(typeof myPerson !== "undefined") {
			ctrl.model = new edit.models.hello({who: myPerson});
		} else {
		//	If not, load it first.
			db.find({type: 'user.edit.user'}).then(function(data) {
				ctrl.model = new edit.models.hello({who: data.result[0].name});
			});
		}

		return ctrl;
	},
	view: function(ctrl) {
		with(sugartags) {
			return [
				//	Add a client side global variable with our data
				SCRIPT("var myPerson = '" + ctrl.model.who() + "'"),
				DIV("G'day " + ctrl.model.who())
			]
		}
	}
};

So this will only load the data on the server side - as you can see, we need to know the shape of the data to use it, and we are using a global variable here to store the data client side - I don't really recommend this approach, as it seems like a lot of work to save a single XHR request. However I understand you might have unique circumstances where the first data load could be a problem, so at least this is an option you can use to cache the data on first page load.

Requiring files

When requiring files, be sure to do so in a static manner so that browserify is able to compile the client side script. Always use:

var miso = require('../server/miso.util.js');

NEVER DO ANY OF THESE:

//  DON'T DO THIS!
var miso = new require('../server/miso.util.js');

This will create an object, which means browserify cannot resolve it statically, and will ignore it.

//  DON'T DO THIS!
var thing = 'miso';
var miso = require('../server/'+thing+'.util.js');

This will create an expression, which means browserify cannot resolve it statically, and will ignore it.

Clone this wiki locally