<!DOCTYPE html>
<html>
<head>
<link data-require="bootstrap@3.3.1" data-semver="3.3.1" rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" />
<link data-require="jasmine@2.2.1" data-semver="2.2.1" rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.css" />
<script data-require="jquery@2.1.3" data-semver="2.1.3" src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
<script data-require="jasmine@2.2.1" data-semver="2.2.1" src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.js"></script>
<script data-require="jasmine@2.2.1" data-semver="2.2.1" src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine-html.js"></script>
<script data-require="jasmine@2.2.1" data-semver="2.2.1" src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/boot.js"></script>
<script data-require="angular.js@1.3.12" data-semver="1.3.12" src="https://code.angularjs.org/1.3.12/angular.js"></script>
<script data-require="lodash.js@2.4.1" data-semver="2.4.1" src="http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.js"></script>
<script data-require="angular-mocks@1.3.12" data-semver="1.3.12" src="https://code.angularjs.org/1.3.12/angular-mocks.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="service.js"></script>
<script src="script.js"></script>
<script src="item.test.js"></script>
<script src="itemsList.test.js"></script>
<script src="searchBox.test.js"></script>
<script src="itemsContainer.test.js"></script>
</head>
<body ng-app="app">
<div id="HTMLReporter" class="jasmine_reporter"></div>
<script>
(function() {
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 250;
var htmlReporter = new jasmine.HtmlReporter();
jasmineEnv.addReporter(htmlReporter);
jasmineEnv.specFilter = function(spec) {
return htmlReporter.specFilter(spec);
};
var currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
execJasmine();
};
function execJasmine() {
jasmineEnv.execute();
}
})();
</script>
</body>
</html>
var app = angular.module('app', ['itemsService']);
app.controller('ItemsContainerController', ['ItemsService',
function(ItemsService) {
var items = ItemsService.fetchAll(),
self = this;
// init
updateItems();
function updateItems(filteredItems) {
var collection = filteredItems || items;
self.activeItems = _.filter(collection, function(item) {
return item.active;
});
self.inactiveItems = _.filter(collection, function(item) {
return !item.active;
});
}
this.switchStatus = function(item) {
item.active = !item.active;
items = ItemsService.update(item);
updateItems();
};
this.updateFilter = function(val, activeOnly) {
if (!val) {
updateItems();
return;
}
var filteredItems = items.filter(function(item) {
return (activeOnly && !item.active) || item.name.indexOf(val) === 0;
});
updateItems(filteredItems);
};
}
]);
app.directive('itemsContainer', function() {
return {
controller: 'ItemsContainerController',
controllerAs: 'ctrl',
bindToController: true,
templateUrl: 'items-container.html'
};
});
app.directive('searchBox', function() {
return {
scope: {
onChange: '&',
searchValue: '@'
},
controllerAs: 'ctrl',
controller: function() {},
bindToController: true,
templateUrl: 'search-box.html'
};
});
app.directive('item', function() {
return {
scope: {
item: '=set',
onClick: '&'
},
controller: function() {},
controllerAs: 'ctrl',
bindToController: true,
restrict: 'EA',
template: '<input type="checkbox" ng-click="ctrl.onClick({item: ctrl.item})" ng-checked="ctrl.item.active" /> {{ ctrl.item.name }}'
}
});
app.directive('itemsList', function() {
return {
scope: {
title: '@',
items: '=',
onClick: '&'
},
restrict: 'EA',
controller: function() {},
controllerAs: 'ctrl',
bindToController: true,
templateUrl: 'items-list.html'
}
});
ul {
margin: 0px;
padding: 0px;
}
ul li {
list-style: none;
}
.main {
padding: 10%;
width: 80%;
}
var app = angular.module('itemsService', []);
app.provider('ItemsService', function() {
var items = [{
id: 1,
name: 'view',
active: true
}, {
id: 2,
name: 'model',
active: true
}, {
id: 3,
name: 'scope',
active: false
}, {
id: 4,
name: 'filter',
active: true
}, {
id: 5,
name: 'directives',
active: false
}];
function update(searchItem) {
var found = _.first(items, function(item) {
return searchItem.id == item.id
});
// do nothing
if (!found) return;
}
return {
$get: function() {
return {
fetchAll: function() {
return items;
},
update: function(item) {
var found = find(item);
if (found) {
var index = _.indexOf(items, _.find(items, item));
items.splice(index, 1, item);
}
return items;
}
}
}
}
});
<div class="main">
<search-box on-change="ctrl.updateFilter(search, active)"></search-box>
<items-list data-title="Active Items" data-items="ctrl.activeItems" data-on-click="ctrl.switchStatus(item)"></items-list>
<items-list data-title="Inactive Items" data-items="ctrl.inactiveItems" data-on-click="ctrl.switchStatus(item)"></items-list>
</div>
<div class="items-list">
<h3>{{ctrl.title}}</h3>
<span ng-if="ctrl.items.length == 0">No items available.</span>
<ul class="items">
<li ng-repeat="item in ctrl.items">
<item data-set="item" on-click="ctrl.onClick({item: item})"></item>
</li>
</ul>
</div>
<div class="search-box">
<input class="form-control"
ng-model="ctrl.searchValue"
ng-model-options="{ debounce: 100 }"
id="search-box"
placeholder="search"
ng-change="ctrl.onChange({search: ctrl.searchValue, active: ctrl.onlyActive})"/>
<item data-set="{name: 'Only active items'}" ng-click="ctrl.onlyActive = !!!ctrl.onlyActive"></item>
</div>
describe('ItemsContainer component', function() {
var element, scope, mockedItemsService, controller, items;
beforeEach(module('app'));
beforeEach(function() {
items = [{
id: 1,
name: 'view',
active: true
}, {
id: 2,
name: 'model',
active: true
}, {
id: 3,
name: 'scope',
active: false
}];
mockedItemsService = {
fetchAll: function() {
return items;
},
update: function(item) {
return items;
}
};
});
beforeEach(inject(function($controller, _$rootScope_, _$compile_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
controller = $controller('ItemsContainerController as ctrl', {
$scope: scope,
ItemsService: mockedItemsService
});
}));
it('should keep track of all active and inactive items', function() {
expect(controller.activeItems.length).toEqual(2);
});
it('should update the active and inactive items when calling updateFilter', function() {
controller.updateFilter('view');
expect(controller.activeItems.length).toEqual(1);
});
it('should only update the active items when calling updateFilter with the activeOnly flag', function() {
controller.updateFilter('view', true);
expect(controller.inactiveItems.length).toEqual(1);
});
it('should call ItemsService.update() when calling switchStatus', function() {
spyOn(mockedItemsService, 'update');
controller.switchStatus(items[0]);
expect(mockedItemsService.update).toHaveBeenCalledWith(items[0]);
});
});
describe('Item component', function() {
var element, scope;
beforeEach(module('app'));
beforeEach(inject(function(_$rootScope_, _$compile_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
element = angular.element('<item data-set="item" on-click="ctrl.callback({item: item})"></item>');
$compile(element)(scope);
}));
it('should display the controller defined title', function() {
var expected = 'Some rendered text';
scope.item = {
name: expected,
active: false
};
scope.$digest();
expect(element.text()).toContain(expected);
});
it('should call the controller defined callback', function() {
scope.ctrl = {
callback: jasmine.createSpy('callback')
};
scope.$digest();
element.find('input[type=checkbox]').eq(0).click();
expect(scope.ctrl.callback).toHaveBeenCalled();
});
});
describe('ItemList component', function() {
var element, scope;
beforeEach(module('app'));
beforeEach(inject(function($templateCache) {
$templateCache.put('items-list.html',
'<div class="items-list">' +
'<h3>{{ctrl.title}}</h3>' +
'<span ng-if="ctrl.items.length == 0">No items available.</span> ' +
'<ul class="items"> ' +
'<li ng-repeat="item in ctrl.items"> ' +
'<item data-set="item" on-click="ctrl.onClick({item: item})"></item> ' +
'</li> ' +
'</ul> ' +
'</div>'
);
}));
beforeEach(inject(function(_$rootScope_, _$compile_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
element = angular.element('<items-list data-title="Testing Items" data-items="items" data-on-click="ctrl.switchStatus(item)"></items-list>');
$compile(element)(scope);
}));
it('the h3 title tag should be defined via the data-title attribute', function() {
scope.$digest();
var title = element.find('.items-list h3').eq(0);
expect(title.text()).toBe('Testing Items');
});
it('creates item components', function() {
scope.items = [{
name: 'foo',
active: false
}, {
name: 'bar',
active: false
}];
scope.$digest();
var items = element.find('.items-list ul li');
expect(items.length).toEqual(2);
});
it('clicking on an item should call the controller defined callback', function() {
scope.items = [{
name: 'foo',
active: false
}, {
name: 'bar',
active: false
}];
scope.ctrl = {
switchStatus: jasmine.createSpy('switchStatus')
};
scope.$digest();
var item = element.find('.items-list ul li input').eq(0);
item.click();
expect(scope.ctrl.switchStatus).toHaveBeenCalledWith(scope.items[0]);
});
});
describe('SearchBox component', function() {
var element, scope;
beforeEach(module('app'));
beforeEach(inject(function($templateCache) {
$templateCache.put('search-box.html',
'<div class="search-box">' +
'<input class="form-control" ' +
'ng-model="ctrl.searchValue"' +
' id="search-box"' +
'placeholder="search"' +
'ng-change="ctrl.onChange({search: ctrl.searchValue, active: ctrl.onlyActive})"/>' +
'<item data-set="{name: \'Only active items\'}" ng-click="ctrl.onlyActive = !!!ctrl.onlyActive"></item>' +
'</div>'
);
}));
beforeEach(inject(function(_$rootScope_, _$compile_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
element = angular.element('<search-box data-search-value="{{ctrl.searchValue}}" on-change="ctrl.updateFilter(search)"></search-box>');
$compile(element)(scope);
}));
it('input should have the controller defined value', function() {
var expected = 'Some rendered text';
scope.ctrl = {
searchValue : expected
};
scope.$digest();
var input = element.find('#search-box').eq(0);
expect(input.val()).toBe(expected);
});
it('changing the search string should trigger the controller defined callback', function() {
scope.ctrl = {
updateFilter : jasmine.createSpy('updateFilter')
};
scope.$digest();
var input = element.find('#search-box').eq(0);
var ngModelCtrl = input.controller('ngModel');
ngModelCtrl.$setViewValue('view');
expect(scope.ctrl.updateFilter).toHaveBeenCalledWith('view');
});
});