Meteor. How to Build a TODO List

JavaScript

In this tutorial I don’t want to discuss why Meteor is the killer of the web, especially because I don’t think so and even like this framework. That’s why I want to show what to start with when developing applications on it. I’ll tell you about what packages there are and what Meteor is.

I don’t have much experience in developing Web applications. I’ve been doing it for two years only, and I got to know Meteor only a few months ago.

I should also warn you that the text is quite long, but the code is twice shorter than the text. I just want to share my experience with Meteor not for creating a simple example, but focus your attention on important things. Therefore, we are going to use plenty of external packages that simplify the process of development.

And another warning: I used the following techniques for writing the example of the given tutorial:

  • jade – an html preprocessor;
  • less – a css preprocessor;
  • coffeescript – a language that compiles into JavaScript.

Here’s a video that demonstrates the application we’ll have in the end:

If still interested, let’s get the party started.

Installing Meteor

Meteor itself bases on nodejs and mongodb. It also has no Windows support, so if you are going to get to know Meteor, you’d better have Linux or MacOS operating systems.

The first step is to install nodejs and mongodb.

Your next step is Meteor installation. It is not in the npm repository, so no need to hurry up and command npm install -g meteor, as the old version will be installed in this case. To perform the installation properly, type the following in the console:

$ curl https://install.meteor.com/ | sh

Creating a Project

After installing Meteor, you command the following right away:

$ meteor create 'todo-list'
todo-list: created.
To run your new app:
   cd todo-list
   meteor
$ cd todo-list
$ meteor
[[[[[ ~/dev/meteor-getting-started/todo-list ]]]]]
=> Started proxy.
=> Started MongoDB.
=> Started your app.
=> App running at: http://localhost:3000/

This means that everything went well and we can check our Hello World in the browser.

Meteor.JS Hello World App

After checking the newly created project, we can delete the files in the root of the project, as they are not really interesting for us. You can also notice that the .meteor directory has been created. It stores service information, and even the automatically generated .gitignore. By the way, to control the packages manually, we can change the packages file, but the console utilities are also handy.

If you have the same result, this means that the minimum environment for the development of Meteor is ready. If something went wrong, check the installation of nodejs, mongodb and meteor. For example, I have the following configuration on my computer:

$ node -v
v0.10.33
$ mongod --version
db version v2.4.12
$ meteor --version
Meteor 1.0

We can now finish the formalities and get down to building our TODO list. I recommend you to open a new tab in the console, as it’s not necessary to restart our Meteor application again. But we are going to use the console interface of the framework to install the packages.

Packages

I don’t want to talk about why Meteor uses its own package manager and why they are so fond of inventing the wheel, as all of this has nothing to do with our tutorial.

Install the packages with the help of the following command:

$ meteor add As I’ve mentioned before, we will develop the application with less, jade and coffeescript, so it’s time to install them. You can find all the packages we’re using, as well as plenty other ones at Atmosphere. Here they are:

  • less, coffeescript are the official packages, so they do not contain the name of the author;
  • mquandalle:jade this package is not official, so its name consists of two components. But it is well made, and I had no problems when using it.

There’s a sourcemap support built in less and coffeescript packages. Therefore, the process of debugging in the browser is going to be simple, sourcemap is supported by Meteor itself: it provides the necessary API to enable this functionality, so we won’t have to configure anything special.

In the course of development, we will add a few more popular packages, and I will try to describe the purpose of each of them. By the way, jquery and underscore are already included in Meteor, as well as plenty of other packages. The full list is available in the ./.meteor/versions file of the created project.

The Application Structure

Now, I guess it’s time to figure out how Meteor includes files into the project and talk about the methods of regulating this. To compile templates, styles and scripts, we won’t have to write configuration files for grant or gulp, as Meteor has taken care about it. For scaffolding, there is this project for Yeoman, but I prefer creating everything manually. In the previous project I used a similar folder structure:

todo-list/           - the root of the project
├── client           - purely client files will be here
│   ├── components   - the application components will consist of the template 
│   │                  and the script that implements its logic
│   ├── config       - configuration files
│   ├── layouts      - basic templates having no logic
│   ├── lib          - various scripts that we may need on the client              
│   ├── routes       - the client routing
│   └── styles       - styles
├── collections      - we’ll store models here
├── lib              - the scripts we may need everywhere
├── public           - statics: pictures, robots.txt and all that
├── server           - files for the server part of the application
│   ├── methods      - server methods will be here, like REST, but more handy
│   ├── publications – sharing data from collections
│   ├── routes       - the server routing, it will be possible to control 
│   │                  http requests
│   └── startup      - the server initialization

Perhaps we won’t need something, but Meteor has no restrictions on naming directories and files anyway, so you can think of any structure convenient for you. But you should keep in mind the following:

  • all the files from the public directory in the root of the project will be available to users through URL and will not be automatically included to the project;
  • all the files from the server directory are only available on the server side of the application;
  • all the files from the client folder are are available on the client side of the application;
  • anything else located in the root, is available in any environment;
  • files are automatically connected to the project, according to the following rules:
  • loading begins from subdirectories and the first one to be processed is the lib directory, then all the files and directories are loaded in alphabetical order.
  • files beginning withmain. are the last one to be loaded.

For example, let’s consider how our project will be loaded in the browser. First of all, files from the lib directory in the root of the project will be loaded. Then, the client folder will be processed, in which files from lib will be the first one to be loaded, and then components -> config ->… ->styles in alphabetical order. After them, files from the collections directory will be loaded. Files from such folders as public and server will not be loaded to the browser, but, for example, we can link, say, scripts stored in the public folder, with the help of the script tag, just like we used to do in other projects. However, developers of the framework do not recommend this approach.

We can also use the following construction to control the runtime in the shared code between the client and the server:

if Meteor.isClient
  # The code, executed in the browser only
if Meteor.isServer
  # The code, executed on the server only

To control the execution time of scripts, we can use the Meteor.startup() method. In the browser, it’s an analogy of $ function from the jQuery library. On the server, the code in this function will be executed right after the load of all scripts in the order of their load. More about these variables and methods.

The Basic Template of the Application

I’m going to use Bootstrap and I do know that everyone is a bit tired of it, but I’m really not good with HTML/CSS, and I’m more or less familiar with Bootstrap.

Install the mizzao:bootstrap-3 package for this purpose. This package is the most popular among other ones, and I think we’ll have no problem using it.

Then, create the head.jade file in the client/layouts folder. This file will be the only one not having a format template. Anyway, we’ll just create the head of the page, and then take a closer look at templates.

//- client/layouts/head.jade
head
  meta(charset='utf-8')
  meta(name='viewport', content='width=device-width, initial-scale=1')
  meta(name='description', content='')
  meta(name='author', content='')
  title Meteor. TODO List.

We can start the browser and make sure that after adding the file the page has the specified title.

First, let’s perform the basic configuration of the client routing. We’ll review this process in detail a bit later. For the routing, we can use a popular method that has the necessary functionality. Let’s install the iron:router package (repository).

After the installation, create the router.coffee file in the client/config directory, containing the following:

# client/config/router.coffee
Router.configure
  layoutTemplate: "application"

It is obvious that we define a basic template for our application and it will be named application. Therefore, we’ll create the application.jade file in the layouts folder. In this file, we will describe the template, an entity that will become the code in JavaScript after the compilation is done. By the way, Meteor uses its own spacebars template engine and the blaze library.

In short, the process of templates operation looks like as follows (as far as I understand from the documentation). spacebars templates are compiled into an object of the Blaze library, that will later work directly with the DOM.

The official description of the library provides the following comparison with other popular libraries:

  • Compared to Backbone and other libraries that simply re-render templates, Blaze does much less re-rendering and doesn’t suffer from the dreaded «nested view» problem, which is when two templates can’t be updated independently of each other because one is nested inside the other. In addition, Blaze automatically determines when re-rendering must occur, using Tracker.
  • Compared to Ember, Blaze offers finer-grained, automatic DOM updates. Because Blaze uses Tracker’s transparent reactivity, you don’t have to perform explicit «data-binding» to get data into your template, or declare the data dependencies of each template helper.
  • Compared to Angular and Polymer, Blaze has a gentler learning curve, simpler concepts, and nicer template syntax that cleanly separates template directives and HTML. Also, Blaze is targeted at today’s browsers and not designed around a hypothetical «browser of the future.»
  • Compared to React, Blaze emphasizes HTML templates rather than JavaScript component classes. Templates are more approachable than JavaScript code and easier to read, write, and style with CSS. Instead of using Tracker, React relies on a combination of explicit «setState» calls and data-model diffing in order to achieve efficient rendering.

I have come across these technologies myself (except for Ember), so I agree with the authors of the library. As for Blaze’s drawbacks, I’d like to point out that it’s dependent on Meteor.

But in our project we do not use explicitly Blaze, or Spacebars. For jade templates, the process of compilation has the following sequence:

jade -> spacebars -> blaze

All templates in Meteor are described in the template tag that should contain an attribute with the name of the template. Remember how we have specified layoutTemplate: «application» in the router settings? application is exactly the name of the template.

Hope you understand now, what templates in Meteor are. It’s time to create the layout; it is going to consist of the header and the footer.

//- client/layouts/application.jade
template(name='application')
  nav.navbar.navbar-default.navbar-fixed-top
    .container
      .navbar-header
        button.navbar-toggle.collapsed(
          type='button',
          data-toggle='collapse',
          data-target='#navbar',
          aria-expanded='false',
          aria-controls='navbar'
        )
          span.sr-only Toggle navigation
          span.icon-bar
          span.icon-bar
          span.icon-bar
        a.navbar-brand(href='#') TODO List
      #navbar.collapse.navbar-collapse
        ul.nav.navbar-nav
  .container
    +yield
  .footer
    .container
      p.text-muted TODO List, 2014.

We should understand, that it’s not jade we’re used to, with its mixins, JavaScript and includes. Jade should compile into the spacebars template, and leads to some peculiarities. We’ll use the syntax of jade, as we just don’t need the rest. The +yield structure is used in the given template. This structure means than the template will be re-rendered instead of yield. It’s one of iron:router features, it substitutes the necessary template, depending on the route. We’ll deal with routers a bit later. As for now, we can make some small changes and look at the result.

// client/styles/main.less
html {
  position: relative;
  min-height: 100%;
}
body {
  margin-bottom: 60px;
  & > .container{
    padding: 60px 15px 0;
  }
}
.footer {
  position: absolute;
  bottom: 0;
  width: 100%;
  height: 60px;
  background-color: #f5f5f5;
  .container .text-muted {
    margin: 20px 0;
  }
}

By the way, when changing styles, we do not need to refresh the page in the browser. It’s enough to save the file, and styles will be applied right away; that’s a handy out-of-the-box tool provided for Meteor developers.

iron:router - Organize your Meteor application

Routing

Meteor has no standard mechanism for routing, so let’s use the iron:router package. It is well documented, actively supported, has a rich functionality and is also the most popular solution for routing in the context of Meteor.

We can also use this library for the server routing. For example, I used it in real project to authenticate users, as the main project is made with Ruby on Rails, but users do not need to think that these are two different applications and authorize in both apps. In general, there are several popular approaches for the server routing and the REST API creation for Meteor.

Let’s create basic routers, so that we could see by the example the way this library works and the functionality it has. We will later add the basic functionality to them.

To begin with, define links to our pages:

//- client/layouts/application.jade
//- ...
#navbar.collapse.navbar-collapse
  ul.nav.navbar-nav
    li
      a(href='/') Home
    li
      a(href='/about') About

Then, create controllers in the folder of client routers, they are just stubs for now.

# client/routes/home.coffee
Router.route '/', name: 'home'
class @HomeController extends RouteController
  action: ->
    console.log 'Home Controller'
    super()
# client/routes/about.coffee
Router.route '/about', name: 'about'
class @AboutController extends RouteController
  action: ->
    console.log 'About Controller'
    super()

We should pass two parameters to the Router.route function. The first one is the route that can be a pattern (e.g.: /:user/orders/:id/info). All parameters of the pattern will be available in the controller object, via the params property. An object with options is passed as the second parameter. To move all the logic separate from the simple definition of the route and the name, we can create controllers. In our case, they are simple stubs. We do not specify names of controllers explicitly, as by default, iron:router tries to find the controller named Controller. And of course, our controllers should be available globally. We do it in CoffeScript by attaching the variable to the current context. As for the regular JS, it’s enough to simply declare the variable via var.

By the way, Meteor does not use, say, amd for the code loading. Files are simply loaded in a specific sequence. Therefore, all the interaction between modules described in various files, is carried out via global variables. As for me, it is quite convenient. But when using CoffeScript, it’s quite difficult to randomly declare a global variable, besides, it will be immediately noticeable.

iron:router also tries to automatically render a template, with the name of the route (but we can also define templates explicitly). Let’s create them:

//- client/components/home/home.jade
template(name='home')
  h1 Home
//- client/components/about/about.jade
template(name='about')
  h1 About

We can open the browser and make sure that our routing is working by clicking on the links in the header. By the way, it operates without the page refresh.

In the course of this tutorial, I will try to add all the updates to the repository, in accordance with the sequence of narration, so that you could trace the entire process, as some moments can be missed out in the article.

Go here to see how it looks in the end, and here you can find the project code in the current state.

Users and Authentication

In many technical tasks coming to our company, the first problem described is the user system. Since this task is quite widespread, I think we should consider the methods of user authentication in our tutorial, especially because Meteor provides standard means for this.

We are not going to go into details of mechanisms, and will just use ready-made solutions that will allow us to create users via username/password or google and github services. I am used to configuring devise and omniauth in rails by a pair of generators and several lines in the config. Guess what, Meteor does not only provide all of this out-of-the-box, but the configuration of services is also really simple.

Let’s install the following packages:

  • accounts-base – the basic package for users of an application on Meteor;
  • accounts-password, accounts-github, accounts-google – let’s add the support for authentication via username/password, and also github and google services;
  • ian:accounts-ui-bootstrap-3 – the package to simplify the integration of accounts into the Bootstrap application.

The ian:accounts-ui-bootstrap-3 package will allow us to add the authentication/registration form to the application with one line of code. It will also provide the interface for configuring third-party services. Here’s the project itself, you’ll find some documentation and screenshots of how the integration of the form and services configuration look like.

Modify our header:

//- client/layouts/application.jade
//- ...
#navbar.collapse.navbar-collapse
  ul.nav.navbar-nav
    li
      a(href='/') Home
    li
      a(href='/about') About
  ul.nav.navbar-nav.navbar-right
    //- the template of the user authorization button 
    //- is in the ian:accounts-ui-bootstrap-3 package
    +loginButtons

We’ll get the following result:

After the configuration, we can make sure that authorization tokens are saved in the database.

$ meteor mongo
MongoDB shell version: 2.4.9
connecting to: 127.0.0.1:3001/meteor
meteor:PRIMARY> show collections
meteor_accounts_loginServiceConfiguration
meteor_oauth_pendingCredentials
system.indexes
users
meteor:PRIMARY> db.meteor_accounts_loginServiceConfiguration.find()
{
  "service" : "github",
  "clientId" : "",
  "secret" : "",
  "_id" : "AjKrfCXAioLs7aBTN"
}
{
  "service" : "google",
  "clientId" : "",
  "secret" : "",
  "_id" : "HaERjHLYmAAhehskY"
}

Let’s configure our user system. Since I want to setup email verification, we should configure smtp. By the way, the email package is used for sending out emails. It is not included into the standard set of Meteor, so we’ll have to install it manually, in case you need to work with the mail.

# server/config/smtp/coffee
smtp =
  username: "[email protected]"
  password: "meteor-todo-list1234"
  server:   "smtp.yandex.ru"
  port:     "587"
# Escaping symbols
_(smtp).each (value, key) -> smtp[key] = encodeURIComponent(value)
# The template of url access to smtp
url = "smtp://#{smtp.username}:#{smtp.password}@#{smtp.server}:#{smtp.port}"
# Define the environment variable, Meteor will use data from it 
process.env.MAIL_URL = url

Configure accounts, so that Meteor would request the email address confirmation.

# server/config/accounts.coffee
emailTemplates =
  from: 'TODO List '
  siteName: 'Meteor. TODO List.'
# replace the standard settings for the mail 
_.deepExtend Accounts.emailTemplates, emailTemplates
# invoke verification
Accounts.config
  sendVerificationEmail: true
# add the custom logic during the registration of users 
Accounts.onCreateUser (options = {}, user) ->
  u = UsersCollection._transform(user)
  options.profile ||= {}
  # save hash addresses, so that we could get the avatar of the user 
  # who has not indicated the public email address 
  options.profile.emailHash = Gravatar.hash(u.getEmail() || "")
  # remember the service, via which the user has registered. 
  options.service = _(user.services).keys()[0] if user.services
  # save additional parameters and return the object
  # that will be written to the database
  _.extend user, options

Our application will not be able to link several services to a single user account, as this requires some tweaking. Perhaps, they will fix this soon. As for now, there’s such ready, more or less normal solution as mondora:connect-with, but it’s still a bit raw. We can try to merge accounts ourselves, there’s nothing difficult about it. Besides, there are plenty of examples and other solutions: one, two, three.

Also, there’s detailed documentation on accounts. We have just installed the packages and can see the magic, and it is not much harder under the hood.

Please don’t kick me for not considering the system of accounts in detail, I just wanted to show that there’s nothing difficult about it. To tell everything in more details, I would have to write a separate article on this subject. As for our tutorial, we’ve created the necessary basic functionality and can keep going to the final result.

Our next step is the user page, but before we get down to it, let’s take a look at the way some things are implemented in Meteor.

Collections, Publishing and Subscriptions

When creating the project, autopublish and insecure packages have been automatically added. It’s time to get rid of them, as they provide unlimited access for the user to all collections of the database, and they can be used for prototyping. Remove the packages with the following command:

$ meteor remove

Collections

We can compare collections of Meteor to Mongo collections — they also have find, insert, update, upsert methods (we can organize aggregation on the server with the help of the zvictor:mongodb-server-aggregation package). One of the collections is already created and we can get access to it via Meteor.users. For instance, try Meteor.users.findOne() in the browser console.

It is important to note here that all data of collections is cached in the browser, and if we run Meteor.users.find(options).fetch() in a loop on the client a million times, you will load nothing but the browser. This is achieved with the help of the minimongo library that is quite smart to make the selection depending on the passed parameters on the client.

Bare data is not very pleasant to work with, so it would be nice to add some business logic to objects of the collection. We can do it with the help of the _transform function in the collection, to which objects are passed after being received from the server, and we can process them there. But not to go into all details, we can use the dburles:collection-helpers package that adds helpers method to a collection. It can accepts an object, from which all the data will be inherited.

Install the package and write methods to update the user data. When creating a user, we have also added a field with the hash of the user’s avatar in the Gravatar service. Let’s add a method that will return a link to an image with some parameters. We’ll also add methods for checking the service of user registration, and methods returning public information.

# collections/users.coffee
Users = Meteor.users
# static methods and properties
_.extend Users,
  # the list of fields available for editing 
  allowFieldsForUpdate: ['profile', 'username']
# add methods and properties to the model 
Users.helpers
  # the method of updating the user, we can call it on the client 
  update: (data) ->
    Users.update @_id, data
  # the method for updating that will only set data 
  # take care of forbidden fields
  set: (data) ->
    d = {}
    f = _(Users.allowFieldsForUpdate)
    for key, value of data when f.include(key)
      d[key] = value
    @update $set: d
  # the method merges the current data with the passed one,
  # so that we could use it later for the update 
  # and lose nothing 
  merge: (data) ->
    current = @get()
    @set _.deepExtend(current, data)
  # receiving the model data only, all methods and properties 
  # declared here are in the prototype 
  get: ->
    r = {}
    r[key] = @[key] for key in _(@).keys()
    r
  # the list of all the email addresses
  getEmails: ->
    p = [@profile?.email]
    s = _(@services).map (value, key) -> value?.email
    e = _(@emails).map (value, key) -> value?.address
    _.compact p.concat(e, s)
  # the primary address
  getEmail: ->
    @getEmails()[0]
  # the public information
  getUsername    : -> @username || @_id
  getName        : -> @profile?.name || "Anonymous"
  getPublicEmail : -> @profile?.email
  urlData: ->
    id: @getUsername()
  # determine the link to Gravatar on the basis of the email address 
  # or the hash that has been determined automatically during the registration
  getAvatar: (size) ->
    size = Number(size) || 200
    options =
      s: size
      d: 'identicon'
      r: 'g'
    hash = "00000000000000000000000000000000"
    if email = @getPublicEmail()
      hash = Gravatar.hash(email)
    else
      hash = @profile?.emailHash || hash
    Gravatar.imageUrl hash, options
  # checking the service being used during the registration  
  isFromGithub:   -> @service == 'github'
  isFromGoogle:   -> @service == 'google'
  isFromPassword: -> @service == 'password'
  # the current user can edit 
  # some data about himself
  isEditable: -> @_id == Meteor.userId()
# Export the collection
@UsersCollection = Users

I guess we’ve clarified what collections in Meteor are like. It should be also mentioned that it is undesirable to store states in the model, as all data in the collection are reactive, and if we change the entry in the database, the stored somewhere in memory model object will lose its relevance and further working with it can lead us to using outdated data. I’ll provide some examples of how we can work with models.

Publishing

I’ve created three user entries in the database

$ meteor mongo
meteor:PRIMARY> db.users.count()
3

Not being authorized, we won’t be able to find any entry in UsersCollection. Otherwise, the collection will contain only objects created by my user.

In our application, we will not hide users from everyone; we’ll just hide private information, like authentication tokens.

Since we have removed the autopublish package, we’ll have to deal with the process of publishing data manually. This will allow us to control the data passed to the user.

Publish the collection of users:

# server/publications/users.coffee
Meteor.publish 'users', (limit = 20) ->
  UsersCollection.find {},
    fields:
      service: 1
      username: 1
      profile: 1
    limit: limit

The given code will provide access to users, you just need to subscribe. I immediately thought about pagination. In case we don’t specify the limit of the output, all entries about users will return at once, which is not really good for obvious reasons. The same will happen when we use autopublish, but automatically and to all the collections.

Also, we have specified data to be returned. Subscribers will see nothing but the username and some information from the profile field. But to provide access to email addresses for authorized users only, we’ll have to create another publication.

# server/publications/profile.coffee
Meteor.publish 'profile', ->
  # check whether the user requesting subscription
  # is authorized
  if @userId
    # subscribe him to his entry in the database
    UsersCollection.find { _id: @userId },
      fields:
        service: 1
        username: 1
        profile: 1
        emails: 1
  else
    # just say that it’s ready 
    @ready()
    return

The second parameter passed to the Meteor.publish method, is the function that should return a cursor of the collection. This function can accept any number of arguments and it is performed in the context of the object, that exposes some methods allowing to inform the user about various changes in the data and providing access to some properties of the connection. For example, we use a ready method in the profile publication. If the user is not authorized, this means that data in the publication is ready, and on the client’s side a callback will be invoked upon subscription, but he will receive no data. More about publishing.

Subscriptions

I have said more than once that to get data and trace changes in it, we should first subscribe to publications. In general, we can easily trace and control all that happens to data in Meteor application. But when simply creating a prototype, in which such things are not so important, we can always use the autopublish package.

We will use iron:router for subscriptions, and it will control the entire process. To control this process manually, we’ll have to monitor a lot of things, and the given library solves all the problems. It’s desirable to output some data by page, so before creating a controller for users, we will abstract a bit and create a class for managing pages that will be inherited from the controller of the iron:router library.

# client/lib/0.pageable_route_controller.coffee
varName = (inst, name = null) ->
  name = name && "_#{name}" || ""
  "#{inst.constructor.name}#{name}_limit"
class @PagableRouteController extends RouteController
  pageable: true # will check what kind of controller it is 
  perPage: 20    # the amount of data per one page
  # the limit of requested data
  limit: (name = null) ->
    Session.get(varName(@, name)) || @perPage
  # the next page
  incLimit: (name = null, inc = null) ->
    inc ||= @perPage
    Session.set varName(@, name), (@limit(name) + inc)
  # reset the amount
  resetLimit: (name = null) ->
    Session.set varName(@, name), null
  # has all the data been loaded?
  loaded: (name = null) ->
    true

Let’s create a template in the form of a button, clicking on which we’ll invoke the incLimit method, for the current controller, of course in case it supports this functionality. We could also perform the infinite scrolling, but it’s easier this way.

//- client/components/next_page_button/next_page_button.jade
template(name='nextPageButton')
  unless loaded
    a.btn.btn-primary.btn-lg.NextPageButton(href = '#')
      | More
# client/components/next_page_button/next_page_button.coffee
Template.nextPageButton.helpers
  loaded: ->
    ctrl = Router.current()
    if ctrl.pageable
      ctrl.loaded(@name)
    else
      true
Template.nextPageButton.events
  'click .NextPageButton': (event) ->
    ctrl = Router.current()
    if ctrl.pageable
      ctrl.incLimit(@name, @perPage)

We should define some logic for the component here. As you can see, the templates are added to the global Template namespace. We can refer to the template via Template.. To describe the methods used in the template, we should use the helpers method, where the object with methods is passed to. In this example, we describe just one method – loaded that checks what the current controller is like and returns a result showing whether all the data has been loaded. In the template, we invoke this method in the unless loaded structure and it’s also possible to use data from the current context. When helpers are used in the template, they can be compared to object prototypes. But there are restrictions in the function itself, as each helper is called something like .apply(context, arguments); so we cannot access to all the helpers of the template inside the function, which can sometimes cause problems.

To process events of the template, we should define them in the events method, where the object is passed to, with keys of the format. The jQuery event and the template, in which it has been called, are passed to the handler, as we can handle child events in the parent template, which can be quite useful sometimes.

Now, we are ready to create a page with the list of all users, and see how we can manage subscriptions in iron:router.

# client/routes/users.coffee
Router.route '/users', name: 'users'
class @UsersController extends PagableRouteController
  # number of users per one page
  perPage: 20
  # subscribe to the user collection, with the specified limit,
  # so that we would not get unnecessary data
  # 
  # subscription is carried via this method, so iron:router
  # would not render the page loading template each time during the subscription 
  # update
  subscriptions: ->
    @subscribe 'users', @limit()
  # return all users from the local collection 
  data: ->
    users: UsersCollection.find()
  # are all users loaded?
  loaded: ->
    @limit() > UsersCollection.find().count()
  # reset the limit each time during the page loading 
  onRun: ->
    @resetLimit()
    @next()

Subscription to the users publication occurs in the subscriptions method. There’s also an almost similar waitOn method that makes the router wait for all the data to be populated, and then will render the page. Till this moment, it will display the template that can be set via the loadingTemplate property. The data returned by the data method, will be bound to the template, and we will be able to use it via the current context. UsersCollection.find() returns a cursor rather than the data itself, but Blaze will make all conversions for us, so it’s just like we’re working with the ready data. Since we subscribe to a limited amount of data, the UsersCollection.find().fetch() call will return only the data loaded on the client. So, if we set a limit to 1, then the find will work with the loaded sample only (one entry), and not with all the data in the collection. For example, here we redefine the loaded method, but we should remember that count will return a number of local entries, which means that it will be equal to limit, till all the data is populated. That’s why the condition has the “greater than” sign.

There are several hooks in iron:router. For instance, it wouldn’t be bad to reset the limit of loaded users every time we open a new page. Otherwise, if we have previously fetched large amounts of data, the page can be rendered for quite a while. Therefore, to reset the limit of data, we can use the onRun hook. It is performed once, during the page load. By the way, this hook will not be performed during the hot swapping of the code performed by Meteor itself, after we save files with the code. So we have to refresh the page manually when debugging the controller that uses this hook (there’s no such problem with other ones). More about hooks and subscriptions.

Reactive Variables and Functions

Thus, we have subscribed to a publication, but you may still be wondering why clicks on the button from the nextPageButton template will lead us to loading new amounts of data. This happens thanks to manipulations with the Session object in PagableRouteController. The data in this object is reactive, and iron:router will automatically trace changes in them. You can type the following in the browser console:

Tracker.autorun( function() {
  console.log( 'autorun test', Session.get('var') );
} )

and try to change the value by calling Session.set(‘var’, ‘value’). The result won’t take long.

Due to a similar mechanism, iron:router understands when it is necessary to update subscription. The same way data in templates is automatically updated. You can read more about reactive variables in the official documentation. In addition to variables, Session also provides the capability to create reactive objects, with set and get methods for managing values that will be also monitored by the tracker and templates. The tracker is something like a listener. We can create a function containing no reactive variables, but it will be monitored by the tracker as well — use Tracker.Dependency for this purpose. This library has other features, but I have not used them in practice.

Here’s another example you can run in the browser console:

var depend = new Tracker.Dependency();
var reactFunc = function() {
  depend.depend(); return 42;
}
Tracker.autorun(function() {
  console.log( reactFunc() );
});
// 42
depend.changed()
// 42
depend.changed()
// 42

A Bit More About Subscriptions

I’ve told you how to use subscriptions by the example of iron:router, but this mechanism is not the only one. The main thing to remember is that we should use subscriptions carefully; otherwise we’ll risk populating large amounts of data and automatically tracing updates in them where necessary. iron:router provides a really simple way to manage subscriptions. It will disable all unnecessary subscriptions and enable the necessary ones and update the current ones when needed, like when loading the next page in our case.

Let’s create a list of users and make sure that everything works in practice.

//- client/components/users.jade
template( name='users' )
  h1 Users
  .row
    //- the data to be passed by the router to the template 
    +each users
      .col-xs-6.col-md-4.col-lg-3
        //- rendering the user card 
        //- the context changes in the each block, so в блоке each контекст меняется, поэтому мы
        //- we can pass no parameters to the template
        +userCard
  //- the button of the next page loading
  +nextPageButton
//- client/components/user_avatar/user_avatar.jade
//- unify the avatar’s template; perhaps, we’ll have to add logic.  
template(name='userAvatar')
  img(src="{{user.getAvatar size}}", alt=user.getUsername, class="{{class}}")
//- client/components/user_card.jade
//- user’s data is used in this template
//- as well as the functions described earlier in the model 
template(name='userCard')
  .panel.panel-default
    .panel-body
      .pull-left
        +userAvatar user=this size=80
      .user-card-info-block
        ul.fa-ul
          //- the service and the username 
          li
            if isFromGithub
              i.fa.fa-li.fa-github
            else if isFromGoogle
              i.fa.fa-li.fa-google
            else
              i.fa.fa-li
            b= getName
          //- identifier or username 
          li
            i.fa.fa-li @
            //- the reference to the user
            a(href="{{ pathFor route='users_show' data=urlData }}")= getUsername
          //- the email address, is specified
          if getPublicEmail
            li
              i.fa.fa-li.fa-envelope
              = getPublicEmail

As a result, pagination works here. Since all the data is reactive, new users of the system will be automatically added to the page, without any refreshes, because we have subscribed to a collection, which means that any data changes in the database on the server will be immediately displayed on the user page. You can try to register a new user in a new tab, or change the value directly in the database with the help of the mongo utility. Changes will appear on the page, and you won’t have to do anything.

To make sure that this approach is working optimally, we can look at the browser logs. I have set the number of users equal to one. The DDP Protocol is simple enough and easy to read, so I will not go into details. We can see in the logs that all unnecessary subscriptions have been unsubscribed, and users have been loaded three times only, one for each update of subscription.

User Page and A Bit More About Templates

Let’s create a user page with the capability to change some data. Now, we can finish working with users and get down to creating our own collections.

First of all, instead of the home page, we will show the current user page for an authorized user. Modify the controller a little bit.

# client/routers/home.coffee
Router.route '/', name: 'home'
class @HomeController extends PagableRouteController
  # is the user authorized?
  isUserPresent: ->
    !!Meteor.userId()
  # subscribe to the profile in case the user is authorized
  waitOn: ->
    if @isUserPresent()
      @subscribe 'profile'
  # return data about the current user, if present 
  data: ->
    if @isUserPresent()
      { user: UsersCollection.findOne Meteor.userId() }
  # render the profile template in case the user is authorized 
  # and the homepage if otherwise
  action: ->
    if @isUserPresent()
      @render 'profile'
    else
      @render 'home'

Also, create the controller that can view any user’s profile.

# client/routers/user_show.coffee
Router.route '/users/:id', name: 'users_show'
class @UsersShowController extends PagableRouteController
  # use the same ready template 
  template: 'profile'
  # subscribe to the necessary user 
  waitOn: ->
    @subscribe 'user', @params.id
  # find the necessary user
  data: ->
    user: UsersCollection.findOneUser(@params.id)

For the convenience of searching users by ID or login, I’ve created additional methods in the collection: one of them returns a cursor, the other one returns data.

# collections/users.coffee
# ...
_.extend Users,
  # ...
  findUser: (id, options) ->
    Users.find { $or: [ { _id: id }, { username: id } ] }, options
  findOneUser: (id, options) ->
    Users.findOne { $or: [ { _id: id }, { username: id } ] }, options

We’re trying to get data for the user page, but the data is not published yet. Let’s fix it.

# server/publications/user.coffee
Meteor.publish 'user', (id) ->
  UsersCollection.findUser id,
    fields:
      service: 1
      username: 1
      profile: 1
    limit: 1

Almost everything is ready. Let’s create a template and look at the result. When creating the template, I’ve decided to add a component, which, depending on access rights, will provide an ability to edit the field of a model.

//- client/components/editable_field/editable_field.jade
//- here’s a mixture of calling helpers
//- and accessing the context data; by the way, if the helper’s name
//- and properties in the current context are the same,
//- the helper is preferred
//- we can access the context explicitly via this.
template(name='editableField')
  .form-group.EditableFiled
    if data.isEditable
      div(class=inputGroupClass)
        if hasIcon
          .input-group-addon
            if icon
              i.fa.fa-fw(class='fa-{{icon}}')
            else
              i.fa.fa-fw=iconSymbol
        input.Field.form-control(placeholder=placeholder, value=value, name=name)
    else
      if defaultValue
        span.form-control-static
          if hasIcon
            if icon
              i.fa.fa-fw(class='fa-{{icon}}')
            else
              i.fa.fa-fw=iconSymbol
          = defaultValue

To interpolate variables in strings and templates, we can use class=‘fa-{{icon}}’, in which icon is a variable.

# client/components/editable_field/editable_field.coffee
Template.editableField.helpers
  value: ->
    ObjAndPath.valueFromPath @data, @path
  name: ->
    ObjAndPath.nameFromPath @scope, @path
  hasIcon: ->
    @icon || @iconSymbol
  inputGroupClass: ->
    (@icon || @iconSymbol) && 'input-group' || ''
Template.editableField.events
  # propagate the "change" event, when the input data is changed
  'change .Field': (event, template) ->
    data  = $(event.target).serializeJSON()
    $(template.firstNode).trigger 'changed', [data]
//- client/components/profile/profile.jade
template(name='profile')
  //- change of the context, and the inside block will not be rendered,
  //- in case there’s no such property
  +with user
    .profile-left-side
      .panel.panel-default
        .panel-body
          .container-fluid
            .row.row-bottom
              //- the users’ avatar; as a parameter, pass structures
              //- of = type, that will develop into a single object
              //- and become the context of the userAvatar template
              +userAvatar user=this size=200 class='profile-left-side-avatar'
            .row
              //- edit fields for the current user
              +editableField fieldUsername
              +editableField fieldName
              +editableField fieldEmail
  .profile-right-side
    h1 Boards
# client/components/profile/profile.coffee
Template.profile.helpers
  fieldUsername: ->
    data:         @
    defaultValue: @getUsername()
    placeholder: 'Username'
    scope:       'user'
    path:        'username'
    iconSymbol:  '@'
  fieldName: ->
    data:         @
    defaultValue: @getName()
    placeholder: 'Name'
    scope:       'user'
    path:        'profile.name'
    icon:        'user'
  fieldEmail: ->
    data:         @
    defaultValue: @getPublicEmail()
    placeholder: 'Public email'
    scope:       'user'
    path:        'profile.email'
    icon:        'envelope'
Template.profile.events
  # catch changes in the edited fields
  # and update the user
  'changed .EditableFiled': (event, template, data) ->
    user = template.data?.user
    return unless user
    data = data.user
    user.merge data

To my mind, jade templates are quite semantic, as we don’t have to think about a lot of things and read much documentation – everything is already quite obvious. But if you have difficulties with understanding the above code, you’d better look through the documentation to mquandalle:jade and spacebars packages. I just had no problems with templates in Meteor, and I think that they’re really handy.

Anyway, everything’s ready. Open the authentication form in the header, log into the system, and your profile will be displayed on the page instead of the “Home” heading, without any refreshes.

If something still remains unclear for you, I advise you to check the current state of the project in the repository. In files, I tried to comment on everything happening (unfortunately, all the comments are in russian). Perhaps, you should also look through everything written above. I tried to highlight all the key points. You can definitely clone the project at this stage and try to “touch” it with your own hands.

I am going to touch upon several subjects related to the server code: how to create our own collections, how to protect data in collections from unwanted editing. I will also tell you about using RPC and npm libraries on the server.

More About Collections and Subscriptions

Before we get down to creating our own collections, let’s create a mechanism to automatically compute some fields when inserting/modifying data in the database. For this purpose, add the aldeed:collection2 package containing aldeed:simple-schema. These packages will allow to easily validate data, and also add indexes to the collection and so on.

Let’s add some new features to the aldeed:simple-schema package.

# lib/simple_schema.coffee
_.extend SimpleSchema,
  # this method will collect one schema from several passed objects
  # and return it
  build: (objects...) ->
    result = {}
    for obj in objects
      _.extend result, obj
    return new SimpleSchema result
  # If we add this object to the schema, 
  # the model will have two fields that will be computed 
  # automatically
  timestamp:
    createdAt:
      type: Date
      denyUpdate: true
      autoValue: ->
        if @isInsert
          return new Date
        if @isUpsert
          return { $setOnInsert: new Date }
        @unset()
    updatedAt:
      type: Date
      autoValue: ->
        new Date

Now, let’s create a new collection

# collections/boards.coffee
# the data schema
boardsSchema = SimpleSchema.build SimpleSchema.timestamp,
  'name':
    type: String
    index: true
  'description':
    type: String
    optional: true # an optional field
  # automatically generate the board owner
  'owner':
    type: String
    autoValue: (doc) ->
      if @isInsert
        return @userId
      if @isUpsert
        return { $setOnInsert: @userId }
      @unset()
  # the list of board users 
  'users':
    type: [String]
    defaultValue: []
  'users.$':
    type: String
    regEx: SimpleSchema.RegEx.Id
# register the collection and add the schema
Boards = new Mongo.Collection 'boards'
Boards.attachSchema boardsSchema
# data protection
Boards.allow
  # any authorized user can create boards
  insert: (userId, doc) ->
    userId && true
  # only the board owner can update data 
  update: (userId, doc) ->
    userId && userId == doc.owner
# static methods
_.extend Boards,
  findByUser: (userId = Meteor.userId(), options) ->
    Boards.find
      $or: [
        { users: userId }
        { owner: userId }
      ]
    , options
  create: (data, cb) ->
    Boards.insert data, cb
# object methods
Boards.helpers
  update: (data, cb) ->
    Boards.update @_id, data, cb
  addUser: (user, cb) ->
    user = user._id if _.isObject(user)
    @update
      $addToSet:
        users: user
    , cb
  removeUser: (user, cb) ->
    user = user._id if _.isObject(user)
    @update
      $pop:
        users: user
    , cb
  updateName: (name, cb) ->
    @update { $set: {name: name} }, cb
  updateDescription: (desc, cb) ->
    @update { $set: {description: desc} }, cb
  # joins
  getOwner: ->
    UsersCollection.findOne @owner
  getUsers: (options) ->
    UsersCollection.find
      $or: [
        { _id: @owner }
        { _id: { $in: @users } }
      ]
    , options
  urlData: ->
    id: @_id
# export
@BoardsCollection = Boards

The first step in creating a collection is defining the schema. This will allow us to validate data and automatically calculate some fields. You can read more about validation at the page of the aldeed:simple-schema package. You’ll find quite a rich functionality there. And if we install an additional aldeed:autoform package by the same author, it will allow us to generate forms that can immediately notify about errors when creating an entry.

We create a new collection in the database by calling Boards = new Mongo.Collection ‘boards’ if it it’s not there yet, or by connecting to the existing one. Basically, that’s all that is necessary for creating new collections. There are also a few options we can specify during creation.

Using the allow method of the collection, we can control the access to modifying data in it. In the current example, we do not allow to create new entries in the collection for all unauthorized users. We also allow changing data only for the board owner. These checks will be carried out on the server and we can not worry that some cool hacker will change this logic on the client. There is also an almost analogous deny method, I guess it’s clear what it is used for. More about allow and deny.

When printing the board card, I want to display data about the board owner. But if we subscribe to boards only, this data will not be passed to the client. But Meteor publications allow subscribing to any data, even to automatically computed, like collection counters and other stuff.

# server/publications/boards.coffee
Meteor.publish 'boards', (userId, limit = 20) ->
  findOptions =
    limit: limit
    sort: { createdAt: -1 }
  if userId
    # boards of a specific user 
    cursor = BoardsCollection.findByUser userId, findOptions
  else
    # all boards
    cursor = BoardsCollection.find {}, findOptions
  inited = false
  userFindOptions =
    fields:
      service: 1
      username: 1
      profile: 1
  # the callback for adding the board owner to subscription 
  addUser = (id, fields) =>
    if inited
      userId = fields.owner
      @added 'users', userId, UsersCollection.findOne(userId, userFindOptions)
  # monitor changes in the collection,
  # so that we could add users to the subscription 
  handle = cursor.observeChanges
    added: addUser
    changed: addUser
  inited = true
  # immediately add users during the initialization,
  # with a single query to the database 
  userIds = cursor.map (b) -> b.owner
  UsersCollection.find({_id: { $in: userIds }}, userFindOptions).forEach (u) =>
    @added 'users', u._id, u
  # stop listening to the collection’s cursor, when the subscription is stopped  
  @onStop ->
    handle.stop()
  return cursor

Since Mongo cannot make queries through several collections and output already processed data, like it happens in relational databases, we’ll have to get data about boards’ owners with the help of another query. Besides, it’s more convenient to work this way within data models.

First of all, depending on the query, we get the necessary boards from the database. Then, we should get users with the help of another query.

added, changed and removed methods within the context of publication can manage the data passed to the client. If we return a cursor of the collection in the publication, these methods will be automatically invoked, depending on the collection state. Therefore, we return a cursor, but also subscribe to the publication itself to changes in the data in the collection of boards, and send to the client the data about the user, as necessary.

With the help of connection logs for web sockets, or by using Meteor DDP Analyzer, we can make sure that such approach will be highly effective. It is important to understand that changes in the user collection will not synchronize with the client in our case, but that’s exactly how we wanted it to be. By the way, for a simple join, we can return an array of cursors as a result of the subscription.

To display user boards, I have added new subscriptions to the routers and created the necessary templates. But I have already reviewed all of this above, so if you are interested in all of the changes, you’ll find them here. As a result, we should get the following, though we will have to use console to create user boards.

To create a reactive publishing, you can also use the mrt:reactive-publish package.

Improving the Server

Let’s add the ability for boards to define the background image. We should configure the server for this, so that it could accept files, process them and return when prompted.

NPM

I prefer to use ImageMagick for image processing, but there are also corresponding packages for Node.js. They represent the interface to this library. To let Meteor use npm packages, add meteorhacks:npm, and then describe all the necessary packages in the packages.json file. For example, I need just one gm package, so my packages.json will look the following way:

{
  "gm": "1.17.0"
}

All the packages defined with the help of meteorhacks:npm will be converted into one Meteor package. So, there should be no problems when building an application with the help of the meteor build command, and all dependencies will be automatically resolved.

We can access npm packages using the Meteor.npmRequire() command that works just like the require function in Node.js.

RPC and Synchronous Calls of Asynchronous Functions

To upload and process an image, create a server method that can be called from the client.

# server/lib/meteor.coffee
Meteor.getUploadFilePath = (filename) ->
  "#{process.env.PWD}/.uploads/#{filename}"
# server/methods/upload_board_image.coffee
# connect up the library to process the image 
gm = Meteor.npmRequire 'gm'
# resize and save the image 
resizeAndWriteAsync = (buffer, path, w, h, cb) ->
  gm(buffer)
  .options({imageMagick: true})
  .resize(w, "#{h}^", ">")
  .gravity('Center')
  .crop(w, h, 0, 0)
  .noProfile()
  .write(path, cb)
# perform the image processing with the synchronous  
resizeAndWrite = Meteor.wrapAsync resizeAndWriteAsync
# register the method to load the image to the board
Meteor.methods
  uploadBoardImage: (boardId, data) ->
    board = BoardsCollection.findOne(boardId)
    if board.owner != @userId
      throw new Meteor.Error('notAuthorized', 'Not authorized')
    data  = new Buffer data, 'binary'
    name  = Meteor.uuid() # a unique name for the image
    path  = Meteor.getUploadFilePath name
    resizeAndWrite data, "#{path}.jpg", 1920, 1080
    resizeAndWrite data, "#{path}_thumb.jpg", 600, 400
    # save the data to the board
    BoardsCollection.update { _id: boardId },
      $set:
        background:
          url:   "/uploads/#{name}.jpg"
          thumb: "/uploads/#{name}_thumb.jpg"
    return

In the uploadBoardImage method, we accept the identifier of the board, to which the image is added, and also a string with binary data of the image.

If the method throws an exception, it will be passed to the user on the client as the first parameter of the callback. And the data returned by the method will come to the client as the second parameter of the callback.

To use exceptions and returns of functions in the asynchronous programming style, Meteor has a server-side method for doing that. It wraps asynchronous functions in synchronous ones, using fibers library. Thanks to this library, calls of wrapped functions will not take a run queue, so we can write synchronous code on the server and not worry about the sequence of code execution. The Meteor.wrapAsync() method wraps the functions accepting the callback as the last parameter. An error should be the first parameter of the callback, and the result should be the second parameter. This format of parameters is provided in all Node.js libraries. If an error comes, the wrapped function will throw out an exception with this error, otherwise the second parameter passed to the callback will be returned.

Routing

I do realize that it’s better to use ready-made and time-tested solutions to output the static data from the server, but I am going to use Node.js for that.

Meteor provides a standard webapp package for the server routing, but we have installed a much more handy solution: iron:router. The same way as on the client, create a server route:

# server/routes/uploads.coffee
fs = Meteor.npmRequire 'fs'
Router.route '/uploads/:file',
  where: 'server'
  action: ->
    try
      filepath = Meteor.getUploadFilePath(@params.file)
      file = fs.readFileSync(filepath)
      @response.writeHead 200, { 'Content-Type': 'image/jpg' }
      @response.end file, 'binary'
    catch e
      @response.writeHead 404, { 'Content-Type': 'text/plain' }
      @response.end '404. Not found.'

The main thing here is to pass the where: ‘server’ property to the router, otherwise the property will not work. In effect, we try to read the specified file from disk, as this directory will contain images of one format only. I have simplified this method as much as possible.

request and response objects are available in the route context. These are objects of classes from Node.js http.IncomingMessage and http.ServerResponse standard libraries.

iron:router also provides the interface for creating REST API.

Using RPC

Let’s create the form for adding a new board.

I have also created the autocomplete for adding users to the board. RPC is also used there. You can find the implementation details in the repository.

//- client/components/new_board_form/new_board_form.jade
template(name='newBoardForm')
  //- a panel with dynamic styles
  .panel.panel-default.new-board-panel(style='{{panelStyle}}')
    .panel-body
      h1 New board
      form(action='#')
        .form-group
          input.form-control(type='text',placeholder='Board name',name='board[name]')
        .form-group
          textarea.form-control(placeholder='Description',name='board[description]')
        .form-group
          //- hide the input with the file, but leave the label to this input, for the beauty of it
          label.btn.btn-default(for='newBoardImage') Board image
          .hide
            input#newBoardImage(type='file', accept='image/*')
        button.btn.btn-primary(type='submit') Submit
# client/components/new_board_form/new_board_form.coffee
# variables for the current user image 
currentImage = null
currentImageUrl = null
currentImageDepend = new Tracker.Dependency
# reset the user image
resetImage = ->
  currentImage = null
  currentImageUrl = null
  currentImageDepend.changed()
# upload the image to the server 
uploadImage = (boardId) ->
  if currentImage
    reader = new FileReader
    reader.onload = (e) ->
      # call the server method 
      Meteor.call 'uploadBoardImage', boardId, e.target.result, (error) ->
        if error
          alertify.error error.message
        else
          alertify.success 'Image uploaded'
    reader.readAsBinaryString currentImage
# helpers of the following form template
Template.newBoardForm.helpers
  # set the background image for the form,
  # the function will be automatically called, as it has a dependency
  panelStyle: ->
    currentImageDepend.depend()
    currentImageUrl && "background-image: url(#{currentImageUrl})" || ''
# this callback is activated every time when the form is rendered to the page  
Template.newBoardForm.rendered = ->
  resetImage()
# events of the form
Template.newBoardForm.events
  # when submitting the form, we create a new entry
  # if all went well, upload the image,
  # and reset the form
  'submit form': (event, template) ->
    event.preventDefault()
    form = event.target
    data = $(form).serializeJSON()
    BoardsCollection.create data.board, (error, id) ->
      if error
        alertify.error error.message
      else
        form.reset()
        alertify.success 'Board created'
        resetUsers()
        uploadImage(id)
        resetImage()
  # when selecting an image, we change the background of the form
  # and remember the current selection 
  'change #newBoardImage': (event, template) ->
    files = event.target.files
    image = files[0]
    unless image and image.type.match('image.*')
      resetImage()
      return
    currentImage = image
    reader = new FileReader
    reader.onload = (e) =>
      currentImageUrl = e.target.result
      currentImageDepend.changed()
    reader.readAsDataURL(image)

To upload and process the image here, we perform a remote method via Meteor.call. As you can see, a remote procedure call on the client does not really differ from a regular call of a function, and all the data passed by arguments will be uploaded to the server via the web socket. To read user files, I have used File API from the HTML5 specification

The example with uploading images may not be the best, but it demonstrates well the features of the server part of Meteor. If you’re writing for the production, you can use the ready-made CollectionFS solution.

New Board. Meteor App

  • The Result, images upload did not work there. Perhaps, it’s about ImageMagick, or maybe, it’s the impossibility to create own directory for storage.
  • The Repository

Basically, that’s all I wanted to cover in this tutorial, but for completeness, I will finish the functionality of cards in boards. Anything missed out in the tutorial will not be used in them. As usual, you will find updates in the repository.

References

Comments

    3,751

    Ropes — Fast Strings

    Most of us work with strings one way or another. There’s no way to avoid them — when writing code, you’re doomed to concatinate strings every day, split them into parts and access certain characters by index. We are used to the fact that strings are fixed-length arrays of characters, which leads to certain limitations when working with them. For instance, we cannot quickly concatenate two strings. To do this, we will at first need to allocate the required amount of memory, and then copy there the data from the concatenated strings.