Commit 05e61d95 authored by Robert Knight's avatar Robert Knight

Rename <simple-search> to <search-input> and convert to JS

 - Convert <simple-search> from CoffeeScript to JS

 - Implement a new set of tests using the createDirective() helper
parent 18bea245
...@@ -148,7 +148,7 @@ module.exports = angular.module('h', [ ...@@ -148,7 +148,7 @@ module.exports = angular.module('h', [
.directive('shareDialog', require('./directive/share-dialog')) .directive('shareDialog', require('./directive/share-dialog'))
.directive('sidebarTutorial', require('./directive/sidebar-tutorial').directive) .directive('sidebarTutorial', require('./directive/sidebar-tutorial').directive)
.directive('signinControl', require('./directive/signin-control')) .directive('signinControl', require('./directive/signin-control'))
.directive('simpleSearch', require('./directive/simple-search')) .directive('searchInput', require('./directive/search-input'))
.directive('sortDropdown', require('./directive/sort-dropdown')) .directive('sortDropdown', require('./directive/sort-dropdown'))
.directive('spinner', require('./directive/spinner')) .directive('spinner', require('./directive/spinner'))
.directive('statusButton', require('./directive/status-button')) .directive('statusButton', require('./directive/status-button'))
......
'use strict';
// @ngInject
function SearchInputController($element, $http, $scope) {
var self = this;
var button = $element.find('button');
var input = $element.find('input')[0];
var form = $element.find('form')[0];
button.on('click', function () {
input.focus();
});
$scope.$watch(
function () { return $http.pendingRequests.length; },
function (count) { self.loading = count > 0; }
);
form.onsubmit = function (e) {
e.preventDefault();
self.onSearch({$query: input.value});
};
this.inputClasses = function () {
return {'is-expanded': self.alwaysExpanded || self.query};
};
this.$onChanges = function (changes) {
if (changes.query) {
input.value = changes.query.currentValue;
}
};
}
// @ngInject
module.exports = function () {
return {
bindToController: true,
controller: SearchInputController,
controllerAs: 'vm',
restrict: 'E',
scope: {
// Specifies whether the search input field should always be expanded,
// regardless of whether the it is focused or has an active query.
//
// If false, it is only expanded when focused or when 'query' is non-empty
alwaysExpanded: '<',
query: '<',
onSearch: '&',
},
template: require('../../../templates/client/search_input.html'),
};
};
module.exports = ->
bindToController: true
controllerAs: 'vm'
controller: ['$element', '$http', '$scope', ($element, $http, $scope) ->
self = this
button = $element.find('button')
input = $element.find('input')[0]
form = $element.find('form')[0]
button.on('click', -> input.focus())
$scope.$watch (-> $http.pendingRequests.length), (pending) ->
self.loading = (pending > 0)
form.onsubmit = (e) ->
e.preventDefault()
self.onSearch({$query: input.value})
this.inputClasses = ->
'is-expanded': self.alwaysExpanded || self.query.length > 0
this.$onChanges = (changes) ->
if changes.query
input.value = changes.query.currentValue
self
]
restrict: 'E'
scope:
# Specifies whether the search input field should always be expanded,
# regardless of whether the it is focused or has an active query.
#
# If false, it is only expanded when focused or when 'query' is non-empty
alwaysExpanded: '<'
query: '<'
onSearch: '&'
template: '''
<form class="simple-search-form"
name="searchForm"
ng-class="!vm.query && 'simple-search-inactive'">
<input class="simple-search-input"
type="text"
name="query"
placeholder="{{vm.loading && 'Loading' || 'Search'}}…"
ng-disabled="vm.loading"
ng-class="vm.inputClasses()"/>
<button type="button" class="simple-search-icon top-bar__btn" ng-hide="vm.loading">
<i class="h-icon-search"></i>
</button>
<button type="button" class="simple-search-icon btn btn-clean" ng-show="vm.loading" disabled>
<span class="btn-icon"><span class="spinner"></span></span>
</button>
</form>
'''
'use strict';
var angular = require('angular');
var util = require('./util');
describe('searchInput', function () {
var fakeHttp;
before(function () {
angular.module('app', [])
.directive('searchInput', require('../search-input'));
});
beforeEach(function () {
fakeHttp = {pendingRequests: []};
angular.mock.module('app', {
$http: fakeHttp,
});
});
it('displays the search query', function () {
var el = util.createDirective(document, 'searchInput', {
query: 'foo',
});
var input = el.find('input')[0];
assert.equal(input.value, 'foo');
});
it('invokes #onSearch() when the query changes', function () {
var onSearch = sinon.stub();
var el = util.createDirective(document, 'searchInput', {
query: 'foo',
onSearch: {
args: ['$query'],
callback: onSearch,
},
});
var input = el.find('input')[0];
var form = el.find('form');
input.value = 'new-query';
form.submit();
assert.calledWith(onSearch, 'new-query');
});
describe('loading indicator', function () {
it('is hidden when there are no network requests in flight', function () {
var el = util.createDirective(document, 'search-input', {});
var spinner = el[0].querySelector('.spinner');
assert.equal(util.isHidden(spinner), true);
});
it('is visible when there are network requests in flight', function () {
var el = util.createDirective(document, 'search-input', {});
var spinner = el[0].querySelector('.spinner');
fakeHttp.pendingRequests.push([{}]);
el.scope.$digest();
assert.equal(util.isHidden(spinner), false);
});
});
});
{module, inject} = angular.mock
describe 'simple-search', ->
$compile = null
$element = null
$scope = null
fakeHttp = null
fakeWindow = null
isolate = null
before ->
angular.module('h', [])
.directive('simpleSearch', require('../simple-search'))
beforeEach module('h')
beforeEach module ($provide) ->
fakeHttp = {pendingRequests: []}
$provide.service('$http', -> fakeHttp)
return
beforeEach inject (_$compile_, _$rootScope_) ->
$compile = _$compile_
$scope = _$rootScope_.$new()
$scope.update = sinon.spy()
$scope.clear = sinon.spy()
template= '''
<simple-search
query="query"
on-search="update(query)"
on-clear="clear()">
</simple-search>
'''
$element = $compile(angular.element(template))($scope)
# add element to document so that it becomes focusable
# and we get default form behaviors
document.body.appendChild($element[0])
$scope.$digest()
isolate = $element.isolateScope()
afterEach ->
document.body.removeChild($element[0])
it 'updates the search-bar', ->
$scope.query = "Test query"
$scope.$digest()
assert.equal(isolate.searchtext, $scope.query)
it 'calls the given search function', ->
isolate.searchtext = "Test query"
isolate.$digest()
$element.find('form').triggerHandler('submit')
assert.calledWith($scope.update, "Test query")
it 'invokes callbacks when the input model changes', ->
$scope.query = "Test query"
$scope.$digest()
assert.calledOnce($scope.update)
$scope.query = ""
$scope.$digest()
assert.calledOnce($scope.clear)
it 'adds a class to the form when there is no input value', ->
$form = $element.find('.simple-search-form')
assert.include($form.prop('className'), 'simple-search-inactive')
it 'removes the class from the form when there is an input value', ->
$scope.query = "Test query"
$scope.$digest()
$form = $element.find('.simple-search-form')
assert.notInclude($form.prop('className'), 'simple-search-inactive')
it 'sets the `loading` scope key when http requests are in progress', ->
fakeHttp.pendingRequests = []
isolate.$digest()
assert.isFalse(isolate.loading)
fakeHttp.pendingRequests = ['bogus']
isolate.$digest()
assert.isTrue(isolate.loading)
it 'expands the search field when the input is non-empty', ->
input = $element.find('.simple-search-input')
assert.isFalse(input.hasClass('is-expanded'))
input.val('query')
input.trigger('change')
isolate.$digest()
assert.isTrue(input.hasClass('is-expanded'))
it 'focuses the search field when clicking the search button', ->
input = $element.find('.simple-search-input')
searchBtn = $element.find('button')
assert.ok(document.activeElement != input[0])
searchBtn.click()
assert.ok(document.activeElement == input[0])
it 'does not update the search when clicking the search button', ->
searchBtn = $element.find('button')
input = $element.find('.simple-search-input')
input.val('query')
input.trigger('change')
searchBtn.click()
assert.notCalled($scope.update)
...@@ -172,8 +172,34 @@ function sendEvent(element, eventType) { ...@@ -172,8 +172,34 @@ function sendEvent(element, eventType) {
element.dispatchEvent(event); element.dispatchEvent(event);
} }
/**
* Return true if a given element is hidden on the page.
*
* There are many possible ways of hiding DOM elements on a page, this just
* looks for approaches that are common in our app.
*/
function isHidden(element) {
var style = window.getComputedStyle(element);
if (style.display === 'none') {
return true;
}
// Test for element or ancestor being hidden with `ng-hide` directive
var el = element;
while (el) {
if (el.classList.contains('ng-hide')) {
return true;
}
el = el.parentElement;
}
return false;
}
module.exports = { module.exports = {
createDirective: createDirective, createDirective: createDirective,
isHidden: isHidden,
ngModule: ngModule, ngModule: ngModule,
sendEvent: sendEvent, sendEvent: sendEvent,
}; };
<form class="simple-search-form"
name="searchForm"
ng-class="!vm.query && 'simple-search-inactive'">
<input class="simple-search-input"
type="text"
name="query"
placeholder="{{vm.loading && 'Loading' || 'Search'}}…"
ng-disabled="vm.loading"
ng-class="vm.inputClasses()"/>
<button type="button" class="simple-search-icon top-bar__btn" ng-hide="vm.loading">
<i class="h-icon-search"></i>
</button>
<button type="button" class="simple-search-icon btn btn-clean" ng-show="vm.loading" disabled>
<span class="btn-icon"><span class="spinner"></span></span>
</button>
</form>
...@@ -3,12 +3,12 @@ ...@@ -3,12 +3,12 @@
<div class="top-bar" ng-class="frame.visible && 'shown'" ng-cloak> <div class="top-bar" ng-class="frame.visible && 'shown'" ng-cloak>
<!-- Legacy design for top bar, as used in the stream !--> <!-- Legacy design for top bar, as used in the stream !-->
<div class="top-bar__inner content" ng-if="::!isSidebar"> <div class="top-bar__inner content" ng-if="::!isSidebar">
<simple-search <search-input
class="simple-search" class="search-input"
query="searchController.query()" query="searchController.query()"
on-search="searchController.update($query)" on-search="searchController.update($query)"
always-expanded="true"> always-expanded="true">
</simple-search> </search-input>
<div class="top-bar__expander"></div> <div class="top-bar__expander"></div>
<signin-control <signin-control
auth="auth" auth="auth"
...@@ -26,12 +26,12 @@ ...@@ -26,12 +26,12 @@
<div class="top-bar__inner content" ng-if="::isSidebar"> <div class="top-bar__inner content" ng-if="::isSidebar">
<group-list class="group-list" auth="auth"></group-list> <group-list class="group-list" auth="auth"></group-list>
<div class="top-bar__expander"></div> <div class="top-bar__expander"></div>
<simple-search <search-input
class="simple-search" class="search-input"
query="searchController.query()" query="searchController.query()"
on-search="searchController.update($query)" on-search="searchController.update($query)"
title="Filter the annotation list"> title="Filter the annotation list">
</simple-search> </search-input>
<sort-dropdown <sort-dropdown
sort-keys-available="sortKeysAvailable" sort-keys-available="sortKeysAvailable"
sort-key="sortKey" sort-key="sortKey"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment