├── .travis.yml ├── .gitignore ├── src ├── utils │ ├── unwrapProperties.js │ └── uniqueId.js ├── templates │ ├── modal │ │ ├── modalBody.html │ │ ├── modalHeader.html │ │ ├── modalFooter.html │ │ └── modal.html │ ├── carousel │ │ ├── carouselContent.html │ │ ├── carouselIndicators.html │ │ ├── carouselControls.html │ │ └── carousel.html │ ├── alert │ │ ├── alert.html │ │ └── alertInner.html │ ├── progress.html │ ├── pager.html │ ├── pagination.html │ └── templatesWrapper.js ├── bindings │ ├── toggleBinding.js │ ├── classBinding.js │ ├── tooltipBinding.js │ ├── radioBinding.js │ ├── progressBinding.js │ ├── alertBinding.js │ ├── checkboxBinding.js │ ├── carouselBinding.js │ ├── modalBinding.js │ ├── popoverBinding.js │ └── paginationBinding.js └── main.js ├── .npmignore ├── tests ├── jasmineExtensions.js ├── bindings │ ├── tooltipBindingBehaviors.js │ ├── toggleBindingBehaviors.js │ ├── classBindingBehaviors.js │ ├── pagerBindingBehaviors.js │ ├── alertBindingBehaviors.js │ ├── radioBindingBehaviors.js │ ├── popoverBindingBehaviors.js │ ├── paginationBindingBehaviors.js │ ├── progressBindingBehaviors.js │ ├── modalBindingBehaviors.js │ ├── checkboxBindingBehaviors.js │ └── carouselBindingBehaviors.js ├── utilsBehaviors.js ├── stringTemplateEngineBehaviors.js └── runner.html ├── bower.json ├── examples-src ├── htmlParts │ ├── header.html │ ├── menu.html │ ├── overview.html │ ├── bindingExamples │ │ ├── classExample.html │ │ ├── tooltipExample.html │ │ ├── pagerExample.html │ │ ├── alertExample.html │ │ ├── popoverExample.html │ │ ├── paginationExample.html │ │ ├── buttonsExample.html │ │ ├── carouselExample.html │ │ ├── progressExample.html │ │ └── modalExample.html │ └── stringTemplateEngineExamples.html ├── css │ └── examples.css ├── index.html └── js │ └── examples.js ├── knockstrap.nuspec ├── LICENSE.txt ├── package.json ├── README.md ├── CHANGELOG.txt └── Gruntfile.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | temp 3 | build 4 | examples -------------------------------------------------------------------------------- /src/utils/unwrapProperties.js: -------------------------------------------------------------------------------- 1 | ko.utils.unwrapProperties = ko.toJS; -------------------------------------------------------------------------------- /src/templates/modal/modalBody.html: -------------------------------------------------------------------------------- 1 |
2 |
-------------------------------------------------------------------------------- /src/templates/carousel/carouselContent.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/templates/alert/alert.html: -------------------------------------------------------------------------------- 1 |
2 |
-------------------------------------------------------------------------------- /src/templates/alert/alertInner.html: -------------------------------------------------------------------------------- 1 | 2 |

-------------------------------------------------------------------------------- /src/templates/modal/modalHeader.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | temp 4 | examples 5 | examples-src 6 | tests 7 | Gruntfile.js 8 | .gitignore 9 | .jamignore 10 | .travis.yml 11 | knockstrap.nuspec 12 | bower.json -------------------------------------------------------------------------------- /src/templates/carousel/carouselIndicators.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/templates/modal/modalFooter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/templates/carousel/carouselControls.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/jasmineExtensions.js: -------------------------------------------------------------------------------- 1 | jasmine.Suite.prototype.prepareTestElement = function (markup) { 2 | 3 | markup = markup || '
'; 4 | 5 | beforeEach(function() { 6 | this.testElement = $(markup).appendTo('body'); 7 | }); 8 | 9 | afterEach(function () { 10 | this.testElement.remove(); 11 | }); 12 | }; -------------------------------------------------------------------------------- /src/utils/uniqueId.js: -------------------------------------------------------------------------------- 1 | ko.utils.uniqueId = (function () { 2 | 3 | var prefixesCounts = { 4 | 'ks-unique-': 0 5 | }; 6 | 7 | return function (prefix) { 8 | prefix = prefix || 'ks-unique-'; 9 | 10 | if (!prefixesCounts[prefix]) { 11 | prefixesCounts[prefix] = 0; 12 | } 13 | 14 | return prefix + prefixesCounts[prefix]++; 15 | }; 16 | })(); -------------------------------------------------------------------------------- /src/templates/progress.html: -------------------------------------------------------------------------------- 1 | 2 |
4 | 5 | % 6 | 7 |
8 | -------------------------------------------------------------------------------- /src/templates/modal/modal.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/templates/pager.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/bindings/toggleBinding.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers.toggle = { 2 | init: function (element, valueAccessor) { 3 | var value = valueAccessor(); 4 | 5 | if (!ko.isObservable(value)) { 6 | throw new Error('toggle binding should be used only with observable values'); 7 | } 8 | 9 | $(element).on('click', function (event) { 10 | event.preventDefault(); 11 | 12 | var previousValue = ko.unwrap(value); 13 | value(!previousValue); 14 | }); 15 | }, 16 | 17 | update: function (element, valueAccessor) { 18 | ko.utils.toggleDomNodeCssClass(element, 'active', ko.unwrap(valueAccessor())); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/bindings/classBinding.js: -------------------------------------------------------------------------------- 1 | // Knockout doesn't allow to use 'css: className' and 'css: { 'class-name': boolValue }' bindings on same element 2 | // This binding can be used together with 'css: { 'class-name': boolValue }' 3 | // Inspired by https://github.com/knockout/knockout/wiki/Bindings---class 4 | var previousClassKey = '__ko__previousClassValue__'; 5 | 6 | ko.bindingHandlers['class'] = { 7 | update: function (element, valueAccessor) { 8 | if (element[previousClassKey]) { 9 | ko.utils.toggleDomNodeCssClass(element, element[previousClassKey], false); 10 | } 11 | 12 | var value = ko.unwrap(valueAccessor()); 13 | ko.utils.toggleDomNodeCssClass(element, value, true); 14 | element[previousClassKey] = value; 15 | } 16 | }; -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knockstrap", 3 | "version": "1.4.1", 4 | "description": "Knockout bindings to Twitter Bootstrap 3 widgets", 5 | "main": "build/knockstrap.js", 6 | "homepage": "http://faulknercs.github.io/Knockstrap/", 7 | "ignore": [ 8 | "src", 9 | "examples", 10 | "examples-src", 11 | "node_modules", 12 | "tests", 13 | ".gitignore", 14 | ".npmignore", 15 | ".jamignore", 16 | ".travis.yml", 17 | "knockstrap.nuspec", 18 | "Gruntfile.js" 19 | ], 20 | "keywords": [ 21 | "knockout", 22 | "bootstrap", 23 | "knockstrap", 24 | "custom", 25 | "bindings", 26 | "library", 27 | "templates" 28 | ], 29 | "author": "Artem Stepanyuk", 30 | "license": "MIT", 31 | "dependencies": { 32 | "jquery": ">=1.9.0", 33 | "knockout": ">=2.3.0", 34 | "bootstrap": "~3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/bindings/tooltipBinding.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers.tooltip = { 2 | init: function (element) { 3 | var $element = $(element); 4 | 5 | ko.utils.domNodeDisposal.addDisposeCallback(element, function () { 6 | if ($element.data('bs.tooltip')) { 7 | $element.tooltip('destroy'); 8 | } 9 | }); 10 | }, 11 | 12 | update: function (element, valueAccessor) { 13 | var $element = $(element), 14 | value = ko.unwrap(valueAccessor()), 15 | options = ko.utils.unwrapProperties(value); 16 | 17 | var tooltipData = $element.data('bs.tooltip'); 18 | 19 | if (!tooltipData) { 20 | $element.tooltip(options); 21 | } else { 22 | ko.utils.extend(tooltipData.options, options); 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/templates/carousel/carousel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples-src/htmlParts/header.html: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 |
21 |

Knockstrap

22 |

Knockout bindings library

23 |
24 |
25 | -------------------------------------------------------------------------------- /tests/bindings/tooltipBindingBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: tooltip', function () { 2 | this.prepareTestElement('
Text
'); 3 | 4 | afterEach(function() { 5 | $('.tooltip').remove(); 6 | }); 7 | 8 | it('Should add tooltip to element', function() { 9 | var vm = { 10 | options: { title: 'test' } 11 | }; 12 | 13 | ko.applyBindings(vm, this.testElement[0]); 14 | this.testElement.mouseover(); 15 | 16 | expect($('.tooltip')).toExist(); 17 | }); 18 | 19 | it('Should update tooltip according to changes of observables', function() { 20 | var vm = { 21 | options: { title: ko.observable('test') } 22 | }; 23 | 24 | ko.applyBindings(vm, this.testElement[0]); 25 | vm.options.title('new text'); 26 | this.testElement.mouseover(); 27 | 28 | expect($('.tooltip')).toContainText('new text'); 29 | }); 30 | }); -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // @echo header 2 | (function (factory) { 3 | 'use strict'; 4 | 5 | if (typeof ko !== 'undefined' && typeof jQuery !== 'undefined') { 6 | //global knockout and jQuery references already present, so use these regardless of whether this module has been included in CommonJS/AMD 7 | factory(ko, jQuery); 8 | } else if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') { 9 | // CommonJS/Node.js 10 | factory(require('knockout'), require('jquery')); 11 | } else if (typeof define === 'function' && define.amd) { 12 | // AMD 13 | define(['knockout', 'jquery'], factory); 14 | } else { 15 | throw new Error('Could not locate current context reference to knockout and jQuery in order to load Knockstrap'); 16 | } 17 | 18 | })(function (ko, $) { 19 | 'use strict'; 20 | 21 | // @include utils.js 22 | 23 | // @include templates.js 24 | 25 | // @include bindings.js 26 | }); 27 | -------------------------------------------------------------------------------- /knockstrap.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Knockstrap 5 | 1.4.1 6 | Artyom Stepanyuk 7 | Artyom Stepanyuk 8 | http://www.opensource.org/licenses/mit-license.php 9 | http://faulknercs.github.io/Knockstrap/ 10 | false 11 | Knockout bindings to Twitter Bootstrap 3 12 | - 13 | Copyright 2014 14 | knockout bootstrap knockstrap custom bindings library templates 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2020 Artem Stepanyuk 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /examples-src/htmlParts/menu.html: -------------------------------------------------------------------------------- 1 | 41 | -------------------------------------------------------------------------------- /examples-src/css/examples.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | } 4 | 5 | a { 6 | cursor: pointer; 7 | } 8 | 9 | .form-fixed-width { 10 | width: 200px; 11 | } 12 | 13 | .examples-menu { 14 | width: 170px; 15 | background-color: #f4f4f4; 16 | border-radius: 5px; 17 | margin-top: 20px; 18 | padding-bottom: 10px; 19 | padding-top: 10px; 20 | } 21 | 22 | .affix { 23 | top: 50px; 24 | } 25 | 26 | .examples-menu .active { 27 | background-color: #ddd; 28 | } 29 | 30 | h1[id] { 31 | margin-top: -40px; 32 | padding-top: 80px; 33 | } 34 | 35 | .page-header { 36 | margin-top: 0; 37 | } 38 | 39 | pre.prettyprint { 40 | border-radius: 4px; 41 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); 42 | } 43 | 44 | .prettyprint [class^="L"] { 45 | list-style-type: decimal; 46 | } 47 | 48 | #alert-default-descr { 49 | margin-top: -65px; 50 | padding-top: 65px; 51 | } 52 | 53 | .tooltip-radios { 54 | margin-top: 5px; 55 | margin-left: 0; 56 | } 57 | 58 | .list-group { 59 | margin-bottom: 0; 60 | } 61 | 62 | .panel code { 63 | white-space: normal; 64 | } 65 | 66 | .panel-heading code { 67 | padding: 2px; 68 | } 69 | 70 | .btn-group { 71 | vertical-align: top; 72 | } 73 | 74 | .class-example-text { 75 | padding: 15px; 76 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knockstrap", 3 | "version": "1.4.1", 4 | "description": "Knockout bindings to Twitter Bootstrap 3", 5 | "homepage": "http://faulknercs.github.io/Knockstrap/", 6 | "bugs": "https://github.com/faulknercs/Knockstrap/issues", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/faulknercs/knockstrap.git" 10 | }, 11 | "keywords": [ 12 | "knockout", 13 | "bootstrap", 14 | "knockstrap", 15 | "custom", 16 | "bindings", 17 | "library", 18 | "templates" 19 | ], 20 | "author": "Artem Stepanyuk", 21 | "license": "MIT", 22 | "readmeFilename": "README.md", 23 | "main": "build/knockstrap.js", 24 | "dependencies": { 25 | "jquery": ">=1.9.0", 26 | "knockout": ">=2.3.0", 27 | "bootstrap": "~3" 28 | }, 29 | "devDependencies": { 30 | "grunt": "^0.4.5", 31 | "grunt-cli": "^1.3.2", 32 | "grunt-contrib-clean": "^0.5.0", 33 | "grunt-contrib-concat": "^0.3.0", 34 | "grunt-contrib-copy": "^0.4.1", 35 | "grunt-contrib-jasmine": "^1.1.0", 36 | "grunt-contrib-jshint": "^0.6.5", 37 | "grunt-contrib-uglify": "^0.2.7", 38 | "grunt-html-convert": "0.0.2", 39 | "grunt-nuget": "~0.1.2", 40 | "grunt-preprocess": "^3.0.1" 41 | }, 42 | "github": "https://github.com/faulknercs/Knockstrap/", 43 | "scripts": { 44 | "test": "grunt travis --verbose" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/templates/pagination.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/bindings/toggleBindingBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: toggle', function () { 2 | this.prepareTestElement(''); 3 | 4 | it('Shoud throw exception for non-observable value', function() { 5 | var el = this.testElement[0]; 6 | 7 | expect(function () { 8 | ko.applyBindings({ value: true }, el); 9 | }).toThrow(); 10 | }); 11 | 12 | it('Should has "active" class at button, if created with "true" init value', function() { 13 | var vm = { value: ko.observable(true) }; 14 | 15 | ko.applyBindings(vm, this.testElement[0]); 16 | 17 | expect(this.testElement).toHaveClass('active'); 18 | }); 19 | 20 | it('Should hasn\'t "active" class, if created with "false" init value', function() { 21 | var vm = { value: ko.observable(false) }; 22 | 23 | ko.applyBindings(vm, this.testElement[0]); 24 | 25 | expect(this.testElement).not.toHaveClass('active'); 26 | }); 27 | 28 | it('Should toggle "active" class, when value changes', function() { 29 | var vm = { value: ko.observable(false) }; 30 | 31 | ko.applyBindings(vm, this.testElement[0]); 32 | 33 | expect(this.testElement).not.toHaveClass('active'); 34 | vm.value(true); 35 | expect(this.testElement).toHaveClass('active'); 36 | vm.value(false); 37 | expect(this.testElement).not.toHaveClass('active'); 38 | }); 39 | 40 | it('Should toggle value, when element was clicked', function() { 41 | var vm = { value: ko.observable(false) }; 42 | 43 | ko.applyBindings(vm, this.testElement[0]); 44 | 45 | this.testElement.click(); 46 | expect(vm.value()).toBe(true); 47 | this.testElement.click(); 48 | expect(vm.value()).toBe(false); 49 | }); 50 | }); -------------------------------------------------------------------------------- /src/bindings/radioBinding.js: -------------------------------------------------------------------------------- 1 | // Knockout checked binding doesn't work with Bootstrap radio-buttons 2 | ko.bindingHandlers.radio = { 3 | init: function (element, valueAccessor) { 4 | 5 | if (!ko.isObservable(valueAccessor())) { 6 | throw new Error('radio binding should be used only with observable values'); 7 | } 8 | 9 | $(element).on('change', 'input:radio', function (e) { 10 | // we need to handle change event after bootsrap will handle its event 11 | // to prevent incorrect changing of radio button styles 12 | setTimeout(function() { 13 | var radio = $(e.target), 14 | value = valueAccessor(), 15 | newValue = radio.val(); 16 | 17 | // we shouldn't change value for disables buttons 18 | if (!radio.prop('disabled')) { 19 | value(newValue); 20 | } 21 | }, 0); 22 | }); 23 | }, 24 | 25 | update: function (element, valueAccessor) { 26 | var tempValue = ko.unwrap(valueAccessor()), 27 | value = tempValue !== null && tempValue !== undefined ? tempValue : '', 28 | $radioButton = $(element).find('input[value="' + value.toString().replace(/"/g, '\\"') + '"]'), 29 | $radioButtonWrapper; 30 | 31 | if ($radioButton.length) { 32 | $radioButtonWrapper = $radioButton.parent(); 33 | 34 | $radioButtonWrapper.siblings().removeClass('active'); 35 | $radioButtonWrapper.addClass('active'); 36 | 37 | $radioButton.prop('checked', true); 38 | } else { 39 | $radioButtonWrapper = $(element).find('.active'); 40 | $radioButtonWrapper.removeClass('active'); 41 | $radioButtonWrapper.find('input').prop('checked', false); 42 | } 43 | } 44 | }; -------------------------------------------------------------------------------- /src/bindings/progressBinding.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers.progress = { 2 | defaults: { 3 | css: 'progress', 4 | text: '', 5 | textHidden: true, 6 | striped: false, 7 | type: '', 8 | animated: false 9 | }, 10 | 11 | init: function (element, valueAccessor) { 12 | var $element = $(element), 13 | value = valueAccessor(), 14 | unwrappedValue = ko.unwrap(value), 15 | defs = ko.bindingHandlers.progress.defaults, 16 | model; 17 | 18 | if(unwrappedValue instanceof Array) { 19 | model = unwrappedValue.map(function(val) { 20 | return normalize(val); 21 | }); 22 | } else { 23 | model = [normalize(value)]; 24 | } 25 | 26 | ko.renderTemplate('progress', model, { templateEngine: ko.stringTemplateEngine.instance }, element); 27 | 28 | $element.addClass(defs.css); 29 | 30 | return { controlsDescendantBindings: true }; 31 | }, 32 | }; 33 | 34 | function normalize(value) { 35 | var unwrappedValue = ko.unwrap(value); 36 | 37 | if (typeof unwrappedValue === 'number') { 38 | return new ProgressBar({ value: value }); 39 | } else if (typeof ko.unwrap(unwrappedValue.value) === 'number') { 40 | return new ProgressBar(unwrappedValue); 41 | } else { 42 | throw new Error('progress binding can accept only numbers or objects with "value" number property'); 43 | } 44 | } 45 | 46 | function ProgressBar(data) { 47 | var self = this; 48 | 49 | $.extend(self, ko.bindingHandlers.progress.defaults, data); 50 | 51 | self.barWidth = ko.computed(function() { 52 | return ko.unwrap(self.value) + '%'; 53 | }); 54 | 55 | self.barType = ko.computed(function() { 56 | var type = ko.unwrap(self.type); 57 | 58 | return type ? 'progress-bar-' + type : ''; 59 | }); 60 | } -------------------------------------------------------------------------------- /tests/utilsBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Utils: uniqueId', function () { 2 | it('Should return different strings for each call', function () { 3 | expect(ko.utils.uniqueId()).not.toEqual(ko.utils.uniqueId()); 4 | }); 5 | 6 | it('Should return id starts with passed prefix', function() { 7 | expect(ko.utils.uniqueId('test-prefix')).toMatch(/^test-prefix/); 8 | }); 9 | 10 | it('Should return separate number sequences for different prefixes', function() { 11 | var firstId = ko.utils.uniqueId('first'), 12 | secondId = ko.utils.uniqueId('second'); 13 | 14 | // last symbols should be equal for different prefixes at this point 15 | expect(firstId[firstId.length - 1]).toEqual(secondId[secondId.length - 1]); 16 | }); 17 | }); 18 | 19 | describe('Utils: uwrapProperties', function() { 20 | it('Should return object with non-observable properties for objects with observables and non-observables', function() { 21 | var testObj = { 22 | observableString: ko.observable('observableStringProperty'), 23 | nonObservableString: 'stringProperty', 24 | }; 25 | 26 | var unwrapped = ko.utils.unwrapProperties(testObj); 27 | 28 | expect(unwrapped.observableString).toEqual(ko.unwrap(testObj.observableString)); 29 | expect(unwrapped.nonObservableString).toEqual(testObj.nonObservableString); 30 | }); 31 | 32 | it('Should return passed argument without changing, if it is not object', function() { 33 | expect(ko.utils.unwrapProperties(42)).toEqual(jasmine.any(Number)); 34 | expect(ko.utils.unwrapProperties('string')).toEqual(jasmine.any(String)); 35 | expect(ko.utils.unwrapProperties(null)).toEqual(null); 36 | expect(ko.utils.unwrapProperties(undefined)).toEqual(undefined); 37 | expect(ko.utils.unwrapProperties(function() {})).toEqual(jasmine.any(Function)); 38 | }); 39 | }); -------------------------------------------------------------------------------- /src/bindings/alertBinding.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers.alert = { 2 | init: function () { 3 | return { controlsDescendantBindings: true }; 4 | }, 5 | 6 | update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { 7 | var $element = $(element), 8 | value = valueAccessor(), 9 | usedTemplateEngine = !value.template ? ko.stringTemplateEngine.instance : null, 10 | userTemplate = ko.unwrap(value.template) || 'alertInner', 11 | template, data; 12 | 13 | // for compatibility with ie8, use '1' and '8' values for node types 14 | if (element.nodeType === (typeof Node !== 'undefined' && Node.ELEMENT_NODE || 1)) { 15 | template = userTemplate; 16 | data = value.data || { message: value.message }; 17 | 18 | // ola lawal added this code to removeclassess for resulable alerts using hide show (issue #29) 19 | $element.removeClass("alert-info alert-danger alert-success "); 20 | 21 | $element.addClass('alert fade in').addClass('alert-' + (ko.unwrap(value.type) || 'info')); 22 | } else if (element.nodeType === (typeof Node !== 'undefined' && Node.COMMENT_NODE || 8)) { 23 | template = 'alert'; 24 | data = { 25 | innerTemplate: { 26 | name: userTemplate , 27 | data: value.data || { message: value.message }, 28 | templateEngine: usedTemplateEngine 29 | }, 30 | type: 'alert-' + (ko.unwrap(value.type) || 'info') 31 | }; 32 | } else { 33 | throw new Error('alert binding should be used with dom elements or ko virtual elements'); 34 | } 35 | 36 | ko.renderTemplate(template, bindingContext.createChildContext(data), ko.utils.extend({ templateEngine: usedTemplateEngine }, value.templateOptions), element); 37 | } 38 | }; 39 | 40 | ko.virtualElements.allowedBindings.alert = true; 41 | -------------------------------------------------------------------------------- /tests/bindings/classBindingBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: class', function () { 2 | this.prepareTestElement('
'); 3 | 4 | it('Should add class to element', function () { 5 | var vm = { 6 | value: ko.observable('test'), 7 | }; 8 | 9 | ko.applyBindings(vm, this.testElement[0]); 10 | 11 | expect(this.testElement).toHaveClass('test'); 12 | }); 13 | 14 | it('Should change class after model changes', function () { 15 | var vm = { 16 | value: ko.observable('test'), 17 | }; 18 | 19 | ko.applyBindings(vm, this.testElement[0]); 20 | 21 | expect(this.testElement).toHaveClass('test'); 22 | 23 | vm.value('test2'); 24 | 25 | expect(this.testElement).not.toHaveClass('test'); 26 | expect(this.testElement).toHaveClass('test2'); 27 | }); 28 | 29 | it('Should remove class when model is empty', function () { 30 | var vm = { 31 | value: ko.observable('test'), 32 | }; 33 | 34 | ko.applyBindings(vm, this.testElement[0]); 35 | 36 | expect(this.testElement).toHaveClass('test'); 37 | 38 | vm.value(''); 39 | 40 | expect(this.testElement).not.toHaveClass('test'); 41 | }); 42 | 43 | it('Should add class list', function () { 44 | var vm = { 45 | value: ko.observable('test test2'), 46 | }; 47 | 48 | ko.applyBindings(vm, this.testElement[0]); 49 | 50 | expect(this.testElement).toHaveClass('test'); 51 | expect(this.testElement).toHaveClass('test2'); 52 | }); 53 | 54 | it('Should remove one class from list', function () { 55 | var vm = { 56 | value: ko.observable('test test2'), 57 | }; 58 | 59 | ko.applyBindings(vm, this.testElement[0]); 60 | 61 | vm.value('test'); 62 | 63 | expect(this.testElement).toHaveClass('test'); 64 | expect(this.testElement).not.toHaveClass('test2'); 65 | }); 66 | }); -------------------------------------------------------------------------------- /tests/bindings/pagerBindingBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: pager', function () { 2 | this.prepareTestElement('
'); 3 | 4 | it('Should add pager widget', function () { 5 | var vm = { 6 | value: { currentPage: ko.observable(1), totalCount: ko.observable(20) } 7 | }; 8 | 9 | ko.applyBindings(vm, this.testElement[0]); 10 | 11 | expect(this.testElement).not.toBeEmpty(); 12 | }); 13 | 14 | it('Should add pager widget with first page selected', function () { 15 | var vm = { 16 | value: { currentPage: ko.observable(), totalCount: ko.observable(20) } 17 | }; 18 | 19 | ko.applyBindings(vm, this.testElement[0]); 20 | 21 | expect(vm.value.currentPage()).toEqual(1); 22 | }); 23 | 24 | it('Should add css classes for aligned state', function () { 25 | var vm = { 26 | value: { currentPage: ko.observable(1), totalCount: ko.observable(30), isAligned: true } 27 | }; 28 | 29 | ko.applyBindings(vm, this.testElement[0]); 30 | 31 | expect(this.testElement.find('li:first')).toHaveClass('previous'); 32 | expect(this.testElement.find('li:last')).toHaveClass('next'); 33 | }); 34 | 35 | it('Should go to next page after next button click', function () { 36 | var vm = { 37 | value: { currentPage: ko.observable(1), totalCount: ko.observable(200) } 38 | }; 39 | 40 | ko.applyBindings(vm, this.testElement[0]); 41 | 42 | this.testElement.find('li:last > a').click(); 43 | 44 | expect(vm.value.currentPage()).toEqual(2); 45 | }); 46 | 47 | it('Should go to previous page after back button click', function () { 48 | var vm = { 49 | value: { currentPage: ko.observable(3), totalCount: ko.observable(200) } 50 | }; 51 | 52 | ko.applyBindings(vm, this.testElement[0]); 53 | 54 | this.testElement.find('li:first > a').click(); 55 | 56 | expect(vm.value.currentPage()).toEqual(2); 57 | }); 58 | }); -------------------------------------------------------------------------------- /examples-src/htmlParts/overview.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |

Knockstrap is binding library for Knockout.js, which provides binding to Twitter Bootstrap widgets

6 |

Features

7 | 15 |
16 |
17 | Download 18 |
19 |
20 |

Usage

21 |

22 | If you have all dependencies, you only need to include knockstrap.min.js or knockstrap.js to your pages: 23 |

<script src="../path/to/knockstrap.min.js"></script>
24 | Or with cdn: 25 |
<script src="https://cdn.jsdelivr.net/npm/knockstrap@1.4.1/build/knockstrap.js"></script>
26 |

27 |

28 | Also you can install it via npm, bower or nuget. 29 |

30 |

Dependencies

31 | 42 |
43 | -------------------------------------------------------------------------------- /examples-src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Knockstrap - Knockout bindings for Bootstrap - Examples 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Knockstrap [![Build Status](https://travis-ci.org/faulknercs/Knockstrap.svg?branch=master)](https://travis-ci.org/faulknercs/Knockstrap) 2 | ========== 3 | 4 | ## ⚠️ Bootstrap versions support ⚠️ 5 | 6 | __If you need Bootstrap 4 support, you may use [KnockstrapPlus](https://github.com/CloudNimble/KnockstrapPlus) fork. Also, you can try to search for more forks [here](https://github.com/faulknercs/Knockstrap/network).__ 7 | 8 | I highly appreciate all contributions and feedback, but I don't use Bootstrap nor Knockout anymore, so I don't have any plans to continue development of this project. 9 | This repository is not going to be updated for Bootstrap 4 (and further) and will continue work only with Bootstrap 3. But I continue merging patches with bugfixes for Bootstrap 3 if any appears. 10 | 11 | ### Description 12 | 13 | Knockstrap is binding library for Knockout.js, which provides bindings to Twitter Bootstrap 3 widgets 14 | 15 | #### Supported widgets: 16 | 17 | - Modal 18 | - Tooltip 19 | - Popover 20 | - Alert 21 | - Progress 22 | - Toggle button 23 | - Radio button 24 | - Checkbox button 25 | - Carousel 26 | - Pagination 27 | - Pager 28 | 29 | [Download](https://github.com/faulknercs/Knockstrap/releases/download/v1.4.1/knockstrap-1.4.1.zip) 30 | 31 | [Documentation/Examples](http://faulknercs.github.io/Knockstrap/) 32 | 33 | ### Dependencies 34 | 35 | - jQuery (Any compatible with Bootstrap 3 version) 36 | - Twitter Bootstrap 3 (CSS and JavaScript) 37 | - Knockout.js (>=2.3.0) 38 | 39 | ### Packages 40 | 41 | [NuGet](http://www.nuget.org/packages/Knockstrap/) | [Bower](http://bower.io/search/?q=knockstrap) | [npm](https://www.npmjs.org/package/knockstrap) 42 | 43 | ### CDN 44 | 45 | [jsDelivr](https://cdn.jsdelivr.net/npm/knockstrap@1.4.1/build/knockstrap.js) 46 | 47 | ### Building 48 | #### Building using grunt: 49 | 50 | Install node.js and grunt plugin. 51 | 52 | Install all grunt plugins: 53 | 54 | npm install 55 | 56 | Then you can build project with: 57 | 58 | grunt 59 | 60 | Also, you can specify custom build and temp directories: 61 | 62 | grunt -buildPath=D:/custom/build -tempPath=D:/custom/temp 63 | 64 | To build examples use: 65 | 66 | grunt examples 67 | 68 | Also, you can specify custom examples directory: 69 | 70 | grunt -examplesPath=D:/custom/examples 71 | 72 | To run unit-tests, use: 73 | 74 | grunt jasmine 75 | 76 | To run building, tests and minification, use: 77 | 78 | grunt release 79 | 80 | ### License: [MIT License](http://www.opensource.org/licenses/mit-license.php) -------------------------------------------------------------------------------- /src/templates/templatesWrapper.js: -------------------------------------------------------------------------------- 1 | // inspired by http://www.knockmeout.net/2011/10/ko-13-preview-part-3-template-sources.html 2 | (function () { 3 | // storage of string templates for all instances of stringTemplateEngine 4 | // @include compiledTemplates.js 5 | 6 | // create new template source to provide storing string templates in storage 7 | ko.templateSources.stringTemplate = function (template) { 8 | this.templateName = template; 9 | 10 | this.data = function (key, value) { 11 | templates.data = templates.data || {}; 12 | templates.data[this.templateName] = templates.data[this.templateName] || {}; 13 | 14 | if (arguments.length === 1) { 15 | return templates.data[this.templateName][key]; 16 | } 17 | 18 | templates.data[this.templateName][key] = value; 19 | }; 20 | 21 | this.text = function (value) { 22 | if (arguments.length === 0) { 23 | return templates[this.templateName]; 24 | } 25 | 26 | templates[this.templateName] = value; 27 | }; 28 | }; 29 | 30 | // create modified template engine, which uses new string template source 31 | ko.stringTemplateEngine = function () { 32 | this.allowTemplateRewriting = false; 33 | }; 34 | 35 | ko.stringTemplateEngine.prototype = new ko.nativeTemplateEngine(); 36 | ko.stringTemplateEngine.prototype.constructor = ko.stringTemplateEngine; 37 | 38 | ko.stringTemplateEngine.prototype.makeTemplateSource = function (template) { 39 | return new ko.templateSources.stringTemplate(template); 40 | }; 41 | 42 | ko.stringTemplateEngine.prototype.getTemplate = function (name) { 43 | return templates[name]; 44 | }; 45 | 46 | ko.stringTemplateEngine.prototype.addTemplate = function (name, template) { 47 | if (arguments.length < 2) { 48 | throw new Error('template is not provided'); 49 | } 50 | 51 | templates[name] = template; 52 | }; 53 | 54 | ko.stringTemplateEngine.prototype.removeTemplate = function (name) { 55 | if (!name) { 56 | throw new Error('template name is not provided'); 57 | } 58 | 59 | delete templates[name]; 60 | }; 61 | 62 | ko.stringTemplateEngine.prototype.isTemplateExist = function (name) { 63 | return !!templates[name]; 64 | }; 65 | 66 | ko.stringTemplateEngine.instance = new ko.stringTemplateEngine(); 67 | })(); 68 | -------------------------------------------------------------------------------- /tests/stringTemplateEngineBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('String Template Engine', function () { 2 | this.prepareTestElement('
'); 3 | 4 | beforeEach(function() { 5 | this.engine = new ko.stringTemplateEngine(); 6 | }); 7 | 8 | it('Should "ko.stringTemplateEngine.instance" exist and be an instanse of ko.stringTemplateEngine', function() { 9 | expect(ko.stringTemplateEngine.instance).toBeDefined(); 10 | expect(ko.stringTemplateEngine.instance).toEqual(jasmine.any(ko.stringTemplateEngine)); 11 | }); 12 | 13 | it('Should add new string template to string template engine', function() { 14 | this.engine.addTemplate('test', 'text'); 15 | 16 | expect(this.engine.isTemplateExist('test')).toBeTruthy(); 17 | }); 18 | 19 | it('Should get added template from string template engine', function () { 20 | this.engine.addTemplate('test', 'text'); 21 | 22 | expect(this.engine.getTemplate('test')).toEqual('text'); 23 | }); 24 | 25 | it('Should remove template from template engine', function () { 26 | this.engine.addTemplate('test', 'text'); 27 | 28 | expect(this.engine.isTemplateExist('test')).toBeTruthy(); 29 | this.engine.removeTemplate('test'); 30 | expect(this.engine.isTemplateExist('test')).toBeFalsy(); 31 | }); 32 | 33 | it('Should render element with added string template', function () { 34 | this.engine.addTemplate('test', 'test text'); 35 | 36 | var vm = { 37 | templ: { name: 'test', templateEngine: this.engine } 38 | }; 39 | 40 | ko.applyBindings(vm, this.testElement[0]); 41 | 42 | expect(this.testElement).toContainText('test text'); 43 | }); 44 | 45 | it('Should not render element with string template engine', function () { 46 | this.engine.addTemplate('test', 'string template engine'); 47 | this.testElement.after(''); 48 | 49 | var vm = { 50 | templ: { name: 'test' } 51 | }; 52 | 53 | ko.applyBindings(vm, this.testElement[0]); 54 | 55 | expect(this.testElement).not.toContainText('string template engine'); 56 | $('#test-template').remove(); 57 | }); 58 | 59 | it('Should throw exception, if trying to remove template without name', function () { 60 | expect(function() { 61 | this.engine.removeTemplate(); 62 | }).toThrow(); 63 | }); 64 | 65 | it('Should throw exception, if trying to add undefined template', function () { 66 | expect(function () { 67 | this.engine.addTemplate('test'); 68 | }).toThrow(); 69 | }); 70 | }); -------------------------------------------------------------------------------- /tests/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Knockstrap Tests Runner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/bindings/checkboxBinding.js: -------------------------------------------------------------------------------- 1 | // Knockout checked binding doesn't work with Bootstrap checkboxes 2 | ko.bindingHandlers.checkbox = { 3 | init: function (element, valueAccessor) { 4 | var $element = $(element), 5 | handler = function (e) { 6 | // we need to handle change event after bootstrap will handle its event 7 | // to prevent incorrect changing of checkbox state 8 | setTimeout(function() { 9 | var $checkbox = $(e.target), 10 | value = valueAccessor(), 11 | data = $checkbox.val(), 12 | isChecked = $checkbox.parent().hasClass('active'); 13 | 14 | if(!$checkbox.prop('disabled')) { 15 | if (ko.unwrap(value) instanceof Array) { 16 | var index = ko.utils.arrayIndexOf(ko.unwrap(value), (data)); 17 | 18 | if (isChecked && (index === -1)) { 19 | value.push(data); 20 | } else if (!isChecked && (index !== -1)) { 21 | value.splice(index, 1); 22 | } 23 | } else { 24 | value(isChecked); 25 | } 26 | } 27 | }, 0); 28 | }; 29 | 30 | if ($element.attr('data-toggle') === 'buttons' && $element.find('input:checkbox').length) { 31 | 32 | if (!(ko.unwrap(valueAccessor()) instanceof Array)) { 33 | throw new Error('checkbox binding should be used only with array or observableArray values in this case'); 34 | } 35 | 36 | $element.on('change', 'input:checkbox', handler); 37 | } else if ($element.attr('type') === 'checkbox') { 38 | 39 | if (!ko.isObservable(valueAccessor())) { 40 | throw new Error('checkbox binding should be used only with observable values in this case'); 41 | } 42 | 43 | $element.on('change', handler); 44 | } else { 45 | throw new Error('checkbox binding should be used only with bootstrap checkboxes'); 46 | } 47 | }, 48 | 49 | update: function (element, valueAccessor) { 50 | var $element = $(element), 51 | value = ko.unwrap(valueAccessor()), 52 | isChecked; 53 | 54 | if (value instanceof Array) { 55 | if ($element.attr('data-toggle') === 'buttons') { 56 | $element.find('input:checkbox').each(function (index, el) { 57 | isChecked = ko.utils.arrayIndexOf(value, el.value) !== -1; 58 | $(el).parent().toggleClass('active', isChecked); 59 | el.checked = isChecked; 60 | }); 61 | } else { 62 | isChecked = ko.utils.arrayIndexOf(value, $element.val()) !== -1; 63 | $element.toggleClass('active', isChecked); 64 | $element.find('input').prop('checked', isChecked); 65 | } 66 | } else { 67 | isChecked = !!value; 68 | $element.prop('checked', isChecked); 69 | $element.parent().toggleClass('active', isChecked); 70 | } 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /examples-src/htmlParts/bindingExamples/classExample.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |

6 | This binding adds or removes one or more CSS classes to the associated DOM element. 7 | Can be used, when you need to use Knockout css binding with static and dynamic classes simultaneously. 8 | In this case, you may use css binding for static classes and class binding for dynamic classes. 9 |

10 | 11 |

Examples

12 |
13 |
14 |

15 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec pharetra eros. 16 | Sed luctus vitae ligula viverra porttitor. Vestibulum porttitor egestas lacus. 17 |

18 | 19 |
20 | 24 | 28 | 32 |
33 |
34 |
35 | 36 |

Html markup

37 |
38 | <p class="class-example-text" data-bind="class: className">
39 |     Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec pharetra eros.
40 |     Sed luctus vitae ligula viverra porttitor. Vestibulum porttitor egestas lacus.
41 | </p>
42 | <div class="btn-group form-group" data-toggle="buttons" data-bind="radio: className">
43 |     <label class="btn btn-primary">
44 |     <input type="radio" name="options" value="bg-primary" />
45 |         bg-primary
46 |     </label>
47 |     <label class="btn btn-primary">
48 |     <input type="radio" name="options" value="bg-success" />
49 |         bg-success
50 |     </label>
51 |     <label class="btn btn-primary">
52 |     <input type="radio" name="options" value="bg-danger" />
53 |         bg-danger
54 |     </label>
55 | </div>
56 | 57 |

View model

58 |
59 | function ClassExampleViewModel() {
60 |     this.className = ko.observable('bg-primary');
61 | }
62 | 63 |

Options

64 | 65 |
66 |
67 | data-bind="class: className" 68 |
69 | 70 |
    71 |
  • 72 |

    className

    73 |

    Type: string, can be observable

    74 |

    75 | Contains class name or list of class names, separated by space. 76 |

    77 |
  • 78 |
79 |
80 |
81 | -------------------------------------------------------------------------------- /src/bindings/carouselBinding.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers.carousel = { 2 | 3 | defaults: { 4 | css: 'carousel slide', 5 | 6 | controlsTemplate: { 7 | name: 'carouselControls', 8 | templateEngine: ko.stringTemplateEngine.instance, 9 | dataConverter: function(value) { 10 | return { 11 | id: ko.computed(function() { 12 | return '#' + ko.unwrap(value.id); 13 | }) 14 | }; 15 | } 16 | }, 17 | 18 | indicatorsTemplate: { 19 | name: 'carouselIndicators', 20 | templateEngine: ko.stringTemplateEngine.instance, 21 | dataConverter: function(value) { 22 | return { 23 | id: ko.computed(function() { 24 | return '#' + ko.unwrap(value.id); 25 | }), 26 | 27 | items: value.content.data 28 | }; 29 | } 30 | }, 31 | 32 | itemTemplate: { 33 | name: 'carouselContent', 34 | templateEngine: ko.stringTemplateEngine.instance, 35 | 36 | converter: function (item) { 37 | return item; 38 | } 39 | } 40 | }, 41 | 42 | init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { 43 | var $element = $(element), 44 | value = valueAccessor(), 45 | defaults = ko.bindingHandlers.carousel.defaults, 46 | extendDefaults = function(defs, type) { 47 | var extended = { 48 | name: defs.name, 49 | data: (value[type] && (value[type].data || value[type].dataConverter && value[type].dataConverter(value))) || defs.dataConverter(value), 50 | }; 51 | 52 | extended = $.extend(true, {}, extended, value[type]); 53 | if (!value[type] || !value[type].name) { 54 | extended.templateEngine = defs.templateEngine; 55 | } 56 | 57 | return extended; 58 | }; 59 | 60 | if (!value.content) { 61 | throw new Error('content option is required for carousel binding'); 62 | } 63 | 64 | // get carousel id from 'id' attribute, or from binding options, or generate it 65 | if (element.id) { 66 | value.id = element.id; 67 | } else if (value.id) { 68 | element.id = ko.unwrap(value.id); 69 | } else { 70 | element.id = value.id = ko.utils.uniqueId('ks-carousel-'); 71 | } 72 | 73 | var model = { 74 | id: value.id, 75 | controlsTemplate: extendDefaults(defaults.controlsTemplate, 'controls'), 76 | indicatorsTemplate: extendDefaults(defaults.indicatorsTemplate, 'indicators'), 77 | 78 | items: value.content.data, 79 | converter: value.content.converter || defaults.itemTemplate.converter, 80 | itemTemplateName: value.content.name || defaults.itemTemplate.name, 81 | templateEngine: !value.content.name ? defaults.itemTemplate.templateEngine : null, 82 | afterRender: value.content.afterRender, 83 | afterAdd: value.content.afterAdd, 84 | beforeRemove: value.content.beforeRemove 85 | }; 86 | 87 | ko.renderTemplate('carousel', bindingContext.createChildContext(model), { templateEngine: ko.stringTemplateEngine.instance }, element); 88 | 89 | $element.addClass(defaults.css); 90 | 91 | return { controlsDescendantBindings: true }; 92 | }, 93 | 94 | update: function (element, valueAccessor) { 95 | var value = valueAccessor(), 96 | options = ko.unwrap(value.options); 97 | 98 | $(element).carousel(options); 99 | } 100 | }; -------------------------------------------------------------------------------- /examples-src/htmlParts/bindingExamples/tooltipExample.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |

Binding to Bootstrap tooltip

6 | 7 |

Examples

8 |
9 |
10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec pharetra eros. 11 | Sed luctus vitae ligula viverra porttitor. Vestibulum porttitor egestas lacus. 12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 | 22 | Left 23 | 24 | Right 25 | 26 | Top 27 | 28 | Bottom 29 |
30 |
31 |
32 |
33 | 34 |

Html markup

35 |
36 | <div>
37 |     Lorem ipsum dolor sit amet, <a data-bind="tooltip: { title: 'Tooltip example' }">consectetur</a> adipiscing elit. 
38 |     Vestibulum nec pharetra eros. Sed luctus vitae ligula 
39 |     <a data-bind="tooltip: { title: tooltipTitle, placement: tooltipPlacement }">viverra</a> porttitor. 
40 |     Vestibulum porttitor egestas lacus.
41 | </div>
42 |     
43 | <!-- ... -->
44 | 
45 | <input type="text" data-bind="value: title, valueUpdate: 'afterkeydown'" />
46 | <div>
47 |     <input type="radio" name="tooltipPlacement" value="left" data-bind="checked: placement" />
48 |     Left
49 |     <input type="radio" name="tooltipPlacement" value="right" data-bind="checked: placement" />
50 |     Right
51 |     <input type="radio" name="tooltipPlacement" value="top" data-bind="checked: placement" />
52 |     Top
53 |     <input type="radio" name="tooltipPlacement" value="bottom" data-bind="checked: placement" />
54 |     Bottom
55 | </div>
56 | 57 |

View model

58 |
59 | function TooltipExampleViewModel() {
60 |     this.tooltipTitle = ko.observable('Observable title');
61 |     this.tooltipPlacement = ko.observable('left');
62 | }
63 | 64 |

Options

65 | 66 |
67 |
68 | data-bind="tooltip: tooltipOptions" 69 |
70 | 71 |
    72 |
  • 73 |

    tooltipOptions

    74 |

    Type: object, can be observable

    75 |

    76 | Uses Bootstrap 3 options. If any option is not specified, uses default value. See Bootstrap documentation. 77 | All of the options can be observables. Also option's object can be observable too. 78 |

    79 |
  • 80 |
81 |
82 |
83 | -------------------------------------------------------------------------------- /src/bindings/modalBinding.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers.modal = { 2 | defaults: { 3 | css: 'modal fade', 4 | dialogCss: '', 5 | attributes: { 6 | role: 'dialog' 7 | }, 8 | 9 | events: { 10 | shown: 'shown.bs.modal', 11 | hidden: 'hidden.bs.modal' 12 | }, 13 | 14 | headerTemplate: { 15 | name: 'modalHeader', 16 | templateEngine: ko.stringTemplateEngine.instance 17 | }, 18 | 19 | bodyTemplate: { 20 | name: 'modalBody', 21 | templateEngine: ko.stringTemplateEngine.instance 22 | }, 23 | 24 | footerTemplate: { 25 | name: 'modalFooter', 26 | templateEngine: ko.stringTemplateEngine.instance, 27 | data: { 28 | closeLabel: 'Close', 29 | primaryLabel: 'Ok' 30 | } 31 | } 32 | }, 33 | 34 | init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { 35 | var $element = $(element), 36 | value = valueAccessor(), 37 | defaults = ko.bindingHandlers.modal.defaults, 38 | events = $.extend({}, defaults.events, ko.toJS(value.events)), 39 | options = ko.utils.extend({ show: $element.data().show || false }, ko.toJS(value.options)), 40 | extendDefaults = function (defs, val) { 41 | var extended = { 42 | name: defs.name, 43 | data: defs.data, 44 | }; 45 | 46 | // reassign to not overwrite default content of data property 47 | extended = $.extend(true, {}, extended, val); 48 | if (!val || !val.name) { 49 | extended.templateEngine = defs.templateEngine; 50 | } 51 | 52 | return extended; 53 | }; 54 | 55 | if (!value.header || !value.body) { 56 | throw new Error('header and body options are required for modal binding.'); 57 | } 58 | 59 | // fix for not working escape button 60 | if (options.keyboard || typeof options.keyboard === 'undefined') { 61 | $element.attr('tabindex', -1); 62 | } 63 | 64 | var model = { 65 | dialogCss: value.dialogCss || defaults.dialogCss, 66 | headerTemplate: extendDefaults(defaults.headerTemplate, ko.unwrap(value.header)), 67 | bodyTemplate: extendDefaults(defaults.bodyTemplate, ko.unwrap(value.body)), 68 | footerTemplate: value.footer ? extendDefaults(defaults.footerTemplate, ko.unwrap(value.footer)) : null 69 | }; 70 | 71 | ko.renderTemplate('modal', bindingContext.createChildContext(model), { templateEngine: ko.stringTemplateEngine.instance }, element); 72 | 73 | $element.addClass(defaults.css).attr(defaults.attributes); 74 | $element.modal(options); 75 | 76 | $element.on(events.shown, function () { 77 | if (typeof value.visible !== 'undefined' && typeof value.visible === 'function' && !ko.isComputed(value.visible)) { 78 | value.visible(true); 79 | } 80 | 81 | $(this).find("[autofocus]:first").focus(); 82 | }); 83 | 84 | if (typeof value.visible !== 'undefined' && typeof value.visible === 'function' && !ko.isComputed(value.visible)) { 85 | $element.on(events.hidden, function() { 86 | value.visible(false); 87 | }); 88 | 89 | // if we need to show modal after initialization, we need also set visible property to true 90 | if (options.show) { 91 | value.visible(true); 92 | } 93 | } 94 | 95 | return { controlsDescendantBindings: true }; 96 | }, 97 | 98 | update: function (element, valueAccessor) { 99 | var value = valueAccessor(); 100 | 101 | if (typeof value.visible !== 'undefined') { 102 | $(element).modal(!ko.unwrap(value.visible) ? 'hide' : 'show'); 103 | } 104 | } 105 | }; -------------------------------------------------------------------------------- /tests/bindings/alertBindingBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: alert', function () { 2 | this.prepareTestElement('
'); 3 | 4 | beforeEach(function () { 5 | this.testElement.after(''); 6 | }); 7 | 8 | afterEach(function () { 9 | $('#test-template').remove(); 10 | }); 11 | 12 | it('Should add "alert" class to target element', function () { 13 | var vm = { 14 | value: { message: '' } 15 | }; 16 | 17 | ko.applyBindings(vm, this.testElement[0]); 18 | 19 | expect(this.testElement).toHaveClass('alert'); 20 | }); 21 | 22 | it('Should render alert with given message text', function() { 23 | var vm = { 24 | value: { message: 'test text' } 25 | }; 26 | 27 | ko.applyBindings(vm, this.testElement[0]); 28 | 29 | expect(this.testElement).toContainText('test text'); 30 | }); 31 | 32 | it('Should render alert with class according to given type', function () { 33 | var vm = { 34 | value: { message: 'text', type: 'info' } 35 | }; 36 | 37 | ko.applyBindings(vm, this.testElement[0]); 38 | 39 | expect(this.testElement).toHaveClass('alert-info'); 40 | }); 41 | 42 | it('Should change alert type class according to changes of type property', function () { 43 | var vm = { 44 | value: { message: 'text', type: ko.observable('info') } 45 | }; 46 | 47 | ko.applyBindings(vm, this.testElement[0]); 48 | 49 | vm.value.type('danger'); 50 | expect(this.testElement).toHaveClass('alert-danger'); 51 | vm.value.type('custom'); 52 | expect(this.testElement).toHaveClass('alert-custom'); 53 | }); 54 | 55 | it('Should render alert with given template id and data', function () { 56 | var vm = { 57 | value: { template: 'test-template', data: { label: 'test text' } } 58 | }; 59 | 60 | ko.applyBindings(vm, this.testElement[0]); 61 | 62 | expect(this.testElement).toContainElement('.test-template'); 63 | expect(this.testElement.find('.test-template')).toContainText('test text'); 64 | }); 65 | 66 | it('Should change template values according to obsrvables changing', function () { 67 | var vm = { 68 | value: { template: 'test-template', data: { label: ko.observable('test text') } } 69 | }; 70 | 71 | ko.applyBindings(vm, this.testElement[0]); 72 | 73 | expect(this.testElement.find('.test-template')).toContainText('test text'); 74 | vm.value.data.label('new text'); 75 | expect(this.testElement.find('.test-template')).toContainText('new text'); 76 | }); 77 | 78 | it('Should change template if template option changes', function () { 79 | var vm = { 80 | value: { template: ko.observable('test-template'), data: { label: 'test text' } } 81 | }; 82 | 83 | this.testElement.after(''); 84 | 85 | ko.applyBindings(vm, this.testElement[0]); 86 | 87 | expect(this.testElement).toContainElement('.test-template'); 88 | vm.value.template('test-template-2'); 89 | expect(this.testElement).toContainElement('.test-template-2'); 90 | 91 | $('#test-template-2').remove(); 92 | }); 93 | 94 | it('Should creates alert for virtual elements', function () { 95 | var vm = { 96 | value: { message: 'test text' } 97 | }; 98 | 99 | // change test element to ko virtual elements for this spec 100 | this.testElement = this.testElement.removeAttr('data-bind').html(''); 101 | 102 | ko.applyBindings(vm, this.testElement[0]); 103 | 104 | expect(this.testElement).toContainElement('.alert'); 105 | expect(this.testElement).toContainText('test text'); 106 | }); 107 | }); -------------------------------------------------------------------------------- /src/bindings/popoverBinding.js: -------------------------------------------------------------------------------- 1 | var popoverDomDataTemplateKey = '__popoverTemplateKey__'; 2 | 3 | ko.bindingHandlers.popover = { 4 | 5 | init: function (element) { 6 | var $element = $(element); 7 | 8 | ko.utils.domNodeDisposal.addDisposeCallback(element, function () { 9 | if ($element.data('bs.popover')) { 10 | $element.popover('destroy'); 11 | } 12 | }); 13 | }, 14 | 15 | update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { 16 | var $element = $(element), 17 | value = ko.unwrap(valueAccessor()), 18 | options = (!value.options && !value.template ? ko.utils.unwrapProperties(value) : ko.utils.unwrapProperties(value.options)) || {}; 19 | 20 | if (value.template) { 21 | // use unwrap to track dependency from template, if it is observable 22 | ko.unwrap(value.template); 23 | 24 | var id = ko.utils.domData.get(element, popoverDomDataTemplateKey); 25 | 26 | var renderPopoverTemplate = function (eventObject) { 27 | 28 | if (eventObject && eventObject.type === 'inserted') { 29 | $element.off('shown.bs.popover'); 30 | } 31 | 32 | var template = ko.unwrap(value.template), 33 | internalModel; 34 | 35 | if(typeof template === 'string') { 36 | internalModel = { 37 | $$popoverTemplate: $.extend({ 38 | name: value.template, 39 | data: value.data 40 | }, value.templateOptions) 41 | }; 42 | 43 | } else { 44 | internalModel = { 45 | $$popoverTemplate: value.template 46 | }; 47 | } 48 | 49 | var childContext = bindingContext.createChildContext(bindingContext.$rawData, null, function(context) { 50 | ko.utils.extend(context, internalModel); 51 | }); 52 | 53 | ko.applyBindingsToDescendants(childContext, document.getElementById(id)); 54 | 55 | // bootstrap's popover calculates position before template renders, 56 | // so we recalculate position, using bootstrap methods 57 | var $popover = $('#' + id).parents('.popover'), 58 | popoverMethods = $element.data('bs.popover'), 59 | position = (popoverMethods.options && popoverMethods.options.placement) || 'right', 60 | offset = popoverMethods.getCalculatedOffset(position, popoverMethods.getPosition(), $popover.outerWidth(), $popover.outerHeight()); 61 | 62 | popoverMethods.applyPlacement(offset, position); 63 | }; 64 | 65 | // if there is no generated id - popover executes first time for this element 66 | if (!id) { 67 | id = ko.utils.uniqueId('ks-popover-'); 68 | ko.utils.domData.set(element, popoverDomDataTemplateKey, id); 69 | 70 | // place template rendering after popover is shown, because we don't have root element for template before that 71 | $element.on('shown.bs.popover inserted.bs.popover', renderPopoverTemplate); 72 | } 73 | 74 | options.content = '
'; 75 | options.html = true; 76 | } 77 | 78 | var popoverData = $element.data('bs.popover'); 79 | 80 | if (!popoverData) { 81 | $element.popover(options); 82 | 83 | $element.on('shown.bs.popover inserted.bs.popover', function () { 84 | (options.container ? $(options.container) : $element.parent()).one('click', '[data-dismiss="popover"]', function () { 85 | $element.popover('hide'); 86 | }); 87 | }); 88 | } else { 89 | ko.utils.extend(popoverData.options, options); 90 | if(popoverData.tip().is(":visible") && popoverData.options.content) { 91 | $element.popover('show'); 92 | } 93 | } 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /tests/bindings/radioBindingBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: radio', function () { 2 | this.prepareTestElement('
' 3 | + '' 4 | + '' 5 | + '' 6 | + '' 7 | + '
'); 8 | 9 | it('Should throw exception for non-observable value', function() { 10 | var el = this.testElement[0]; 11 | 12 | expect(function () { 13 | ko.applyBindings({ value: "A" }, el); 14 | }).toThrow(); 15 | }); 16 | 17 | it('Should not check any button, if init value is not specified', function() { 18 | var vm = { value: ko.observable() }; 19 | 20 | ko.applyBindings(vm, this.testElement[0]); 21 | 22 | expect(this.testElement.children()).not.toHaveClass('active'); 23 | expect(this.testElement.find('input')).not.toBeChecked(); 24 | }); 25 | 26 | it('Should check button corresponding to the init value', function () { 27 | var vm = { value: ko.observable('A') }; 28 | 29 | ko.applyBindings(vm, this.testElement[0]); 30 | 31 | expect(this.testElement.find('.active input:checked')).toHaveValue('A'); 32 | }); 33 | 34 | it('Should check button according to value changes', function () { 35 | var vm = { value: ko.observable('A') }; 36 | 37 | ko.applyBindings(vm, this.testElement[0]); 38 | 39 | expect(this.testElement.find('.active input:checked')).toHaveValue('A'); 40 | vm.value('B'); 41 | expect(this.testElement.find('.active input:checked')).toHaveValue('B'); 42 | vm.value('A'); 43 | expect(this.testElement.find('.active input:checked')).toHaveValue('A'); 44 | vm.value('X"'); 45 | expect(this.testElement.find('.active input:checked')).toHaveValue('X"'); 46 | vm.value(1); 47 | expect(this.testElement.find('.active input:checked')).toHaveValue('1'); 48 | }); 49 | 50 | it('Should change value according to clicked button', function () { 51 | var vm = { value: ko.observable() }; 52 | //need because of realization of binding 53 | jasmine.clock().install(); 54 | 55 | ko.applyBindings(vm, this.testElement[0]); 56 | 57 | this.testElement.children().eq(0).click(); 58 | jasmine.clock().tick(1); 59 | expect(vm.value()).toEqual('A'); 60 | 61 | this.testElement.children().eq(1).click(); 62 | jasmine.clock().tick(1); 63 | expect(vm.value()).toEqual('B'); 64 | 65 | jasmine.clock().uninstall(); 66 | }); 67 | 68 | it('Should uncheck all buttons, if no button has set value', function () { 69 | var vm = { value: ko.observable('A') }; 70 | 71 | ko.applyBindings(vm, this.testElement[0]); 72 | 73 | vm.value('Z'); 74 | 75 | expect(this.testElement.children()).not.toHaveClass('active'); 76 | expect(this.testElement.find('input')).not.toBeChecked(); 77 | }); 78 | 79 | it('Should change value according to clicked button for dynamically added radiobuttons', function () { 80 | var vm = { value: ko.observable() }; 81 | 82 | this.testElement.append(''); 83 | 84 | //need because of realization of binding 85 | jasmine.clock().install(); 86 | 87 | ko.applyBindings(vm, this.testElement[0]); 88 | 89 | this.testElement.children().last().click(); 90 | jasmine.clock().tick(1); 91 | expect(vm.value()).toEqual('C'); 92 | 93 | jasmine.clock().uninstall(); 94 | }); 95 | 96 | it('Should check dynamically added button according to value changes', function () { 97 | var vm = { value: ko.observable() }; 98 | 99 | this.testElement.append(''); 100 | 101 | ko.applyBindings(vm, this.testElement[0]); 102 | vm.value('C'); 103 | expect(this.testElement.find('.active input:checked')).toHaveValue('C'); 104 | }); 105 | }); -------------------------------------------------------------------------------- /tests/bindings/popoverBindingBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: popover', function () { 2 | this.prepareTestElement('
Test
'); 3 | 4 | afterEach(function() { 5 | $('.popover').remove(); 6 | }); 7 | 8 | it('Should add popover to element when value is popover options', function () { 9 | var vm = { 10 | value: { title: 'title', content: 'test' } 11 | }; 12 | 13 | ko.applyBindings(vm, this.testElement[0]); 14 | this.testElement.click(); 15 | 16 | expect($('.popover')).toExist(); 17 | expect($('.popover')).toContainText('test'); 18 | }); 19 | 20 | it('Should add popover to element when value contains options object', function () { 21 | var vm = { 22 | value: { 23 | options: { title: 'title', content: 'test' } 24 | } 25 | }; 26 | 27 | ko.applyBindings(vm, this.testElement[0]); 28 | this.testElement.click(); 29 | 30 | expect($('.popover')).toExist(); 31 | expect($('.popover')).toContainText('test'); 32 | }); 33 | 34 | 35 | it('Should update popover according to changes of observables', function () { 36 | var vm = { 37 | value: { title: ko.observable('test') } 38 | }; 39 | 40 | ko.applyBindings(vm, this.testElement[0]); 41 | vm.value.title('new text'); 42 | this.testElement.click(); 43 | 44 | expect($('.popover')).toContainText('new text'); 45 | }); 46 | 47 | it('Should render template with passed template id and data', function(done) { 48 | var vm = { 49 | value: { 50 | options: { title: 'test', content: 'test' }, 51 | template: 'test-template', 52 | data: { testText: 'test data' } 53 | } 54 | }; 55 | 56 | this.testElement.after(''); 57 | 58 | ko.applyBindings(vm, this.testElement[0]); 59 | 60 | // content renders only after popover shown fully 61 | this.testElement.on('shown.bs.popover', function () { 62 | expect($('.popover')).toContainElement('#test'); 63 | expect($('.popover')).toContainText('test data'); 64 | 65 | done(); 66 | 67 | $('#test-template').remove(); 68 | }); 69 | 70 | this.testElement.click(); 71 | }); 72 | 73 | it('Should close popover when template contains button with "data-dismiss" attribute and it was clicked', function (done) { 74 | var vm = { 75 | value: { 76 | options: { title: 'test', content: 'test' }, 77 | template: 'test-template', 78 | data: { } 79 | } 80 | }; 81 | 82 | this.testElement.after(''); 83 | 84 | ko.applyBindings(vm, this.testElement[0]); 85 | 86 | var spyEvent = spyOnEvent(this.testElement, 'hide.bs.popover'); 87 | 88 | // content renders only after popover shown fully 89 | this.testElement.on('shown.bs.popover', function () { 90 | expect($('.popover')).toExist(); 91 | $('.popover [data-dismiss="popover"]').click(); 92 | expect(spyEvent).toHaveBeenTriggered(); 93 | 94 | done(); 95 | 96 | $('#test-template').remove(); 97 | }); 98 | 99 | this.testElement.click(); 100 | }); 101 | 102 | it('Should render template with passed template id and data via object', function(done) { 103 | var vm = { 104 | value: { 105 | options: { title: 'test', content: 'test' }, 106 | template: { name: 'test-template', data: { testText: 'test data' } } 107 | } 108 | }; 109 | 110 | this.testElement.after(''); 111 | 112 | ko.applyBindings(vm, this.testElement[0]); 113 | 114 | // content renders only after popover shown fully 115 | this.testElement.on('shown.bs.popover', function () { 116 | expect($('.popover')).toContainElement('#test'); 117 | expect($('.popover')).toContainText('test data'); 118 | 119 | done(); 120 | 121 | $('#test-template').remove(); 122 | }); 123 | 124 | this.testElement.click(); 125 | }); 126 | }); -------------------------------------------------------------------------------- /examples-src/htmlParts/bindingExamples/pagerExample.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |

Binding to Bootstrap pager component.

6 | 7 |

Examples

8 |
9 |
10 |
11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 |
20 | 21 |
22 | 23 | 24 |
25 |
26 | 27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 | 35 |

Html markup

36 |
 37 | <div data-bind="pager: { currentPage: page, totalCount: total, isAligned: aligned, text: text }"></div>
 38 | 
39 | 40 |

View model

41 |
 42 | function PagerExampleViewModel() {
 43 |     this.page = ko.observable(1);
 44 |     this.total = ko.observable(100);
 45 |     this.aligned = ko.observable(false);
 46 |     this.text = {
 47 |         back: ko.observable('←'),
 48 |         forward: ko.observable('→')
 49 |     };
 50 | }
 51 | 
52 | 53 |

Options

54 |

All of the options can be observables.

55 | 56 |
57 |
58 | data-bind="pager: { currentPage: page, totalCount: count, pageSize: size, isAligned: aligned, text: textObj }" 59 |
60 | 61 |
    62 |
  • 63 |

    currentPage

    64 |

    Type: number, should be observable (default: 1)

    65 |

    Contains selected page.

    66 |
  • 67 | 68 |
  • 69 |

    totalCount

    70 |

    Type: number, can be observable

    71 |

    Contains total count of items, for which paging is used. Used to determine pages amount.

    72 |
  • 73 | 74 |
  • 75 |

    pageSize

    76 |

    Type: number, can be observable (default: 10)

    77 |

    Specifies amount of items per page. Used to determine pages amount.

    78 |
  • 79 | 80 |
  • 81 |

    isAligned

    82 |

    Type: boolean, can be observable (default: false)

    83 |

    If true, adds next and previous css classes to buttons.

    84 |
  • 85 | 86 |
  • 87 |

    text

    88 |

    Type: object, can be observable

    89 |

    Contains text for back and forward buttons.

    90 |

    Default values of text object:

    91 | 92 |
      93 |
    • 94 |

      back

      95 |

      Type: string, can be observable (default: '←')

      96 |

      Contains text for "Back" button

      97 |
    • 98 | 99 |
    • 100 |

      forward

      101 |

      Type: string, can be observable (default: '→')

      102 |

      Contains text for "Forward" button

      103 |
    • 104 |
    105 |
  • 106 |
107 |
108 |
109 | -------------------------------------------------------------------------------- /examples-src/htmlParts/bindingExamples/alertExample.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |

Binding to Bootstrap alert widget. This binding can be used with virtual elements. Supports custom templates with observables

6 | 7 |

Examples

8 |
9 |
10 |
11 | 12 |
13 | 14 | 15 | 16 | 17 | 21 |
22 |
23 | 24 |

Html markup

25 |
26 | <div data-bind="alert: { message: message, type: type }"></div>
27 | 
28 | <div data-bind="alert: { message: 'Using custom alert template', template: 'alertExampleTemplate', type: 'danger' }"></div>
29 | 
30 | <!-- ko alert: { message: 'Using virtual root element', type: 'success' } -->
31 | <!-- /ko -->
32 | 
33 | <script type="text/html" id="alertExampleTemplate">
34 |     <p data-bind="text: message"></p>
35 |     <button class="btn btn-primary" data-dismiss="alert">Close</button>
36 | </script>
37 | 38 |

View model

39 |
40 | function AlertExampleViewModel() {
41 |     this.type = ko.observable('info');
42 |     
43 |     this.message = ko.observable('Alert message');
44 | }
45 | 46 |

Options

47 |

All of the options can be observables.

48 | 49 |
50 |
51 | data-bind="alert: { message: message, type: type, template: template, data: data, templateOptions: templateOptions }" 52 |
53 | 54 |
    55 |
  • 56 |

    message

    57 |

    Type: string, can be observable

    58 |

    Text of alert message. Doesn't used if data property is specified.

    59 |
  • 60 |
  • 61 |

    type

    62 |

    Type: string, can be observable (default: 'info')

    63 |

    64 | Type of alert message. Possible values are 'info', 'warning', 'danger', 'success'. 65 | To specify your own type you should define css-styles for this type. 66 | For exmaple, for type 'my-custom-type', you shoud provide css class 'alert-my-custom-type'. 67 |

    68 |
  • 69 |
  • 70 |

    template

    71 |

    Type: string, can be observable (default: 'alertTemplate')

    72 |

    73 | Name of template for alert content. Default template: 74 |

    75 |
    76 | <button class="close" data-dismiss="alert" aria-hidden="true">×</button>
    77 | <p data-bind="text: message"></p>
    78 | 
    79 |
  • 80 |
  • 81 |

    data

    82 |

    Type: object, can be observable

    83 |

    Data for template. If this option is specified, message option will be ignored.

    84 |

    For default template, you should provide message property (or, if it will not provided via data, message option of binding will be used).

    85 |
  • 86 |
  • 87 |

    templateOptions

    88 |

    Type: object

    89 |

    Contains options for template.

    90 |

    91 | They are: templateEngine, afterRender, beforeRender and afterAdd. 92 | Please, see Knockout documentation for details. 93 |

    94 |
  • 95 |
96 |
97 |
98 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | v1.4.1 2 | -Fixed issue with appearing popover when binding is dynamically changed when popover is hidden 3 | -Fixed issue with numeric 0 value for radio buttons 4 | -Fixed issue with updating popver placement when template is used 5 | 6 | v1.4.0 7 | -Added support of stacked progress-bars for progress binding 8 | -Added support of custom event names for modal binding (so if you override events of modal, you still would be able to use modal binding) 9 | -Added support of usage of all template options with popover binding 10 | -Added possibility to use object with template options for popover binding's template property 11 | -Fixed issue with clickable disabled checkboxes in checkbox binding 12 | -Fixed not updating popover when its data was dynamically changed while popover is opened 13 | -Fixed issue with not disabled controls in pagination binding for cases, when pagesCount is zero 14 | -Fixed issue with numeric values in radio binding 15 | -Removed Bootstrap dependency from nuspec, since there is a lot of different packages with Bootstrap 16 | -Removed jamjs repository link since it is closed now 17 | 18 | v1.3.2 19 | -Fixed changing of selected radio button, when it is disabled 20 | -Fixed changing of selected checkbox, when it is disabled 21 | 22 | v1.3.1 23 | -Fixed not working close button in popover, when bootstrap 3.3.5 used 24 | -Removed unwrapProperties implementation as ko.toJS do the same. ko.utils.unwrapProperties still available as alias for ko.toJS 25 | -Prioritised loading reference to knockout and jQuery from global context. It fixes cases, when knockout and jQuery are loaded from cdn and knockstarp is loaded via 'require' 26 | 27 | v1.3.0 28 | -Added pagination binding 29 | -Added pager binding 30 | -Added class binding 31 | -Updated bower dependencies with official knockout package 32 | -Updated to use anonymous module when exporting via AMD 33 | -Fixed alert and checkbox binding to work in ie8 34 | -Fixed carousel binding to add "active" class to first indicator after loading 35 | -Fixed radioBinding using value with quote 36 | -Fixed the previous css class is not removed from alert element 37 | -Popover now subscribes to the bootstrap 3.3.5 event inserted.bs.popover if available 38 | -Changed grunt-templates-concat to grunt-html-convert in build process, because grunt-templates-concat was removed from npm repo for unknown reasons. 39 | 40 | v1.2.1 41 | -Fixed issue with submitting forms with toggle buttons 42 | -Fixed attemting to set visible property for computed value for modal binding 43 | -Updated docs to work via https 44 | 45 | v1.2.0 46 | -Changed dependency name from jQuery to official jquery 47 | -Added dialogCss property to modal binding 48 | 49 | v1.1.0 50 | -Added possibility to use popover binding with other bindings on same element and with other bindings on child elements 51 | -Added support of close button via 'data-dismiss="popover"' for popover binding 52 | -Made visible property optional for modal binding 53 | -Added possibility of using observable header, body and footer properties to modal binding 54 | -Added possibility to obtain added templates from stringTemplateEngine 55 | -Added possibility to provide different options for progress-bar in progress binding 56 | -Added possibility to change default css and attributes of root element for modal, progress and carousel bindings 57 | 58 | v1.0.0 59 | -Fixed popover positioning, when template is used 60 | -Fixed not changing template, when popover is opened 61 | -Added possibility to add new string-based templates dynamically 62 | -Added possibility to change default template engine for default templates of Knockstrap bindings 63 | -Rewrote stringTemplateEngine implementation 64 | -Added tests for stringTemplateEngine 65 | -Added possibility to change default template engine for header, footer and body in modal binding 66 | -Added possibility to pass modal options via data-attributes 67 | -Reorganized and updated documentation 68 | -Added travis-ci support 69 | 70 | v0.4.0 71 | -Added unit tests via jasmine for all bindings and utility functions 72 | -Added throwing exception, when binding is used with incompatible values 73 | -Added unchecking of buttons for radio binding, when no buttons have passed value 74 | -Fixed checkboxes don't have 'checked' property set to true, when they are clicked for checkbox binding 75 | -Made 'footer' property optional for modal binding 76 | -Fixed carousel binding bugs 77 | -Removed close callback from alert binding ('close.bs.alert' event from bootstrap can be used instead) 78 | -Updated alert binding default template: now it require only 'message' property, 'type' property doesn't depend on template data 79 | -Updated unwrapPoperties function: now if you pass not objects, it returns them without transformation into objects. 80 | -Added nuget package manager support 81 | 82 | v0.3.0 83 | -Added checkbox binding 84 | -Added carousel binding 85 | -Changed bindings documentation 86 | -Updated uniqueId function: now it will generate separate sequences for different prefixes 87 | -Updated radio binding: now it will support dynamically added buttons 88 | -Added short notation for popover binding 89 | -Added bower and jam package managers support 90 | 91 | v0.2.0 92 | -Added radio binding 93 | -Added toggle binding 94 | -Dividing examples into separate files 95 | -Added sources for examples 96 | -Small fixes 97 | 98 | v0.1.0 99 | -First version -------------------------------------------------------------------------------- /examples-src/htmlParts/stringTemplateEngineExamples.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |

Knockstrap provides implementation of Knockout-compatible stringTemplateEngine, which can use strings as html templates.

6 |

7 | For developers, constructor of engine available via ko.stringTemplateEngine. 8 | Also it contains instance property, so it isn't necessary to create your own. 9 | It can be used everywhere, where Knockout's nativeTemplateEngine is acceptable. 10 |

11 |

Template engine contains storage of templates, which is shared between all instances.

12 | 13 |

API

14 |

stringTemplateEngine contains:

15 | 16 |
17 |
18 | addTemplate(name, template) 19 |
20 |
21 |
Adds new template with given name to engine storage
22 |
23 | 24 |
    25 |
  • 26 |

    name

    27 |

    Type: string

    28 |

    29 | Name of new template. If template already exists - it will be overwritten. 30 |

    31 |
  • 32 |
  • 33 |

    template

    34 |

    Type: string

    35 |

    36 | Body of template. May contain any html. 37 |

    38 |
  • 39 |
40 | 41 |
42 | removeTemplate(name) 43 |
44 |
45 |
Removes template with given name from engine storage
46 |
47 | 48 |
    49 |
  • 50 |

    name

    51 |

    Type: string

    52 |

    53 | Name of template for deleting. If template doesn't exist - nothing happens. 54 |

    55 |
  • 56 |
57 | 58 |
59 | getTemplate(name) 60 |
61 |
62 |
Returns template with given name from engine storage or returns undefined, if there is no template
63 |
64 | 65 |
    66 |
  • 67 |

    name

    68 |

    Type: string

    69 |

    70 | Name of template. 71 |

    72 |
  • 73 |
74 | 75 |
76 | isTemplateExist(name) 77 |
78 |
79 |
Return true, if template with given name was saved in engine storage. Otherwise - false.
80 |
81 | 82 |
    83 |
  • 84 |

    name

    85 |

    Type: string

    86 |

    87 | Name of template. 88 |

    89 |
  • 90 |
91 | 92 |
93 | instance 94 |
95 |
96 |
Instance of stringTemplateEngine.
97 |
98 |
99 | 100 |

Examples

101 |
102 |
103 |

String template engine:

104 |

105 |

Native template engine:

106 |

107 | 108 | 111 |
112 |
113 | 114 |

Html markup

115 |
116 | <p>String template engine:</p>
117 | <p data-bind="template: { name: 'demo', templateEngine: ko.stringTemplateEngine.instance }"></p>
118 | <p>Native template engine:</p>
119 | <p data-bind="template: { name: 'demo' }"></p>
120 |             
121 | <script id="demo" type="text/html">
122 |     <span>It's a <strong>native template engine</strong>!</span>
123 | </script>
124 | 
125 | 126 |

View model

127 |
128 | var ste = ko.stringTemplateEngine.instance;
129 | 
130 | ste.addTemplate('demo', '<span>It\'s a <strong>string template engine</strong>!</span>');
131 | 
132 | 133 |
134 | -------------------------------------------------------------------------------- /tests/bindings/paginationBindingBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: pagination', function () { 2 | this.prepareTestElement('
'); 3 | 4 | it('Should add pagination widget', function () { 5 | var vm = { 6 | value: { currentPage: ko.observable(1), totalCount: ko.observable(20) } 7 | }; 8 | 9 | ko.applyBindings(vm, this.testElement[0]); 10 | 11 | expect(this.testElement).not.toBeEmpty(); 12 | }); 13 | 14 | it('Should add pagination widget with first page selected', function () { 15 | var vm = { 16 | value: { currentPage: ko.observable(), totalCount: ko.observable(20) } 17 | }; 18 | 19 | ko.applyBindings(vm, this.testElement[0]); 20 | 21 | expect(vm.value.currentPage()).toEqual(1); 22 | expect(this.testElement.find('.active')).toHaveText(1); 23 | }); 24 | 25 | it('Should add pagination widget with second page selected', function () { 26 | var vm = { 27 | value: { currentPage: ko.observable(2), totalCount: ko.observable(20) } 28 | }; 29 | 30 | ko.applyBindings(vm, this.testElement[0]); 31 | 32 | expect(vm.value.currentPage()).toEqual(2); 33 | expect(this.testElement.find('.active')).toHaveText(2); 34 | }); 35 | 36 | it('Should change selected page after currenPage changes', function () { 37 | var vm = { 38 | value: { currentPage: ko.observable(1), totalCount: ko.observable(20) } 39 | }; 40 | 41 | ko.applyBindings(vm, this.testElement[0]); 42 | 43 | expect(this.testElement.find('.active')).toHaveText(1); 44 | vm.value.currentPage(2); 45 | expect(this.testElement.find('.active')).toHaveText(2); 46 | }); 47 | 48 | it('Should change currentPage property after click on page link', function () { 49 | var vm = { 50 | value: { currentPage: ko.observable(1), totalCount: ko.observable(20) } 51 | }; 52 | 53 | ko.applyBindings(vm, this.testElement[0]); 54 | 55 | expect(vm.value.currentPage()).toEqual(1); 56 | this.testElement.find('.active + li > a').click(); 57 | expect(vm.value.currentPage()).toEqual(2); 58 | }); 59 | 60 | it('Should remove boundary links after property changes', function () { 61 | var vm = { 62 | value: { currentPage: ko.observable(1), totalCount: ko.observable(20), boundary: ko.observable(true) } 63 | }; 64 | 65 | ko.applyBindings(vm, this.testElement[0]); 66 | 67 | expect(this.testElement).toContainElement('.ks-boundary-first'); 68 | expect(this.testElement).toContainElement('.ks-boundary-last'); 69 | 70 | vm.value.boundary(false); 71 | 72 | expect(this.testElement).not.toContainElement('.ks-boundary-first'); 73 | expect(this.testElement).not.toContainElement('.ks-boundary-last'); 74 | }); 75 | 76 | it('Should remove directions links after property changes', function () { 77 | var vm = { 78 | value: { currentPage: ko.observable(1), totalCount: ko.observable(20), directions: ko.observable(true) } 79 | }; 80 | 81 | ko.applyBindings(vm, this.testElement[0]); 82 | 83 | expect(this.testElement).toContainElement('.ks-direction-back'); 84 | expect(this.testElement).toContainElement('.ks-direction-forward'); 85 | 86 | vm.value.directions(false); 87 | 88 | expect(this.testElement).not.toContainElement('.ks-direction-back'); 89 | expect(this.testElement).not.toContainElement('.ks-direction-forward'); 90 | }); 91 | 92 | it('Should go to first page after first button click', function () { 93 | var vm = { 94 | value: { currentPage: ko.observable(3), totalCount: ko.observable(200) } 95 | }; 96 | 97 | ko.applyBindings(vm, this.testElement[0]); 98 | 99 | this.testElement.find('.ks-boundary-first > a').click(); 100 | 101 | expect(vm.value.currentPage()).toEqual(1); 102 | }); 103 | 104 | it('Should go to last page after last button click', function () { 105 | var vm = { 106 | value: { currentPage: ko.observable(1), totalCount: ko.observable(30) } 107 | }; 108 | 109 | ko.applyBindings(vm, this.testElement[0]); 110 | 111 | this.testElement.find('.ks-boundary-last > a').click(); 112 | 113 | expect(vm.value.currentPage()).toEqual(3); 114 | }); 115 | 116 | it('Should go to next page after next button click', function () { 117 | var vm = { 118 | value: { currentPage: ko.observable(1), totalCount: ko.observable(200) } 119 | }; 120 | 121 | ko.applyBindings(vm, this.testElement[0]); 122 | 123 | this.testElement.find('.ks-direction-forward > a').click(); 124 | 125 | expect(vm.value.currentPage()).toEqual(2); 126 | }); 127 | 128 | it('Should go to previous page after back button click', function () { 129 | var vm = { 130 | value: { currentPage: ko.observable(3), totalCount: ko.observable(200) } 131 | }; 132 | 133 | ko.applyBindings(vm, this.testElement[0]); 134 | 135 | this.testElement.find('.ks-direction-back > a').click(); 136 | 137 | expect(vm.value.currentPage()).toEqual(2); 138 | }); 139 | }); -------------------------------------------------------------------------------- /src/bindings/paginationBinding.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers.pagination = { 2 | defaults: { 3 | maxPages: 5, 4 | 5 | pageSize: 10, 6 | 7 | directions: true, 8 | 9 | boundary: true, 10 | 11 | text: { 12 | first: 'First', 13 | last: 'Last', 14 | back: '«', 15 | forward: '»' 16 | } 17 | }, 18 | 19 | init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { 20 | var value = $.extend(true, {}, ko.bindingHandlers.pagination.defaults, valueAccessor()); 21 | 22 | if (!ko.isObservable(value.currentPage)) { 23 | throw new TypeError('currentPage should be observable'); 24 | } 25 | 26 | if (!$.isNumeric(value.currentPage())) { 27 | value.currentPage(1); 28 | } 29 | 30 | var model = new Pagination(value); 31 | 32 | ko.renderTemplate('pagination', bindingContext.createChildContext(model), { templateEngine: ko.stringTemplateEngine.instance }, element); 33 | 34 | return { controlsDescendantBindings: true }; 35 | } 36 | }; 37 | 38 | ko.bindingHandlers.pager = { 39 | defaults: { 40 | pageSize: 10, 41 | 42 | text: { 43 | back: '←', 44 | forward: '→' 45 | } 46 | }, 47 | 48 | init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { 49 | var value = $.extend(true, {}, ko.bindingHandlers.pager.defaults, valueAccessor()); 50 | 51 | if (!ko.isObservable(value.currentPage)) { 52 | throw new TypeError('currentPage should be observable'); 53 | } 54 | 55 | if (!$.isNumeric(value.currentPage())) { 56 | value.currentPage(1); 57 | } 58 | 59 | var model = new Pager(value); 60 | 61 | ko.renderTemplate('pager', bindingContext.createChildContext(model), { templateEngine: ko.stringTemplateEngine.instance }, element); 62 | 63 | return { controlsDescendantBindings: true }; 64 | } 65 | }; 66 | 67 | function Pager(data) { 68 | var self = this; 69 | 70 | self.isAligned = data.isAligned; 71 | 72 | self.currentPage = data.currentPage; 73 | 74 | self.totalCount = data.totalCount; 75 | 76 | self.pageSize = data.pageSize; 77 | 78 | self.text = data.text; 79 | 80 | self.pagesCount = ko.computed(function () { 81 | var total = ko.unwrap(self.totalCount), 82 | pageSize = ko.unwrap(self.pageSize); 83 | 84 | return Math.max(Math.ceil(total / pageSize), 1); 85 | }); 86 | 87 | self.isBackDisabled = ko.computed(function () { 88 | return self.currentPage() === 1; 89 | }); 90 | 91 | self.isForwardDisabled = ko.computed(function () { 92 | return self.currentPage() === self.pagesCount(); 93 | }); 94 | 95 | self.goBack = function() { 96 | if (self.isBackDisabled()) { 97 | return; 98 | } 99 | 100 | var current = self.currentPage(); 101 | self.currentPage(current - 1); 102 | }; 103 | 104 | self.goForward = function() { 105 | if (self.isForwardDisabled()) { 106 | return; 107 | } 108 | 109 | var current = self.currentPage(); 110 | self.currentPage(current + 1); 111 | }; 112 | } 113 | 114 | function Pagination(data) { 115 | var self = this; 116 | 117 | Pager.call(self, data); 118 | 119 | var getStartPage = function () { 120 | var maxPages = +ko.unwrap(self.maxPages); 121 | 122 | return ((Math.ceil(self.currentPage() / maxPages) - 1) * maxPages) + 1; 123 | }; 124 | 125 | var getLastPage = function (startPage) { 126 | var maxPages = +ko.unwrap(self.maxPages); 127 | 128 | return Math.min(startPage + maxPages - 1, self.pagesCount()); 129 | }; 130 | 131 | self.maxPages = data.maxPages; 132 | 133 | self.boundary = data.boundary; 134 | 135 | self.directions = data.directions; 136 | 137 | self.text = data.text; 138 | 139 | self.pages = ko.computed(function () { 140 | var pages = []; 141 | 142 | var startPage = getStartPage(), 143 | endPage = getLastPage(startPage); 144 | 145 | for (var pageNumber = startPage; pageNumber <= endPage; pageNumber++) { 146 | pages.push(new Page(pageNumber, pageNumber, pageNumber === self.currentPage())); 147 | } 148 | 149 | if (startPage > 1) { 150 | pages.unshift(new Page(startPage - 1, '...')); 151 | } 152 | 153 | if (endPage < self.pagesCount()) { 154 | pages.push(new Page(endPage + 1, '...')); 155 | } 156 | 157 | return pages; 158 | }); 159 | 160 | self.selectPage = function (page) { 161 | self.currentPage(page.number); 162 | }; 163 | 164 | self.goFirst = function() { 165 | if (self.isBackDisabled()) { 166 | return; 167 | } 168 | 169 | self.currentPage(1); 170 | }; 171 | 172 | self.goLast = function () { 173 | if (self.isForwardDisabled()) { 174 | return; 175 | } 176 | 177 | self.currentPage(self.pagesCount()); 178 | }; 179 | } 180 | 181 | // page model 182 | function Page(number, text, isActive) { 183 | this.number = number; 184 | 185 | this.text = text || number; 186 | 187 | this.isActive = !!isActive; 188 | } 189 | -------------------------------------------------------------------------------- /tests/bindings/progressBindingBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: progress', function() { 2 | this.prepareTestElement('
'); 3 | 4 | it('Should add "progress" class to target element', function() { 5 | ko.applyBindings({ value: 1 }, this.testElement[0]); 6 | 7 | expect(this.testElement).toHaveClass('progress'); 8 | }); 9 | 10 | it('Should add width in percents and aria-valuenow attr according to value to progress-bar', function () { 11 | 12 | ko.applyBindings({ value: 20 }, this.testElement[0]); 13 | 14 | // need, because if element is visible, jQuery calculate its width in pixels 15 | // see http://stackoverflow.com/a/19873744 for details 16 | this.testElement.hide(); 17 | 18 | expect(this.testElement.find('.progress-bar')).toHaveCss({ width: '20%' }); 19 | expect(this.testElement.find('.progress-bar')).toHaveAttr('aria-valuenow', '20'); 20 | }); 21 | 22 | it('Should change width and aria-valuenow attr according to value changes', function () { 23 | var vm = { 24 | value: ko.observable(20) 25 | }; 26 | 27 | ko.applyBindings(vm, this.testElement[0]); 28 | 29 | this.testElement.hide(); 30 | vm.value(30); 31 | 32 | expect(this.testElement.find('.progress-bar')).toHaveCss({ width: '30%' }); 33 | expect(this.testElement.find('.progress-bar')).toHaveAttr('aria-valuenow', '30'); 34 | }); 35 | 36 | it('Should throw exception for not Number value or for Object without Number property "value"', function () { 37 | 38 | var el = this.testElement[0]; 39 | 40 | expect(function() { 41 | ko.applyBindings({ value: "stringVal" }, el); 42 | }).toThrow(); 43 | }); 44 | 45 | it('Should add "active" class, when animated property is set to true', function () { 46 | var vm = { 47 | value: { 48 | value: ko.observable(20), 49 | animated: ko.observable(false) 50 | } 51 | }; 52 | 53 | ko.applyBindings(vm, this.testElement[0]); 54 | 55 | expect(this.testElement.find('.progress-bar')).not.toHaveClass('active'); 56 | vm.value.animated(true); 57 | expect(this.testElement.find('.progress-bar')).toHaveClass('active'); 58 | }); 59 | 60 | it('Should add "progress-bar-striped" class, when striped property is set to true', function () { 61 | var vm = { 62 | value: { 63 | value: ko.observable(20), 64 | striped: ko.observable(false) 65 | } 66 | }; 67 | 68 | ko.applyBindings(vm, this.testElement[0]); 69 | 70 | expect(this.testElement.find('.progress-bar')).not.toHaveClass('progress-bar-striped'); 71 | vm.value.striped(true); 72 | expect(this.testElement.find('.progress-bar')).toHaveClass('progress-bar-striped'); 73 | }); 74 | 75 | it('Should add "sr-only" class to span, when textHidden property is set to true', function () { 76 | var vm = { 77 | value: { 78 | value: ko.observable(20), 79 | textHidden: ko.observable(false) 80 | } 81 | }; 82 | 83 | ko.applyBindings(vm, this.testElement[0]); 84 | 85 | expect(this.testElement.find('.progress-bar > span')).not.toHaveClass('sr-only'); 86 | vm.value.textHidden(true); 87 | expect(this.testElement.find('.progress-bar > span')).toHaveClass('sr-only'); 88 | }); 89 | 90 | it('Should update text in sapn according to changes at model', function () { 91 | var vm = { 92 | value: { 93 | value: ko.observable(20), 94 | text: ko.observable('Test') 95 | } 96 | }; 97 | 98 | ko.applyBindings(vm, this.testElement[0]); 99 | 100 | expect(this.testElement.find('.progress-bar span:last')).toContainText('Test'); 101 | vm.value.text('Updated'); 102 | expect(this.testElement.find('.progress-bar span:last')).toContainText('Updated'); 103 | }); 104 | 105 | it('Should render progress bar with class according to given type', function () { 106 | var vm = { 107 | value: { 108 | value: ko.observable(20), 109 | type: ko.observable('info') 110 | } 111 | }; 112 | 113 | ko.applyBindings(vm, this.testElement[0]); 114 | 115 | expect(this.testElement.find('.progress-bar')).toHaveClass('progress-bar-info'); 116 | }); 117 | 118 | it('Should change progress bar type class according to changes of type property', function () { 119 | var vm = { 120 | value: { 121 | value: ko.observable(20), 122 | type: ko.observable('info') 123 | } 124 | }; 125 | 126 | ko.applyBindings(vm, this.testElement[0]); 127 | 128 | vm.value.type('danger'); 129 | expect(this.testElement.find('.progress-bar')).toHaveClass('progress-bar-danger'); 130 | vm.value.type('custom'); 131 | expect(this.testElement.find('.progress-bar')).toHaveClass('progress-bar-custom'); 132 | }); 133 | 134 | it('Should add multiple stacked progress bars if array passed', function() { 135 | var vm = { 136 | value: [{ 137 | value: ko.observable(20), 138 | type: ko.observable('info') 139 | }, { 140 | value: ko.observable(40), 141 | type: ko.observable('danger') 142 | }] 143 | }; 144 | 145 | ko.applyBindings(vm, this.testElement[0]); 146 | 147 | expect(this.testElement.find('.progress-bar')).toHaveLength(2); 148 | }); 149 | }); -------------------------------------------------------------------------------- /examples-src/htmlParts/bindingExamples/popoverExample.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |

Binding to Bootstrap popover. Supports custom templates with observables for popover content

6 | 7 |

Examples

8 |
9 |
10 | 14 | 15 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 |

Html markup

31 |
 32 | <button class="btn btn-primary" data-bind="popover: { 
 33 |         options: { title: 'Popover with template', placement: 'top' }, 
 34 |         template: popoverTemplate, 
 35 |         data: { text: 'First template' } 
 36 |     }">Click</button>
 37 | 
 38 | <button class="btn" data-bind="click: switchTemplates">Switch templates</button>
 39 | 
 40 | <button class="btn btn-primary" data-bind="popover: { 
 41 |         options: { title: 'Focus', content: 'Focus popover', trigger: 'focus', placement: 'bottom' } 
 42 |     }">Focus</button>
 43 | 
 44 | <button class="btn btn-primary" data-bind="popover: { 
 45 |         options: { title: 'Hover', content: 'Hover popover', trigger: 'hover' } 
 46 |     }">Hover</button>
 47 |         
 48 | <script type="text/html" id="firstPopoverTemplate">
 49 |     <button class="close pull-right" type="button" data-dismiss="popover">×</button>
 50 |     <p data-bind="text: text"></p>
 51 | </script>
 52 | 
 53 | <script type="text/html" id="secondPopoverTemplate">
 54 |     <p>Second template</p>
 55 | </script>
56 | 57 |

View model

58 |
 59 | function PopoverExampleViewModel() {
 60 |     var self = this;
 61 |     
 62 |     self.popoverTemplate = ko.observable('firstPopoverTemplate');
 63 |     self.switchTemplates = function() {
 64 |         self.popoverTemplate() === 'firstPopoverTemplate' 
 65 |                                     ? self.popoverTemplate('secondPopoverTemplate') 
 66 |                                     : self.popoverTemplate('firstPopoverTemplate');
 67 |     };
 68 | }
69 | 70 |

Options

71 |

All of the options can be observables (besides trigger, see details), also option's object can be observable too

72 | 73 |
74 |
75 | data-bind="popover: { options: options, template: templateName, data: templateData, templateOptions: templateOptions }" 76 |
77 | 78 |
    79 |
  • 80 |

    options

    81 |

    Type: object, can be observable

    82 |

    83 | Bootstrap options for popover. If any option is not specified, uses default value. Please, see Bootstrap documentation 84 |

    85 |
  • 86 |
  • 87 |

    templateName

    88 |

    Type: string, can be observable

    89 |

    Name of template for popover content. If template is not specified, uses content property from options object

    90 |
  • 91 |
  • 92 |

    templateData

    93 |

    Type: object, can be observable

    94 |

    Data for template

    95 |
  • 96 |
  • 97 |

    templateOptions

    98 |

    Type: object

    99 |

    Contains options for template.

    100 |

    101 | They are: templateEngine, afterRender, beforeRender and afterAdd. 102 | Please, see Knockout documentation for details. 103 |

    104 |
  • 105 |
106 |
107 | 108 |

Short notation

109 |

If you don't need use template, you can use short notation of binding, which uses only option object.

110 |
111 |
112 | data-bind="popover: popoverOptions" 113 |
114 | 115 |
    116 |
  • 117 |

    popoverOptions

    118 |

    Type: object, can be observable

    119 |

    120 | Bootstrap options for popover. If any option is not specified, uses default value. Please, see Bootstrap documentation 121 |

    122 |
  • 123 |
124 |
125 |
126 | -------------------------------------------------------------------------------- /examples-src/js/examples.js: -------------------------------------------------------------------------------- 1 | function TooltipExampleViewModel() { 2 | this.title = ko.observable('Observable title'); 3 | this.placement = ko.observable('left'); 4 | } 5 | 6 | function ModalExampleViewModel() { 7 | 8 | var self = this; 9 | 10 | var firstTemplateData = { 11 | text: 'First template', 12 | label: ko.observable('Observable label') 13 | }; 14 | 15 | var secondTemplateData = { 16 | text: 'Second template', 17 | simpleLabel: 'Simple text label' 18 | }; 19 | 20 | self.modalVisible = ko.observable(false); 21 | 22 | self.show = function() { 23 | self.modalVisible(true); 24 | }; 25 | 26 | self.headerLabel = ko.observable('Some header text'); 27 | self.bodyTemplate = ko.observable('firstModalTemplate'); 28 | self.bodyData = ko.computed(function() { 29 | return self.bodyTemplate() === 'firstModalTemplate' ? firstTemplateData : secondTemplateData; 30 | }); 31 | 32 | self.okText = ko.observable(); 33 | 34 | self.switchTemplates = function() { 35 | self.bodyTemplate() === 'firstModalTemplate' ? self.bodyTemplate('secondModalTemplate') : self.bodyTemplate('firstModalTemplate'); 36 | }; 37 | 38 | self.modalSize = ko.observable('modal-lg'); 39 | } 40 | 41 | function PopoverExampleViewModel() { 42 | var self = this; 43 | 44 | self.popoverTemplate = ko.observable('firstPopoverTemplate'); 45 | self.switchTemplates = function() { 46 | self.popoverTemplate() === 'firstPopoverTemplate' ? self.popoverTemplate('secondPopoverTemplate') : self.popoverTemplate('firstPopoverTemplate'); 47 | }; 48 | } 49 | 50 | function AlertExampleViewModel() { 51 | var self = this; 52 | 53 | self.type = ko.observable('info'); 54 | 55 | self.message = ko.observable('Alert message'); 56 | } 57 | 58 | function ButtonsExampleViewModel() { 59 | var self = this; 60 | 61 | self.isToggled = ko.observable(false); 62 | 63 | self.radioValue = ko.observable(); 64 | 65 | self.checkboxArray = ko.observableArray(); 66 | 67 | self.checkboxValueA = ko.observable(true); 68 | 69 | self.checkboxValueB = ko.observable(false); 70 | } 71 | 72 | function CarouselExampleViewModel() { 73 | var self = this; 74 | 75 | self.itemsFirst = ko.observableArray([ 76 | { 77 | src: 'holder.js/900x200/text:First image', 78 | alt: 'First image', 79 | content: 'First caption' 80 | }, { 81 | src: 'holder.js/900x200/text:Second image', 82 | alt: 'Second image', 83 | content: 'Second caption' 84 | }, { 85 | src: 'holder.js/900x200/text:Third image', 86 | alt: 'Third image', 87 | content: 'Third caption' 88 | } 89 | ]); 90 | 91 | self.itemsSecond = ko.observableArray([ 92 | { 93 | src: 'holder.js/900x270/text:First image', 94 | alt: 'First image', 95 | primary: 'First caption', 96 | secondary: 'First subcaption' 97 | }, { 98 | src: 'holder.js/900x270/text:Second image', 99 | alt: 'Second image', 100 | primary: 'Second caption', 101 | secondary: 'Second subcaption' 102 | }, { 103 | src: 'holder.js/900x270/text:Third image', 104 | alt: 'Third image', 105 | primary: 'Third caption', 106 | secondary: 'Third subcaption' 107 | } 108 | ]); 109 | } 110 | 111 | function ProgressExampleViewModel() { 112 | 113 | this.value = ko.observable(50); 114 | 115 | this.animated = ko.observable(); 116 | 117 | this.striped = ko.observable(); 118 | 119 | this.type = ko.observable('info'); 120 | 121 | this.text = ko.observable('Complete'); 122 | 123 | this.textHidden = ko.observable(true); 124 | 125 | // for number as modal 126 | this.progress = ko.observable(20); 127 | } 128 | 129 | function StackedProgressExampleViewModel() { 130 | this.firstBar = { 131 | value: ko.observable(50), 132 | animated: ko.observable(), 133 | striped: ko.observable(), 134 | type: ko.observable('warning'), 135 | text: ko.observable('Complete'), 136 | textHidden: ko.observable(true) 137 | }; 138 | 139 | this.secondBar = ko.observable(20); 140 | 141 | this.progressBars = ko.observableArray([ 142 | this.firstBar, 143 | this.secondBar 144 | ]); 145 | } 146 | 147 | function PaginationExampleViewModel() { 148 | this.page = ko.observable(1); 149 | 150 | this.total = ko.observable(100); 151 | 152 | this.maxPages = ko.observable(5); 153 | 154 | this.directions = ko.observable(true); 155 | 156 | this.boundary = ko.observable(true); 157 | 158 | this.text = { 159 | first: ko.observable('First'), 160 | last: ko.observable('Last'), 161 | back: ko.observable('«'), 162 | forward: ko.observable('»') 163 | }; 164 | } 165 | 166 | function PagerExampleViewModel() { 167 | this.page = ko.observable(1); 168 | 169 | this.total = ko.observable(100); 170 | 171 | this.aligned = ko.observable(false); 172 | 173 | this.text = { 174 | back: ko.observable('←'), 175 | forward: ko.observable('→') 176 | }; 177 | } 178 | 179 | function ClassExampleViewModel() { 180 | this.className = ko.observable('bg-primary'); 181 | } 182 | 183 | function ExamplesViewModel() { 184 | var self = this; 185 | 186 | self.tooltipExample = new TooltipExampleViewModel(); 187 | 188 | self.progressExample = new ProgressExampleViewModel(); 189 | 190 | self.stackedProgressExample = new StackedProgressExampleViewModel(); 191 | 192 | self.modalExample = new ModalExampleViewModel(); 193 | 194 | self.popoverExample = new PopoverExampleViewModel(); 195 | 196 | self.alertExample = new AlertExampleViewModel(); 197 | 198 | self.buttonsExample = new ButtonsExampleViewModel(); 199 | 200 | self.carouselExample = new CarouselExampleViewModel(); 201 | 202 | self.paginationExample = new PaginationExampleViewModel(); 203 | 204 | self.pagerExample = new PagerExampleViewModel(); 205 | 206 | self.classExample = new ClassExampleViewModel(); 207 | 208 | var ste = ko.stringTemplateEngine.instance; 209 | 210 | ste.addTemplate('demo', 'It\'s a string template engine!'); 211 | } 212 | 213 | if (!ko.bindingHandlers.modal) { 214 | alert('You need build Knockstrap.js at first'); 215 | } 216 | 217 | var vm = new ExamplesViewModel(); 218 | ko.applyBindings(vm); 219 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = function (grunt) { 4 | 5 | var buildPath = grunt.option('buildPath') || './build', 6 | tempPath = grunt.option('tempPath') || './temp', 7 | examplesPath = grunt.option('examplesPath') || './examples'; 8 | 9 | grunt.initConfig({ 10 | buildPath: buildPath, 11 | tempPath: tempPath, 12 | examplesPath: examplesPath, 13 | fileHeader: '/*! <%= pkg.name %> <%= pkg.version %> <%= pkg.homepage %> | (c) 2013-2020 <%= pkg.author %> | http://www.opensource.org/licenses/mit-license */\n', 14 | 15 | pkg: grunt.file.readJSON('package.json'), 16 | 17 | jshint: { 18 | options: { 19 | curly: true, 20 | camelcase: true, 21 | eqeqeq: true, 22 | immed: true, 23 | latedef: true, 24 | strict: true, 25 | undef: true, 26 | unused: true, 27 | browser: true, 28 | sub: true, 29 | globals: { 30 | jQuery: false, 31 | ko: false, 32 | require: false, 33 | exports: false, 34 | define: false 35 | }, 36 | }, 37 | sources: { 38 | src: ['<%= buildPath %>/knockstrap.js'] 39 | } 40 | }, 41 | 42 | concat: { 43 | bindings: { 44 | src: ['src/bindings/*.js'], 45 | dest: '<%= tempPath %>/bindings.js' 46 | }, 47 | utils: { 48 | src: ['src/utils/*.js'], 49 | dest: '<%= tempPath %>/utils.js' 50 | } 51 | }, 52 | 53 | htmlConvert: { 54 | options: { 55 | quoteChar: '\'', 56 | rename: function (name) { 57 | return path.basename(name, '.html'); 58 | } 59 | }, 60 | templates: { 61 | src: ['src/templates/**/*.html'], 62 | dest: '<%= tempPath %>/compiledTemplates.js' 63 | }, 64 | }, 65 | 66 | copy: { 67 | templates: { 68 | expand: true, 69 | src: ['src/templates/templatesWrapper.js', 'src/main.js'], 70 | dest: '<%= tempPath %>/', 71 | flatten: true 72 | }, 73 | 74 | examples: { 75 | files: [ 76 | { expand: true, cwd: 'examples-src', src: ['css/*', 'js/*'], dest: '<%= examplesPath %>/' }, 77 | { expand: true, flatten: true, src: ['build/*'], dest: '<%= examplesPath %>/js/' } 78 | ] 79 | } 80 | }, 81 | 82 | clean: { 83 | build: ['<%= buildPath %>'], 84 | temp: ['<%= tempPath %>'], 85 | examples: ['<%= examplesPath %>'] 86 | }, 87 | 88 | uglify: { 89 | options: { 90 | banner: '<%= fileHeader %>' 91 | }, 92 | release: { 93 | files: { 94 | '<%= buildPath %>/knockstrap.min.js': ['<%= buildPath %>/knockstrap.js'] 95 | } 96 | } 97 | }, 98 | 99 | preprocess: { 100 | templates: { 101 | src: '<%= tempPath %>/templatesWrapper.js', 102 | dest: '<%= tempPath %>/templates.js' 103 | }, 104 | 105 | main: { 106 | options: { 107 | context: { 108 | header: '<%= fileHeader %>' 109 | } 110 | }, 111 | 112 | src: '<%= tempPath %>/main.js', 113 | dest: '<%= buildPath %>/knockstrap.js' 114 | }, 115 | 116 | examples: { 117 | options: { 118 | context: { 119 | knockstrap: 'js/knockstrap.js' 120 | } 121 | }, 122 | files: { 123 | '<%= examplesPath %>/index.html': 'examples-src/index.html', 124 | } 125 | }, 126 | 127 | examplesRelease: { 128 | options: { 129 | context: { 130 | knockstrap: 'js/knockstrap.min.js' 131 | } 132 | }, 133 | files: { 134 | '<%= examplesPath %>/index.html': 'examples-src/index.html', 135 | } 136 | } 137 | }, 138 | 139 | nugetpack: { 140 | release: { 141 | src: 'knockstrap.nuspec', 142 | dest: 'build/', 143 | 144 | options: { 145 | version: '<%= pkg.version %>' 146 | } 147 | } 148 | }, 149 | 150 | jasmine: { 151 | test: { 152 | src: 'build/knockstrap.js', 153 | options: { 154 | specs: ['tests/utilsBehaviors.js', 'tests/stringTemplateEngineBehaviors.js', 'tests/bindings/*.js'], 155 | vendor: [ 156 | 'http://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js', 157 | 'http://netdna.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js', 158 | 'http://cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js' 159 | ], 160 | helpers: [ 161 | 'tests/jasmineExtensions.js', 162 | 'http://cdn.rawgit.com/velesin/jasmine-jquery/2.0.3/lib/jasmine-jquery.js' 163 | ], 164 | styles: 'http://netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css' 165 | } 166 | } 167 | } 168 | }); 169 | 170 | grunt.loadNpmTasks('grunt-contrib-clean'); 171 | grunt.loadNpmTasks('grunt-contrib-concat'); 172 | grunt.loadNpmTasks('grunt-contrib-copy'); 173 | grunt.loadNpmTasks('grunt-contrib-jshint'); 174 | grunt.loadNpmTasks('grunt-contrib-uglify'); 175 | grunt.loadNpmTasks('grunt-contrib-jasmine'); 176 | grunt.loadNpmTasks('grunt-html-convert'); 177 | grunt.loadNpmTasks('grunt-preprocess'); 178 | grunt.loadNpmTasks('grunt-nuget'); 179 | 180 | grunt.registerTask('default', ['clean:build', 'htmlConvert', 'copy:templates', 'concat', 'preprocess:templates', 'preprocess:main', 'clean:temp', 'jshint']); 181 | grunt.registerTask('release', ['default', 'jasmine', 'uglify']); 182 | 183 | grunt.registerTask('examples', ['default', 'clean:examples', 'preprocess:examples', 'copy:examples']); 184 | grunt.registerTask('examples-release', ['release', 'clean:examples', 'preprocess:examplesRelease', 'copy:examples']); 185 | 186 | grunt.registerTask('nuget', ['release', 'nugetpack']); 187 | grunt.registerTask('travis', ['default', 'jasmine']); 188 | } 189 | -------------------------------------------------------------------------------- /tests/bindings/modalBindingBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: modal', function () { 2 | this.prepareTestElement('
'); 3 | 4 | beforeEach(function() { 5 | this.testElement.after(''); 6 | }); 7 | 8 | afterEach(function() { 9 | $('#test-template').remove(); 10 | }); 11 | 12 | it('Should create visible modal for true init value for "visible" option', function (done) { 13 | var vm = { 14 | value: { 15 | visible: ko.observable(true), 16 | header: { data: { label: 'test' } }, 17 | body: { name: 'test-template' }, 18 | footer: { data: { action: function () { } } } 19 | } 20 | }; 21 | 22 | ko.applyBindings(vm, this.testElement[0]); 23 | 24 | var el = this.testElement; 25 | 26 | // modal doesn't appears immediately, so check visibility after shown event 27 | this.testElement.on('shown.bs.modal', function() { 28 | expect(el).toBeVisible(); 29 | 30 | // clean up, for some reasons, backdrop doesn't disappear, if modal was closed immediately after open (possible bootstrap bug) 31 | el.modal('hide'); 32 | $('.modal-backdrop').remove(); 33 | $('body').removeClass('modal-open'); 34 | done(); 35 | }); 36 | }); 37 | 38 | it('Should throw exception, if there is no "header" property in options', function() { 39 | var vm = { 40 | value: { 41 | body: { name: 'test-template' }, 42 | footer: { data: { action: function () { } } } 43 | } 44 | }; 45 | 46 | var el = this.testElement[0]; 47 | 48 | expect(function() { 49 | ko.applyBindings(vm, el); 50 | }).toThrow(); 51 | }); 52 | 53 | it('Should throw exception, if there is no "body" property in options', function () { 54 | var vm = { 55 | value: { 56 | header: { data: { label: 'test' } }, 57 | footer: { data: { action: function () { } } } 58 | } 59 | }; 60 | 61 | var el = this.testElement[0]; 62 | 63 | expect(function () { 64 | ko.applyBindings(vm, el); 65 | }).toThrow(); 66 | }); 67 | 68 | it('Should not render footer, if there is no "footer" property in options', function () { 69 | var vm = { 70 | value: { 71 | header: { data: { label: 'test' } }, 72 | body: { name: 'test-template' }, 73 | } 74 | }; 75 | 76 | ko.applyBindings(vm, this.testElement[0]); 77 | 78 | expect(this.testElement).not.toContainElement('.modal-footer'); 79 | }); 80 | 81 | it('Should render default header with passed text', function () { 82 | var vm = { 83 | value: { 84 | header: { data: { label: 'test-header' } }, 85 | body: { name: 'test-template' }, 86 | footer: { data: { action: function () { } } } 87 | } 88 | }; 89 | 90 | ko.applyBindings(vm, this.testElement[0]); 91 | 92 | expect(this.testElement).toContainText('test-header'); 93 | }); 94 | 95 | it('Should render body with passed template', function () { 96 | var vm = { 97 | value: { 98 | header: { data: { label: 'test' } }, 99 | body: { name: 'test-template' }, 100 | footer: { data: { action: function () { } } } 101 | } 102 | }; 103 | 104 | ko.applyBindings(vm, this.testElement[0]); 105 | 106 | expect(this.testElement).toContainElement('#test'); 107 | }); 108 | 109 | it('Should render default footer with passed text', function () { 110 | var vm = { 111 | value: { 112 | header: { data: { label: 'test' } }, 113 | body: { name: 'test-template' }, 114 | footer: { data: { action: function () { }, primaryLabel: 'primary', closeLabel: 'close' } } 115 | } 116 | }; 117 | 118 | ko.applyBindings(vm, this.testElement[0]); 119 | 120 | expect(this.testElement.find('.modal-footer .btn-primary')).toContainText('primary'); 121 | expect(this.testElement.find('.modal-footer .btn-default')).toContainText('close'); 122 | }); 123 | 124 | it('Should call passed action, when primary button was clicked in default footer template', function () { 125 | var spy = jasmine.createSpy(), 126 | vm = { 127 | value: { 128 | header: { data: { label: 'test' } }, 129 | body: { name: 'test-template' }, 130 | footer: { data: { action: spy } } 131 | } 132 | }; 133 | 134 | ko.applyBindings(vm, this.testElement[0]); 135 | this.testElement.find('.modal-footer .btn-primary').click(); 136 | 137 | expect(spy).toHaveBeenCalled(); 138 | }); 139 | 140 | it('Should render default footer only with close button, if action was not passed', function() { 141 | var vm = { 142 | value: { 143 | header: { data: { label: 'test' } }, 144 | body: { name: 'test-template' }, 145 | footer: {} 146 | } 147 | }; 148 | 149 | ko.applyBindings(vm, this.testElement[0]); 150 | 151 | expect(this.testElement).not.toContainElement('.btn-primary'); 152 | expect(this.testElement).toContainElement('.btn-default'); 153 | }); 154 | 155 | it('Should render footer with passed template', function () { 156 | var vm = { 157 | value: { 158 | header: { data: { label: 'test' } }, 159 | body: { name: 'test-template' }, 160 | footer: { name: 'test-template' } 161 | } 162 | }; 163 | 164 | ko.applyBindings(vm, this.testElement[0]); 165 | 166 | expect(this.testElement).toContainElement('.modal-footer #test'); 167 | }); 168 | 169 | it('Should render header with passed template', function() { 170 | var vm = { 171 | value: { 172 | header: { name: 'test-template' }, 173 | body: { name: 'test-template' }, 174 | footer: { data: { action: function () { }, primaryLabel: 'primary', closeLabel: 'close' } } 175 | } 176 | }; 177 | 178 | ko.applyBindings(vm, this.testElement[0]); 179 | 180 | expect(this.testElement).toContainElement('.modal-header #test'); 181 | }); 182 | 183 | it('Should render modal-dialog element with passed css classes', function () { 184 | var vm = { 185 | value: { 186 | dialogCss: 'test-class', 187 | header: { name: 'test-template' }, 188 | body: { name: 'test-template' }, 189 | } 190 | }; 191 | 192 | ko.applyBindings(vm, this.testElement[0]); 193 | 194 | expect(this.testElement).toContainElement('.modal-dialog.test-class'); 195 | }); 196 | }); -------------------------------------------------------------------------------- /examples-src/htmlParts/bindingExamples/paginationExample.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |

Binding to Bootstrap pagination component.

6 | 7 |

Examples

8 |
9 |
10 |
11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 |
20 | 21 |
22 | 23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 | 31 |
32 | 33 |
34 | 35 | 36 |
37 | 38 |
39 | 40 | 41 |
42 | 43 |
44 | 45 | 46 |
47 |
48 | 49 |
50 | 51 | 52 |
53 | 54 |
55 |
56 |
57 | 58 |

Html markup

59 |
 60 | <div data-bind="pagination: { currentPage: page, totalCount: total, maxPages: maxPages, directions: directions, boundary: boundary, text: text }"></div>
 61 | 
62 | 63 |

View model

64 |
 65 | function PaginationExampleViewModel() {
 66 |     this.page = ko.observable(1);
 67 |     this.total = ko.observable(100);
 68 |     this.maxPages = ko.observable(5);
 69 |     this.directions = ko.observable(true);
 70 |     this.boundary = ko.observable(true);
 71 |     this.text = {
 72 |         first: ko.observable('First'),
 73 |         last: ko.observable('Last'),
 74 |         back: ko.observable('«'),
 75 |         forward: ko.observable('»')
 76 |     };
 77 | }
 78 | 
79 | 80 |

Options

81 |

All of the options can be observables.

82 | 83 |
84 |
85 | data-bind="pagination: { currentPage: page, totalCount: count, pageSize: size, maxPages: maxPages, directions: directions, boundary: boundary, text: textObj }" 86 |
87 | 88 |
    89 |
  • 90 |

    currentPage

    91 |

    Type: number, should be observable (default: 1)

    92 |

    Contains selected page.

    93 |
  • 94 | 95 |
  • 96 |

    totalCount

    97 |

    Type: number, can be observable

    98 |

    Contains total count of items, for which paging is used. Used to determine pages amount.

    99 |
  • 100 | 101 |
  • 102 |

    pageSize

    103 |

    Type: number, can be observable (default: 10)

    104 |

    Specifies amount of items per page. Used to determine pages amount.

    105 |
  • 106 | 107 |
  • 108 |

    maxPages

    109 |

    Type: number, can be observable (default: 5)

    110 |

    Specifies maximum amount of pages, which will be displayed in pagaination control.

    111 |
  • 112 | 113 |
  • 114 |

    directions

    115 |

    Type: boolean, can be observable (default: true)

    116 |

    Specifies, should back and forward buttons be displayed.

    117 |
  • 118 | 119 |
  • 120 |

    boundary

    121 |

    Type: boolean, can be observable (default: true)

    122 |

    Specifies, should last and first buttons be displayed.

    123 |
  • 124 | 125 |
  • 126 |

    text

    127 |

    Type: object, can be observable

    128 |

    Contains text for first, last, back and forward buttons.

    129 |

    Default values of text object:

    130 | 131 |
      132 |
    • 133 |

      first

      134 |

      Type: string, can be observable (default: 'First')

      135 |

      Contains text for "First" button

      136 |
    • 137 | 138 |
    • 139 |

      last 140 |

      141 |

      Type: string, can be observable (default: 'Last') 142 |

      143 |

      Contains text for "Last" button

      144 | 145 |
    • 146 | 147 |
    • 148 |

      back 149 |

      150 |

      Type: string, can be observable (default: '«') 151 |

      152 |

      Contains text for "Back" button

      153 |
    • 154 | 155 |
    • 156 |

      forward

      157 |

      Type: string, can be observable (default: '»')

      158 |

      Contains text for "Forward" button

      159 |
    • 160 |
    161 |
  • 162 |
163 |
164 |
165 | -------------------------------------------------------------------------------- /tests/bindings/checkboxBindingBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: checkbox', function () { 2 | describe('Array case', function () { 3 | this.prepareTestElement('
' 4 | + '' 5 | + '' 6 | + '' 7 | + '
'); 8 | 9 | function getValuesArray($elementsArray) { 10 | return $elementsArray.toArray().map(function (e) { return e.value; }); 11 | } 12 | 13 | it('Should throw exception for non-array value', function () { 14 | var el = this.testElement[0]; 15 | 16 | expect(function () { 17 | ko.applyBindings({ value: "A" }, el); 18 | }).toThrow(); 19 | }); 20 | 21 | it('Should not check any button, if init array is empty', function () { 22 | var vm = { value: ko.observableArray() }; 23 | 24 | ko.applyBindings(vm, this.testElement[0]); 25 | 26 | expect(this.testElement.children()).not.toHaveClass('active'); 27 | expect(this.testElement.find('input')).not.toBeChecked(); 28 | }); 29 | 30 | it('Should check button corresponding to the values, which are in array at init', function () { 31 | var vm = { value: ko.observableArray(['A', 'B']) }; 32 | 33 | ko.applyBindings(vm, this.testElement[0]); 34 | 35 | expect(getValuesArray(this.testElement.find('.active input:checked'))).toEqual(['A', 'B']); 36 | expect(this.testElement.find('input:checked')).toHaveLength(2); 37 | }); 38 | 39 | it('Should check buttons according to adding/removing values from array', function () { 40 | var vm = { value: ko.observableArray() }; 41 | 42 | ko.applyBindings(vm, this.testElement[0]); 43 | 44 | vm.value.push('B'); 45 | expect(getValuesArray(this.testElement.find('.active input:checked'))).toContain('B'); 46 | 47 | vm.value.push('A'); 48 | expect(getValuesArray(this.testElement.find('.active input:checked'))).toContain('A'); 49 | 50 | vm.value.remove('B'); 51 | expect(getValuesArray(this.testElement.find('.active input:checked'))).not.toContain('B'); 52 | }); 53 | 54 | it('Should add/remove values to array according to clicked buttons', function () { 55 | var vm = { value: ko.observableArray() }; 56 | //need because of realization of binding 57 | jasmine.clock().install(); 58 | 59 | ko.applyBindings(vm, this.testElement[0]); 60 | 61 | this.testElement.children().eq(0).click(); 62 | jasmine.clock().tick(1); 63 | expect(vm.value()).toContain('A'); 64 | 65 | this.testElement.children().eq(1).click(); 66 | jasmine.clock().tick(1); 67 | expect(vm.value()).toContain('B'); 68 | 69 | this.testElement.children().eq(0).click(); 70 | jasmine.clock().tick(1); 71 | expect(vm.value()).not.toContain('A'); 72 | expect(vm.value()).toContain('B'); 73 | 74 | jasmine.clock().uninstall(); 75 | }); 76 | 77 | it('Should change array values according to clicked button for dynamically added checkboxes', function () { 78 | var vm = { value: ko.observableArray() }; 79 | 80 | //need because of realization of binding 81 | jasmine.clock().install(); 82 | 83 | ko.applyBindings(vm, this.testElement[0]); 84 | 85 | this.testElement.append(''); 86 | 87 | this.testElement.children().eq(3).click(); 88 | jasmine.clock().tick(1); 89 | expect(vm.value()).toContain('D'); 90 | 91 | jasmine.clock().uninstall(); 92 | }); 93 | 94 | it('Should check dynamically added button according to array changes', function () { 95 | var vm = { value: ko.observableArray() }; 96 | 97 | ko.applyBindings(vm, this.testElement[0]); 98 | 99 | this.testElement.append(''); 100 | 101 | vm.value.push('D'); 102 | expect(getValuesArray(this.testElement.find('.active input:checked'))).toContain('D'); 103 | }); 104 | }); 105 | 106 | describe('Boolean case', function () { 107 | this.prepareTestElement('
' 108 | + '' 109 | + '
'); 110 | 111 | it('Should throw exception for non-observable value', function () { 112 | var el = this.testElement[0]; 113 | 114 | expect(function () { 115 | ko.applyBindings({ value: true }, el); 116 | }).toThrow(); 117 | }); 118 | 119 | it('Should check button, for true init value', function () { 120 | var vm = { value: ko.observable(true) }; 121 | 122 | ko.applyBindings(vm, this.testElement[0]); 123 | 124 | expect(this.testElement.children()).toHaveClass('active'); 125 | expect(this.testElement.find('input')).toBeChecked(); 126 | }); 127 | 128 | it('Should check button according to value changes', function () { 129 | var vm = { value: ko.observable(true) }; 130 | 131 | ko.applyBindings(vm, this.testElement[0]); 132 | 133 | vm.value(false); 134 | // checking 'active' class together with checked state of input 135 | expect(this.testElement).not.toContainElement('.active input:checked'); 136 | vm.value(true); 137 | expect(this.testElement).toContainElement('.active input:checked'); 138 | }); 139 | 140 | it('Should change value according to button state', function () { 141 | var vm = { value: ko.observable() }; 142 | //need because of realization of binding 143 | jasmine.clock().install(); 144 | 145 | ko.applyBindings(vm, this.testElement[0]); 146 | 147 | this.testElement.children().eq(0).click(); 148 | jasmine.clock().tick(1); 149 | expect(vm.value()).toBeTruthy(); 150 | 151 | this.testElement.children().eq(0).click(); 152 | jasmine.clock().tick(1); 153 | expect(vm.value()).toBeFalsy(); 154 | 155 | jasmine.clock().uninstall(); 156 | }); 157 | 158 | it('Should uncheck button for any falthy value', function () { 159 | var vm = { value: ko.observable(true) }; 160 | 161 | ko.applyBindings(vm, this.testElement[0]); 162 | 163 | vm.value(0); 164 | expect(this.testElement).not.toContainElement('.active input:checked'); 165 | 166 | vm.value(true); 167 | vm.value(''); 168 | expect(this.testElement).not.toContainElement('.active input:checked'); 169 | 170 | vm.value(true); 171 | vm.value(null); 172 | expect(this.testElement).not.toContainElement('.active input:checked'); 173 | 174 | vm.value(true); 175 | vm.value(undefined); 176 | expect(this.testElement).not.toContainElement('.active input:checked'); 177 | }); 178 | }); 179 | }); -------------------------------------------------------------------------------- /tests/bindings/carouselBindingBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: carousel', function () { 2 | this.prepareTestElement('
'); 3 | 4 | beforeEach(function() { 5 | this.testElement.after(''); 6 | }); 7 | 8 | afterEach(function() { 9 | $('#test-template').remove(); 10 | }); 11 | 12 | var testData = [{ 13 | src: 'testsrc-one', 14 | alt: 'First image', 15 | content: 'First caption' 16 | }, { 17 | src: 'testsrc-two', 18 | alt: 'Second image', 19 | content: 'Second caption' 20 | }]; 21 | 22 | it('Should throw exception, if "content" is not passed', function() { 23 | var el = this.testElement[0]; 24 | 25 | expect(function() { 26 | ko.applyBindings({}, el); 27 | }).toThrow(); 28 | }); 29 | 30 | it('Should create carousel with passed id', function () { 31 | var vm = { 32 | value: { 33 | id: 'test-id', 34 | content: { data: testData } 35 | } 36 | }; 37 | 38 | ko.applyBindings(vm, this.testElement[0]); 39 | 40 | expect(this.testElement).toHaveId('test-id'); 41 | // contrlos target 42 | expect(this.testElement.find('.carousel-control')).toHaveAttr('href', '#test-id'); 43 | //indicators target 44 | expect(this.testElement.find('ol li')).toHaveData('target', '#test-id'); 45 | }); 46 | 47 | it('Should create carousel using id, specified via "id" attribute', function () { 48 | var vm = { 49 | value: { 50 | id: 'test-id-nonattr', 51 | content: { data: testData } 52 | } 53 | }; 54 | 55 | this.testElement.attr('id', 'test-id'); 56 | 57 | ko.applyBindings(vm, this.testElement[0]); 58 | 59 | expect(this.testElement).toHaveId('test-id'); 60 | expect(this.testElement.find('.carousel-control')).toHaveAttr('href', '#test-id'); 61 | expect(this.testElement.find('ol li')).toHaveData('target', '#test-id'); 62 | }); 63 | 64 | it('Should render carousel controls with passed template id and template data', function () { 65 | var vm = { 66 | value: { 67 | content: { data: testData }, 68 | controls: { name: 'test-template', data: { label: 'test text' } } 69 | } 70 | }; 71 | 72 | ko.applyBindings(vm, this.testElement[0]); 73 | 74 | expect(this.testElement).toContainElement('.test-template'); 75 | expect(this.testElement.find('.test-template')).toHaveText('test text'); 76 | }); 77 | 78 | it('Should use "dataConverter" and pass binding value to it for controls template, when "data" was not specified', function () { 79 | var spy = jasmine.createSpy().and.callFake(function() { 80 | return { 81 | label: 'test' 82 | }; 83 | }); 84 | 85 | var vm = { 86 | value: { 87 | content: { data: testData }, 88 | controls: { name: 'test-template', dataConverter: spy } 89 | } 90 | }; 91 | 92 | ko.applyBindings(vm, this.testElement[0]); 93 | 94 | expect(spy).toHaveBeenCalledWith(vm.value); 95 | }); 96 | 97 | it('Should not use "dataConverter" for controls template, if "data" was passed', function () { 98 | var spy = jasmine.createSpy(), 99 | vm = { 100 | value: { 101 | content: { data: testData }, 102 | controls: { name: 'test-template', data: { label: 'test text' }, dataConverter: spy } 103 | } 104 | }; 105 | 106 | ko.applyBindings(vm, this.testElement[0]); 107 | 108 | expect(spy).not.toHaveBeenCalled(); 109 | }); 110 | 111 | it('Should render carousel indicators with passed template id and template data', function () { 112 | var vm = { 113 | value: { 114 | content: { data: testData }, 115 | indicators: { name: 'test-template', data: { label: 'test text' } } 116 | } 117 | }; 118 | 119 | ko.applyBindings(vm, this.testElement[0]); 120 | 121 | expect(this.testElement).toContainElement('.test-template'); 122 | expect(this.testElement.find('.test-template')).toHaveText('test text'); 123 | }); 124 | 125 | it('Should use "dataConverter" and pass binding value to it for indicators template, when "data" was not specified', function () { 126 | var spy = jasmine.createSpy().and.callFake(function () { 127 | return { 128 | label: 'test' 129 | }; 130 | }); 131 | 132 | var vm = { 133 | value: { 134 | content: { data: testData }, 135 | indicators: { name: 'test-template', dataConverter: spy } 136 | } 137 | }; 138 | 139 | ko.applyBindings(vm, this.testElement[0]); 140 | 141 | expect(spy).toHaveBeenCalledWith(vm.value); 142 | }); 143 | 144 | it('Should not use "dataConverter" for indicators template, if "data" was passed', function () { 145 | var spy = jasmine.createSpy(), 146 | vm = { 147 | value: { 148 | content: { data: testData }, 149 | indicators: { name: 'test-template', data: { label: 'test text' }, dataConverter: spy } 150 | } 151 | }; 152 | 153 | ko.applyBindings(vm, this.testElement[0]); 154 | 155 | expect(spy).not.toHaveBeenCalled(); 156 | }); 157 | 158 | it('Should render carousel items with default item template', function () { 159 | var vm = { 160 | value: { 161 | content: { data: testData }, 162 | } 163 | }; 164 | 165 | ko.applyBindings(vm, this.testElement[0]); 166 | 167 | expect(this.testElement.find('.item')).toHaveLength(testData.length); 168 | 169 | expect(this.testElement.find('.item img').eq(0)).toHaveAttr('src', 'testsrc-one'); 170 | expect(this.testElement.find('.item img').eq(1)).toHaveAttr('src', 'testsrc-two'); 171 | 172 | expect(this.testElement.find('.item img').eq(0)).toHaveAttr('alt', 'First image'); 173 | expect(this.testElement.find('.item img').eq(1)).toHaveAttr('alt', 'Second image'); 174 | 175 | expect(this.testElement.find('.item')).toContainText('First caption'); 176 | expect(this.testElement.find('.item')).toContainText('Second caption'); 177 | }); 178 | 179 | it('Should render carousel items with passed item template', function () { 180 | var vm = { 181 | value: { 182 | content: { data: [{src: 'src', alt: 'alt', label: 'some text'}], name: 'test-template' }, 183 | } 184 | }; 185 | 186 | ko.applyBindings(vm, this.testElement[0]); 187 | 188 | expect(this.testElement.find('.item')).toHaveLength(1); 189 | 190 | expect(this.testElement.find('.item')).toContainText('some text'); 191 | }); 192 | 193 | it('Should render carousel items with using of "convertor" for passed items', function () { 194 | var spy = jasmine.createSpy().and.callFake(function(item) { 195 | return { src: 'src', alt: 'alt', content: item.label + ' converted' }; 196 | }); 197 | 198 | var vm = { 199 | value: { 200 | content: { data: [{ label: 'first label' }, { label: 'second label' }], converter: spy }, 201 | } 202 | }; 203 | 204 | ko.applyBindings(vm, this.testElement[0]); 205 | 206 | expect(this.testElement.find('.item')).toHaveLength(2); 207 | expect(this.testElement.find('.item')).toContainText('first label converted'); 208 | expect(this.testElement.find('.item')).toContainText('second label converted'); 209 | 210 | expect(spy.calls.count()).toEqual(2); 211 | }); 212 | }); -------------------------------------------------------------------------------- /examples-src/htmlParts/bindingExamples/buttonsExample.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 |

Toggle buttons

7 |

Binding to Bootstrap toggle button.

8 | 9 |

Examples

10 |
11 |
12 |
13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 |

Html markup

21 |
 22 | <button class="btn btn-info" data-bind="toggle: isToggled">Toggle button</button>
 23 | 
24 | 25 |

Viem model

26 |
 27 | function ButtonsExampleViewModel() {
 28 |     //...
 29 | 
 30 |     this.isToggled = ko.observable(false);
 31 | }
 32 | 
33 | 34 |

Options

35 |
36 |
37 | data-bind="toggle: value" 38 |
39 | 40 |
    41 |
  • 42 |

    value

    43 |

    Type: boolean, should be observable

    44 |

    45 | Observable value is set in true, when button is toggled, otherwise, value is set in false. This binding is two-way, 46 | if observable value changes, 'active' class will be set to element. 47 |

    48 |
  • 49 |
50 |
51 | 52 |

Radio buttons

53 |

Two-way binding to Bootstrap radio buttons. Default Knockout checked binding doesn't work with Bootstrap's buttons, so you can use this binding

54 | 55 |

Examples

56 |
57 |
58 |
59 | 63 | 67 | 71 |
72 |
73 | 74 | 75 |
76 |
77 |
78 | 79 |

Html markup

80 |
 81 | <div class="btn-group form-group" data-toggle="buttons" data-bind="radio: radioValue">
 82 |     <label class="btn btn-primary">
 83 |         <input type="radio" name="options" value="A" />
 84 |         A
 85 |     </label>
 86 |     <label class="btn btn-primary">
 87 |         <input type="radio" name="options" value="B" />
 88 |         B
 89 |     </label>
 90 |     <label class="btn btn-primary">
 91 |         <input type="radio" name="options" value="C" />
 92 |         C
 93 |     </label>
 94 | </div>
 95 | 
96 | 97 |

View model

98 |
 99 | function ButtonsExampleViewModel() {
100 |     //...
101 | 
102 |     this.radioValue = ko.observable();
103 | }
104 | 
105 | 106 |

Options

107 |
108 |
109 | data-bind="radio: value" 110 |
111 | 112 |
    113 |
  • 114 |

    value

    115 |

    Type: string, should be observable

    116 |

    117 | Observable value is set to value of chosen radiobutton. Initially, when no radio button is toggled, observable value is undefined. 118 | This binding is two-way. For correct work all radio buttons should have value attribute. 119 |

    120 |
  • 121 |
122 |
123 | 124 |

Checkbox buttons

125 |

Two-way binding to Bootstrap checkbox buttons. Default Knockout checked binding doesn't work with Bootstrap's buttons, so you can use this binding

126 | 127 |

Examples

128 |
129 |
130 |

131 |
132 | 136 | 140 | 144 |
145 |
146 | 147 | 148 |
149 | 150 |

151 |
152 | 156 | 160 |
161 |
162 | 163 |
A value:
164 |
B value:
165 |
166 |
167 |
168 | 169 |

Html markup

170 |
171 | <div class="btn-group form-group" data-toggle="buttons" data-bind="checkbox: checkboxArray">
172 |     <label class="btn btn-primary">
173 |         <input type="checkbox" value="A" />
174 |         A
175 |     </label>
176 |     <label class="btn btn-primary">
177 |         <input type="checkbox" value="B" />
178 |         B
179 |     </label>
180 |     <label class="btn btn-primary">
181 |         <input type="checkbox" value="C" />
182 |         C
183 |     </label>
184 | </div>
185 | 
186 | <div class="btn-group form-group" data-toggle="buttons">
187 |     <label class="btn btn-primary">
188 |         <input type="checkbox" data-bind="checkbox: checkboxValueA" />
189 |         A
190 |     </label>
191 |     <label class="btn btn-primary">
192 |         <input type="checkbox" data-bind="checkbox: checkboxValueB" />
193 |         B
194 |     </label>
195 | </div>
196 | 
197 | 198 |

View model

199 |
200 | function ButtonsExampleViewModel() {
201 |     //...
202 | 
203 |     this.checkboxArray = ko.observableArray();
204 |         
205 |     this.checkboxValueA = ko.observable(true);
206 |         
207 |     this.checkboxValueB = ko.observable(false);
208 | }
209 | 
210 | 211 |

Options

212 |
213 |
214 | data-bind="checkbox: value" 215 |
216 | 217 |
    218 |
  • 219 |

    value

    220 |

    Type: array or boolean, should be observable

    221 |

    222 | Array, contains selected values or boolean, which is set to true or false, depending of checkbox state. 223 | This binding is two-way, if array values changed, corresponding checkbox is set up, if value is deleted from array, otherwise, it is set down. 224 | For boolean case, if observable value changes, corrseponding checkbox is set up, if value is true, otherwise, it is set down. 225 |

    226 |
  • 227 |
228 |
229 |
-------------------------------------------------------------------------------- /examples-src/htmlParts/bindingExamples/carouselExample.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |

Binding to Bootstrap Carousrel widget

6 | 7 |

Examples

8 | 9 |

With default templates and options:

10 |
11 |
12 |
13 |
14 |
15 | 16 |

Html markup

17 |
<div data-bind="carousel: { content: { data: itemsFirst } }"></div>
18 | 19 |

View model

20 |
 21 | function CarouselExampleViewModel() {
 22 |     // ...
 23 | 
 24 |     self.itemsFirst = ko.observableArray([
 25 |         {
 26 |             src: 'holder.js/900x200/text:First image',
 27 |             alt: 'First image',
 28 |             content: 'First caption'
 29 |         }, {
 30 |             src: 'holder.js/900x200/text:Second image',
 31 |             alt: 'Second image',
 32 |             content: 'Second caption'
 33 |         }, {
 34 |             src: 'holder.js/900x200/text:Third image',
 35 |             alt: 'Third image',
 36 |             content: 'Third caption'
 37 |         }
 38 |     ]);
 39 | }
 40 | 
41 | 42 |

Carousel with custom item template:

43 |
44 |
45 |
46 | 47 | 51 |
52 |
53 | 54 |

Html markup

55 |
 56 | <div data-bind="carousel: { content: { name: 'carouselItemTemplate', data: itemsSecond } }"></div>
 57 |             
 58 | <script type="text/html" id="Script1">
 59 |     <h4 data-bind="text: primary"></h4>
 60 |     <p data-bind="text: secondary"></p>
 61 | </script>
 62 | 
63 | 64 |

View model

65 |
 66 | function CarouselExampleViewModel() {
 67 |     // ...
 68 | 
 69 |     self.itemsSecond = ko.observableArray([
 70 |         {
 71 |             src: 'holder.js/900x270/text:First image',
 72 |             alt: 'First image',
 73 |             primary: 'First caption',
 74 |             secondary: 'First subcaption'
 75 |         }, {
 76 |             src: 'holder.js/900x270/text:Second image',
 77 |             alt: 'Second image',
 78 |             primary: 'Second caption',
 79 |             secondary: 'Second subcaption'
 80 |         }, {
 81 |             src: 'holder.js/900x270/text:Third image',
 82 |             alt: 'Third image',
 83 |             primary: 'Third caption',
 84 |             secondary: 'Third subcaption'
 85 |         }
 86 |     ]);
 87 | }       
 88 | 
89 | 90 |

Options

91 | 92 |
93 |
94 | data-bind="carousel: { id: id, options: options, content: content, indicators: indicators, controls: controls }" 95 |
96 | 97 |
    98 |
  • 99 |

    id

    100 |

    Type: string, can be observable

    101 |

    Id of carousel. It will be ignored, if carousel has id attribute. If there is no id attribute and id option is not specified, id will be generated.

    102 |
  • 103 | 104 |
  • 105 |

    options

    106 |

    Type: object, can be observable

    107 |

    Bootstrap options for carousel. Please, see Bootstrap documentation.

    108 |
  • 109 | 110 |
  • 111 |

    content

    112 |

    Type: object, can be observable

    113 |

    114 | Template binding for single carousel item, uses options from Knockout template binding. 115 | Please, see Knockout documentation for details. 116 |

    117 |

    Default values of item template:

    118 | 119 |
      120 |
    • 121 |

      name

      122 |

      Type: string, can be observable (default: 'carouselContent')

      123 |

      Name of single item temlate for carousel. Default template:

      124 | 125 |
      <div data-bind="text: content"></div>
      126 |
    • 127 | 128 |
    • 129 |

      data

      130 |

      Type: array, can be observable

      131 |

      132 | Items for carousel. Each item will be passed to content template. Each item should contains src, altproperties. 133 | For default template item should contain content property. 134 |

      135 |
    • 136 | 137 |
    • 138 |

      converter

      139 |

      Type: function

      140 |

      Function, which applies for each item and returning data object for template. Default function just return item itself.

      141 |
    • 142 |
    143 |
  • 144 | 145 |
  • 146 |

    indicators

    147 |

    Type: object, can be observable

    148 |

    149 | Template binding for carousel indicators, uses options from Knockout template binding. 150 | Please, see Knockout documentation for details. 151 |

    152 |

    Default values of indicators object:

    153 | 154 |
      155 |
    • 156 |

      name

      157 |

      Type: string, can be observable (default: 'carouselIndicators')

      158 |

      Name of controls temlate for carousel. Default template:

      159 | 160 |
      161 | <ol class="carousel-indicators" data-bind="foreach: items">
      162 |     <li data-bind="attr: { 'data-target': $parent.id, 'data-slide-to': $index }"></li>
      163 | </ol>
      164 | 
      165 |
    • 166 | 167 |
    • 168 |

      data

      169 |

      Type: object, can be observable

      170 |

      Data for indicators template. For default template, it should contains id and items properties.

      171 |
    • 172 | 173 |
    • 174 |

      dataConverter

      175 |

      Type: function

      176 |

      Function, which creates data object for indicators template. Accepts binding value as parameter. Will be ignored, if data property is specified. Default function:

      177 | 178 |
      179 | function(value) {
      180 |     return {
      181 |         id: ko.computed(function() {
      182 |             return '#' + ko.unwrap(value.id);
      183 |         }),
      184 |                     
      185 |         items: value.content.data
      186 |     };
      187 | }
      188 | 
      189 |
    • 190 |
    191 |
  • 192 | 193 |
  • 194 |

    controls

    195 |

    Type: object, can be observable

    196 |

    197 | Template binding for carousel controls, uses options from Knockout template binding. 198 | Please, see Knockout documentation for details. 199 |

    200 |

    Default values of controls object:

    201 | 202 |
      203 |
    • 204 |

      name

      205 |

      Type: string, can be observable (default: 'carouselControls')

      206 |

      Name of controls temlate for carousel. Default template:

      207 | 208 |
      209 | <a class="left carousel-control" data-bind="attr: { href: id }" data-slide="prev">
      210 |     <span class="icon-prev"></span>
      211 | </a>
      212 | <a class="right carousel-control" data-bind="attr: { href: id }" data-slide="next">
      213 |     <span class="icon-next"></span>
      214 | </a>
      215 | 
      216 |
    • 217 | 218 |
    • 219 |

      data

      220 |

      Type: object, can be observable

      221 |

      Data for controls template. For default template, it should contains id property.

      222 |
    • 223 | 224 |
    • 225 |

      dataConverter

      226 |

      Type: function

      227 |

      Function, which creates data object for controls template. Accepts binding value as parameter. Will be ignored, if data property is specified. Default function:

      228 | 229 |
      230 | function(value) {
      231 |     return {
      232 |         id: ko.computed(function() {
      233 |             return '#' + ko.unwrap(value.id);
      234 |         })
      235 |     };
      236 | }
      237 | 
      238 |
    • 239 |
    240 |
  • 241 |
242 |
243 | 244 |

Default values

245 |

246 | Default values for carousel binding located in ko.bindingHandlers.carousel.defaults object. 247 | It contains default css for root element of carousel and default values for controls, indicators and item templtates. 248 | Can be changed before ko.applyBindigs() is called. 249 |

250 |
251 | defaults: {
252 |     css: 'carousel slide',
253 | 
254 |     controlsTemplate: {
255 |         name: 'carouselControls',
256 |         templateEngine: ko.stringTemplateEngine.instance,
257 |         dataConverter: function(value) {
258 |             return {
259 |                 id: ko.computed(function() {
260 |                     return '#' + ko.unwrap(value.id);
261 |                 })
262 |             };
263 |         }
264 |     },
265 |         
266 |     indicatorsTemplate: {
267 |         name: 'carouselIndicators',
268 |         templateEngine: ko.stringTemplateEngine.instance,
269 |         dataConverter: function(value) {
270 |             return {
271 |                 id: ko.computed(function() {
272 |                     return '#' + ko.unwrap(value.id);
273 |                 }),
274 |                     
275 |                 items: value.content.data
276 |             };
277 |         }
278 |     }, 
279 |         
280 |     itemTemplate: {
281 |         name: 'carouselContent',
282 |         templateEngine: ko.stringTemplateEngine.instance,
283 | 
284 |         converter: function (item) {
285 |             return item;
286 |         }
287 |     }
288 | }
289 | 
290 |
291 | -------------------------------------------------------------------------------- /examples-src/htmlParts/bindingExamples/progressExample.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |

Binding to Bootatrap progress bar

6 | 7 |

Examples

8 |
9 |
10 |
11 |
12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 | Striped 26 |
27 |
28 | 29 | Animated 30 |
31 |
32 | 33 | Text hidden 34 |
35 |
36 |
37 |
38 |
39 | 40 |
41 | 42 | Success 43 | 44 | Info 45 | 46 | Warning 47 | 48 | Danger 49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 | 57 |

Html markup

58 |
 59 | <div data-bind="progress: { value: value, type: type, text: text, textHidden: textHidden, animated: animated, striped: striped }"></div>
 60 | 
61 | 62 |

View model

63 |
 64 | function ProgressExampleViewModel() {
 65 |     this.value = ko.observable(50);
 66 | 
 67 |     this.animated = ko.observable();
 68 | 
 69 |     this.striped = ko.observable();
 70 | 
 71 |     this.type = ko.observable('info');
 72 | 
 73 |     this.text = ko.observable('Complete');
 74 | 
 75 |     this.textHidden = ko.observable(true);
 76 | }
 77 | 
78 | 79 |

Options

80 |

All of the options can be observables.

81 | 82 |
83 |
84 | data-bind="progress: { value: value, type: type, text: text, textHidden: hidden, animated: animated, striped: striped }" 85 |
86 | 87 |
    88 |
  • 89 |

    value

    90 |

    Type: number, can be observable

    91 |

    Progress value in percents. Values greater than 100, assumed equal to 100

    92 |
  • 93 | 94 |
  • 95 |

    type

    96 |

    Type: string, can be observable (default: none)

    97 |

    98 | Type of progress bar. Possible values are 'info', 'warning', 'danger', 'success'. 99 | To specify your own type you should define css-styles for it. 100 | For exmaple, for type 'my-custom-type', you shoud provide css class 'progress-bar-custom-type'. 101 |

    102 |
  • 103 | 104 |
  • 105 |

    text

    106 |

    Type: string, can be observable (default: empty string)

    107 |

    Text on progress bar after percents

    108 |
  • 109 | 110 |
  • 111 |

    textHidden

    112 |

    Type: boolean, can be observable (default: true)

    113 |

    Hides percents of progress and text on progress bar, if true, otherwise hides.

    114 |
  • 115 | 116 |
  • 117 |

    striped

    118 |

    Type: boolean, can be observable (default: false)

    119 |

    Adds stripes to progress bar, if true. Work only in Bootstrap >= 3.2.0

    120 |
  • 121 | 122 |
  • 123 |

    animated

    124 |

    Type: boolean, can be observable (default: false)

    125 |

    Animates stripes on progress bar, if true. Work only in Bootstrap >= 3.2.0

    126 |
  • 127 |
128 |
129 | 130 |

Short notation

131 |

Examples

132 |

If you only need use progress value, you can use short notation of binding, which uses only number as model.

133 |
134 |
135 |
136 |
137 | 138 | 139 |
140 |
141 | 142 |
143 |
144 |
145 | 146 |

Html markup

147 |
148 | <div data-bind="progress: progress"></div>
149 | 
150 | 151 |

View model

152 |
153 | function ProgressExampleViewModel() {
154 |     //...
155 | 
156 |     this.progress = ko.observable(20);
157 | }
158 | 
159 | 160 |

Options

161 |
162 |
163 | data-bind="progress: progressValue" 164 |
165 | 166 |
    167 |
  • 168 |

    progressValue

    169 |

    Type: number, can be observable

    170 |

    Progress value in percents. Values greater than 100, assumed equal to 100

    171 |
  • 172 |
173 |
174 | 175 |

Stacked progress bars

176 |

Examples

177 |

If you need use multiple bars into the same .progress to stack them, you can pass array of options objects to binding:

178 |
179 |
180 |
181 |
182 | 183 |
184 |
185 | 186 | 187 |
188 |
189 | 190 | 191 |
192 | 193 |
194 | 195 | 196 |
197 | 198 |
199 | 200 |
201 | 202 | Striped 203 |
204 |
205 | 206 | Animated 207 |
208 |
209 | 210 | Text hidden 211 |
212 |
213 |
214 |
215 |
216 | 217 |
218 | 219 | Success 220 | 221 | Info 222 | 223 | Warning 224 | 225 | Danger 226 |
227 |
228 |
229 | 230 | 231 |
232 |
233 | 234 |
235 |
236 | 237 |
238 |
239 | 240 | 241 |
242 |
243 | 244 |
245 |
246 |
247 | 248 |

Html markup

249 |
250 | <div data-bind="progress: progressBars"></div>
251 | 
252 | 253 |

View model

254 |
255 | function ProgressExampleViewModel() {
256 |     this.firstBar = {
257 |         value: ko.observable(50),
258 |         animated: ko.observable(),
259 |         striped: ko.observable(),
260 |         type: ko.observable('warning'),
261 |         text: ko.observable('Complete'),
262 |         textHidden: ko.observable(true)
263 |     };
264 | 
265 |     this.secondBar = ko.observable(20);
266 | 
267 |     this.progressBars = ko.observableArray([
268 |         this.firstBar,
269 |         this.secondBar
270 |     ]);
271 | }
272 | 
273 | 274 |

Options

275 |
276 |
277 | data-bind="progress: progressBarsArray" 278 |
279 | 280 |
    281 |
  • 282 |

    progressBarsArray

    283 |

    Type: array, can be observable

    284 |

    Array of data-objects for progress bar. Each element in array should contain options for progress bar in full or short notation

    285 |
  • 286 |
287 |
288 | 289 |

Default values

290 |

291 | Default values for progress binding located in ko.bindingHandlers.progress.defaults object. 292 | It contains default css for root element of progress-bar and default values for its options. 293 | Can be changed before ko.applyBindigs() is called. 294 |

295 |
296 | defaults: {
297 |     css: 'progress',
298 |     text: '',
299 |     textHidden: true,
300 |     striped: false,
301 |     type: '',
302 |     animated: false
303 | }
304 | 
305 |
306 | -------------------------------------------------------------------------------- /examples-src/htmlParts/bindingExamples/modalExample.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 |

Binding to Bootstrap modal widget.

7 |

Examples

8 | 9 |
10 |
11 | 12 | 13 |
14 | 18 | 22 | 26 |
27 | 28 |
35 |
36 | 43 | 47 |
48 |
49 | 50 |

Html markup

51 |
 52 | <button class="btn btn-primary" data-bind="click: show">Show modal</button>
 53 | 
 54 | <div class="btn-group form-group" data-toggle="buttons" data-bind="radio: modalSize">
 55 |     <label class="btn btn-default">
 56 |         <input type="radio" name="modalSizing" value="" />
 57 |         None
 58 |     </label>
 59 |     <label class="btn btn-default">
 60 |         <input type="radio" name="modalSizing" value="modal-lg" />
 61 |         Large
 62 |     </label>
 63 |     <label class="btn btn-default">
 64 |         <input type="radio" name="modalSizing" value="modal-sm" />
 65 |         Small
 66 |     </label>
 67 | </div>
 68 | 
 69 | <div data-bind="modal: {
 70 |     visible: modalVisible,
 71 |     dialogCss: modalSize,
 72 |     header: { data: { label: headerLabel } },
 73 |     body: { name: bodyTemplate, data: bodyData },
 74 |     footer: { data: { action: switchTemplates, closeLabel: 'Custom text', primaryLabel: 'Change body template' } }
 75 | }"></div>
 76 | 
 77 | <script type="text/html" id="firstModalTemplate">
 78 |     <p data-bind="text: text"></p>
 79 |     <div class="form-group">
 80 |         <label data-bind="text: label"></label>
 81 |         <input type="text" data-bind="value: label, valueUpdate: 'afterkeydown'" class="form-control" />
 82 |     </div>
 83 | </script>
 84 | 
 85 | <script type="text/html" id="secondModalTemplate">
 86 |     <p data-bind="text: text"></p>
 87 |     <p data-bind="text: simpleLabel"></p>
 88 | </script>
 89 | 
90 | 91 |

View model

92 |
 93 | function ModalExampleViewModel() {
 94 | 
 95 |     var self = this;
 96 | 
 97 |     var firstTemplateData = {
 98 |         text: 'First template',
 99 |         label: ko.observable('Observable label')
100 |     };
101 | 
102 |     var secondTemplateData = {
103 |         text: 'Second template',
104 |         simpleLabel: 'Simple text label'
105 |     };
106 | 
107 |     self.modalVisible = ko.observable(false);
108 | 
109 |     self.show = function() {
110 |         self.modalVisible(true);
111 |     };
112 | 
113 |     self.headerLabel = ko.observable('Some header text');
114 |     self.bodyTemplate = ko.observable('firstModalTemplate');
115 |     self.bodyData = ko.computed(function() {
116 |         return self.bodyTemplate() === 'firstModalTemplate' ? firstTemplateData : secondTemplateData;
117 |     });
118 | 
119 |     self.okText = ko.observable();
120 | 
121 |     self.switchTemplates = function() {
122 |         self.bodyTemplate() === 'firstModalTemplate' 
123 |                             ? self.bodyTemplate('secondModalTemplate') 
124 |                             : self.bodyTemplate('firstModalTemplate');
125 |     };
126 |         
127 |     self.modalSize = ko.observable('modal-lg');
128 | }
129 | 
130 | 131 |

Options

132 |

All of the options can be observables.

133 | 134 |
135 |
136 | 137 | data-bind="modal: { options: options, visible: visible, dialogCss: dialogCss, header: header, body: body, footer: footer, events: events }" 138 | 139 |
140 | 141 |
    142 |
  • 143 |

    options

    144 |

    Type: object, can be observable

    145 |

    146 | Bootstrap options for modal. If any option is not specified, it uses default values (except show, it default value is false in comparing with Bootstrap). 147 | Also, can be specified via data-attributes. 148 | Please, see Bootstrap documentation for details. 149 |

    150 |
  • 151 | 152 |
  • 153 |

    visible

    154 |

    Type: boolean, can be observable (default: false)

    155 |

    Shows modal, if true, otherwise hides modal. By default is false.

    156 |

    Can be omitted in order to use Bootstrap's attributes for showing or closing modal.

    157 |
  • 158 | 159 |
  • 160 |

    dialogCss

    161 |

    Type: string, can be observable (default: undefined)

    162 |

    Used to add classes to modal-dialog element. For example, it can accept modal-lg value for large modal or modal-sm for small.

    163 |

    Please, see Bootstrap documentation for more details.

    164 |
  • 165 | 166 |
  • 167 |

    header

    168 |

    Type: object, can be observable

    169 |

    170 | Template binding for modal header, uses options from Knockout template binding. 171 | Please, see Knockout documentation for details. 172 |

    173 |

    Default values of header object:

    174 | 175 |
      176 |
    • 177 |

      name

      178 |

      Type: string, can be observable (default: 'modalHeader')

      179 |

      Name of template for modal header. Default template:

      180 |
      181 | <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
      182 | <h3 data-bind="text: label"></h3>
      183 | 
      184 |
    • 185 |
    • 186 |

      data

      187 |

      Type: object

      188 |

      Data for header template. For default template, you should provide label property

      189 |
    • 190 |
    191 |
  • 192 | 193 |
  • 194 |

    body

    195 |

    Type: object, can be observable

    196 |

    197 | Template binding for modal body, uses options from Knockout template binding. 198 | Please, see Knockout documentation for details. 199 |

    200 |

    Default values of body object:

    201 | 202 |
      203 |
    • 204 |

      name

      205 |

      Type: string, can be observable (default: 'modalBody')

      206 |

      Name of template for modal body. Default template:

      207 |
      <div data-bind="html: content"></div>
      208 |
    • 209 |
    • 210 |

      data

      211 |

      Type: object

      212 |

      Data for body template. For default template, you should provide content property, which can contain html.

      213 |
    • 214 |
    215 |
  • 216 | 217 |
  • 218 |

    footer

    219 |

    Type: object, can be observable

    220 |

    221 | Template binding for modal footer, uses options from Knockout template binding. 222 | Please, see Knockout documentation for details. 223 |

    224 |

    If omitted, no footer will be rendered.

    225 |

    Default values of footer object:

    226 | 227 |
      228 |
    • 229 |

      name

      230 |

      Type: string, can be observable (default: 'modalFooter')

      231 |

      Name of template for modal footer. Default template:

      232 |
      233 | <!-- ko if: $data.action -->
      234 | <a href="#" class="btn btn-primary" data-bind="click: action, html: primaryLabel"></a>
      235 | <!-- /ko -->
      236 | <a href="#" class="btn btn-default" data-bind="html: closeLabel" data-dismiss="modal"></a>
      237 | 
      238 |
    • 239 | 240 |
    • 241 |

      data

      242 |

      Type: object

      243 |

      Data for footer template. For default template, data object contains:

      244 | 245 |
        246 |
      • 247 |

        action

        248 |

        Type: function

        249 |

        250 | Function, which will be called, when user clicks on primary button. 251 | If this property is omitted for default template, primary button wouldn't render. 252 |

        253 | 254 |
      • 255 | 256 |
      • 257 |

        primaryLabel

        258 |

        Type: string, can be observable (default: 'Ok')

        259 |

        Text for primary button in default template.

        260 |
      • 261 | 262 |
      • 263 |

        closeLabel

        264 |

        Type: string, can be observable (default: 'Close')

        265 |

        Text for closing button in default template.

        266 |
      • 267 |
      268 |
    • 269 |
    270 |
  • 271 | 272 |
  • 273 |

    events

    274 |

    Type: object, can be observable

    275 |

    276 | Names of Bootstrap events, which are used by binding. 277 | Some bootstrap plugins change them, so you can pass new events to binding. 278 |

    279 |

    Default values of events object:

    280 | 281 |
      282 |
    • 283 |

      shown

      284 |

      Type: string, can be observable (default: 'shown.bs.modal')

      285 |

      Name of Bootstrap event, which fired when the modal has been made visible to the user

      286 |
    • 287 |
    • 288 |

      hidden

      289 |

      Type: string, can be observable (default: 'hidden.bs.modal')

      290 |

      Name of Bootstrap event, which fired when the modal has finished being hidden from the user

      291 |
    • 292 |
    293 |
  • 294 |
295 |
296 | 297 |

Default values

298 |

299 | Default values for modal binding located in ko.bindingHandlers.modal.defaults object. 300 | It contains default css and attributes for root element of modal and default values for header, body and footer. 301 | Can be changed before ko.applyBindigs() is called. 302 |

303 |
304 | defaults: {
305 |     css: 'modal fade',
306 |     attributes: {
307 |         role: 'dialog'  
308 |     },
309 | 
310 |     events: {
311 |         shown: 'shown.bs.modal',
312 |         hidden: 'hidden.bs.modal'
313 |     },
314 | 
315 |     headerTemplate: {
316 |         name: 'modalHeader',
317 |         templateEngine: ko.stringTemplateEngine.instance
318 |     },
319 | 
320 |     bodyTemplate: {
321 |         name: 'modalBody',
322 |         templateEngine: ko.stringTemplateEngine.instance
323 |     },
324 | 
325 |     footerTemplate: {
326 |         name: 'modalFooter',
327 |         templateEngine: ko.stringTemplateEngine.instance,
328 |         data: {
329 |             closeLabel: 'Close',
330 |             primaryLabel: 'Ok'
331 |         }
332 |     }
333 | }
334 | 
335 |
336 | --------------------------------------------------------------------------------