Namek Dev
a developer's log
NamekDev

Beginning with Meteor and AngularJS 1/2 on top of TypeScript

November 15, 2015

Probably the best starting point for Angular and Meteor combo is angular-meteor.com website. There’s a neat tutorial for both AngularJS versions - 1.x and 2.0. But it’s still JavaScript and we want TypeScript (because of reasons), right?

What really tormented me was a real usage of TypeScript with Meteor and some quirks about it, as for beginner. Spent some time around it, so I’m going to share the final result.

Ummm, Angular 2.0?

Personally, I recommend starting with 1.x. Shocked? Please, take your time.

Latest conference in London - AngularConnect 2015 (Oct 20 & 21) - showed that there are still many things that are going to be improved. Or rather - are planned to be better, meaning - still in development.

Angular Team makes a promises that some kind of migration instruction and maybe even polyfills will appear. At the moment interesting links about the topic are:

ng-forward  is for those who want Angular 2 syntax in Angular 1.x. But syntax is not the most important thing - architecture is. Most of the future upgrade effort could be taken down now just by taking route of component pattern/approach. So, instead of future fighting with new syntax, you can code directives as if they were components by trying to not use controllers. In Angular 2 you’re going to do that anyway.

Sounds weird? There’s an excellent article for those who want to refactor existing Angular 1.x code to component approach - piece by piece:

http://teropa.info/blog/2015/10/18/refactoring-angular-apps-to-components.html

It’s totally worth reading, understanding and implementing.

Come on, Give me TypeScript already!

Not yet!!!

Before coding, you’re going to use a little more deterministic programming language than JavaScript so change your editor. Really. Atom together with TypeScript package or Visual Studio Code. You’re going to work with cross-module auto-completion! Awesomeness!

Code? Oh, yeah. But wait, first things first…

Create a project

Open system console, go to your Projects  directory (wherever it is) and throw some commands at it:

meteor create recipes
cd recipes
meteor remove blaze-html-templates
meteor remove ecmascript
meteor add pbastowski:typescript
meteor add angular
meteor add angularui:angular-ui-router
meteor remove autopublish insecure

Done? OK. So this is a base for us.  We removed standard package blaze-html-templates  in exchange for Angular. ecmascript  package is also removed in exchange for TypeScript compiler. Also, I assumed that everyone needs  ui-router . We remove autopublish  and insecure  because these are just prototyping pleasure. It’s enough of playing with packages, for now.

As we’re not use those, remove files generated by Meteor: recipes.js, recipes.css, recipes.html.

Setup file structure

Following the File Structure in Meteor Docs we’re gonna create some folders and files. This is my proposition for your file/folder structure:

- Projects/recipes/
  - client/
    - components/
      - recipes/
        - recipes.html
        - recipes.ts
        - recipes.css
    - app.ts
    - index.html
  - model/
    - Recipe.ts
  - private/
    - Recipes.json
  - server/
    - dataseed.ts
    - Recipe.ts
  - typings/
    - ext/
    - all-definitions.d.ts
    - custom.d.ts
  - .gitignore
  - tsconfig.json

Now, It would be best to go and create whole this structure, including empty files.

Let’s configure some for the starters.

typings/ext/
.vscode/





{
    "filesGlob": [
        "./typings/**/*.d.ts",
        "./model/**/*.ts",
        "./server/**/*.ts",
        "./client/**/*.ts"
    ],
    "compilerOptions": {
        "module": "commonjs"
    },
    "compileOnSave": false,
    "buildOnSave": false
}

compileOnSave/buildOnSave is for Atom - make it not compiling .ts files by itself. module: commonjs is to shut up TypeScript package error, let’s don’t bother.

TypeScript typings

Just one more thing before real code to be written (or copied). Typings.  SinceTypeScript is a superset of JavaScript it works altogether with pure JavaScript code. “Thanks to” this it doesn’t know types of many things. Here come DefinetelyTyped - huge type library for certain libraries, such as Angular-Meteor, Angular or jQuery. We’re going to install some of them - commands, go:

npm install -g tsd
tsd init

Now typings  folder will be created, remove it’s content. Then open tsd.json  file and reconfigure paths for ext  folder, I’ll explain in a moment:

"path": "typings/ext",
"bundle": "typings/ext/tsd.d.ts",

And one more command:

tsd install angular-meteor --save

You should have angular-meteor, angular, jquery and meteor typings now installed.

Why ext folder? Because from time to time it will happen that we’ll need to add something custom to global typings. While typings/ext/tsd.d.ts  contains global package typings, the typings/custom.d.ts  may look like this:

/// <reference path="ext/tsd.d.ts" />
interface Window {
    encodeURIComponent: Function
    decodeURIComponent: Function
    ourProjectNamespace?: any
}
  • basically, you’ll add here things that are not provided by external typings.

Finally, typings/ext/all-definitions.d.ts  joins externals and customs together:

/// <reference path="ext/tsd.d.ts" />
/// <reference path="custom.d.ts" />

tsd.json  should be added to your git so on another machine instead of manually adding all typings you can just call tsd install .

Now we code

Begin with entry point of application - setup angular modules and routing:

angular.module('recipes', [
    'angular-meteor',
    'ui.router',
])
.config(['$urlRouterProvider', '$stateProvider', '$locationProvider', configureApp])
.controller('RecipesComponent', RecipesComponent)

function configureApp($urlRouterProvider, $stateProvider, $locationProvider) {
    $locationProvider.html5Mode(true)

    $urlRouterProvider.otherwise('/')

    $stateProvider
        .state('recipes', {
            url: '/',
            templateUrl: 'client/components/recipes/recipes.html',
            controller: 'RecipesComponent',
            controllerAs: 'vm'
        })
}

And here’s an initialization entry point for our Angular module:

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <base href="/">
</head>
<body>
    <div ng-app="recipes">
        <div ui-view></div>
    </div>
</body>

Note that there’s no need for  tag, Meteor will handle this and few other things.

Recipe Controller Component

Sure, component approach is the future but we’re going to code some controller in Meteor+AngularJS+TS combo. So, again - we’ll use controller as it would be a component.

class RecipesComponent {
    recipes: RecipesCollection

    constructor(
        private $scope: ng.IScope,
        private $meteor: ng.meteor.IMeteorService,
        private $log: ng.ILogService
    ) {
        let vm: RecipesComponent = this
        vm.recipes = $meteor.collection(Recipes).subscribe('best_recipes')
    }
}
RecipesComponent.$inject = ['$scope', '$meteor', '$log']
this.RecipesComponent = RecipesComponent
  1. we have four class fields here (look at the constructor syntax)
  2. The last line will probably cause a little shock. Well, after this file is compiled, Meteor puts it into IIFE.
  3. vm  variable is pretty handy when searching for variable usage between views or components and controllers or directives. AND it’s useful in anonymous functions (where this could be something different, depending on context) - read more about vm  in controllerAs with vm pattern in John’s Papa Angular Style Guide
  4. you can put semicolons in the end of lines but I decided to not do this

Model

interface Recipe {
  _id?: string
  fname?: string
  lname: string
}

declare var Recipes: Mongo.Collection("recipes")
type RecipesCollection = angular.meteor.AngularMeteorCollection2<Recipe, Recipe>
  1.  _id comes from MongoDB. It’s best to make it optional (? operator) so it’s possible to create a new Recipe without ID, it will be then created automatically. On the rest of fields you decide - optional or not.
  2. recipes  is name of collection (think of it as a table in relational databases)
  3. the type is just a helper for auto-completion that we’re going to use in controller!

Put Model into Controller Component

class RecipesComponent {
    recipes: RecipesCollection

    constructor(
        private $scope: ng.IScope,
        private $meteor: ng.meteor.IMeteorService,
        private $log: ng.ILogService
    ) {
        let vm: RecipesComponent = this
        vm.recipes = $meteor.collection(Recipes).subscribe('best_recipes')
    }
}
RecipesComponent.$inject = ['$scope', '$meteor', '$log']
this.RecipesComponent = RecipesComponent

We’re going to subscribe for data but not just recipes, instead - best_recipes  which is published by the Server like this:

Meteor.publish('best_recipes', function() {
    return Recipes.find({rating: 5})
})

Seed database

[{
    "name": "Recipe 1",
    "text": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
    "products": ["product 1", "product 2"],
    "rating": 4
}, {
    "name": "Recipe 2",
    "text": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "products": ["product 1", "product 3", "product 4"],
    "rating": 5
}, {
    "name": "Recipe 3",
    "text": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "products": ["product 3", "product 4"],
    "rating": 4
}, {
    "name": "Recipe 4",
    "text": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat",
    "products": ["product 2", "product 3", "product 4"],
    "rating": 5
}, {
    "name": "Recipe 5",
    "text": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "products": ["product 3", "product 4"],
    "rating": 5
}]

The above is just a file that’s in special private  folder which for Meteor means that’s a Server-ish thing and Client has nothing to do about it. We’re going to have a use of that fact by seeding data if there’s nothing in database:

Meteor.startup(function() {
    if (Recipes.find().count() === 0) {
        seedRecipes()
    }
})

function seedRecipes() {
    var recipes = JSON.parse(Assets.getText('Recipes.json'))

    for (var i = 0; i < recipes.length; ++i) {
        Recipes.insert(recipes[i])
    }

    // and one more recipe...
    let recipe: Recipe = {
        name: 'Recipe 6',
        text: 'No text here, sorry',
        rating: 5
    }
}

Script above loads recipes from the file and adds one more so you can see a way to create a new Recipe.

Let’s View see

Now, we can simply bind to vm.recipes by ng-repeat directive as if it was an array.

<h1>Best Recipes</h1>
<div ng-repeat="recipe in vm.recipes">
    <h2>{{recipe.name}}</h2>
    <p>{{recipe.text}}</p>
    <ul>
        <li ng-repeat="product in recipe.products">{{product}}</li>
    </ul>
</div>

Now just type meteor  in system console, wait a moment and enter http://localhost:3000. You shall see Best Recipes which are only those of rating equal to 5.

Whole code

Full code can be found on repository here: https://github.com/Namek/meteor-angular-typescript and you can treat it as a skeleton for your app.

Final thoughts

TypeScript is great. Thanks to compilation step it prevents user from making stupid mistakes that JavaScript programmers always struggle with. While it’s not that cool in small projects, it’s definetely what big projects want.

Angular is there since 2009 so it already proved some quality about frontend development. Of course, it has many drawbacks but it showed the world that declarative approach is the way to go.

Finally, there’s Meteor. It’s almost there - in my opinion. People like it for not having to create a Gulpfile/Gruntfile but I don’t like it because of that same fact. Why? Sass package can’t have autoprefixer because Meteor team still didn’t provide a good architecture for it: issue #5129. So that’s weak. There’s even more and more struggle about Meteor but in the other hand there’s Meteor’s Reactivity which keeps us away from callback hell for data fetching and sorting. And that’s very nice.

meteor, typescript-lang, web
comments powered by Disqus