angular-formly
Version:
AngularJS directive which takes JSON representing a form and renders to HTML
758 lines (656 loc) • 26.2 kB
JavaScript
/* eslint max-len:0 */
/* eslint max-nested-callbacks:0 */
/* eslint no-shadow:0 */
/* eslint no-console:0 */
/* eslint no-unused-vars:0 */
import angular from 'angular-fix'
import testUtils from '../test.utils.js'
const {getNewField, basicForm, shouldWarn, shouldNotWarn} = testUtils
describe('formlyConfig', () => {
beforeEach(window.module('formly'))
let formlyConfig
beforeEach(inject((_formlyConfig_) => {
formlyConfig = _formlyConfig_
}))
describe('setWrapper/getWrapper', () => {
let getterFn, setterFn, $log
const template = '<span>This is my <formly-transclude></formly-transclude> template'
const templateUrl = '/path/to/my/template.html'
const typesString = 'checkbox'
const types = ['text', 'textarea']
const name = 'hi'
const name2 = 'name2'
const template2 = template + '2'
beforeEach(inject(function(_$log_) {
getterFn = formlyConfig.getWrapper
setterFn = formlyConfig.setWrapper
$log = _$log_
}))
describe('\(^O^)/ path', () => {
describe('the default template', () => {
it('can be a string without a name', () => {
setterFn(template)
expect(getterFn()).to.eql({name: 'default', template, types: []})
})
it('can be a string with a name', () => {
setterFn(template, name)
expect(getterFn(name)).to.eql({name, template, types: []})
})
it('can be an object with a template', () => {
setterFn({template})
expect(getterFn()).to.eql({name: 'default', template, types: []})
})
it('can be an object with a template and a name', () => {
setterFn({template, name})
expect(getterFn(name)).to.eql({name, template, types: []})
})
it('can be an object with a templateUrl', () => {
setterFn({templateUrl})
expect(getterFn()).to.eql({name: 'default', templateUrl, types: []})
})
it('can be an object with a templateUrl and a name', () => {
setterFn({name, templateUrl})
expect(getterFn(name)).to.eql({name, templateUrl, types: []})
})
it('can be an array of objects with names, urls, and/or templates', () => {
setterFn([
{templateUrl},
{name, template},
{name: name2, template: template2},
])
expect(getterFn()).to.eql({templateUrl, name: 'default', types: []})
expect(getterFn(name)).to.eql({template, name, types: []})
expect(getterFn(name2)).to.eql({template: template2, name: name2, types: []})
})
it('can specify types as a string (using types as the name when not specified)', () => {
setterFn({types: typesString, template})
expect(getterFn(typesString)).to.eql({template, name: typesString, types: [typesString]})
})
it('can specify types as an array (using types as the name when not specified)', () => {
setterFn({types, template})
expect(getterFn(types.join(' '))).to.eql({template, name: types.join(' '), types})
})
})
})
describe('(◞‸◟;) path', () => {
it('should throw an error when providing both a template and templateUrl', () => {
expect(() => setterFn({template, templateUrl}, name)).to.throw(/`template` must be `ifNot\[templateUrl]`/i)
})
it('should throw an error when the template does not use formly-transclude', () => {
const error = /templates.*?must.*?<formly-transclude><\/formly-transclude>/
expect(() => setterFn({template: 'no formly-transclude'})).to.throw(error)
})
it('should throw an error when specifying an array type where not all items are strings', () => {
const error = /types.*?typeOrArrayOf.*?String.*?/i
expect(() => setterFn({template, types: ['hi', 2, false, 'cool']})).to.throw(error)
})
it('should warn when attempting to override a template wrapper', () => {
shouldWarn(/overwrite/, function() {
setterFn({template})
setterFn({template})
})
})
it('should not warn when attempting to override a template wrapper if overwriteOk is true', () => {
shouldNotWarn(() => {
setterFn({template})
setterFn({template, overwriteOk: true})
})
})
})
describe(`apiCheck`, () => {
testApiCheck('setWrapper', 'getWrapper')
})
})
describe('getWrapperByType', () => {
let getterFn, setterFn
const types = ['input', 'checkbox']
const types2 = ['input', 'select']
const templateUrl = '/path/to/file.html'
beforeEach(inject(function(formlyConfig) {
setterFn = formlyConfig.setWrapper
getterFn = formlyConfig.getWrapperByType
}))
describe('\(^O^)/ path', () => {
it('should return a template wrapper that has the same type', () => {
const option = setterFn({templateUrl, types})
expect(getterFn(types[0])).to.eql([option])
})
it('should return an array when multiple wrappers have the same time', () => {
setterFn({templateUrl, types})
setterFn({templateUrl, types: types2})
const inputWrappers = getterFn('input')
expect(inputWrappers).to.be.instanceOf(Array)
expect(inputWrappers).to.have.length(2)
})
})
})
describe('removeWrapper', () => {
let remove, removeForType, setterFn, getterFn, getByTypeFn
const template = '<div>Something <formly-transclude></formly-transclude> cool</div>'
const name = 'name'
const types = ['input', 'checkbox']
const types2 = ['input', 'something else']
const types3 = ['checkbox', 'something else']
beforeEach(inject((formlyConfig) => {
remove = formlyConfig.removeWrapperByName
removeForType = formlyConfig.removeWrappersForType
setterFn = formlyConfig.setWrapper
getterFn = formlyConfig.getWrapper
getByTypeFn = formlyConfig.getWrapperByType
}))
it('should allow you to remove a wrapper', () => {
setterFn(template, name)
remove(name)
expect(getterFn(name)).to.be.empty
})
it('should allow you to remove a wrapper for a type', () => {
setterFn({types, template})
setterFn({types: types2, template})
const checkboxAndSomethingElseWrapper = setterFn({types: types3, template})
removeForType('input')
expect(getByTypeFn('input')).to.be.empty
const checkboxWrappers = getByTypeFn('checkbox')
expect(checkboxWrappers).to.eql([checkboxAndSomethingElseWrapper])
})
})
describe('setType/getType/getTypes', () => {
let getterFn, setterFn, getTypesFn
const name = 'input'
const template = '<input type="{{options.inputType}}" />'
const templateUrl = '/input.html'
const wrapper = 'input'
const wrapper2 = 'input2'
beforeEach(inject(function(formlyConfig) {
getterFn = formlyConfig.getType
setterFn = formlyConfig.setType
getTypesFn = formlyConfig.getTypes
}))
describe('\(^O^)/ path', () => {
it('should accept an object with a name and a template', () => {
setterFn({name, template})
expect(getterFn(name).template).to.equal(template)
})
it('should accept an object with a name and a templateUrl', () => {
setterFn({name, templateUrl})
expect(getterFn(name).templateUrl).to.equal(templateUrl)
})
it('should accept an array of objects', () => {
setterFn([
{name, template},
{name: 'type2', templateUrl},
])
expect(getterFn(name).template).to.equal(template)
expect(getterFn('type2').templateUrl).to.equal(templateUrl)
})
it('should expose the mapping from type name to config', () => {
setterFn([
{name, template},
{name: 'type2', templateUrl},
])
expect(getTypesFn()).to.eql({[name]: getterFn(name), type2: getterFn('type2')})
})
it('should allow you to set a wrapper as a string', () => {
setterFn({name, template, wrapper})
expect(getterFn(name).wrapper).to.equal(wrapper)
})
it('should allow you to set a wrapper as an array', () => {
setterFn({name, template, wrapper: [wrapper, wrapper2]})
expect(getterFn(name).wrapper).to.eql([wrapper, wrapper2])
})
describe(`extends`, () => {
describe(`object case`, () => {
beforeEach(() => {
setterFn([
{
name,
template,
defaultOptions: {
templateOptions: {
required: true,
min: 3,
},
},
},
{
name: 'type2',
extends: name,
defaultOptions: {
templateOptions: {
required: false,
max: 4,
},
data: {
extraStuff: [1, 2, 3],
},
},
},
])
})
it(`should inherit all fields that it does not have itself`, () => {
expect(getterFn('type2').template).to.eql(template)
})
it(`should merge objects that it shares`, () => {
expect(getterFn('type2').defaultOptions).to.eql({
templateOptions: {
required: false,
min: 3,
max: 4,
},
data: {
extraStuff: [1, 2, 3],
},
})
})
it(`should not error when extends is specified without a template, templateUrl, or defaultOptions`, () => {
expect(() => setterFn({name: 'type3', extends: 'type2'})).to.not.throw()
})
})
describe(`abstractType function case`, () => {
beforeEach(() => {
setterFn([
{
name,
template,
defaultOptions: function(options) {
return {
templateOptions: {
required: true,
min: 3,
},
}
},
},
{
name: 'type2',
extends: name,
defaultOptions: function(options) {
return {
templateOptions: {
required: false,
max: 4,
},
}
},
},
{
name: 'type3',
extends: name,
defaultOptions: {
templateOptions: {
required: false,
max: 4,
},
},
},
])
})
it(`should merge options when extending defaultOptions is a function`, () => {
expect(getterFn('type2').defaultOptions({})).to.eql({
templateOptions: {
required: false,
min: 3,
max: 4,
},
})
})
it(`should merge options when extending defaultOptions is an object`, () => {
expect(getterFn('type3').defaultOptions({})).to.eql({
templateOptions: {
required: false,
min: 3,
max: 4,
},
})
})
})
describe(`template/templateUrl Cases`, () => {
it('should use templateUrl if type defines it and its parent has template defined', function() {
setterFn([
{
name,
template,
},
{
name: 'type2',
extends: name,
templateUrl,
},
])
expect(getterFn('type2').templateUrl).not.to.be.undefined
expect(getterFn('type2').template).to.be.undefined
})
it('should use template if type defines it and its parent had templateUrl defined', function() {
setterFn([
{
name,
templateUrl,
},
{
name: 'type2',
extends: name,
template,
},
])
expect(getterFn('type2').template).not.to.be.undefined
expect(getterFn('type2').templateUrl).to.be.undefined
})
})
describe(`function cases`, () => {
let args, fakeScope, parentFn, childFn, parentDefaultOptions, childDefaultOptions, argsAndParent
beforeEach(() => {
args = {data: {someData: true}}
fakeScope = {}
parentDefaultOptions = {
data: {extraOptions: true},
templateOptions: {placeholder: 'hi'},
}
childDefaultOptions = {
templateOptions: {placeholder: 'hey', required: true},
}
parentFn = sinon.stub().returns(parentDefaultOptions)
childFn = sinon.stub().returns(childDefaultOptions)
argsAndParent = {
data: {someData: true, extraOptions: true},
templateOptions: {placeholder: 'hi'},
}
})
it(`should call the extended parent's defaultOptions function and its own defaultOptions function`, () => {
setterFn([
{name, defaultOptions: parentFn},
{name: 'type2', extends: name, defaultOptions: childFn},
])
getterFn('type2').defaultOptions(args, fakeScope)
expect(parentFn).to.have.been.calledWith(args, fakeScope)
expect(childFn).to.have.been.calledWith(argsAndParent, fakeScope)
})
it(`should call the extended parent's defaultOptions function when it doesn't have one of its own`, () => {
setterFn([
{name, defaultOptions: parentFn},
{name: 'type2', extends: name},
])
getterFn('type2').defaultOptions(args, fakeScope)
expect(parentFn).to.have.been.calledWith(args, fakeScope)
})
it(`should call its own defaultOptions function when the parent doesn't have one`, () => {
setterFn([
{name, template},
{name: 'type2', extends: name, defaultOptions: childFn},
])
getterFn('type2').defaultOptions(args, fakeScope)
expect(childFn).to.have.been.calledWith(args, fakeScope)
})
it(`should extend its defaultOptions object with the parent's defaultOptions object`, () => {
const objectMergedDefaultOptions = {
data: {extraOptions: true},
templateOptions: {placeholder: 'hey', required: true},
}
setterFn([
{name, defaultOptions: parentDefaultOptions},
{name: 'type2', extends: name, defaultOptions: childDefaultOptions},
])
expect(getterFn('type2').defaultOptions).to.eql(objectMergedDefaultOptions)
})
it(`should call its defaultOptions with the parent's defaultOptions object merged with the given args`, () => {
setterFn([
{name, defaultOptions: parentDefaultOptions},
{name: 'type2', extends: name, defaultOptions: childFn},
])
const returned = getterFn('type2').defaultOptions(args, fakeScope)
expect(childFn).to.have.been.calledWith(argsAndParent, fakeScope)
expect(returned).to.eql(childDefaultOptions)
})
})
describe(`link functions`, () => {
let linkArgs, parentFn, childFn
beforeEach(inject(($rootScope) => {
linkArgs = [$rootScope.$new(), angular.element('<div></div>'), {}]
parentFn = sinon.spy()
childFn = sinon.spy()
}))
it(`should call the parent link function when there is no child function`, () => {
setterFn([
{name, template, link: parentFn},
{name: 'type2', extends: name},
])
getterFn('type2').link(...linkArgs)
expect(parentFn).to.have.been.calledWith(...linkArgs)
})
it(`should call the child link function when there is no parent function`, () => {
setterFn([
{name, template},
{name: 'type2', extends: name, link: childFn},
])
getterFn('type2').link(...linkArgs)
expect(childFn).to.have.been.calledWith(...linkArgs)
})
it(`should call the child link function and the parent link function when they are both present`, () => {
setterFn([
{name, template, link: parentFn},
{name: 'type2', extends: name, link: childFn},
])
getterFn('type2').link(...linkArgs)
expect(parentFn).to.have.been.calledWith(...linkArgs)
expect(childFn).to.have.been.calledWith(...linkArgs)
})
})
describe(`controller functions`, () => {
let parentFn, childFn, $controller, $scope
beforeEach(inject(($rootScope, _$controller_) => {
$scope = $rootScope.$new()
$controller = _$controller_
parentFn = sinon.spy()
parentFn.$inject = ['$log']
childFn = sinon.spy()
childFn.$inject = ['$http']
}))
it(`should call the parent controller function when there is no child controller function`, inject(($log) => {
setterFn([
{name, template, controller: parentFn},
{name: 'type2', extends: name},
])
$controller(getterFn('type2').controller, {$scope})
expect(parentFn).to.have.been.calledWith($log)
}))
it(`should call the parent controller function and the child's when there is a child controller function`, inject(($log, $http) => {
setterFn([
{name, template, controller: parentFn},
{name: 'type2', extends: name, controller: childFn},
])
$controller(getterFn('type2').controller, {$scope})
expect(parentFn).to.have.been.calledWith($log)
expect(childFn).to.have.been.calledWith($http)
}))
it(`should call the child controller function when there's no parent controller`, inject(($http) => {
setterFn([
{name, template},
{name: 'type2', extends: name, controller: childFn},
])
$controller(getterFn('type2').controller, {$scope})
expect(childFn).to.have.been.calledWith($http)
}))
})
})
})
describe('(◞‸◟;) path', () => {
it('should throw an error when the first argument is not an object or an array', () => {
expect(() => setterFn('string')).to.throw(/must.*provide.*object.*array/)
expect(() => setterFn(324)).to.throw(/must.*provide.*object.*array/)
expect(() => setterFn(false)).to.throw(/must.*provide.*object.*array/)
})
it('should throw an error when a name is not provided', () => {
expect(() => setterFn({templateUrl})).to.throw(/formlyConfig\.setType/)
})
it(`should throw an error when specifying both a template and a templateUrl`, () => {
expect(() => setterFn({name, template, templateUrl})).to.throw(/formlyConfig\.setType/)
})
it(`should throw an error when an extra property is provided`, () => {
expect(() => setterFn({name, templateUrl, extra: true})).to.throw(/formlyConfig\.setType/)
})
it('should warn when attempting to override a type', () => {
shouldWarn(/overwrite/, function() {
setterFn({name, template})
setterFn({name, template})
})
})
})
describe(`apiCheck`, () => {
testApiCheck('setType', 'getType')
})
})
function testApiCheck(setterName, getterName) {
const template = 'something with <formly-transclude></formly-transclude>'
const name = 'input'
let setterFn, getterFn, formlyApiCheck
beforeEach(inject((_formlyApiCheck_, formlyConfig) => {
formlyApiCheck = _formlyApiCheck_
setterFn = formlyConfig[setterName]
getterFn = formlyConfig[getterName]
}))
it(`should allow you to specify an apiCheck function that will be used to validate your options`, () => {
expect(() => {
setterFn({
name,
apiCheck,
template,
})
}).to.not.throw()
expect(getterFn(name).apiCheck).to.equal(apiCheck)
function apiCheck() {
return {
templateOptions: {},
data: {},
}
}
})
describe(`apiCheckInstance`, () => {
let apiCheckInstance
beforeEach(() => {
apiCheckInstance = require('api-check')()
})
it(`should allow you to specify an instance of your own apiCheck so messaging will be custom`, () => {
expect(() => {
setterFn({name, apiCheck, apiCheckInstance, template})
}).to.not.throw()
expect(getterFn(name).apiCheckInstance).to.equal(apiCheckInstance)
})
it(`should throw an error if you specify an instance without specifying an apiCheck`, () => {
expect(() => {
setterFn({name, apiCheckInstance, template})
}).to.throw()
})
function apiCheck() {
return {
templateOptions: {},
data: {},
}
}
})
describe(`apiCheckFunction`, () => {
it(`should allow you to specify warn or throw as the `, () => {
expect(() => {
setterFn({name, apiCheck, apiCheckFunction: 'warn', template})
}).to.not.throw()
expect(getterFn(name).apiCheckFunction).to.equal('warn')
expect(() => {
setterFn({name: 'name2', apiCheck, apiCheckFunction: 'throw', template})
}).to.not.throw()
expect(getterFn('name2').apiCheckFunction).to.equal('throw')
})
it(`should throw an error if you specify anything other than warn or throw`, () => {
expect(() => {
setterFn({name, apiCheckFunction: 'other', template})
}).to.throw()
})
function apiCheck() {
return {
templateOptions: {},
data: {},
}
}
})
}
describe(`extras`, () => {
describe(`that impact field rendering`, () => {
let scope, $compile, el, field
beforeEach(inject(($rootScope, _$compile_) => {
scope = $rootScope.$new()
$compile = _$compile_
scope.fields = [{template: '<input ng-model="model[options.key]" />'}]
}))
describe(`defaultHideDirective`, () => {
it(`should default formly-form to use ng-if when not specified`, () => {
compileAndDigest(`
<formly-form fields="fields" model="model"></formly-form>
`)
const fieldNode = getFieldNode()
expect(fieldNode.getAttribute('ng-if')).to.exist
})
it(`should default formly-form to use the specified directive for hiding and showing`, () => {
formlyConfig.extras.defaultHideDirective = 'ng-show'
compileAndDigest(`
<formly-form fields="fields" model="model"></formly-form>
`)
const fieldNode = getFieldNode()
expect(fieldNode.getAttribute('ng-show')).to.exist
})
it(`should be overrideable on a per-form basis`, () => {
formlyConfig.extras.defaultHideDirective = '(╯°□°)╯︵ ┻━┻'
compileAndDigest(`
<formly-form fields="fields" model="model" hide-directive="ng-show"></formly-form>
`)
const fieldNode = getFieldNode()
expect(fieldNode.getAttribute('ng-show')).to.exist
expect(fieldNode.getAttribute('(╯°□°)╯︵ ┻━┻')).to.not.exist
})
})
describe(`getFieldId`, () => {
it(`should allow you to specify your own function for generating the IDs for a field`, () => {
scope.fields = [
getNewField({id: 'custom'}),
getNewField({model: {foo: 'bar', id: '1234'}, key: 'foo'}),
getNewField({key: 'bar'}),
]
formlyConfig.extras.getFieldId = function(options, model, scope) {
if (options.id) {
return options.id
}
return [scope.index, (model && model.id) || 'new-model', options.key].join('_')
}
compileAndDigest()
const field0 = getFieldNgModelNode(0)
const field1 = getFieldNgModelNode(1)
const field2 = getFieldNgModelNode(2)
expect(field0.id).to.eq('custom')
expect(field1.id).to.eq('1_1234_foo')
expect(field2.id).to.eq('2_new-model_bar')
})
})
function compileAndDigest(template) {
el = $compile(template || basicForm)(scope)
scope.$digest()
field = scope.fields[0]
return el
}
function getFieldNode(index = 0) {
return el[0].querySelectorAll('.formly-field')[index]
}
function getFieldNgModelNode(index = 0) {
return getFieldNode(index).querySelector('[ng-model]')
}
})
})
describe(`getTypeHeritage`, () => {
it(`should get the heritage of all type extensions`, () => {
formlyConfig.setType([
{name: 'grandparent'},
{name: 'parent', extends: 'grandparent'},
{name: 'child', extends: 'parent'},
{name: 'extra', extends: 'grandparent'},
{name: 'extra2'},
])
expect(formlyConfig.getTypeHeritage('child')).to.eql([
formlyConfig.getType('parent'), formlyConfig.getType('grandparent'),
])
})
})
})