yesod-static-angular-0.1.3: Yesod generators for embedding AngularJs code into yesod-static at compile time

Safe HaskellNone

Yesod.EmbeddedStatic.AngularJavascript

Contents

Description

Integrate an AngularJS application written in Javascript into the static subsite.

AngularJS is a javascript web framework for enhancing HTML to provide dynamic web applications. This module integrates an AngularJS application written in pure javascript into a Yesod application. (As of January 2014, there is some experiemental development work on Fay bindings to AngularJS, but unfortunately it is not currently usable.)

An AngularJS application consists of several pieces:

  • Javascript code consisting of controllers, directives, and services attached to Angular modules. The goal of this javascript is to produce a domain specific language extending HTML. embedNgModule uses the static subsite to serve this javascript code, serving a minimized and compressed file during production and serving individual files during development.
  • Directive Templates. In Angular, directives should be the only components which manipulate the DOM. This can happen in the directive javascript code or through a directive HTML template. embedNgModule supports templates written in Hamlet, converting the Hamlet to HTML at compile time before embedding the HTML into the generated javascript.
  • The View. In Angular, the view is written in the DSL extending HTML. Normal Yesod Handlers and Widgets work great for the view (so nothing needed from this module). Note that your Yesod Widgets will not have any julius or attached javascript code, the javascript is entirely managed by embedNgModule.
  • Testing. Angular makes testing (both unit and end-to-end) easy. For unit and mid-level testing, the normal Angular test runner karma is the best. hamletTestTemplate assists with integrating Hamlet directive templates into karma. For end2end testing, the Test.WebDriver.Commands.Angular module in the webdriver-angular package works well.

There is an example in the source code which shows an application, unit testing with karma, and end2end testing with webdriver.

Synopsis

File Layout

This Haskell module assists with the management of Angular modules. Each Angular module cooresponds to a directory where each controller/directive/service/factory/etc. is located in separate files within this directory. For example, you might have files angular/myctrl.js, angular/somedirective.js, angular/myservice.js, and so on. Also, for each directive that does not use an inline template, you should have the corresponding directive template in a file with the same name as the javascript file but with a hamlet extension (e.g. angular/somedirective.hamlet). For larger applications, you can organize the Angular code into multiple Angular modules by using one directory per module.

Javascript

All files with a .js extension in the given directory will be loaded. Each javascript file should be written to assume that a variable named module is already defined and holds the angular module. For example,

module.controller("MyController", function($scope) {
    $scope.hello = "Hello, World!";
});

When compiling for production, these .js files will be concatenated, combined with the code defining the module variable, DI annotated, minified, compressed, and then embedded into the executable to be served from the given location. That is, the following javascript will be automatically created:

(function(module) {

module.controller("MyController", ["$scope", function($scope) {
    $scope.hello = "Hello, world!";
}]);

<contents of all the other .js files>
<optionally contents of the directive templates (see below)>

})(angular.module("module-name", <contents of module-deps.json>));

Note the module-deps.json file: if this file exists within the directory, it will be parsed and used as the module dependencies (so therefore the contents should be a list of strings). If this file does not exist, an empty list of dependencies is used.

Note also the inline dependency injection annotation of the $scope parameter. When compiling for production, before running the minimizer, the javascript is parsed and DI parameters are annotated. The arguments to the top-level call to module.somefunction will be annotated. Also, if the call is module.directive, the body of the factory function will be scanned for a return of an object literal (must be an object literal, not a variable). The object literal is scanned for a controller property, and if found that controller property is also DI annotated (the example program shows this in action).

Directives

If a directive uses an external hamlet template, the directive hamlet and directive javascript should be in two files with the same name but .js and .hamlet extensions. To embed the directive template so that the javascript is able to find it, the template ID must be loaded. To do so, the javascript is parsed to find the value of templateUrl. The .js file must have a top-level call to module.directive. Some statement directly inside the factory function must be a return of an object literal. This object literal must have a templateUrl property with a single string literal as the value. This string literal is used as the ID when embedding the template. For example, if my-dialog.js contains

module.directive("my-dialog", function() {
    return {
        restrict: "E",
        transclude: true,
        templateUrl: "mydialog-template",
    };
});

then something like the following will be automatically created and then inserted into the generated file:

module.run(["$templateCache", function($templateCache) {
    $templateCache.put("mydialog-template", <rendered my-dialog.hamlet>);
}]);

Generators

embedNgModuleSource

Arguments

:: String

Angular module name

-> Location

location within the static subsite for the resulting javascript file

-> FilePath

directory on disk (relative to build directory/directory containing the .cabal file) which contains the contents of the module.

-> (ByteString -> IO ByteString)

minimizer such as uglifyJs or jasmine to use during production.

-> Generator 

Embed the javascript and directive templates from a single directory into the static subsite as a single Angular module file. For this to work, the directive templates cannot use any hamlet variable interpolation.

During development, each .js file is served separately (with some code to define the module variable) and at the location where the combined file would appear during production, a small script which just loads all the .js files from the directory is served. This makes debugging much easier.

This generator produces one variable definition of type Route EmbeddedStatic which is named by passing the location through pathToName.

embedNgModulesSource

Arguments

:: Location

directory location within the static subsite where the modules should appear

-> FilePath

directory on disk (relative to build directory/directory containing the .cabal file) containing angular modules.

-> (ByteString -> IO ByteString)

minimizer such as uglifyJs or jasmine to use during production.

-> Generator 

Embed multiple angular modules into the static subsite.

All subdirectories within the given directory are assumed to be angular modules, and each subdirectory is embedded by calling embedNgModule on it. The subdirectory name is used for the module name. The location for the module will be the location given to this generator combined with the subdirectory name and then .js.

Custom Directive Template Processing

embedNgModuleWithoutTemplatesSource

Arguments

:: String

Angular module name

-> Location

location within the static subsite for the resulting javascript file

-> FilePath

directory on disk (relative to build directory/directory containing the .cabal file) which contains the contents of the module.

-> (ByteString -> IO ByteString)

minimizer such as uglifyJs or jasmine to use during production.

-> Generator 

Same as embedNgModule but the directive templates are not included. Use this if your directive templates require variable/type safe route interpolation. Your directive templates should then instead be inserted into a WidgetT site IO () using directiveTemplates and then embedded into the final page.

embedNgModulesWithoutTemplatesSource

Arguments

:: Location

directory location within the static subsite where the modules should appear

-> FilePath

directory on disk (relative to build directory/directory containing the .cabal file) containing angular modules.

-> (ByteString -> IO ByteString)

minimizer such as uglifyJs or jasmine to use during production.

-> Generator 

Embed multiple angular modules without templates into the static subsite. All subdirectories within the given directory are assumed to be angular modules, and each subdirectory is embedded by calling embedNgModuleWithoutTemplates on it.

directiveTemplates :: FilePath -> ExpQSource

Create a WidgetT site IO () which contains all the directive templates written in Hamlet from the passed in directory. This is only needed if you use embedNgModuleWithoutTemplates because your directive templates use variable/url interpolation. The template will be inside a <script type="text/ng-template" id="someid">, where the ID is found by parsing the javascript code for templateUrl. This widget must be inside the tag which has the ng-app attribute.

directiveTemplatesWithSettings :: HamletSettings -> FilePath -> ExpQSource

Same as directiveTemplates but allows you to specify the hamlet settings.

directiveWidgetSource

Arguments

:: Text

ID of the directive

-> WidgetT site IO ()

the body of the template

-> WidgetT site IO () 

Wrap a widget in a <script type="text/ng-template" id="someid"> block.

Testing

hamletTestTemplate :: FilePath -> ExpQSource

Convert a hamlet file to javascript for unit testing.

When unit testing the Angular code, the javascript is executed directly (without any processing by embedNgModule). But for the directives to work, the Hamlet templates must still be converted to javascript which inserts the template into the Angular $templateCache. This TH splice takes a path to a hamlet file and produces a ByteString which contains this javascript. Before unit testing the javascript code, this TH splice must be run on every directive hamlet template.

If you use karma, the karma-ng-hamlet2js-preprocessor does this automatically by using runghc to run a small Haskell script which calls hamletTestTemplate. The example application uses this karma preprocessor.