/ng-classify

Convert CoffeeScript classes to AngularJS modules

Primary LanguageCoffeeScriptMIT LicenseMIT

ng-classify

License Version Build Status Dependency Status

Convert CoffeeScript classes to AngularJS modules
Write less JavaScript. Write less CoffeeScript. Write less Angular.

Watch the screencast
Demo

Install

Install with npm

$ npm install ng-classify

Usage

CoffeeScript

ngClassify = require 'ng-classify'

content = '''
class Home extends Controller
	constructor: ($log) ->
		$log.info 'homeController instantiated'
'''

angularModule = ngClassify content

JavaScript

var ngClassify = require('ng-classify');

var content = '\
class Home extends Controller\n\
	constructor: ($log) ->\n\
		$log.info \'homeController instantiated\'\
';

var angularModule = ngClassify(content);

Gulp

gulp-ng-classify

$ npm install gulp-ng-classify

Grunt

grunt-ng-classify

$ npm install grunt-ng-classify

Ruby Gem

ng_classify

$ gem install ng_classify

Table of Contents

Overview

AngularJS is well suited to take advantage of the CoffeeScript class syntax. However there's still a bit of boilerplate code we have to work through. ng-classify addresses this. Note: all examples are valid CoffeeScript.

Here's how you write a controller using ng-classify

class Admin extends Controller
	constructor: ($scope, someService) ->
		$scope.coolMethod = someService.coolMethod()

which is equivalent to

angular.module('app').controller('adminController', ['$scope', 'someService', function ($scope, someService) {
	$scope.coolMethod = someService.coolMethod();
}]);

Why?

Take the following typical AngularJS controller declaration (same as above)

angular.module('app').controller('adminController', ['$scope', 'someService', function ($scope, someService) {
	$scope.coolMethod = someService.coolMethod();
}]);

So what's wrong with this?

  • App name, angular.module('app').controller, is required within the declaration
    • some avoid this by the use of a global variable, app.controller, which is not good JavaScript hygiene
  • Parameter names are duplicated, one for the getters, '$scope', 'someService', and one for the function parameters, function ($scope, someService)
    • this duplication is required to make the module minifiable
    • some avoid this by the use of ngmin
  • Depending upon the desired naming format, module type (controller) and module name (adminController) have duplication, due to the suffixed controller in this example
  • The function is anonymous (unnamed), making it more difficult to debug
  • Generally verbose

How?

Write AngularJS modules using the following syntaxes. NOTE: {{}} denotes placeholders

class {{appName}} extends {{Animation|Config|Controller|Directive|Factory|Filter|Provider|Run|Service}}
	constructor: ({{params}}) ->
		# module body here

or

class {{name}} extends {{App|Constant|Value}}
	constructor: ->
		return {{value}}

CoffeeScript Classes

The typical way to use CoffeeScript classes with AngularJS is as follows.

# 203 characters
class AdminController
	constructor: ($scope, someService) ->
		$scope.coolMethod = someService.coolMethod()

angular.module('app').controller 'adminController', ['$scope', 'someService', AdminController]

which is equivalent to

// 177 characters
angular.module('app').controller('adminController', ['$scope', 'someService', function AdminController ($scope, someService) {
	$scope.coolMethod = someService.coolMethod();
}]);

with ng-classify, this is all you need

# 116 characters
class Admin extends Controller
	constructor: ($scope, someService) ->
		$scope.coolMethod = someService.coolMethod()

Benefits

  • Removes unnecessary ceremonial code (angular.module('app'))
  • App name is not required when writing a module. It is now configurable.
  • Parameters are needed only once via the constructor function. No need for the array syntax to make your code minifiable.
  • No need to suffix the module name with the module type, e.g. myController, myCtrl, etc.
  • The function is named, making debugging more convenient
  • The syntax is arguably concise. Bring your code to the forefront with the elimination of cruft.

Considerations

  • To avoid the use of global variables, it is advised to use the bare: false CoffeeScript compilation option. see CoffeeScript Usage

Controller As Syntax

AngularJS provides two styles for writing and consuming controllers

  1. $scope
  2. this with Controller as

$scope example

class Admin extends Controller
	constructor: ($scope, someService) ->
		$scope.coolMethod = someService.coolMethod()

view for $scope example

<div ng-controller="adminController">
	<button ng-click="coolMethod()">Cool It Down!</button>
</div>

this example

class Admin extends Controller
	constructor: (someService) ->
		@coolMethod = someService.coolMethod()

view for this example

<div ng-controller="adminController as controller">
	<button ng-click="controller.coolMethod()">Cool It Down!</button>
</div>

Module Types

App

Although there is no AngularJS App module type, it is included for consistency.

class App extends App
	constructor: ->
		return [
			'ngAnimate'
			'ngRoute'
		]

equivalent to

angular.module('app', [
	'ngAnimate',
	'ngRoute'
]);

You may wish to use the then CoffeeScript syntax to highlight your code even more by eliminating the need for extra lines of code and indentation, as follows. Note: this can be leveraged for any CoffeeScript class.

class App extends App then constructor: -> return [
	'ngAnimate'
	'ngRoute'
]

Note: the app name is configured via the appName option, not the class name

Animation

class MyCrazyFader extends Animation
	constructor: ->
		return {
			enter: (element, done) ->
				# run the animation here and call done when the animation is complete

				cancellation = (element) ->
					# this (optional) function will be called when the animation
					# completes or when the animation is cancelled (the cancelled
					# flag will be set to true if cancelled).
		}

equivalent to

angular.module('app').animation('.my-crazy-fader', [function MyCrazyFader () {
	return {
		enter: function (element, done) {
			// run the animation here and call done when the animation is complete

			var cancellation = function (element) {
				// this (optional) function will be called when the animation
				// completes or when the animation is cancelled (the cancelled
				// flag will be set to true if cancelled).
			};

			return cancellation;
		}
	};
}]);

Config

class Routes extends Config
	constructor: ($routeProvider) ->
		$routeProvider
		.when '/home',
			controller: 'homeController'
			templateUrl: 'home.html'
		.when '/about',
			controller: 'aboutController'
			templateUrl: 'about.html'
		.otherwise
			redirectTo: '/home'

equivalent to

angular.module('app').config(['$routeProvider', function Routes ($routeProvider) {
	$routeProvider
	.when('/home', {
		controller: 'homeController',
		templateUrl: 'home.html'
	})
	.when('/about', {
		controller: 'aboutController',
		templateUrl: 'about.html'
	})
	.otherwise({
		redirectTo: '/home'
	});
}]);

Constant

class HttpStatusCodes extends Constant
	constructor: ->
		return {
			'401': 'Unauthorized'
			'403': 'Forbidden'
			'404': 'Not Found'
		}

equivalent to

angular.module('app').constant('HTTP_STATUS_CODES', {
	'401': 'Unauthorized',
	'403': 'Forbidden',
	'404': 'Not Found'
});

Controller

The example below uses the this syntax

class Home extends Controller
	constructor: (userService) ->
		@save = (username) ->
			userService.addUser username

equivalent to

angular.module('app').controller('homeController', ['userService', function Home (userService) {
	this.save = function (username) {
		return userService.addUser(username);
	};
}]);

Directive

class Dialog extends Directive
	constructor: ->
		return {
			restrict: 'E'
			transclude: true
			templateUrl: 'dialog.html'
		}

equivalent to

angular.module('app').directive('dialog', [function Dialog () {
	return {
		restrict: 'E',
		transclude: true,
		templateUrl: 'dialog.html'
	};
}]);

Factory

class Greeting extends Factory
	constructor: ($log) ->
		return {
			sayHello: (name) ->
				$log.info name
		}

equivalent to

angular.module('app').factory('Greeting', ['$log', function Greeting ($log) {
	return {
		sayHello: function (name) {
			$log.info(name);
		}
	};
}]);

Another nice feature is the ability to return classes

class User extends Factory
	constructor: ($log) ->
		return class UserInstance
			constructor: (firstName, lastName) ->
				@getFullName = ->
					"#{firstName} #{lastName}"

usage

user = new User 'Cary', 'Landholt'
fullName = user.getFullName() # Cary Landholt

Filter

class Twitterfy extends Filter
	constructor: ->
		return (username) ->
			"@#{username}"

equivalent to

angular.module('app').filter('twitterfy', [function Twitterfy () {
	return function (username) {
		return '@' + username;
	};
}]);

Provider

class Greetings extends Provider
	constructor: ($log) ->
		@name = 'default'

		@$get = ->
			name = @name

			sayHello: ->
				$log.info name

		@setName = (name) ->
			@name = name

equivalent to

angular.module('app').provider('greetingsProvider', ['$log', function Greetings ($log) {
	this.name = 'default';

	this.$get = function () {
		var name = this.name;

		return {
			sayHello: function () {
				return $log.info(name);
			}
		};
	};

	this.setName = function (name) {
		return this.name = name;
	};
}]);

Run

class ViewsBackend extends Run
	constructor: ($httpBackend) ->
		$httpBackend.whenGET(/^.*\.(html|htm)$/).passThrough()

equivalent to

angular.module('app').run(['$httpBackend', function ViewsBackend ($httpBackend) {
	$httpBackend.whenGET(/^.*\.(html|htm)$/).passThrough();
}]);

Service

class Greeting extends Service
	constructor: ($log) ->
		@sayHello = (name) ->
			$log.info name

equivalent to

angular.module('app').service('greetingService', ['$log', function Greeting ($log) {
	this.sayHello = function (name) {
		return $log.info(name);
	};
}]);

Value

class People extends Value
	constructor: ->
		return [
			{
				name: 'Luke Skywalker'
				age: 26
			}
			{
				name: 'Han Solo'
				age: 35
			}
		]

equivalent to

angular.module('app').value('people',
	[
		{
			name: 'Luke Skywalker',
			age: 26
		}, {
			name: 'Han Solo',
			age: 35
		}
	]
);

Multiple Apps

Although using multiple apps in an AngularJS application is unnecessary, some may still wish to do so.

Simply provide the app name as a parameter to the module type.

In the example below, a Controller is created within the 'common' app.

class Home extends Controller('common')
	constructor: ($log) ->
		$log.info 'homeController instantiated'

equivalent to

angular.module('common').controller('homeController', ['$log', function ($log) {
	$log.info('homeController instantiated');
})];

API

ngClassify(content, options)

content

Required
Type: String
Default: undefined

The content that may contain CoffeeScript classes to convert to AngularJS modules

options

Type: Object
Default: undefined

options.appName

Type: String
Default: 'app'

The name of the AngularJS app

// for example
angular.module('app')
options.prefix

Type: String
Default: ''

To avoid potential collisions, the moduleType prefix may be set (ex: options.prefix = 'Ng')

class Home extends Ng.Controller
	constructor: ($log) ->
		$log.info 'homeController instantiated'
options.animation

Type: Object
Default: {format: 'spinalCase', prefix: '.'}

options.constant

Type: Object
Default: {format: 'screamingSnakeCase'}

options.controller

Type: Object
Default: {format: 'camelCase', suffix: 'Controller'}

options.directive

Type: Object
Default: {format: 'camelCase'}

options.factory

Type: Object
Default: {format: 'upperCamelCase'}

options.filter

Type: Object
Default: {format: 'camelCase'}

options.provider

Type: Object
Default: {format: 'camelCase', suffix: 'Provider'}

options.service

Type: Object
Default: {format: 'camelCase', suffix: 'Service'}

options.value

Type: Object
Default: {format: 'camelCase'}

Supported Formats

Format Example
  • | no change camelCase | camelCase lowerCamelCase | lowerCamelCase lowerCase | lowercase screamingSnakeCase | SCREAMING_SNAKE_CASE snakeCase | snake_case spinalCase | spinal-case trainCase | Train-Case upperCamelCase | UpperCamelCase upperCase | UPPERCASE

Contributing

See CONTRIBUTING.md

Changelog

See CHANGELOG.md

License

See LICENSE