Last Updated: February 25, 2016
·
2.834K
· optilude

Meteor code organisation

Some "current thinking" on how best to use Meteor (https://www.meteor.com/):

Packages

Use Meteor 0.9, which includes the package management features previously found in Meteorite.

Some useful packages (install with meteor add)

Code organisation

Here is a simple code layout that fits my way of thinking:

.
├── client/              Client-side-only assets
│   ├── layout.html      Defines an overall page layout for routed pages
│   ├── notfound.html    A default "not found" page for routed requests
│   ├── *.html           Top level pages rendered by the router
│   ├── components/      Shared templates used by top level pages
│   │   ├── *.html
│   ├── css/             Custom stylesheets
│   │   └── *.css
│   ├── js/              Client-side JavaScript logic
│   │   ├── *.js         Template helpers and events for components
│   │   └── startup.js   `Meteor.startup()` and `iron-router` configuration
├── lib/                 Shared library code, loads early
│   └── _namespaces.js   Defines global namespaces
├── models.js            Shared model/collection definitions
├── server/              Server-side-only methods
│   └── *.js

lib/_namespaces.js defines top level namespaces. See https://coderwall.com/p/tk5yug. For example:

/* global Models: true, Collections: true, Schemata: true */

// This file exists so that we can do "use strict" in all other files and
// still have some global namespace variables.

// It is called what it's called and placed where it's placed so that it loads
// as early as possible.

Schemata = {};
Collections = {};

models.js (or possibly a directory of files if there are many models) defines collections and schemata. For example:

/* global Collections, Schemata, SimpleSchema */
"use strict";

Schemata.MyModel = new SimpleSchema({

    ...

});


Collections.MyModels = new Meteor.Collection("MyModels", {
    schema: Schemata.MyModel
});

if(Meteor.isServer) {
    Meteor.publish("models", function() {
        return Collections.MyModels.find({});
    });
} else if(Meteor.isClient) {
    Meteor.subscribe("models");
}

Here is an example of a client/layout.html that uses a simple Bootstrap 3 template:

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Analysator</title>

    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
      <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->
</head>

<template name="layout">

    {{> header }}

    <div class="container-fluid">

        <div class="row">
            <div class="col-sm-2 col-md-2 sidebar">
                {{> sidebar }}
            </div>
            <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
                {{> yield }}
            </div>
        </div>

    </div>

    {{> yield region='footer'}}

</template>

and a client/notfound.html:

<template name="notFound">
    <h1 class="page-header">Not found</h1>
    <p>
        The page you are looking for cannot be found.
    </p>
</template>

The various components like header and sidebar are defined in client/components/*.html as separate templates, with associated logic (e.g. Template.[name].helpers() and Template.[name].events) in client/js/*.js.

Other pages in client/*.html contain alternative pages. These are then navigated to using iron-router. The configuration is in client/js/startup.js. Here's an example that routes to two pages, forces login on anything but the home page, and has a 404 type handler:

/* global Collections */
"use strict";

Meteor.startup(function() {

    // any initialisation here

});

Router.map(function() {

    // if `data` returns undefined/null, redirect to the `notFoundTemplate`.
    Router.onBeforeAction('dataNotFound');
    Router.configure({
        layoutTemplate: 'layout',
        notFoundTemplate: 'notFound'
    });

    // force login
    Router.onBeforeAction(function(pause) {
        var self = this;

        if (!this.ready()) {
            return;
        }

        var userId = Meteor.userId(),
            currentRoute = Router.current();

        // Redirect to home page if user is not logged in
        if(currentRoute && currentRoute.route.name !== 'home' && !userId) {
            pause();
            Router.go('home');
            return;
        }
    });

    // Routes

    this.route('home', {
        path: '/',
        data: {}
    });

    this.route('new', {
        path: '/new-entry',
        template: 'edit-entry',
        yieldTemplates: {
            configurationModal: {to: 'footer'}
        },
        onRun: function() {
            // any preparation, e.g. update Session
        },
        data: function() {
            // return a blank object
            return {foo: null, bar: null};
        }
    });

    this.route('entry', {
        path: '/entry/:_id',
        template: 'edit-entry',
        yieldTemplates: {
            configurationModal: {to: 'footer'}
        },
        onRun: function() {
            // any preparation, e.g. update Session
        },
        data: function() {
            // find the data object
            return Collections.MyModels.findOne(this.params._id);
        }
    });
});

Editor configuration