Unverified Commit 4e442391 authored by Sean Hammond's avatar Sean Hammond Committed by GitHub

Merge pull request #543 from hypothesis/remove-form-input

Remove form-input and form-validate directives
parents 5a7f9839 b7ee7e1c
......@@ -160,14 +160,11 @@ module.exports = angular.module('h', [
.component('timestamp', require('./components/timestamp'))
.component('topBar', require('./components/top-bar'))
.directive('formInput', require('./directive/form-input'))
.directive('formValidate', require('./directive/form-validate'))
.directive('hAutofocus', require('./directive/h-autofocus'))
.directive('hBranding', require('./directive/h-branding'))
.directive('hOnTouch', require('./directive/h-on-touch'))
.directive('hTooltip', require('./directive/h-tooltip'))
.directive('spinner', require('./directive/spinner'))
.directive('statusButton', require('./directive/status-button'))
.directive('windowScroll', require('./directive/window-scroll'))
.service('analytics', require('./analytics'))
......@@ -179,7 +176,6 @@ module.exports = angular.module('h', [
.service('drafts', require('./drafts'))
.service('features', require('./features'))
.service('flash', require('./flash'))
.service('formRespond', require('./form-respond'))
.service('frameSync', require('./frame-sync').default)
.service('groups', require('./groups'))
.service('localStorage', require('./local-storage'))
......
module.exports = ->
link: (scope, elem, attr, [model, validator]) ->
return unless model
fieldClassName = 'form-field'
errorClassName = 'form-field-error'
render = model.$render
resetResponse = (value) ->
model.$setValidity('response', true)
value
toggleClass = (addClass) ->
elem.toggleClass(errorClassName, addClass)
elem.parent().toggleClass(errorClassName, addClass)
model.$parsers.unshift(resetResponse)
model.$render = ->
toggleClass(model.$invalid and model.$dirty)
render()
if validator?
validator.addControl(model)
scope.$on '$destroy', -> validator.removeControl model
scope.$watch ->
if model.$modelValue? or model.$pristine
toggleClass(model.$invalid and model.$dirty)
return
require: ['?ngModel', '^?formValidate']
restrict: 'C'
module.exports = ->
controller: ->
controls = {}
addControl: (control) ->
if control.$name
controls[control.$name] = control
removeControl: (control) ->
if control.$name
delete controls[control.$name]
submit: ->
# make all the controls dirty and re-render them
for _, control of controls
control.$setViewValue(control.$viewValue)
control.$render()
link: (scope, elem, attr, ctrl) ->
elem.on 'submit', ->
ctrl.submit()
# Augments a button to provide loading/status flags for asynchronous actions.
#
# Requires that the attribute provide a "target" form name. It will then listen
# to "formState" events on the scope. These events are expected to provide a
# the form name and a status.
#
# Example
#
# <button status-button="test-form">Submit</button>
module.exports = ['$compile', ($compile) ->
STATE_ATTRIBUTE = 'status-button-state'
STATE_LOADING = 'loading'
STATE_SUCCESS = 'success'
template = '''
<span class="btn-with-message">
<span class="btn-message btn-message-loading">
<span class="btn-icon spinner"></span>
</span>
<span class="btn-message btn-message-success">
<span class="btn-message-text">Saved!</span> <i class="btn-message-icon h-icon-check"></i>
</span>
</span>
'''
link: (scope, placeholder, attr, ctrl, transclude) ->
targetForm = attr.statusButton
unless targetForm
throw new Error('status-button attribute should provide a form name')
elem = angular.element(template).attr(STATE_ATTRIBUTE, '')
$compile(elem)(scope)
placeholder.after(elem)
transclude(scope, (clone) -> elem.append(clone))
scope.$on 'formState', (event, formName, formState) ->
return unless formName == targetForm
unless formState in [STATE_LOADING, STATE_SUCCESS]
formState = ''
elem.attr(STATE_ATTRIBUTE, formState)
transclude: 'element'
]
{module, inject} = angular.mock
describe 'form-input', ->
$compile = null
$field = null
$scope = null
before ->
angular.module('h', ['ng'])
.directive('formInput', require('../form-input'))
beforeEach module('h')
beforeEach inject (_$compile_, _$rootScope_) ->
$compile = _$compile_
$scope = _$rootScope_.$new()
beforeEach ->
$scope.model = {username: undefined}
template = '''
<div class="form-field">
<input type="text" class="form-input" name="username"
ng-model="model.username" name="username"
required ng-minlength="3" />
</div>
'''
$field = $compile(angular.element(template))($scope)
$scope.$digest()
it 'should remove an error class to an valid field on change', ->
$field.addClass('form-field-error')
$input = $field.find('[name=username]').addClass('form-field-error')
$input.controller('ngModel').$setViewValue('abc')
$scope.$digest()
assert.notInclude($field.prop('className'), 'form-field-error')
assert.notInclude($input.prop('className'), 'form-field-error')
it 'should apply an error class to an invalid field on render', ->
$input = $field.find('[name=username]')
$input.triggerHandler('input') # set dirty
$input.controller('ngModel').$render()
assert.include($field.prop('className'), 'form-field-error')
it 'should remove an error class from a valid field on render', ->
$field.addClass('form-field-error')
$input = $field.find('[name=username]')
$input.val('abc').triggerHandler('input')
$input.controller('ngModel').$render()
assert.notInclude($field.prop('className'), 'form-field-error')
it 'should remove an error class on valid input', ->
$field.addClass('form-field-error')
$input = $field.find('[name=username]')
$input.val('abc').triggerHandler('input')
assert.notInclude($field.prop('className'), 'form-field-error')
it 'should not add an error class on invalid input', ->
$input = $field.find('[name=username]')
$input.val('ab').triggerHandler('input')
assert.notInclude($field.prop('className'), 'form-field-error')
it 'should reset the "response" error when the view changes', ->
$input = $field.find('[name=username]')
controller = $input.controller('ngModel')
controller.$setViewValue('abc')
controller.$setValidity('response', false)
controller.responseErrorMessage = 'fail'
$scope.$digest()
assert.include($field.prop('className'), 'form-field-error', 'Fail fast check')
controller.$setViewValue('def')
$scope.$digest()
assert.notInclude($field.prop('className'), 'form-field-error')
it 'should hide errors if the model is marked as pristine', ->
$field.addClass('form-field-error')
$input = $field.find('[name=username]')
controller = $input.controller('ngModel')
$input.triggerHandler('input') # set dirty
controller.$setValidity('response', false)
controller.responseErrorMessage = 'fail'
$scope.$digest()
assert.include($field.prop('className'), 'form-field-error', 'Fail fast check')
# Then clear it out and mark it as pristine
controller.$setPristine()
$scope.$digest()
assert.notInclude($field.prop('className'), 'form-field-error')
describe 'with form-validate', ->
link = require('../form-input')().link
it 'should register its model with the validator', ->
model = {'$parsers': []}
validator = {addControl: sinon.spy(), removeControl: sinon.spy()}
link($scope, $field, null, [model, validator])
assert.calledOnce(validator.addControl)
assert.calledWith(validator.addControl, model)
assert.notCalled(validator.removeControl)
$scope.$destroy()
assert.calledOnce(validator.removeControl)
assert.calledWith(validator.removeControl, model)
{module, inject} = angular.mock
describe 'form-validate', ->
$compile = null
$element = null
$scope = null
controller = null
before ->
angular.module('h', [])
.directive('formValidate', require('../form-validate'))
beforeEach module('h')
beforeEach inject (_$compile_, _$rootScope_) ->
$compile = _$compile_
$scope = _$rootScope_.$new()
template = '<form form-validate onsubmit="return false"></form>'
$element = $compile(angular.element(template))($scope)
controller = $element.controller('formValidate')
it 'performs validation and rendering on registered controls on submit', ->
mockControl =
'$name': 'babbleflux'
'$setViewValue': sinon.spy()
'$render': sinon.spy()
controller.addControl(mockControl)
$element.triggerHandler('submit')
assert.calledOnce(mockControl.$setViewValue)
assert.calledOnce(mockControl.$render)
mockControl2 =
'$name': 'dubbledabble'
'$setViewValue': sinon.spy()
'$render': sinon.spy()
controller.removeControl(mockControl)
controller.addControl(mockControl2)
$element.triggerHandler('submit')
assert.calledOnce(mockControl.$setViewValue)
assert.calledOnce(mockControl.$render)
assert.calledOnce(mockControl2.$setViewValue)
assert.calledOnce(mockControl2.$render)
{module, inject} = angular.mock
describe 'h:directives.status-button', ->
$scope = null
$compile = null
$element = null
before ->
angular.module('h', [])
.directive('statusButton', require('../status-button'))
beforeEach module('h')
beforeEach inject (_$compile_, _$rootScope_) ->
$compile = _$compile_
$scope = _$rootScope_.$new()
beforeEach ->
template = '''
<button status-button="test">Test Button</button>
'''
$element = $compile(angular.element(template))($scope).next()
it 'wraps the button with status labels', ->
assert.include($element.prop('className'), 'btn-with-message')
assert.equal($element.find('.btn-message-loading').length, 1)
assert.equal($element.find('.btn-message-success').length, 1)
it 'sets the status-button-state attribute when a loading event is triggered', ->
$scope.$emit('formState', 'test', 'loading')
assert.equal($element.attr('status-button-state'), 'loading')
it 'sets the status-button-state attribute when a success event is triggered', ->
$scope.$emit('formState', 'test', 'success')
assert.equal($element.attr('status-button-state'), 'success')
it 'unsets the status-button-state attribute when another event is triggered', ->
$scope.$emit('formState', 'test', 'reset')
assert.equal($element.attr('status-button-state'), '')
# Takes a FormController instance and an object of errors returned by the
# API and updates the validity of the form. The field.$errors.response
# property will be true if there are errors and the responseErrorMessage
# will contain the API error message.
module.exports = ->
(form, errors, reason) ->
form.$setValidity('response', !reason)
form.responseErrorMessage = reason
for own field, error of errors
# If there's an empty-string field, it's a top-level form error. Set the
# overall form validity from this field, but only if there wasn't already
# a reason.
if !reason and field == ''
form.$setValidity('response', false)
form.responseErrorMessage = error
continue
form[field].$setValidity('response', false)
form[field].responseErrorMessage = error
{module, inject} = angular.mock
describe 'form-respond', ->
$scope = null
formRespond = null
form = null
before ->
angular.module('h', [])
.service('formRespond', require('../form-respond'))
beforeEach module('h')
beforeEach inject (_$rootScope_, _formRespond_) ->
$scope = _$rootScope_.$new()
formRespond = _formRespond_
form =
$setValidity: sinon.spy()
username: {$setValidity: sinon.spy()}
password: {$setValidity: sinon.spy()}
it 'sets the "response" error key for each field with errors', ->
formRespond form,
username: 'must be at least 3 characters'
password: 'must be present'
assert.calledWith(form.username.$setValidity, 'response', false)
assert.calledWith(form.password.$setValidity, 'response', false)
it 'adds an error message to each input controller', ->
formRespond form,
username: 'must be at least 3 characters'
password: 'must be present'
assert.equal(form.username.responseErrorMessage, 'must be at least 3 characters')
assert.equal(form.password.responseErrorMessage, 'must be present')
it 'sets the "response" error key if the form has a top-level error', ->
formRespond form, {'': 'Explosions!'}
assert.calledWith(form.$setValidity, 'response', false)
it 'adds an error message if the form has a top-level error', ->
formRespond form, {'': 'Explosions!'}
assert.equal(form.responseErrorMessage, 'Explosions!')
it 'sets the "response" error key if the form has a failure reason', ->
formRespond form, null, 'fail'
assert.calledWith(form.$setValidity, 'response', false)
it 'adds an reason message as the response error', ->
formRespond form, null, 'fail'
assert.equal(form.responseErrorMessage, 'fail')
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