From 9657d9b651dd16d5b78277a87205c7b466c89a7c Mon Sep 17 00:00:00 2001 From: gcanti Date: Sun, 10 Jul 2016 08:56:43 +0200 Subject: [PATCH 1/4] allow to set a default value in Android date picker template, fix #187 --- lib/templates/bootstrap/datepicker.android.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/templates/bootstrap/datepicker.android.js b/lib/templates/bootstrap/datepicker.android.js index 7ce73cf7..be698e37 100644 --- a/lib/templates/bootstrap/datepicker.android.js +++ b/lib/templates/bootstrap/datepicker.android.js @@ -65,7 +65,7 @@ function datepicker(locals) { } }); } else { - DatePickerAndroid.open({date: new Date()}) + DatePickerAndroid.open({date: locals.value || new Date()}) .then(function (date) { if (date.action !== DatePickerAndroid.dismissedAction) { var newDate = new Date(date.year, date.month, date.day); From b76a3eedae1510414c2bd203623a798dd7ee27ac Mon Sep 17 00:00:00 2001 From: gcanti Date: Sun, 10 Jul 2016 09:22:55 +0200 Subject: [PATCH 2/4] remove setState warnings from tests --- lib/components.js | 6 +- test/index.js | 1951 ++++++++++++++++++++++----------------------- 2 files changed, 971 insertions(+), 986 deletions(-) diff --git a/lib/components.js b/lib/components.js index dc1d0bc8..7a9a787b 100644 --- a/lib/components.js +++ b/lib/components.js @@ -113,8 +113,12 @@ class Component extends React.Component { this.setState({hasError: false}); } + pureValidate() { + return t.validate(this.getValue(), this.props.type, this.getValidationOptions()); + } + validate() { - const result = t.validate(this.getValue(), this.props.type, this.getValidationOptions()); + const result = this.pureValidate(); this.setState({hasError: !result.isValid()}); return result; } diff --git a/test/index.js b/test/index.js index 5d76cc9a..a54d5567 100644 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,6 @@ 'use strict'; -var tape = require('tape'); +var test = require('tape'); var t = require('tcomb-validation'); var bootstrap = { checkbox: function () {}, @@ -39,1136 +39,1117 @@ var ctxNone = { path: [] }; -tape('Textbox', function (tape) { - - var Textbox = core.Textbox; - - tape.test('label', function (tape) { - tape.plan(3); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {}, - ctx: ctx - }).getLocals().label, - 'ctxDefaultLabel', - 'should have a default label'); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {label: 'mylabel'}, - ctx: ctx - }).getLocals().label, - 'mylabel', - 'should handle `label` option'); - - tape.strictEqual( - new Textbox({ - type: t.maybe(t.String), - options: {}, - ctx: ctx - }).getLocals().label, - 'ctxDefaultLabel (optional)', - 'should handle optional types'); +var Textbox = core.Textbox; - }); +test('Textbox:label', function (tape) { + tape.plan(3); - tape.test('placeholder', function (tape) { - tape.plan(6); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {}, - ctx: ctx - }).getLocals().placeholder, - undefined, - 'default placeholder should be undefined'); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {placeholder: 'myplaceholder'}, - ctx: ctx - }).getLocals().placeholder, - 'myplaceholder', - 'should handle placeholder option'); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {label: 'mylabel', placeholder: 'myplaceholder'}, - ctx: ctx - }).getLocals().placeholder, - 'myplaceholder', - 'should handle placeholder option even if a label is specified'); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {}, - ctx: ctxPlaceholders - }).getLocals().placeholder, - 'ctxDefaultLabel', - 'should have a default placeholder if auto = placeholders'); - - tape.strictEqual( - new Textbox({ - type: t.maybe(t.String), - options: {}, - ctx: ctxPlaceholders - }).getLocals().placeholder, - 'ctxDefaultLabel (optional)', - 'should handle optional types if auto = placeholders'); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {placeholder: 'myplaceholder'}, - ctx: ctxNone - }).getLocals().placeholder, - 'myplaceholder', - 'should handle placeholder option even if auto === none'); + tape.strictEqual( + new Textbox({ + type: t.String, + options: {}, + ctx: ctx + }).getLocals().label, + 'ctxDefaultLabel', + 'should have a default label'); - }); + tape.strictEqual( + new Textbox({ + type: t.String, + options: {label: 'mylabel'}, + ctx: ctx + }).getLocals().label, + 'mylabel', + 'should handle `label` option'); - tape.test('editable', function (tape) { - tape.plan(3); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {}, - ctx: ctx - }).getLocals().editable, - undefined, - 'default editable should be undefined'); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {editable: true}, - ctx: ctx - }).getLocals().editable, - true, - 'should handle editable = true'); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {editable: false}, - ctx: ctx - }).getLocals().editable, - false, - 'should handle editable = false'); - }); + tape.strictEqual( + new Textbox({ + type: t.maybe(t.String), + options: {}, + ctx: ctx + }).getLocals().label, + 'ctxDefaultLabel (optional)', + 'should handle optional types'); - tape.test('help', function (tape) { - tape.plan(1); +}); - tape.strictEqual( - new Textbox({ - type: t.String, - options: {help: 'myhelp'}, - ctx: ctx - }).getLocals().help, - 'myhelp', - 'should handle help option'); +test('Textbox:placeholder', function (tape) { + tape.plan(6); - }); + tape.strictEqual( + new Textbox({ + type: t.String, + options: {}, + ctx: ctx + }).getLocals().placeholder, + undefined, + 'default placeholder should be undefined'); - tape.test('value', function (tape) { - tape.plan(3); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {}, - ctx: ctx - }).getLocals().value, - '', - 'default value should be empty string'); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {}, - ctx: ctx, - value: 'a' - }).getLocals().value, - 'a', - 'should handle value option'); - - tape.strictEqual( - new Textbox({ - type: t.Number, - options: {}, - ctx: ctx, - value: 1.1 - }).getLocals().value, - '1.1', - 'should handle numeric values'); + tape.strictEqual( + new Textbox({ + type: t.String, + options: {placeholder: 'myplaceholder'}, + ctx: ctx + }).getLocals().placeholder, + 'myplaceholder', + 'should handle placeholder option'); - }); + tape.strictEqual( + new Textbox({ + type: t.String, + options: {label: 'mylabel', placeholder: 'myplaceholder'}, + ctx: ctx + }).getLocals().placeholder, + 'myplaceholder', + 'should handle placeholder option even if a label is specified'); - tape.test('transformer', function (tape) { - tape.plan(2); + tape.strictEqual( + new Textbox({ + type: t.String, + options: {}, + ctx: ctxPlaceholders + }).getLocals().placeholder, + 'ctxDefaultLabel', + 'should have a default placeholder if auto = placeholders'); + + tape.strictEqual( + new Textbox({ + type: t.maybe(t.String), + options: {}, + ctx: ctxPlaceholders + }).getLocals().placeholder, + 'ctxDefaultLabel (optional)', + 'should handle optional types if auto = placeholders'); - var transformer = { - format: function (value) { - return Array.isArray(value) ? value.join(' ') : value; - }, - parse: function (value) { - return value.split(' '); - } - }; - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {transformer: transformer}, - ctx: ctx, - value: ['a', 'b'] - }).getLocals().value, - 'a b', - 'should handle transformer option (format)'); - - tape.deepEqual( - new Textbox({ - type: t.String, - options: {transformer: transformer}, - ctx: ctx, - value: ['a', 'b'] - }).validate().value, - ['a', 'b'], - 'should handle transformer option (parse)'); + tape.strictEqual( + new Textbox({ + type: t.String, + options: {placeholder: 'myplaceholder'}, + ctx: ctxNone + }).getLocals().placeholder, + 'myplaceholder', + 'should handle placeholder option even if auto === none'); - }); +}); - tape.test('hasError', function (tape) { - tape.plan(4); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {}, - ctx: ctx - }).getLocals().hasError, - false, - 'default hasError should be false'); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {hasError: true}, - ctx: ctx - }).getLocals().hasError, - true, - 'should handle hasError option'); - - var textbox = new Textbox({ +test('Textbox:editable', function (tape) { + tape.plan(3); + + tape.strictEqual( + new Textbox({ type: t.String, options: {}, ctx: ctx - }); + }).getLocals().editable, + undefined, + 'default editable should be undefined'); + + tape.strictEqual( + new Textbox({ + type: t.String, + options: {editable: true}, + ctx: ctx + }).getLocals().editable, + true, + 'should handle editable = true'); + + tape.strictEqual( + new Textbox({ + type: t.String, + options: {editable: false}, + ctx: ctx + }).getLocals().editable, + false, + 'should handle editable = false'); +}); + +test('Textbox:help', function (tape) { + tape.plan(1); + + tape.strictEqual( + new Textbox({ + type: t.String, + options: {help: 'myhelp'}, + ctx: ctx + }).getLocals().help, + 'myhelp', + 'should handle help option'); + +}); - textbox.validate(); +test('Textbox:value', function (tape) { + tape.plan(3); - tape.strictEqual( - textbox.getLocals().hasError, - false, - 'after a validation error hasError should be true'); + tape.strictEqual( + new Textbox({ + type: t.String, + options: {}, + ctx: ctx + }).getLocals().value, + '', + 'default value should be empty string'); - textbox = new Textbox({ + tape.strictEqual( + new Textbox({ type: t.String, options: {}, ctx: ctx, - value: 'a' - }); + value: 'a' + }).getLocals().value, + 'a', + 'should handle value option'); + + tape.strictEqual( + new Textbox({ + type: t.Number, + options: {}, + ctx: ctx, + value: 1.1 + }).getLocals().value, + '1.1', + 'should handle numeric values'); - textbox.validate(); +}); - tape.strictEqual( - textbox.getLocals().hasError, - false, - 'after a validation success hasError should be false'); +test('Textbox:transformer', function (tape) { + tape.plan(2); - }); + var transformer = { + format: function (value) { + return Array.isArray(value) ? value.join(' ') : value; + }, + parse: function (value) { + return value.split(' '); + } + }; - tape.test('error', function (tape) { - tape.plan(3); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {}, - ctx: ctx - }).getLocals().error, - undefined, - 'default error should be undefined'); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {error: 'myerror', hasError: true}, - ctx: ctx - }).getLocals().error, - 'myerror', - 'should handle error option'); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: { - error: function (value) { - return 'error: ' + value; - }, - hasError: true - }, - ctx: ctx, - value: 'a' - }).getLocals().error, - 'error: a', - 'should handle error option as a function'); - }); - - tape.test('template', function (tape) { - tape.plan(2); - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {}, - ctx: ctx - }).getTemplate(), - bootstrap.textbox, - 'default template should be bootstrap.textbox'); - - var template = function () {}; - - tape.strictEqual( - new Textbox({ - type: t.String, - options: {template: template}, - ctx: ctx - }).getTemplate(), - template, - 'should handle template option'); + tape.strictEqual( + new Textbox({ + type: t.String, + options: {transformer: transformer}, + ctx: ctx, + value: ['a', 'b'] + }).getLocals().value, + 'a b', + 'should handle transformer option (format)'); - }); + tape.deepEqual( + new Textbox({ + type: t.String, + options: {transformer: transformer}, + ctx: ctx, + value: ['a', 'b'] + }).pureValidate().value, + ['a', 'b'], + 'should handle transformer option (parse)'); }); -tape('Select', function (tape) { +test('Textbox:hasError', function (tape) { + tape.plan(4); + + tape.strictEqual( + new Textbox({ + type: t.String, + options: {}, + ctx: ctx + }).getLocals().hasError, + false, + 'default hasError should be false'); - var Select = core.Select; - var Country = t.enums({ - 'IT': 'Italy', - 'FR': 'France', - 'US': 'United States' + tape.strictEqual( + new Textbox({ + type: t.String, + options: {hasError: true}, + ctx: ctx + }).getLocals().hasError, + true, + 'should handle hasError option'); + + var textbox = new Textbox({ + type: t.String, + options: {}, + ctx: ctx }); - tape.test('label', function (tape) { - tape.plan(3); - - tape.strictEqual( - new Select({ - type: Country, - options: {}, - ctx: ctx - }).getLocals().label, - 'ctxDefaultLabel', - 'should have a default label'); - - tape.strictEqual( - new Select({ - type: Country, - options: {label: 'mylabel'}, - ctx: ctx - }).getLocals().label, - 'mylabel', - 'should handle `label` option'); - - tape.strictEqual( - new Select({ - type: t.maybe(Country), - options: {}, - ctx: ctx - }).getLocals().label, - 'ctxDefaultLabel (optional)', - 'should handle optional types'); + textbox.pureValidate(); + + tape.strictEqual( + textbox.getLocals().hasError, + false, + 'after a validation error hasError should be true'); + textbox = new Textbox({ + type: t.String, + options: {}, + ctx: ctx, + value: 'a' }); - tape.test('help', function (tape) { - tape.plan(1); + textbox.pureValidate(); - tape.strictEqual( - new Select({ - type: Country, - options: {help: 'myhelp'}, - ctx: ctx - }).getLocals().help, - 'myhelp', - 'should handle help option'); + tape.strictEqual( + textbox.getLocals().hasError, + false, + 'after a validation success hasError should be false'); - }); +}); - tape.test('value', function (tape) { - tape.plan(2); - - tape.strictEqual( - new Select({ - type: Country, - options: {}, - ctx: ctx - }).getLocals().value, - '', - 'default value should be nullOption.value'); - - tape.strictEqual( - new Select({ - type: Country, - options: {}, - ctx: ctx, - value: 'a' - }).getLocals().value, - 'a', - 'should handle value option'); +test('Textbox:error', function (tape) { + tape.plan(3); - }); + tape.strictEqual( + new Textbox({ + type: t.String, + options: {}, + ctx: ctx + }).getLocals().error, + undefined, + 'default error should be undefined'); - tape.test('transformer', function (tape) { - tape.plan(2); + tape.strictEqual( + new Textbox({ + type: t.String, + options: {error: 'myerror', hasError: true}, + ctx: ctx + }).getLocals().error, + 'myerror', + 'should handle error option'); - var transformer = { - format: function (value) { - return t.String.is(value) ? value : value === true ? '1' : '0'; - }, - parse: function (value) { - return value === '1'; - } - }; - - tape.strictEqual( - new Select({ - type: t.maybe(t.Boolean), - options: { - transformer: transformer, - options: [ - {value: '0', text: 'No'}, - {value: '1', text: 'Yes'} - ] - }, - ctx: ctx, - value: true - }).getLocals().value, - '1', - 'should handle transformer option (format)'); - - tape.deepEqual( - new Select({ - type: t.maybe(t.Boolean), - options: { - transformer: transformer, - options: [ - {value: '0', text: 'No'}, - {value: '1', text: 'Yes'} - ] + tape.strictEqual( + new Textbox({ + type: t.String, + options: { + error: function (value) { + return 'error: ' + value; }, - ctx: ctx, - value: true - }).validate().value, - true, - 'should handle transformer option (parse)'); + hasError: true + }, + ctx: ctx, + value: 'a' + }).getLocals().error, + 'error: a', + 'should handle error option as a function'); +}); - }); +test('Textbox:template', function (tape) { + tape.plan(2); + + tape.strictEqual( + new Textbox({ + type: t.String, + options: {}, + ctx: ctx + }).getTemplate(), + bootstrap.textbox, + 'default template should be bootstrap.textbox'); + + var template = function () {}; + + tape.strictEqual( + new Textbox({ + type: t.String, + options: {template: template}, + ctx: ctx + }).getTemplate(), + template, + 'should handle template option'); + +}); + +var Select = core.Select; +var Country = t.enums({ + 'IT': 'Italy', + 'FR': 'France', + 'US': 'United States' +}); + +test('Select:label', function (tape) { + tape.plan(3); - tape.test('hasError', function (tape) { - tape.plan(4); - - tape.strictEqual( - new Select({ - type: Country, - options: {}, - ctx: ctx - }).getLocals().hasError, - false, - 'default hasError should be false'); - - tape.strictEqual( - new Select({ - type: Country, - options: {hasError: true}, - ctx: ctx - }).getLocals().hasError, - true, - 'should handle hasError option'); - - var select = new Select({ + tape.strictEqual( + new Select({ type: Country, options: {}, ctx: ctx - }); + }).getLocals().label, + 'ctxDefaultLabel', + 'should have a default label'); - select.validate(); + tape.strictEqual( + new Select({ + type: Country, + options: {label: 'mylabel'}, + ctx: ctx + }).getLocals().label, + 'mylabel', + 'should handle `label` option'); + + tape.strictEqual( + new Select({ + type: t.maybe(Country), + options: {}, + ctx: ctx + }).getLocals().label, + 'ctxDefaultLabel (optional)', + 'should handle optional types'); - tape.strictEqual( - select.getLocals().hasError, - false, - 'after a validation error hasError should be true'); +}); + +test('Select:help', function (tape) { + tape.plan(1); - select = new Select({ + tape.strictEqual( + new Select({ + type: Country, + options: {help: 'myhelp'}, + ctx: ctx + }).getLocals().help, + 'myhelp', + 'should handle help option'); + +}); + +test('Select:value', function (tape) { + tape.plan(2); + + tape.strictEqual( + new Select({ + type: Country, + options: {}, + ctx: ctx + }).getLocals().value, + '', + 'default value should be nullOption.value'); + + tape.strictEqual( + new Select({ type: Country, options: {}, ctx: ctx, - value: 'IT' - }); + value: 'a' + }).getLocals().value, + 'a', + 'should handle value option'); - select.validate(); +}); - tape.strictEqual( - select.getLocals().hasError, - false, - 'after a validation success hasError should be false'); +test('Select:transformer', function (tape) { + tape.plan(2); + + var transformer = { + format: function (value) { + return t.String.is(value) ? value : value === true ? '1' : '0'; + }, + parse: function (value) { + return value === '1'; + } + }; + + tape.strictEqual( + new Select({ + type: t.maybe(t.Boolean), + options: { + transformer: transformer, + options: [ + {value: '0', text: 'No'}, + {value: '1', text: 'Yes'} + ] + }, + ctx: ctx, + value: true + }).getLocals().value, + '1', + 'should handle transformer option (format)'); + + tape.deepEqual( + new Select({ + type: t.maybe(t.Boolean), + options: { + transformer: transformer, + options: [ + {value: '0', text: 'No'}, + {value: '1', text: 'Yes'} + ] + }, + ctx: ctx, + value: true + }).pureValidate().value, + true, + 'should handle transformer option (parse)'); - }); +}); - tape.test('error', function (tape) { - tape.plan(3); - - tape.strictEqual( - new Select({ - type: Country, - options: {}, - ctx: ctx - }).getLocals().error, - undefined, - 'default error should be undefined'); - - tape.strictEqual( - new Select({ - type: Country, - options: {error: 'myerror', hasError: true}, - ctx: ctx - }).getLocals().error, - 'myerror', - 'should handle error option'); - - tape.strictEqual( - new Select({ - type: Country, - options: { - error: function (value) { - return 'error: ' + value; - }, - hasError: true - }, - ctx: ctx, - value: 'a' - }).getLocals().error, - 'error: a', - 'should handle error option as a function'); - }); +test('Select:hasError', function (tape) { + tape.plan(4); - tape.test('template', function (tape) { - tape.plan(2); - - tape.strictEqual( - new Select({ - type: Country, - options: {}, - ctx: ctx - }).getTemplate(), - bootstrap.select, - 'default template should be bootstrap.select'); - - var template = function () {}; - - tape.strictEqual( - new Select({ - type: Country, - options: {template: template}, - ctx: ctx - }).getTemplate(), - template, - 'should handle template option'); + tape.strictEqual( + new Select({ + type: Country, + options: {}, + ctx: ctx + }).getLocals().hasError, + false, + 'default hasError should be false'); + tape.strictEqual( + new Select({ + type: Country, + options: {hasError: true}, + ctx: ctx + }).getLocals().hasError, + true, + 'should handle hasError option'); + + var select = new Select({ + type: Country, + options: {}, + ctx: ctx }); - tape.test('options', function (tape) { - tape.plan(1); - - tape.deepEqual( - new Select({ - type: Country, - options: { - options: [ - {value: 'IT', text: 'Italia'}, - {value: 'US', text: 'Stati Uniti'} - ] - }, - ctx: ctx - }).getLocals().options, - [ - { text: '-', value: '' }, - { text: 'Italia', value: 'IT' }, - { text: 'Stati Uniti', value: 'US' } - ], - 'should handle options option'); + select.pureValidate(); + tape.strictEqual( + select.getLocals().hasError, + false, + 'after a validation error hasError should be true'); + + select = new Select({ + type: Country, + options: {}, + ctx: ctx, + value: 'IT' }); - tape.test('order', function (tape) { - tape.plan(2); - - tape.deepEqual( - new Select({ - type: Country, - options: {order: 'asc'}, - ctx: ctx - }).getLocals().options, - [ - { text: '-', value: '' }, - { text: 'France', value: 'FR' }, - { text: 'Italy', value: 'IT' }, - { text: 'United States', value: 'US' } - ], - 'should handle order = asc option'); - - tape.deepEqual( - new Select({ - type: Country, - options: {order: 'desc'}, - ctx: ctx - }).getLocals().options, - [ - { text: '-', value: '' }, - { text: 'United States', value: 'US' }, - { text: 'Italy', value: 'IT' }, - { text: 'France', value: 'FR' } - ], - 'should handle order = desc option'); + select.pureValidate(); - }); + tape.strictEqual( + select.getLocals().hasError, + false, + 'after a validation success hasError should be false'); - tape.test('nullOption', function (tape) { - tape.plan(2); +}); - tape.deepEqual( - new Select({ - type: Country, - options: { - nullOption: {value: '', text: 'Select a country'} - }, - ctx: ctx - }).getLocals().options, - [ - { value: '', text: 'Select a country' }, - { text: 'Italy', value: 'IT' }, - { text: 'France', value: 'FR' }, - { text: 'United States', value: 'US' } - ], - 'should handle nullOption option'); - - tape.deepEqual( - new Select({ - type: Country, - options: { - nullOption: false - }, - ctx: ctx, - value: 'US' - }).getLocals().options, - [ - { text: 'Italy', value: 'IT' }, - { text: 'France', value: 'FR' }, - { text: 'United States', value: 'US' } - ], - 'should skip the nullOption if nullOption = false'); +test('Select:error', function (tape) { + tape.plan(3); - }); + tape.strictEqual( + new Select({ + type: Country, + options: {}, + ctx: ctx + }).getLocals().error, + undefined, + 'default error should be undefined'); + tape.strictEqual( + new Select({ + type: Country, + options: {error: 'myerror', hasError: true}, + ctx: ctx + }).getLocals().error, + 'myerror', + 'should handle error option'); + + tape.strictEqual( + new Select({ + type: Country, + options: { + error: function (value) { + return 'error: ' + value; + }, + hasError: true + }, + ctx: ctx, + value: 'a' + }).getLocals().error, + 'error: a', + 'should handle error option as a function'); }); -tape('Checkbox', function (tape) { +test('Select:template', function (tape) { + tape.plan(2); - var Checkbox = core.Checkbox; + tape.strictEqual( + new Select({ + type: Country, + options: {}, + ctx: ctx + }).getTemplate(), + bootstrap.select, + 'default template should be bootstrap.select'); - tape.test('label', function (tape) { - tape.plan(2); + var template = function () {}; - tape.strictEqual( - new Checkbox({ - type: t.Boolean, - options: {}, - ctx: ctx - }).getLocals().label, - 'ctxDefaultLabel', - 'should have a default label'); + tape.strictEqual( + new Select({ + type: Country, + options: {template: template}, + ctx: ctx + }).getTemplate(), + template, + 'should handle template option'); - tape.strictEqual( - new Checkbox({ - type: t.Boolean, - options: {label: 'mylabel'}, - ctx: ctx - }).getLocals().label, - 'mylabel', - 'should handle `label` option'); +}); - }); +test('Select:options', function (tape) { + tape.plan(1); - tape.test('help', function (tape) { - tape.plan(1); + tape.deepEqual( + new Select({ + type: Country, + options: { + options: [ + {value: 'IT', text: 'Italia'}, + {value: 'US', text: 'Stati Uniti'} + ] + }, + ctx: ctx + }).getLocals().options, + [ + { text: '-', value: '' }, + { text: 'Italia', value: 'IT' }, + { text: 'Stati Uniti', value: 'US' } + ], + 'should handle options option'); - tape.strictEqual( - new Checkbox({ - type: t.Boolean, - options: {help: 'myhelp'}, - ctx: ctx - }).getLocals().help, - 'myhelp', - 'should handle help option'); +}); - }); +test('Select:order', function (tape) { + tape.plan(2); - tape.test('value', function (tape) { - tape.plan(2); - - tape.strictEqual( - new Checkbox({ - type: t.Boolean, - options: {}, - ctx: ctx - }).getLocals().value, - false, - 'default value should be false'); - - tape.strictEqual( - new Checkbox({ - type: t.Boolean, - options: {}, - ctx: ctx, - value: true - }).getLocals().value, - true, - 'should handle value option'); + tape.deepEqual( + new Select({ + type: Country, + options: {order: 'asc'}, + ctx: ctx + }).getLocals().options, + [ + { text: '-', value: '' }, + { text: 'France', value: 'FR' }, + { text: 'Italy', value: 'IT' }, + { text: 'United States', value: 'US' } + ], + 'should handle order = asc option'); + + tape.deepEqual( + new Select({ + type: Country, + options: {order: 'desc'}, + ctx: ctx + }).getLocals().options, + [ + { text: '-', value: '' }, + { text: 'United States', value: 'US' }, + { text: 'Italy', value: 'IT' }, + { text: 'France', value: 'FR' } + ], + 'should handle order = desc option'); - }); +}); - tape.test('transformer', function (tape) { - tape.plan(2); +test('Select:nullOption', function (tape) { + tape.plan(2); - var transformer = { - format: function (value) { - return t.String.is(value) ? value : value === true ? '1' : '0'; + tape.deepEqual( + new Select({ + type: Country, + options: { + nullOption: {value: '', text: 'Select a country'} + }, + ctx: ctx + }).getLocals().options, + [ + { value: '', text: 'Select a country' }, + { text: 'Italy', value: 'IT' }, + { text: 'France', value: 'FR' }, + { text: 'United States', value: 'US' } + ], + 'should handle nullOption option'); + + tape.deepEqual( + new Select({ + type: Country, + options: { + nullOption: false }, - parse: function (value) { - return value === '1'; - } - }; - - tape.strictEqual( - new Checkbox({ - type: t.Boolean, - options: {transformer: transformer}, - ctx: ctx, - value: true - }).getLocals().value, - '1', - 'should handle transformer option (format)'); - - tape.deepEqual( - new Checkbox({ - type: t.Boolean, - options: {transformer: transformer}, - ctx: ctx, - value: true - }).validate().value, - true, - 'should handle transformer option (parse)'); + ctx: ctx, + value: 'US' + }).getLocals().options, + [ + { text: 'Italy', value: 'IT' }, + { text: 'France', value: 'FR' }, + { text: 'United States', value: 'US' } + ], + 'should skip the nullOption if nullOption = false'); - }); +}); - tape.test('hasError', function (tape) { - tape.plan(4); - - var True = t.subtype(t.Boolean, function (value) { return value === true; }); - - tape.strictEqual( - new Checkbox({ - type: True, - options: {}, - ctx: ctx - }).getLocals().hasError, - false, - 'default hasError should be false'); - - tape.strictEqual( - new Checkbox({ - type: True, - options: {hasError: true}, - ctx: ctx - }).getLocals().hasError, - true, - 'should handle hasError option'); - - var checkbox = new Checkbox({ - type: True, +var Checkbox = core.Checkbox; + +test('Checkbox:label', function (tape) { + tape.plan(2); + + tape.strictEqual( + new Checkbox({ + type: t.Boolean, options: {}, ctx: ctx - }); + }).getLocals().label, + 'ctxDefaultLabel', + 'should have a default label'); + + tape.strictEqual( + new Checkbox({ + type: t.Boolean, + options: {label: 'mylabel'}, + ctx: ctx + }).getLocals().label, + 'mylabel', + 'should handle `label` option'); - checkbox.validate(); +}); - tape.strictEqual( - checkbox.getLocals().hasError, - false, - 'after a validation error hasError should be true'); +test('Checkbox:help', function (tape) { + tape.plan(1); - checkbox = new Checkbox({ - type: True, + tape.strictEqual( + new Checkbox({ + type: t.Boolean, + options: {help: 'myhelp'}, + ctx: ctx + }).getLocals().help, + 'myhelp', + 'should handle help option'); + +}); + +test('Checkbox:value', function (tape) { + tape.plan(2); + + tape.strictEqual( + new Checkbox({ + type: t.Boolean, options: {}, + ctx: ctx + }).getLocals().value, + false, + 'default value should be false'); + + tape.strictEqual( + new Checkbox({ + type: t.Boolean, + options: {}, + ctx: ctx, + value: true + }).getLocals().value, + true, + 'should handle value option'); + +}); + +test('Checkbox:transformer', function (tape) { + tape.plan(2); + + var transformer = { + format: function (value) { + return t.String.is(value) ? value : value === true ? '1' : '0'; + }, + parse: function (value) { + return value === '1'; + } + }; + + tape.strictEqual( + new Checkbox({ + type: t.Boolean, + options: {transformer: transformer}, + ctx: ctx, + value: true + }).getLocals().value, + '1', + 'should handle transformer option (format)'); + + tape.deepEqual( + new Checkbox({ + type: t.Boolean, + options: {transformer: transformer}, ctx: ctx, value: true - }); + }).pureValidate().value, + true, + 'should handle transformer option (parse)'); - checkbox.validate(); +}); - tape.strictEqual( - checkbox.getLocals().hasError, - false, - 'after a validation success hasError should be false'); +test('Checkbox:hasError', function (tape) { + tape.plan(4); - }); + var True = t.subtype(t.Boolean, function (value) { return value === true; }); - tape.test('error', function (tape) { - tape.plan(3); - - tape.strictEqual( - new Checkbox({ - type: t.Boolean, - options: {}, - ctx: ctx - }).getLocals().error, - undefined, - 'default error should be undefined'); - - tape.strictEqual( - new Checkbox({ - type: t.Boolean, - options: {error: 'myerror', hasError: true}, - ctx: ctx - }).getLocals().error, - 'myerror', - 'should handle error option'); - - tape.strictEqual( - new Checkbox({ - type: t.Boolean, - options: { - error: function (value) { - return 'error: ' + value; - }, - hasError: true - }, - ctx: ctx, - value: 'a' - }).getLocals().error, - 'error: a', - 'should handle error option as a function'); + tape.strictEqual( + new Checkbox({ + type: True, + options: {}, + ctx: ctx + }).getLocals().hasError, + false, + 'default hasError should be false'); + + tape.strictEqual( + new Checkbox({ + type: True, + options: {hasError: true}, + ctx: ctx + }).getLocals().hasError, + true, + 'should handle hasError option'); + + var checkbox = new Checkbox({ + type: True, + options: {}, + ctx: ctx }); - tape.test('template', function (tape) { - tape.plan(2); - - tape.strictEqual( - new Checkbox({ - type: t.Boolean, - options: {}, - ctx: ctx - }).getTemplate(), - bootstrap.checkbox, - 'default template should be bootstrap.checkbox'); - - var template = function () {}; - - tape.strictEqual( - new Checkbox({ - type: t.Boolean, - options: {template: template}, - ctx: ctx - }).getTemplate(), - template, - 'should handle template option'); + checkbox.pureValidate(); + + tape.strictEqual( + checkbox.getLocals().hasError, + false, + 'after a validation error hasError should be true'); + checkbox = new Checkbox({ + type: True, + options: {}, + ctx: ctx, + value: true }); -}); + checkbox.pureValidate(); -tape('DatePicker', function (tape) { - - var DatePicker = core.DatePicker; - var date = new Date(1973, 10, 30); - - tape.test('label', function (tape) { - tape.plan(2); - - tape.strictEqual( - new DatePicker({ - type: t.Date, - options: {}, - ctx: ctx, - value: date - }).getLocals().label, - 'ctxDefaultLabel', - 'should have a default label'); - - tape.strictEqual( - new DatePicker({ - type: t.Date, - options: {label: 'mylabel'}, - ctx: ctx, - value: date - }).getLocals().label, - 'mylabel', - 'should handle `label` option'); + tape.strictEqual( + checkbox.getLocals().hasError, + false, + 'after a validation success hasError should be false'); - }); +}); - tape.test('help', function (tape) { - tape.plan(1); +test('Checkbox:error', function (tape) { + tape.plan(3); - tape.strictEqual( - new DatePicker({ - type: t.Date, - options: {help: 'myhelp'}, - ctx: ctx, - value: date - }).getLocals().help, - 'myhelp', - 'should handle help option'); + tape.strictEqual( + new Checkbox({ + type: t.Boolean, + options: {}, + ctx: ctx + }).getLocals().error, + undefined, + 'default error should be undefined'); + + tape.strictEqual( + new Checkbox({ + type: t.Boolean, + options: {error: 'myerror', hasError: true}, + ctx: ctx + }).getLocals().error, + 'myerror', + 'should handle error option'); + + tape.strictEqual( + new Checkbox({ + type: t.Boolean, + options: { + error: function (value) { + return 'error: ' + value; + }, + hasError: true + }, + ctx: ctx, + value: 'a' + }).getLocals().error, + 'error: a', + 'should handle error option as a function'); +}); - }); +test('Checkbox:template', function (tape) { + tape.plan(2); - tape.test('value', function (tape) { - tape.plan(1); + tape.strictEqual( + new Checkbox({ + type: t.Boolean, + options: {}, + ctx: ctx + }).getTemplate(), + bootstrap.checkbox, + 'default template should be bootstrap.checkbox'); - tape.strictEqual( - new DatePicker({ - type: t.Date, - options: {}, - ctx: ctx, - value: date - }).getLocals().value, - date, - 'should handle value option'); + var template = function () {}; - }); + tape.strictEqual( + new Checkbox({ + type: t.Boolean, + options: {template: template}, + ctx: ctx + }).getTemplate(), + template, + 'should handle template option'); - tape.test('transformer', function (tape) { - tape.plan(2); +}); - var transformer = { - format: function (value) { - return Array.isArray(value) ? value : [value.getFullYear(), value.getMonth(), value.getDate()]; - }, - parse: function (value) { - return new Date(value[0], value[1], value[2]); - } - }; - - tape.deepEqual( - new DatePicker({ - type: t.String, - options: {transformer: transformer}, - ctx: ctx, - value: date - }).getLocals().value, - [1973, 10, 30], - 'should handle transformer option (format)'); - - tape.deepEqual( - transformer.format(new DatePicker({ - type: t.Date, - options: {transformer: transformer}, - ctx: ctx, - value: [1973, 10, 30] - }).validate().value), - [1973, 10, 30], - 'should handle transformer option (parse)'); +var DatePicker = core.DatePicker; +var date = new Date(1973, 10, 30); - }); +test('DatePicker:label', function (tape) { + tape.plan(2); - tape.test('hasError', function (tape) { - tape.plan(4); - - tape.strictEqual( - new DatePicker({ - type: t.Date, - options: {}, - ctx: ctx, - value: date - }).getLocals().hasError, - false, - 'default hasError should be false'); - - tape.strictEqual( - new DatePicker({ - type: t.Date, - options: {hasError: true}, - ctx: ctx, - value: date - }).getLocals().hasError, - true, - 'should handle hasError option'); - - var datePicker = new DatePicker({ + tape.strictEqual( + new DatePicker({ type: t.Date, options: {}, ctx: ctx, - value: date - }); + value: date + }).getLocals().label, + 'ctxDefaultLabel', + 'should have a default label'); - datePicker.validate(); + tape.strictEqual( + new DatePicker({ + type: t.Date, + options: {label: 'mylabel'}, + ctx: ctx, + value: date + }).getLocals().label, + 'mylabel', + 'should handle `label` option'); + +}); - tape.strictEqual( - datePicker.getLocals().hasError, - false, - 'after a validation error hasError should be true'); +test('DatePicker:help', function (tape) { + tape.plan(1); - datePicker = new DatePicker({ + tape.strictEqual( + new DatePicker({ + type: t.Date, + options: {help: 'myhelp'}, + ctx: ctx, + value: date + }).getLocals().help, + 'myhelp', + 'should handle help option'); + +}); + +test('DatePicker:value', function (tape) { + tape.plan(1); + + tape.strictEqual( + new DatePicker({ type: t.Date, options: {}, ctx: ctx, value: date - }); + }).getLocals().value, + date, + 'should handle value option'); - datePicker.validate(); +}); - tape.strictEqual( - datePicker.getLocals().hasError, - false, - 'after a validation success hasError should be false'); +test('DatePicker:transformer', function (tape) { + tape.plan(2); - }); + var transformer = { + format: function (value) { + return Array.isArray(value) ? value : [value.getFullYear(), value.getMonth(), value.getDate()]; + }, + parse: function (value) { + return new Date(value[0], value[1], value[2]); + } + }; - tape.test('error', function (tape) { - tape.plan(3); - - tape.strictEqual( - new DatePicker({ - type: t.Date, - options: {}, - ctx: ctx, - value: date - }).getLocals().error, - undefined, - 'default error should be undefined'); - - tape.strictEqual( - new DatePicker({ - type: t.Date, - options: {error: 'myerror', hasError: true}, - ctx: ctx, - value: date - }).getLocals().error, - 'myerror', - 'should handle error option'); - - tape.strictEqual( - new DatePicker({ - type: t.Date, - options: { - error: function (value) { - return 'error: ' + value.getFullYear(); - }, - hasError: true - }, - ctx: ctx, - value: date - }).getLocals().error, - 'error: 1973', - 'should handle error option as a function'); - }); + tape.deepEqual( + new DatePicker({ + type: t.String, + options: {transformer: transformer}, + ctx: ctx, + value: date + }).getLocals().value, + [1973, 10, 30], + 'should handle transformer option (format)'); + + tape.deepEqual( + transformer.format(new DatePicker({ + type: t.Date, + options: {transformer: transformer}, + ctx: ctx, + value: [1973, 10, 30] + }).pureValidate().value), + [1973, 10, 30], + 'should handle transformer option (parse)'); + +}); - tape.test('template', function (tape) { - tape.plan(2); - - tape.strictEqual( - new DatePicker({ - type: t.Date, - options: {}, - ctx: ctx, - value: date - }).getTemplate(), - bootstrap.datepicker, - 'default template should be bootstrap.datepicker'); - - var template = function () {}; - - tape.strictEqual( - new DatePicker({ - type: t.Date, - options: {template: template}, - ctx: ctx, - value: date - }).getTemplate(), - template, - 'should handle template option'); +test('DatePicker:hasError', function (tape) { + tape.plan(4); + tape.strictEqual( + new DatePicker({ + type: t.Date, + options: {}, + ctx: ctx, + value: date + }).getLocals().hasError, + false, + 'default hasError should be false'); + + tape.strictEqual( + new DatePicker({ + type: t.Date, + options: {hasError: true}, + ctx: ctx, + value: date + }).getLocals().hasError, + true, + 'should handle hasError option'); + + var datePicker = new DatePicker({ + type: t.Date, + options: {}, + ctx: ctx, + value: date }); -}); + datePicker.pureValidate(); -tape('List', ({ test }) => { + tape.strictEqual( + datePicker.getLocals().hasError, + false, + 'after a validation error hasError should be true'); - var List = core.List; + datePicker = new DatePicker({ + type: t.Date, + options: {}, + ctx: ctx, + value: date + }); - test('should support unions', (assert) => { - assert.plan(2) + datePicker.pureValidate(); - const AccountType = t.enums.of([ - 'type 1', - 'type 2', - 'other' - ], 'AccountType') + tape.strictEqual( + datePicker.getLocals().hasError, + false, + 'after a validation success hasError should be false'); - const KnownAccount = t.struct({ - type: AccountType - }, 'KnownAccount') +}); - const UnknownAccount = KnownAccount.extend({ - label: t.String, - }, 'UnknownAccount') +test('DatePicker:error', function (tape) { + tape.plan(3); - const Account = t.union([KnownAccount, UnknownAccount], 'Account') + tape.strictEqual( + new DatePicker({ + type: t.Date, + options: {}, + ctx: ctx, + value: date + }).getLocals().error, + undefined, + 'default error should be undefined'); - Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount + tape.strictEqual( + new DatePicker({ + type: t.Date, + options: {error: 'myerror', hasError: true}, + ctx: ctx, + value: date + }).getLocals().error, + 'myerror', + 'should handle error option'); - let component = new List({ - type: t.list(Account), + tape.strictEqual( + new DatePicker({ + type: t.Date, + options: { + error: function (value) { + return 'error: ' + value.getFullYear(); + }, + hasError: true + }, ctx: ctx, + value: date + }).getLocals().error, + 'error: 1973', + 'should handle error option as a function'); +}); + +test('DatePicker:template', function (tape) { + tape.plan(2); + + tape.strictEqual( + new DatePicker({ + type: t.Date, options: {}, - value: [ - { type: 'type 1' } - ] - }) + ctx: ctx, + value: date + }).getTemplate(), + bootstrap.datepicker, + 'default template should be bootstrap.datepicker'); - assert.strictEqual(component.getItems()[0].input.props.type, KnownAccount) + var template = function () {}; - component = new List({ - type: t.list(Account), + tape.strictEqual( + new DatePicker({ + type: t.Date, + options: {template: template}, ctx: ctx, - options: {}, - value: [ - { type: 'other' } - ] - }) - - assert.strictEqual(component.getItems()[0].input.props.type, UnknownAccount) - }) -}) + value: date + }).getTemplate(), + template, + 'should handle template option'); + +}); + +var List = core.List; + +test('List:should support unions', (assert) => { + assert.plan(2); + + const AccountType = t.enums.of([ + 'type 1', + 'type 2', + 'other' + ], 'AccountType'); + + const KnownAccount = t.struct({ + type: AccountType + }, 'KnownAccount'); + + const UnknownAccount = KnownAccount.extend({ + label: t.String + }, 'UnknownAccount'); + + const Account = t.union([KnownAccount, UnknownAccount], 'Account'); + + Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount; + + let component = new List({ + type: t.list(Account), + ctx: ctx, + options: {}, + value: [ + { type: 'type 1' } + ] + }); + + assert.strictEqual(component.getItems()[0].input.props.type, KnownAccount); + + component = new List({ + type: t.list(Account), + ctx: ctx, + options: {}, + value: [ + { type: 'other' } + ] + }); + + assert.strictEqual(component.getItems()[0].input.props.type, UnknownAccount); +}); From cf916d6e0371d4c9a06b017633e2d01bab74b678 Mon Sep 17 00:00:00 2001 From: gcanti Date: Sun, 10 Jul 2016 09:53:52 +0200 Subject: [PATCH 3/4] Number keypad should show for numeric fields, fix #171 --- lib/components.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/components.js b/lib/components.js index 7a9a787b..2e192831 100644 --- a/lib/components.js +++ b/lib/components.js @@ -229,10 +229,19 @@ class Textbox extends Component { return placeholder; } + getKeyboardType() { + const keyboardType = this.props.options.keyboardType; + if (t.Nil.is(keyboardType) && this.typeInfo.innerType === t.Number) { + return 'numeric'; + } + return keyboardType; + } + getLocals() { const locals = super.getLocals(); locals.placeholder = this.getPlaceholder(); locals.onChangeNative = this.props.options.onChange; + locals.keyboardType = this.getKeyboardType(); [ 'help', @@ -241,7 +250,6 @@ class Textbox extends Component { 'autoFocus', 'blurOnSubmit', 'editable', - 'keyboardType', 'maxLength', 'multiline', 'onBlur', From b74c9a8b63ad994843da04f14ff5573c7416aa59 Mon Sep 17 00:00:00 2001 From: gcanti Date: Sun, 10 Jul 2016 11:05:32 +0200 Subject: [PATCH 4/4] update docs --- CHANGELOG.md | 8 ++++ README.md | 112 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 97 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc89cb2b..e8bb3651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ **Note**: Gaps between patch versions are faulty/broken releases. +## v0.5.2 + +- **New Feature** + - add support for unions, fix #118 (@gcanti) + - add support for lists, fix #80 (@gcanti) +- **Bug Fix** + - allow to set a default value in Android date picker template, fix #187 + ## v0.5.1 - **New Feature** diff --git a/README.md b/README.md index 5e4bcad4..2533687e 100644 --- a/README.md +++ b/README.md @@ -951,6 +951,51 @@ var options = { This will completely skip the rendering of the component, while the default value will be available for validation purposes. +# Unions + +**Code Example** + +```js +const AccountType = t.enums.of([ + 'type 1', + 'type 2', + 'other' +], 'AccountType') + +const KnownAccount = t.struct({ + type: AccountType +}, 'KnownAccount') + +// UnknownAccount extends KnownAccount so it owns also the type field +const UnknownAccount = KnownAccount.extend({ + label: t.String, +}, 'UnknownAccount') + +// the union +const Account = t.union([KnownAccount, UnknownAccount], 'Account') + +// the final form type +const Type = t.list(Account) + +const options = { + item: [ // one options object for each concrete type of the union + { + label: 'KnownAccount' + }, + { + label: 'UnknownAccount' + } + ] +} +``` + +Generally `tcomb`'s unions require a `dispatch` implementation in order to select the suitable type constructor for a given value and this would be the key in this use case: + +```js +// if account type is 'other' return the UnknownAccount type +Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount +``` + # Lists You can handle a list with the `t.list` combinator: @@ -996,9 +1041,40 @@ const Person = t.struct({ const Persons = t.list(Person); ``` -# Managing unions +## Internationalization + +You can override the default language (english) with the `i18n` option: + +```js +const options = { + i18n: { + optional: ' (optional)', + required: '', + add: 'Add', // add button + remove: '✘', // remove button + up: '↑', // move up button + down: '↓' // move down button + } +}; +``` + +## Buttons configuration + +You can prevent operations on lists with the following options: + +- `disableAdd`: (default `false`) prevents adding new items +- `disableRemove`: (default `false`) prevents removing existing items +- `disableOrder`: (default `false`) prevents sorting existing items + +```js +const options = { + disableOrder: true +}; +``` + +## List with Dynamic Items (Different structs based on selected value) -**Example** +Lists of different types are not supported. This is because a `tcomb`'s list, by definition, contains only values of the same type. You can define a union though: ```js const AccountType = t.enums.of([ @@ -1011,33 +1087,23 @@ const KnownAccount = t.struct({ type: AccountType }, 'KnownAccount') -const UnknownAccount = t.struct({ - type: AccountType, - label: t.String +// UnknownAccount extends KnownAccount so it owns also the type field +const UnknownAccount = KnownAccount.extend({ + label: t.String, }, 'UnknownAccount') -// the union type +// the union const Account = t.union([KnownAccount, UnknownAccount], 'Account') -// you must define a dispatch function returning the suitable type -// based on the current form value -Account.dispatch = value => { - return value && value.type === 'other' ? UnknownAccount : KnownAccount -} +// the final form type +const Type = t.list(Account) +``` -const Type = Account +Generally `tcomb`'s unions require a `dispatch` implementation in order to select the suitable type constructor for a given value and this would be the key in this use case: -// options can be a list of options, one for each type of the union -const options = [ - // options of the KnownAccount type - { - label: 'KnownAccount' - }, - // options of theUnknownAccount type - { - label: 'UnknownAccount' - } -] +```js +// if account type is 'other' return the UnknownAccount type +Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount ``` # Customizations