AngularJs

AngularJS, Pact, and Jasmine

  1. Follow the steps in pact-consumer-js-dsl (yes, with the gem ‘pact-mock_service’)
  2. Here is where the important stuff comes with AngularJS:

given service:

angular.module('ng-cherie.services.current-theme', [
    'ng-cherie.constants'
])
    .factory('currentThemeService', function currentThemeService($http, API) {
        var service = {
            getCurrentTheme: getCurrentTheme
        };

        function getCurrentTheme() {
            var theme;
            return $http.get(API.BASE_PATH + API.CURRENT_THEME)
                .then(function getCurrentThemeSuccess(response) {
                    theme = response.data;

                    return theme;
                }, function() {
                    return null;
                });
        }

        return service;
    });

then pact with ngMidwayTester

/* globals ngMidwayTester */ 
/* globals Pact */
describe('currentThemeService', function() {
    var API,
        currentThemeProvider,
        currentThemeService,
        mockCurrentTheme,
        $httpBackend,
        tester;

    beforeEach(function() {
        angular.module('currentThemeServiceSpec', [
          'ng-cherie.services.current-theme',
          'ng-cherie.constants',
          'ng-cherie.mocks.current-theme',
          'ngMockE2E'
        ]);

        tester = ngMidwayTester('currentThemeServiceSpec');
        currentThemeService = tester.inject('currentThemeService');
        API = tester.inject('API');
        mockCurrentTheme = tester.inject('mockCurrentTheme');

        $httpBackend = tester.inject('$httpBackend');
        $httpBackend.whenGET(/\/api\/v1\/currenttheme/).passThrough();

        currentThemeProvider = Pact.mockService({
            consumer: 'ng-cherie.services.current-theme',
            provider: 'CherieApp',
            port: 1234,
            done: function (error) {
                expect(error).toBe(null);
            }
        });
    });

    afterEach(function () {
        tester.destroy();
        tester = null;
    });

    describe('getCurrentTheme', function() {
       it('should return the current theme via pact', function(done) {

           API.BASE_PATH = "http://localhost:1234" + API.BASE_PATH;

           currentThemeProvider
               .uponReceiving("a request for the current theme")
               .withRequest("get", "/api/v1/currenttheme")
               .willRespondWith(200, { "Content-Type": "application/json" }, mockCurrentTheme);

           currentThemeProvider.run(done, function(runComplete) {
               currentThemeService.getCurrentTheme().then(function(response) {
                   expect(response).toEqual(mockCurrentTheme);
                   runComplete();
               });
           });
       });
    });

});

line 12: the key is to set up a new module that contains ngMockE2E so we can use `passthrough`
line 45: make sure to replace the domain with the pact server location, in this case, it is localhost:1234.

AngularJS compile vs templateUrl

Hello, hello, it has been a while since my last post of ‘given, then, show’. If you are new to here, this is not a recommendation on how to implement, but rather just a show and tell of what you can do.

Given:

– Implement a directive that handles these providers:
– Provider A, B, C, and D share the same template
– Provider A and B have different description
– Provider C and D have no description
– Provider E and F have different templates and different description

Then:

– Assuming that templates and provider descriptions are dumb aka not stored in controllers
– Implement a directive that shows the default template, which is share across the similar providers (A, B, C, D) and a separate template per unique provider E and F.
– Implement a directive just for passing in unique descriptions

————————————>>>>>>————————————
Call the directive

<demo provider="provider"></demo>

Method 1a: use compiler


angular.module('ng-cherie.directives.demo.directive', [])
    .directive('demo', function demoDirective($compile, $templateCache) {
        return {
            bindToController: true,
            controller: 'DemoCtrl as demoCtrl',
            link: function demoLink(scope, elem, attrs, demoCtrl) {
                var defaultTemplatePath = 'app/directives/demo/templates/default.tpl.html',
                    templatePath = defaultTemplatePath.replace(/default/, demoCtrl.provider.id),
                    template;

                template = $templateCache.get(templatePath) || $templateCache.get(defaultTemplatePath);
                $compile(elem.html(template).contents())(scope);
            },
            scope: {
                provider: '='
            }
        };
    });

Method 1b: use compiler

angular.module('ng-cherie.directives.demo.directive', [])
    .directive('demo', function demoDirective($compile, $templateCache) {
        return {
            bindToController: true,
            controller: 'DemoCtrl as demoCtrl',
            link: function demoLink(scope, elem, attrs, demoCtrl) {
                var defaultTemplatePath = 'app/directives/demo/templates/default.tpl.html',
                    templatePath = defaultTemplatePath.replace(/default/, demoCtrl.provider.id),
                    template;

                template = $templateCache.get(templatePath) || $templateCache.get(defaultTemplatePath);
                elem.append($compile(template)(scope));
            },
            scope: {
                provider: '='
            }
        };
    });

Method 2: use ng-include

angular.module('ng-cherie.directives.demo.directive', [])
    .directive('demo', function demoDirective() {
        return {
            bindToController: true,
            controller: 'DemoCtrl as demoCtrl',
            scope: {
                provider: '='
            },
            templateUrl: 'app/directives/demo/templates/demo.tpl.html'
        };
    });

where app/directives/demo/templates/demo.tpl.html is

<ng-include src="demoCtrl.showTemplate()"></ng-include>

where showTemplate() determines which template to show depending on the provider (similar to demoLink())

————————————>>>>>>————————————
in default.tpl.html:
use a directive (demoDescription directive) to pass in the description and a control to show the description, so that when demo directive (see above) renders the default template directly without the description, it won’t fail on missing transclusion.

<p class="demo-description" ng-transclude ng-if="demoDescriptionCtrl.isTemplateUsedWithDirective"></p>

call the directive:

<demo-description>
    {{ 'Provider description here.' | translate }}
</demo-description>

————————————>>>>>>————————————
jasmine test

describe('Demo directive', function() {
    var _,
        $compile,
        element,
        providerMockData,
        scope;

    beforeEach(module('ng-cherie.directives.demo'));
    beforeEach(module('ng-cherie.mocks.provider'));

    beforeEach(inject(function($injector) {
        _ = $injector.get('_');
        $compile = $injector.get('$compile');
        scope = $injector.get('$rootScope').$new();
        providerMockData = $injector.get('providerMockData');
    }));

    beforeEach(function() {
        scope.provider = _.first(providerMockData.providers);
    });

    function compileDirective(scope) {
        var compiledElement = $compile('<demo provider="provider"></demo>')(scope);

        scope.$digest();

        return compiledElement;
    }

    describe('compiled markup', function() {
        it('contains the expected markup', function() {
            element = compileDirective(scope);

            expect(element[0].querySelector('.demo-body')).not.toBeNull();
        });

        //some other tests here... mostly expecting certain classes to be or not be there

        it('shows the default template when the provider is unknown', function () {
            scope.provider.id = "unexpected_provider";
            element = compileDirective(scope);

            expect(element.find('demo-description').length).toBe(0);
        });
    });
});

angularjs rails simple_form name

when using rails simple_form and angularjs for form validation

= simple_form_for :address, url: account_path(account.id), 
                            method: :patch, wrapper: :custom,
                            defaults: { input_html: { required: true } },
                            html: { name: 'addressForm',
                                    novalidate: true,
                                    'ng-submit' => "addressCtrl.submitForm(addressForm, $event)" } do |f|

  %fieldset.form-fieldset
    = f.input :address_1, input_html: { name: 'contact[street_address_1]', 
                                        'ng-model' => 'addressCtrl.address.street_address_1', 
                                        'ng-init' => "addressCtrl.address.street_address_1='#{account.street_address_1}'" }, 
                          wrapper_html: { 'ng-class' => "{'form-field--error': addressForm['contact[street_address_1]'].$invalid && addressForm['contact[street_address_1]'].$touched}" },
                          error_wrapper_html: { 'translate' => 'address.error.required.street_address_1', 
                                                'ng-show' => "addressForm['contact[street_address_1]'].$error.required && addressForm['contact[street_address_1]'].$touched" }

...

given input name

  name: 'contact[street_address_1]'

angularjs calls it by:

  addressForm['contact[street_address_1]']