Tag Archives: meteor

Dropdown menus in Meteor

I’ve been developing a site in Meteor, using their default templating engine Blaze.

And here’s a question: what’s the Meteor-like way to show a dropdown menu?

I’m using Bootstrap, so of course we can use Bootstrap’s javascript (based on jQuery) to do this. But it doesn’t feel quite right – shouldn’t we use a reactive approach, eg. with a reactive Session variable telling us whether the dropdown is open?

Surprisingly, I haven’t seen any such examples in my Google travels.

So until I get my act together to publish this as a Meteor package, here is a way to do it.

Define this html template:

<template name="dropdown">
    {{> Template.dynamic template=templateName data=templateData}}
</template>

And put this javascript in your client folder. Note it uses underscore for _.extend, but you should be able to remove this dependency if you need to.

//
// if the ESC key is pressed or a mouse is clicked anywhere, close any open dropdowns
//

$(document).keyup(function(evt) {
    if (evt.keyCode === 27) {
        Session.set('dropdown', null);
    }
});

// You will need to change the word "layout" here to be a template
// that you define which covers your entire webpage
Template.layout.events({
    'click': function() {
        Session.set('dropdown', null);
    }
});

Template.dropdown.helpers({

    "templateName": function() {
        return 'dropdown_' + this.name;
    },

    "templateData": function() {
        // add an 'open' property to the template for the child to tell if it is open
        // _.extend(dest, src) copies all the properties in src to dest and returns dest
        return _.extend({open: Session.equals('dropdown', this.name)}, this);
    }

});

Template.dropdown.events({

    'click button': function(evt) {
        evt.preventDefault();
        evt.stopPropagation();  // stops the full-page click handler above from firing
        Session.set('dropdown', this.name);
    },

    'click .dropdown-menu li a': function(evt) {
        evt.preventDefault();
        Session.set('dropdown', null);
    }

});

Note that I’ve defined a click handler which fires on any click anywhere on the webpage (you need to change the line Template.layout.events to your own page-wide template), and which closes any open dropdowns. I then use evt.stopPropagation() to stop that firing when you click the button which opens the dropdown (otherwise it could never open). If you define any other click events which should not affect open dropdowns, then you need to include evt.stopPropagation() in their definition.

With that done, it’s easy to put a dropdown in your template. Just include:

{{>dropdown name='example'}}

This finds the template dropdown_example, which will have access to an “open” method to tell if it is open. So you can define it like this:

<template name="dropdown_example">
    {{#if open}}
        <ul class="dropdown-menu x">
            <li><a href="#">Option 1</a></li>
            <li><a href="#">Option 2</a></li>
            <li><a href="#">Option 3</a></li>
        </ul>
        <button class="invisible btn">Try this</button>
    {{else}}
        <button class="btn">Try this</button>
    {{/if}}
</template>

If you need to pass the dropdown menu template some data, you can pass it directly:

{{>dropdown name='example' someData='foo'}}

and then dropdown_example will have access to the variable someData.

Hope that helps! If you have a better solution, or find any mistakes, please let me know.

  

Experience with Meteor

Meteor Day is upon us!

Here are the slides I am going to present on my experience developing with Meteor. In a nutshell -

  • Love the principles – they made it easy to develop a great webapp quickly
  • I’ve done a few cool things, I think! (like encryption, undo, admin panel with datatables, custom art:accounts-ui)
  • But… SEO is killing me (only two keywords showing!?)
  • The Paypal IPN has been a pain – still unresolved
  • Can’t escape the usual browser issues – and Iron Router adds a few for IE9
  • Speed has been an intermittent problem
  • Still feeling my way towards a good programming style
  • Please check out the app!
(Just click on the slide above and then you can advance through the deck using the arrow keys. Alternatively, click here to see them in a new tab.)


You can see the slides from the talk online here.

  

Nicely format tables without using table-layout:fixed

I have recently helped to develop Signup Zone, a very handy app where people can easily create their own signup sheets and rosters. People have started using it for interesting purposes, including registering to help injured wildlife! And this has led to people entering in unexpected data, such as very long email addresses; and creating sheets with a lot of columns.

At its heart, the signup sheet is simply an HTML table such as this:

Date Name Contact email address
10/11/2014 Racing Tadpole racingtadpole@example.com

This table has the code:

<table>
    <thead>
        <tr>
            <th scope="col">Date</th>
            <th scope="col">Name</th>
            <th scope="col">Contact email address</th>
        </tr>
    <tbody>
        <tr>
            <td>10/11/2014</th>
            <td>Racing Tadpole</th>
            <td>racingtadpole@example.com</th>
        </tr>
    </tbody>
</table>

Since I don’t know in advance how to size the different columns, I actually really like the browser’s built-in ability to choose its own column widths.

However, the browser will not let any content spill outside a cell, and it only breaks words at white space. So if one user enters a long email address (or URL) into a cell, that column becomes very wide and the table either spills outside the page margins or looks unsightly – or both, like this:

Date Name Email
10/11/2014 Racing Tadpole racingtadpole@example.com
11/12/2014 Another Tadpole anotherracingtadpole@quitelong.verylong.superlong.megalong.example.com

Here are two solutions:

  • Use the new css command hyphens: auto. It won’t work in Opera, Chrome (!) or older browsers (eg. IE 9), and it can hyphenate too many words, but it looks ok, eg:
    Date Name Email
    10/11/2014 Racing Tadpole racingtadpole@example.com
    11/12/2014 Another Tadpole anotherracingtadpole@quitelong.verylong.superlong.megalong.example.com
  • Run some javascript to insert a “zero-width space” or an invisible soft hyphen in places where the offending text can break, eg. at the . or @ symbols. This gives you control over where the words break, eg:
    Date Name Email
    10/11/2014 Racing Tadpole racingtadpole@​example​.com
    11/12/2014 Another Tadpole anotherracingtadpole@​quitelong​.verylong​.superlong​.megalong​.example​.com

    However, if someone copies the email address it will also copy the invisible characters, which could cause trouble.

Here’s some javascript code to implement the second solution in Meteor:

<template name="example">
    ...
    <td>{{{email}}}</td>
    ...
</template>
function htmlEscape(str) {
    return String(str).replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
}

function myMarkup(str) {
    return String(str).replace(/@/g, '&#8203;@')
            .replace(/\./g, '&#8203;.')
            .replace(/\//g, '&#8203;/');
}

Template.example.email = function(){
    // suppose s is the string we want to show
    return myMarkup(htmlEscape(s));
}

The htmlEscape function is inspired by this Stack Overflow post. An alternative is to use Spacebars.SafeString.

Hope that’s helpful!

  

Hosting Meteor with MongoDB on Webfaction

Meteor is constantly evolving, and so are the ways to host it. I couldn’t find any single source that explained how to deploy a Meteor site to Webfaction using the latest technology (ie. as of March 2014). So here is how I did it for my site, Signup Zone.

First, follow Webfaction’s instructions to install a MongoDB app and add a data directory. I have called the app mongodb1. Note down its port number, eg. 18000.

The instructions say to add a user with the ["userAdminAnyDatabase"] role. I have also added a second user with the roles ['clusterAdmin', 'userAdminAnyDatabase', 'readAnyDatabase'], based on this Stackoverflow answer.

The instructions then say to run this in an SSH window:

./mongodb-linux-x86_64-2.4.9/bin/mongod --auth --dbpath $HOME/webapps/mongodb1/data/ --port 18000

Use Webfaction’s one-click installer to install a node.js app, and note down its port number too, eg. 17000. Upgrade it to the latest stable version using the “n” package, following Webfaction’s instructions for node.

Following this post, I demeteorized my meteor app, pushed it up to a git repo on Webfaction, and pulled it down into $HOME/webapps/nodeapp/demeteoredapp/.

As mentioned in the Webfaction instructions, in the nodeapp directory, type export PATH=$PWD/bin/:$PATH. Then cd demeteoredapp and type npm install in that directory.

In another SSH shell, enter (the DB name is “admin”):

export MONGO_URL="mongodb://mongo_user:mongo_password@localhost:18000/admin?autoReconnect=true"  # 2
export MAIL_URL='smtp://user:password@smtp.webfaction.com'
export PORT="17000"
export ROOT_URL="http://example.com"

Then type:

./bin/node $HOME/webapps/nodeapp/demeteoredapp/main.js

You can adjust the default bin/start script to do this too.

Having installed the app this way, I have developed a process to update the code. I rename the previously demeteorized app directory, eg. to old; go into my Meteor app directory and type: demeteorize -o ../demeteoredapp, then mv ../old/.git ../demeteoredapp. At that point I can do git add --all and commit and push the new version up to my git repo on Webfaction. I then ssh onto the server and type:

cd webapps/nodeapp/demeteoredapp/appname
git pull; ../bin/stop ; ../bin/start

to pull in the new changes and launch them.

Note there is no need for the forever package when you do it this way. I’m not sure we need the demeteorizer package either, or if the Meteor bundler is sufficient.

I found some comments online that there can be memory problems if you use Meteor and MongoDB on Webfaction. I haven’t experienced this problem. (I’m keeping a close eye on it – I have even built an admin panel which includes a call to a Meteor method that executes the memory_usage.py script provided by Webfaction.)

Ongoing jobs

You will then want to set up cron jobs to start your MongoDB app if it fails for any reason. I have modelled this script off Webfaction’s start script for node.js:

#!/bin/sh
mkdir -p $HOME/webapps/mongoapp/run
mkdir -p $HOME/webapps/mongoapp/log
pid=$(/sbin/pidof $HOME/webapps/mongoapp/mongodb-linux-x86_64-2.4.9/bin/mongod)
if echo "$pid" | grep -q " "; then
  pid=""
fi
if [ -n "$pid" ]; then
  user=$(ps -p $pid -o user | tail -n 1)
  if [ $user = "your-username" ]; then
    exit 0
  fi
fi
nohup $HOME/webapps/mongoapp/mongodb-linux-x86_64-2.4.9/bin/mongod --auth --fork --logpath $HOME/webapps/mongoapp/log/mongo.log --dbpath $HOME/webapps/mongoapp/data/ --port 18000 > /dev/null 2>&1 &
/sbin/pidof $HOME/webapps/mongoapp/mongodb-linux-x86_64-2.4.9/bin/mongod > $HOME/webapps/mongoapp/run/mongo.pid

Put this into your scheduled tasks using crontab -e.

You will also want to back up your database. A simple command to back up the database to a directory called dbbackup is:

$HOME/webapps/mongoapp/mongodb-linux-x86_64-2.4.9/bin/mongodump --port 18000 -o $HOME/dbbackup

You will need to add the admin username and password.

I like to have daily, weekly and monthly backups, so I make three directories $HOME/mongo_backup/daily, weekly, monthly and have a script for each:

The daily script is:

#!/bin/sh
# $HOME/scripts/cron_daily_mongo_backup.sh
# add this to crontab, e.g. to run at 15:45 server time every day:
# 45 15 * * * ~/scripts/cron_daily_mongo_backup.sh

# back up the database
$HOME/webapps/mongoapp/mongodb-linux-x86_64-2.4.9/bin/mongodump --port 18000 -o $HOME/mongo_backup/daily/dump-`date +\%Y\%m\%d` 2>> $HOME/mongo_backup/daily/cron.log

# delete backup directories older than 7 days
find $HOME/mongo_backup/daily -type d -ctime +7 | xargs -r rm -rf  # Be careful using -rf

The weekly script is:

#!/bin/sh
# add this to crontab as, e.g. to run at 16:20 server time every Sunday:
# 20 16 * * 0 ~/scripts/cron_weekly_mongo_backup.sh
cp -R $HOME/mongo_backup/daily/dump-`date +\%Y\%m\%d` $HOME/mongo_backup/weekly
find $HOME/mongo_backup/weekly -type d -ctime +32 | xargs -r rm -rf

And the monthly script is:

#!/bin/sh
# add this to crontab as, e.g. to run at 16:25 server time every 1st of the month:
# 25 16 1 * * ~/scripts/cron_monthly_mongo_backup.sh
#
cp -R $HOME/mongo_backup/daily/dump-`date +\%Y\%m\%d` $HOME/mongo_backup/monthly
find $HOME/mongo_backup/monthly -type d -ctime +370 | xargs -r rm -rf

If you’ve read this far, please check out the finished product at signup.zone!