├── tests ├── helpers │ └── .gitkeep ├── unit │ └── .gitkeep ├── dummy │ ├── app │ │ ├── styles │ │ │ └── app.css │ │ ├── resolver.js │ │ ├── locales │ │ │ ├── es │ │ │ │ └── translations.js │ │ │ └── fr │ │ │ │ └── translations.js │ │ ├── controllers │ │ │ ├── primitives-test.js │ │ │ ├── plain-objects-test.js │ │ │ ├── long-option-list-test.js │ │ │ ├── block-params-test.js │ │ │ ├── immutability-test.js │ │ │ └── ember-objects-test.js │ │ ├── templates │ │ │ ├── primitives-test.hbs │ │ │ ├── long-option-list-test.hbs │ │ │ ├── plain-objects-test.hbs │ │ │ ├── application.hbs │ │ │ ├── block-params-test.hbs │ │ │ ├── ember-objects-test.hbs │ │ │ └── immutability-test.hbs │ │ ├── app.js │ │ ├── router.js │ │ └── index.html │ ├── public │ │ ├── robots.txt │ │ └── crossdomain.xml │ └── config │ │ ├── optional-features.json │ │ ├── ember-cli-update.json │ │ ├── targets.js │ │ └── environment.js ├── .eslintrc.js ├── test-helper.js ├── index.html └── integration │ └── multiselect-checkboxes-test.js ├── .watchmanconfig ├── .bowerrc ├── .prettierrc.js ├── .template-lintrc.js ├── index.js ├── config ├── environment.js └── ember-try.js ├── app └── components │ └── multiselect-checkboxes.js ├── .ember-cli ├── .prettierignore ├── .eslintignore ├── addon ├── templates │ └── components │ │ └── multiselect-checkboxes.hbs └── components │ └── multiselect-checkboxes.js ├── .editorconfig ├── .gitignore ├── .npmignore ├── testem.js ├── CONTRIBUTING.md ├── ember-cli-build.js ├── LICENSE.md ├── .eslintrc.js ├── .travis.yml ├── package.json ├── README.md └── CHANGELOG.md /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | }; 6 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | embertest: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | }; 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: require('./package').name, 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (/* environment, appConfig */) { 4 | return {}; 5 | }; 6 | -------------------------------------------------------------------------------- /app/components/multiselect-checkboxes.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-multiselect-checkboxes/components/multiselect-checkboxes'; 2 | -------------------------------------------------------------------------------- /tests/dummy/app/locales/es/translations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "Lisa": "Luisa", 3 | "Bob": "Roberto", 4 | "John": "Juan" 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/app/locales/fr/translations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "Lisa": "Louise", 3 | "Bob": "Robert", 4 | "John": "Jean" 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/primitives-test.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | 3 | export default Controller.extend({ 4 | init() { 5 | this._super(...arguments); 6 | this.fruits = ["apple", "banana", "orange"]; 7 | this.selectedFruits = []; 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/primitives-test.hbs: -------------------------------------------------------------------------------- 1 |

Test with array of primitives

2 | 3 |

4 | 5 |

Selected fruits: 6 |

11 |

12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .*/ 17 | .eslintcache 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /package.json.ember-try 23 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/long-option-list-test.hbs: -------------------------------------------------------------------------------- 1 |

Test with long option list

2 | 3 |

Selected options: 4 |

9 |

10 | 11 |

12 | 13 |

14 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'dummy/app'; 2 | import config from 'dummy/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/plain-objects-test.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | 3 | export default Controller.extend({ 4 | init() { 5 | this._super(...arguments); 6 | this.cars = [ 7 | { make: "BMW", color: "black"}, 8 | { make: "Ferari", color: "red"}, 9 | { make: "Volvo", color: "blue"} 10 | ]; 11 | this.selectedCars = []; 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/plain-objects-test.hbs: -------------------------------------------------------------------------------- 1 |

Test with array of plain js objects

2 | 3 |

4 | 5 |

Selected cars: 6 |

11 |

12 | -------------------------------------------------------------------------------- /addon/templates/components/multiselect-checkboxes.hbs: -------------------------------------------------------------------------------- 1 | {{#each this.checkboxes as |checkbox index|}} 2 | {{#if (has-block)}} 3 | {{yield checkbox.option checkbox.isSelected index}} 4 | {{else}} 5 |
  • 6 | 10 |
  • 11 | {{/if}} 12 | {{/each}} 13 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/long-option-list-test.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { A } from '@ember/array'; 3 | 4 | let options = []; 5 | 6 | for (let i = 0; i < 1000; i++) { 7 | options.push(i); 8 | } 9 | 10 | export default Controller.extend({ 11 | init() { 12 | this._super(...arguments); 13 | this.options = A(options); 14 | this.selectedOptions = A(); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'dummy/config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /connect.lock 17 | /coverage/ 18 | /libpeerconnection.log 19 | /npm-debug.log* 20 | /testem.log 21 | /yarn-error.log 22 | 23 | # ember-try 24 | /.node_modules.ember-try/ 25 | /bower.json.ember-try 26 | /package.json.ember-try 27 | -------------------------------------------------------------------------------- /tests/dummy/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "3.28.6", 7 | "blueprints": [ 8 | { 9 | "name": "addon", 10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output", 11 | "codemodsSource": "ember-addon-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": [ 14 | "--welcome" 15 | ] 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |
    2 |

    Ember multiselect-checkboxes tests

    3 | 4 | {{#link-to 'primitives-test'}}Primivites test{{/link-to}} | 5 | {{#link-to 'plain-objects-test'}}Plain JS objects test{{/link-to}} | 6 | {{#link-to 'ember-objects-test'}}Ember objects test{{/link-to}} | 7 | {{#link-to 'block-params-test'}}Block params test{{/link-to}} | 8 | {{#link-to 'long-option-list-test'}}Long option list test{{/link-to}} 9 | {{#link-to 'immutability-test'}}Immutability test{{/link-to}} 10 |
    11 | 12 | {{outlet}} 13 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/block-params-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import Controller from '@ember/controller'; 3 | 4 | var Person = EmberObject.extend({ 5 | name: null, 6 | 7 | gender: null 8 | }); 9 | 10 | export default Controller.extend({ 11 | init() { 12 | this._super(...arguments); 13 | this.persons = [ 14 | Person.create({ name: "Lisa", gender: "Female" }), 15 | Person.create({ name: "Bob", gender: "Male" }), 16 | Person.create({ name: "John", gender: "Male"}) 17 | ]; 18 | this.selectedPersons = []; 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/block-params-test.hbs: -------------------------------------------------------------------------------- 1 |

    Block Params Test

    2 | 3 | 4 |
  • 5 | 9 |
  • 10 |
    11 | 12 |

    Selected persons: 13 |

    18 |

    19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.env* 13 | /.eslintcache 14 | /.eslintignore 15 | /.eslintrc.js 16 | /.git/ 17 | /.gitignore 18 | /.prettierignore 19 | /.prettierrc.js 20 | /.template-lintrc.js 21 | /.travis.yml 22 | /.watchmanconfig 23 | /bower.json 24 | /config/ember-try.js 25 | /CONTRIBUTING.md 26 | /ember-cli-build.js 27 | /testem.js 28 | /tests/ 29 | /yarn-error.log 30 | /yarn.lock 31 | .gitkeep 32 | 33 | # ember-try 34 | /.node_modules.ember-try/ 35 | /bower.json.ember-try 36 | /package.json.ember-try 37 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/ember-objects-test.hbs: -------------------------------------------------------------------------------- 1 |

    Test with array of Ember objects

    2 | 3 |

    4 | 5 |

    6 | Select all 7 | Clear all 8 | Toggle Enabled/Disabled 9 |

    10 | 11 |

    Selected persons: 12 |

    17 |

    18 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from 'dummy/config/environment'; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function () { 10 | this.route('primitives-test', { path: '/primitives-test' }); 11 | this.route('plain-objects-test', { path: '/plain-objects-test' }); 12 | this.route('ember-objects-test', { path: '/ember-objects-test' }); 13 | this.route('block-params-test', { path: '/block-params-test' }); 14 | this.route('long-option-list-test', { path: '/long-option-list-test' }); 15 | this.route('immutability-test', { path: '/immutability-test' }); 16 | }); 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd ember-multiselect-checkboxes` 7 | * `npm install` 8 | 9 | ## Linting 10 | 11 | * `npm run lint` 12 | * `npm run lint:fix` 13 | 14 | ## Running tests 15 | 16 | * `ember test` – Runs the test suite on the current Ember version 17 | * `ember test --server` – Runs the test suite in "watch mode" 18 | * `ember try:each` – Runs the test suite against multiple Ember versions 19 | 20 | ## Running the dummy application 21 | 22 | * `ember serve` 23 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 24 | 25 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 26 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function (defaults) { 6 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | const { maybeEmbroider } = require('@embroider/test-setup'); 18 | return maybeEmbroider(app, { 19 | skipBabel: [ 20 | { 21 | package: 'qunit', 22 | }, 23 | ], 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions', 7 | ]; 8 | 9 | // Ember's browser support policy is changing, and IE11 support will end in 10 | // v4.0 onwards. 11 | // 12 | // See https://deprecations.emberjs.com/v3.x#toc_3-0-browser-support-policy 13 | // 14 | // If you need IE11 support on a version of Ember that still offers support 15 | // for it, uncomment the code block below. 16 | // 17 | // const isCI = Boolean(process.env.CI); 18 | // const isProduction = process.env.EMBER_ENV === 'production'; 19 | // 20 | // if (isCI || isProduction) { 21 | // browsers.push('ie 11'); 22 | // } 23 | 24 | module.exports = { 25 | browsers, 26 | }; 27 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/immutability-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import Controller from '@ember/controller'; 3 | import { A } from '@ember/array'; 4 | 5 | let Person = EmberObject.extend({ 6 | name: null, 7 | 8 | gender: null 9 | }); 10 | 11 | export default Controller.extend({ 12 | init() { 13 | this._super(...arguments); 14 | this.persons = A([ 15 | Person.create({ name: "Lisa", gender: "Female" }), 16 | Person.create({ name: "Bob", gender: "Male" }), 17 | Person.create({ name: "John", gender: "Male"}) 18 | ]); 19 | this.activeSelection = A(); 20 | this.selectionHistory = A(); 21 | }, 22 | 23 | actions: { 24 | updateSelection: function (newSelection) { 25 | this.get('selectionHistory').addObject(newSelection); 26 | this.set('activeSelection', newSelection); 27 | } 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/immutability-test.hbs: -------------------------------------------------------------------------------- 1 |

    Test with an immutable array of Ember objects

    2 | 3 | 9 | 10 |

    Active selection: 11 |

    16 |

    17 | 18 |

    Selection history: 19 |

      20 | {{#each this.selectionHistory as |selectionState|}} 21 |
    1. 22 |
        23 | {{#each selectionState as |person|}} 24 |
      • name: {{person.name}}, gender: {{person.gender}}
      • 25 | {{/each}} 26 |
      27 |
    2. 28 | {{/each}} 29 |
    30 |

    31 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/ember-objects-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import Controller from '@ember/controller'; 3 | 4 | var Person = EmberObject.extend({ 5 | name: null, 6 | 7 | gender: null 8 | }); 9 | 10 | export default Controller.extend({ 11 | init() { 12 | this._super(...arguments); 13 | this.persons = [ 14 | Person.create({ name: "Lisa", gender: "Female" }), 15 | Person.create({ name: "Bob", gender: "Male" }), 16 | Person.create({ name: "John", gender: "Male"}) 17 | ]; 18 | this.selectedPersons = []; 19 | }, 20 | 21 | personsDisabled: false, 22 | 23 | actions: { 24 | selectAllPersons: function() { 25 | this.set("selectedPersons", this.get('persons').slice()); 26 | }, 27 | 28 | clearPersons: function() { 29 | this.set("selectedPersons", []); 30 | }, 31 | 32 | toggleDisabled: function() { 33 | this.toggleProperty("personsDisabled"); 34 | } 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2014 R.S. Schermer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | legacyDecorators: true, 11 | }, 12 | }, 13 | plugins: ['ember'], 14 | extends: [ 15 | 'eslint:recommended', 16 | 'plugin:ember/recommended', 17 | 'plugin:prettier/recommended', 18 | ], 19 | env: { 20 | browser: true, 21 | }, 22 | rules: {}, 23 | overrides: [ 24 | // node files 25 | { 26 | files: [ 27 | './.eslintrc.js', 28 | './.prettierrc.js', 29 | './.template-lintrc.js', 30 | './ember-cli-build.js', 31 | './index.js', 32 | './testem.js', 33 | './blueprints/*/index.js', 34 | './config/**/*.js', 35 | './tests/dummy/config/**/*.js', 36 | ], 37 | parserOptions: { 38 | sourceType: 'script', 39 | }, 40 | env: { 41 | browser: false, 42 | node: true, 43 | }, 44 | plugins: ['node'], 45 | extends: ['plugin:node/recommended'], 46 | }, 47 | { 48 | // Test files: 49 | files: ['tests/**/*-test.{js,ts}'], 50 | extends: ['plugin:qunit/recommended'], 51 | }, 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{content-for "body-footer"}} 38 | {{content-for "test-body-footer"}} 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | i18n: { 10 | defaultLocale: 'es' 11 | }, 12 | EmberENV: { 13 | FEATURES: { 14 | // Here you can enable experimental features on an ember canary build 15 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 16 | }, 17 | EXTEND_PROTOTYPES: { 18 | // Prevent Ember Data from overriding Date.parse. 19 | Date: false, 20 | } 21 | }, 22 | 23 | APP: { 24 | // Here you can pass flags/options to your application instance 25 | // when it is created 26 | }, 27 | }; 28 | 29 | if (environment === 'development') { 30 | // ENV.APP.LOG_RESOLVER = true; 31 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 32 | // ENV.APP.LOG_TRANSITIONS = true; 33 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 34 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 35 | } 36 | 37 | if (environment === 'test') { 38 | // Testem prefers this... 39 | ENV.locationType = 'none'; 40 | 41 | // keep test console output quieter 42 | ENV.APP.LOG_ACTIVE_GENERATION = false; 43 | ENV.APP.LOG_VIEW_LOOKUPS = false; 44 | 45 | ENV.APP.rootElement = '#ember-testing'; 46 | ENV.APP.autoboot = false; 47 | } 48 | 49 | if (environment === 'production') { 50 | // here you can enable a production-specific feature 51 | } 52 | 53 | return ENV; 54 | }; 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | # we recommend testing addons with the same minimum supported node version as Ember CLI 5 | # so that your addon works for all apps 6 | - "12" 7 | 8 | dist: xenial 9 | 10 | addons: 11 | chrome: stable 12 | 13 | cache: 14 | directories: 15 | - $HOME/.npm 16 | 17 | env: 18 | global: 19 | # See https://git.io/vdao3 for details. 20 | - JOBS=1 21 | 22 | branches: 23 | only: 24 | - master 25 | # npm version tags 26 | - /^v\d+\.\d+\.\d+/ 27 | 28 | jobs: 29 | fast_finish: true 30 | allow_failures: 31 | - env: EMBER_TRY_SCENARIO=ember-canary 32 | 33 | include: 34 | # runs linting and tests with current locked deps 35 | - stage: "Tests" 36 | name: "Tests" 37 | script: 38 | - npm run lint 39 | - npm run test:ember 40 | 41 | - stage: "Additional Tests" 42 | name: "Floating Dependencies" 43 | install: 44 | - npm install --no-package-lock 45 | script: 46 | - npm run test:ember 47 | 48 | # we recommend new addons test the current and previous LTS 49 | # as well as latest stable release (bonus points to beta/canary) 50 | - env: EMBER_TRY_SCENARIO=ember-lts-3.24 51 | - env: EMBER_TRY_SCENARIO=ember-lts-3.28 52 | - env: EMBER_TRY_SCENARIO=ember-lts-3.28-with-i18n 53 | - env: EMBER_TRY_SCENARIO=ember-release 54 | - env: EMBER_TRY_SCENARIO=ember-beta 55 | - env: EMBER_TRY_SCENARIO=ember-canary 56 | - env: EMBER_TRY_SCENARIO=ember-default-with-jquery 57 | - env: EMBER_TRY_SCENARIO=ember-classic 58 | - env: EMBER_TRY_SCENARIO=embroider-safe 59 | - env: EMBER_TRY_SCENARIO=embroider-optimized 60 | 61 | script: 62 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO 63 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); 5 | 6 | module.exports = async function () { 7 | return { 8 | scenarios: [ 9 | { 10 | name: 'ember-lts-3.24', 11 | npm: { 12 | devDependencies: { 13 | 'ember-source': '~3.24.3', 14 | }, 15 | }, 16 | }, 17 | { 18 | name: 'ember-lts-3.28', 19 | npm: { 20 | devDependencies: { 21 | 'ember-source': '~3.28.0', 22 | }, 23 | }, 24 | }, 25 | { 26 | name: 'ember-lts-3.28-with-i18n', 27 | npm: { 28 | devDependencies: { 29 | 'ember-source': '~3.28.0', 30 | 'ember-i18n': '5.3.1', 31 | }, 32 | }, 33 | }, 34 | { 35 | name: 'ember-release', 36 | npm: { 37 | devDependencies: { 38 | 'ember-source': await getChannelURL('release'), 39 | }, 40 | }, 41 | }, 42 | { 43 | name: 'ember-beta', 44 | npm: { 45 | devDependencies: { 46 | 'ember-source': await getChannelURL('beta'), 47 | }, 48 | }, 49 | }, 50 | { 51 | name: 'ember-canary', 52 | npm: { 53 | devDependencies: { 54 | 'ember-source': await getChannelURL('canary'), 55 | }, 56 | }, 57 | }, 58 | { 59 | name: 'ember-default-with-jquery', 60 | env: { 61 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 62 | 'jquery-integration': true, 63 | }), 64 | }, 65 | npm: { 66 | devDependencies: { 67 | '@ember/jquery': '^1.1.0', 68 | }, 69 | }, 70 | }, 71 | { 72 | name: 'ember-classic', 73 | env: { 74 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 75 | 'application-template-wrapper': true, 76 | 'default-async-observers': false, 77 | 'template-only-glimmer-components': false, 78 | }), 79 | }, 80 | npm: { 81 | devDependencies: { 82 | 'ember-source': '~3.28.0', 83 | }, 84 | ember: { 85 | edition: 'classic', 86 | }, 87 | }, 88 | }, 89 | embroiderSafe(), 90 | embroiderOptimized(), 91 | ], 92 | }; 93 | }; 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-multiselect-checkboxes", 3 | "version": "0.12.0", 4 | "description": "Simple Ember component for allowing multiple selection from a certain collection (a hasMany property for example) using checkboxes.", 5 | "keywords": [ 6 | "ember-addon", 7 | "ember-component", 8 | "multiselect-checkboxes", 9 | "multiselect", 10 | "checkboxes" 11 | ], 12 | "license": "MIT", 13 | "author": "Roland Schermer", 14 | "directories": { 15 | "doc": "doc", 16 | "test": "tests" 17 | }, 18 | "repository": "https://github.com/rsschermer/ember-multiselect-checkboxes", 19 | "scripts": { 20 | "build": "ember build --environment=production", 21 | "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"", 22 | "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", 23 | "lint:hbs": "ember-template-lint .", 24 | "lint:hbs:fix": "ember-template-lint . --fix", 25 | "lint:js": "eslint . --cache", 26 | "lint:js:fix": "eslint . --fix", 27 | "start": "ember serve", 28 | "test": "npm-run-all lint test:*", 29 | "test:ember": "ember test", 30 | "test:ember-compatibility": "ember try:each" 31 | }, 32 | "dependencies": { 33 | "ember-cli-babel": "^7.26.10", 34 | "ember-cli-htmlbars": "^5.7.2" 35 | }, 36 | "devDependencies": { 37 | "@ember/jquery": "^2.0.0", 38 | "@ember/optional-features": "^2.0.0", 39 | "@ember/test-helpers": "^2.6.0", 40 | "@embroider/test-setup": "^0.48.1", 41 | "@glimmer/component": "^1.0.4", 42 | "@glimmer/tracking": "^1.0.4", 43 | "babel-eslint": "^10.1.0", 44 | "broccoli-asset-rev": "^3.0.0", 45 | "ember-auto-import": "^1.12.0", 46 | "ember-cli": "~3.28.6", 47 | "ember-cli-dependency-checker": "^3.2.0", 48 | "ember-cli-inject-live-reload": "^2.1.0", 49 | "ember-cli-sri": "^2.1.1", 50 | "ember-cli-terser": "^4.0.2", 51 | "ember-disable-prototype-extensions": "^1.1.3", 52 | "ember-export-application-global": "^2.0.1", 53 | "ember-load-initializers": "^2.1.2", 54 | "ember-maybe-import-regenerator": "^0.1.6", 55 | "ember-page-title": "^6.2.2", 56 | "ember-qunit": "^5.1.5", 57 | "ember-resolver": "^8.0.3", 58 | "ember-source": "https://s3.amazonaws.com/builds.emberjs.com/release/shas/e12dbe8fd866777275b633f1f108e09352d9509d.tgz", 59 | "ember-source-channel-url": "^3.0.0", 60 | "ember-template-lint": "^3.15.0", 61 | "ember-try": "^1.4.0", 62 | "ember-welcome-page": "^4.1.0", 63 | "eslint": "^7.32.0", 64 | "eslint-config-prettier": "^8.3.0", 65 | "eslint-plugin-ember": "^10.5.8", 66 | "eslint-plugin-node": "^11.1.0", 67 | "eslint-plugin-prettier": "^3.4.1", 68 | "eslint-plugin-qunit": "^6.2.0", 69 | "loader.js": "^4.7.0", 70 | "npm-run-all": "^4.1.5", 71 | "prettier": "^2.5.1", 72 | "qunit": "^2.17.2", 73 | "qunit-dom": "^1.6.0" 74 | }, 75 | "engines": { 76 | "node": "12.* || 14.* || >= 16" 77 | }, 78 | "ember": { 79 | "edition": "octane" 80 | }, 81 | "ember-addon": { 82 | "configPath": "tests/dummy/config" 83 | }, 84 | "bugs": { 85 | "url": "https://github.com/rsschermer/ember-multiselect-checkboxes/issues" 86 | }, 87 | "homepage": "https://github.com/rsschermer/ember-multiselect-checkboxes" 88 | } -------------------------------------------------------------------------------- /addon/components/multiselect-checkboxes.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import Component from '@ember/component'; 3 | import { computed } from '@ember/object'; 4 | import { A } from '@ember/array'; 5 | import { getOwner } from '@ember/application'; 6 | import layout from '../templates/components/multiselect-checkboxes'; 7 | 8 | let Checkbox = EmberObject.extend({ 9 | isSelected: computed('value', 'selection.[]', { 10 | get() { 11 | return this.get('selection').includes(this.get('value')); 12 | }, 13 | 14 | set(_, checked) { 15 | let selection = this.get('selection'); 16 | let selected = selection.includes(this.get('value')); 17 | let onchange = this.get('onchange'); 18 | let updateSelectionValue = this.get('updateSelectionValue'); 19 | let isMutable = typeof selection.addObject === 'function' && typeof selection.removeObject === 'function'; 20 | 21 | // Dispatch onchange event to handler with updated selection if handler is specified 22 | if (onchange) { 23 | let updated = A(selection.slice()); 24 | let operation; 25 | 26 | if (checked && !selected) { 27 | operation = 'added'; 28 | updated.addObject(this.get('value')); 29 | } else if (!checked && selected) { 30 | operation = 'removed'; 31 | updated.removeObject(this.get('value')); 32 | } 33 | 34 | onchange(updated, this.get('value'), operation); 35 | } 36 | 37 | // Mutate selection if updateSelectionValue is true and selection is mutable 38 | if (updateSelectionValue !== false && isMutable) { 39 | if (checked && !selected) { 40 | selection.addObject(this.get('value')); 41 | } else if (!checked && selected) { 42 | selection.removeObject(this.get('value')); 43 | } 44 | 45 | return checked; 46 | } else { 47 | 48 | // Only change the checked status of the checkbox when selection is mutated, because if 49 | // it is not mutated and the onchange handler does not update the bound selection value the 50 | // displayed checkboxes would be out of sync with bound selection value. 51 | return !checked; 52 | } 53 | } 54 | }) 55 | }); 56 | 57 | export default Component.extend({ 58 | layout, 59 | classNames: ['multiselect-checkboxes'], 60 | 61 | tagName: 'ul', 62 | 63 | i18n: computed(function () { 64 | return getOwner(this).lookup('service:i18n'); 65 | }), 66 | 67 | checkboxes: computed('options.[]', 'labelProperty', 'valueProperty', 'selection', 'translate', 'i18n.locale', function () { 68 | let labelProperty = this.get('labelProperty'); 69 | let valueProperty = this.get('valueProperty'); 70 | let selection = A(this.get('selection')); 71 | let onchange = this.get('onchange'); 72 | let updateSelectionValue = this.get('updateSelectionValue') !== undefined ? this.get('updateSelectionValue') : true; 73 | let options = A(this.get('options')); 74 | let translate = this.get('translate'); 75 | 76 | let checkboxes = options.map((option) => { 77 | let label, value; 78 | 79 | if (labelProperty) { 80 | if (typeof option.get === 'function') { 81 | label = option.get(labelProperty); 82 | } else { 83 | label = option[labelProperty]; 84 | } 85 | } else { 86 | label = String(option); 87 | } 88 | 89 | if (translate && label && this.get('i18n')) { 90 | label = this.get('i18n').t(label); 91 | } 92 | 93 | if (valueProperty) { 94 | if (typeof option.get === 'function') { 95 | value = option.get(valueProperty); 96 | } else { 97 | value = option[valueProperty]; 98 | } 99 | } else { 100 | value = option; 101 | } 102 | 103 | return Checkbox.create({ 104 | option: option, 105 | label: label, 106 | value: value, 107 | selection: selection, 108 | onchange: onchange, 109 | updateSelectionValue: updateSelectionValue 110 | }); 111 | }); 112 | 113 | return A(checkboxes); 114 | }) 115 | }); 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember-multiselect-checkboxes [![](https://travis-ci.org/RSSchermer/ember-multiselect-checkboxes.svg?branch=master)](https://travis-ci.org/RSSchermer/ember-multiselect-checkboxes) 2 | 3 | Simple Ember component for allowing multiple selection from a certain collection (a `hasMany` property for example) 4 | using checkboxes. 5 | 6 | ## Demo 7 | Demo available [here](https://rsschermer.github.io/ember-multiselect-checkboxes/). 8 | 9 | ## Installation 10 | 11 | `ember install ember-multiselect-checkboxes` 12 | 13 | ## Usage 14 | 15 | Example: 16 | 17 | ``` handlebars 18 | 19 | ``` 20 | 21 | This component can be used with an array of primitives as the options, an array of plain javascript objects as the 22 | options, or an array of Ember objects as the options. The following attributes should always be set: 23 | 24 | * `options`: a collection of Ember objects that can be selected. 25 | * `selection`: the subset of the options that is currently selected. The selection will automatically be updated when 26 | the user checks or unchecks options through Ember's two-way bindings. 27 | 28 | When using this component with an array of javascript objects or an array of Ember objects you should also set the 29 | `labelProperty` attribute: 30 | 31 | * `labelProperty`: the property on the plain javascript object or the Ember object that will be used as a label for the 32 | checkbox. By default this property will render as plain text. If translation is desired, set `translate` to true. 33 | 34 | ```handlebars 35 | 40 | ``` 41 | 42 | When using this component with an array of javascript objects or an array of Ember objects you may optionally specify 43 | the `valueProperty` attribute: 44 | 45 | * `valueProperty`: the property on the plain javascript object or the Ember object that will be used to represent this 46 | object in the selection. Example: when using an array of car objects as the options, if you set the `valueProperty` 47 | as their "color" property, the selection will be an array of color strings (not an array of cars). 48 | 49 | [This controller for the demo application](https://github.com/RSSchermer/ember-multiselect-checkboxes/blob/gh-pages/demo-app/app/controllers/application.js) 50 | provides an example of what your controller code could look like for each type of options collection. 51 | 52 | An action can be bound to the `onchange` attribute: 53 | 54 | ```handlebars 55 | 60 | ``` 61 | 62 | When a checkbox is checked or unchecked, this action will be triggered. The action handler will receive the following 63 | parameters: 64 | 65 | * `newSelection`: the subset of the options that is currently selected. 66 | * `value`: the corresponding value of the checkbox that was checked or unchecked. 67 | * `operation`: a string describing the operation performed on the selection. There are two possible values: 'added' when 68 | the value was added to the selection and 'removed' when the value was removed from the selection. 69 | 70 | ```js 71 | actions: { 72 | updateSelection: function (newSelection, value, operation) { 73 | ... 74 | } 75 | } 76 | ``` 77 | 78 | By default, the component will update the value bound to the `selection` attribute automatically. If you prefer to 79 | update the value bound to the `selection` attribute yourself, this can be disabled by setting the `updateSelectionValue` 80 | attribute to `false`: 81 | 82 | ```handlebars 83 | 89 | ``` 90 | 91 | You should then update the value bound to the `selection` property in the action bound to `onchange`, e.g.: 92 | 93 | ```js 94 | actions: { 95 | updateSelection: function (newSelection, value, operation) { 96 | this.set('selection', newSelection); 97 | 98 | ... 99 | } 100 | } 101 | ``` 102 | 103 | Note that for long option lists, allowing the component to automatically update the value bound to the `selection` 104 | attribute may result in significantly better performance. 105 | 106 | It's also possible to pass a custom template block should you want to customize the option list in some way (requires 107 | Ember 1.13 or newer). This template block will receive 3 block parameters: the option itself, a boolean value indicating 108 | whether or not the option is selected, and the option's index: 109 | 110 | ```handlebars 111 | 112 | 113 | 114 | ``` 115 | 116 | The initial example without a custom template block is essentially equivalent to the following example with a custom 117 | template block: 118 | 119 | ```handlebars 120 | 121 |
  • 122 | 126 |
  • 127 |
    128 | ``` 129 | 130 | Note that the `labelProperty` attribute is superfluous when using a custom template block; instead, `{{user.name}}` is 131 | used directly in the template block. 132 | 133 | By default the `multiselect-checkboxes` tag will render as an `ul` element. This can be customized by specifying the 134 | `tagName` attribute: 135 | 136 | ```handlebars 137 | 138 | ... 139 | 140 | ``` 141 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Ember-multiselect-checkboxes change log 2 | 3 | ## 0.12.0 4 | 5 | Upgraded for Ember 3.28. 6 | 7 | ## 0.11.0 8 | 9 | Upgraded for Ember 3, no changes to the public API. 10 | 11 | ## 0.10.0 12 | 13 | A custom template block new receives the index of an option as a third parameter: 14 | 15 | ```handlebars 16 | {{#multiselect-checkboxes options=users selection=selectedUsers as |user isSelected index|}} 17 | ... 18 | {{/multiselect-checkboxes}} 19 | ``` 20 | 21 | ## 0.9.0 22 | 23 | Action handlers bound to the `onchange` attribute now receive additional parameters. Previously only the updated 24 | selection was passed to the handler. It now receives 3 parameters: 25 | 26 | * `newSelection`: the subset of the options that is currently selected. 27 | * `value`: the corresponding value of the checkbox that was checked or unchecked. 28 | * `operation`: a string describing the operation performed on the selection. There are two possible values: 'added' when 29 | the value was added to the selection and 'removed' when the value was removed from the selection. 30 | 31 | ```js 32 | actions: { 33 | updateSelection: function (newSelection, value, operation) { 34 | ... 35 | } 36 | } 37 | ``` 38 | 39 | ## 0.8.0 40 | 41 | Added `translate` attribute. If `translate` is set to true and the [Ember-i18n addon](https://www.npmjs.com/package/ember-i18n) 42 | is installed, then the labels will be used as keys for translation lookup and the translations are displayed instead. 43 | 44 | ```handlebars 45 | {{multiselect-checkboxes 46 | options=users 47 | labelProperty='name' 48 | selection=selectedUsers 49 | translate=true}}} 50 | ``` 51 | 52 | ## 0.7.0 53 | 54 | Added `onchange` attribute. An action can be bound to the `onchange` attribute: 55 | 56 | ```handlebars 57 | {{multiselect-checkboxes 58 | options=users 59 | labelProperty='name' 60 | selection=selectedUsers 61 | onchange=(action 'updateSelection')}} 62 | ``` 63 | 64 | When a checkbox is checked or unchecked, this action will be triggered and it will receive the new selection as a 65 | parameter. 66 | 67 | Added `updateSelectionValue` attribute. By default, the component will update the value bound to the `selection` 68 | attribute automatically. If you prefer to update the value bound to the `selection` attribute yourself, this can be 69 | disabled by setting the `updateSelectionValue` attribute to `false`: 70 | 71 | ```handlebars 72 | {{multiselect-checkboxes 73 | options=users 74 | labelProperty='name' 75 | selection=selectedUsers 76 | onchange=(action 'updateSelection') 77 | updateSelectionValue=false}} 78 | ``` 79 | 80 | You should then update the value bound to the `selection` property in the action bound to `onchange`, e.g.: 81 | 82 | ```js 83 | actions: { 84 | updateSelection: function (newSelection) { 85 | this.set('selection', newSelection); 86 | 87 | ... 88 | } 89 | } 90 | ``` 91 | 92 | Note that for long option lists, allowing the component to automatically update the value bound to the `selection` 93 | attribute may result in significantly better performance. 94 | 95 | ## 0.6.0 96 | 97 | BC break: 98 | 99 | As suggested by @nadnoslen, passing the option itself as a block param to a custom template block should allow more 100 | flexibility than passing the label and option value. In previous versions, a custom template block received 3 block 101 | parameters: the option label, a boolean value indicating whether or not the option is selected, and the option value. As 102 | of this version, a custom template block now receives 2 parameters: the option itself and a boolean value indicating 103 | whether or not the option is selected. 104 | 105 | The following is an example of the old version with a custom template block: 106 | 107 | ```handlebars 108 | {{#multiselect-checkboxes options=users labelProperty='name' selection=selectedUsers as |label isSelected value|}} 109 |
  • 110 | 114 |
  • 115 | {{/multiselect-checkboxes}} 116 | ``` 117 | 118 | This should now be replaced with the following: 119 | 120 | ```handlebars 121 | {{#multiselect-checkboxes options=users selection=selectedUsers as |user isSelected|}} 122 |
  • 123 | 127 |
  • 128 | {{/multiselect-checkboxes}} 129 | ``` 130 | 131 | Note that the `labelProperty` attribute is now superfluous when using a custom template block; instead, `{{user.name}}` 132 | is referenced directly in the template block. 133 | 134 | ## 0.5.0 135 | 136 | Thanks to @techthumb, the options and checkboxes should now properly update when updating the bound options array or the 137 | bound selection array externally. Also adds the value as an optional third block param that can be used in a custom 138 | template block for displaying options. 139 | 140 | ## 0.4.0 141 | 142 | This release requires Ember 1.13 or newer. 143 | 144 | It's now possible to pass a custom template block should you want to customize the option list is some way. The 145 | following example without a template block: 146 | 147 | ```handlebars 148 | {{multiselect-checkboxes options=users labelProperty='name' selection=selectedUsers}} 149 | ``` 150 | 151 | Is equivalent to this example with a template block: 152 | 153 | ```handlebars 154 | {{#multiselect-checkboxes options=users labelProperty='name' selection=selectedUsers as |label isSelected|}} 155 |
  • 156 | 160 |
  • 161 | {{/multiselect-checkboxes}} 162 | ``` 163 | 164 | ## 0.3.0 165 | 166 | Adds `valueProperty` option. This option can be used to change how plain js objects or Ember js objects are represented 167 | in the selection. If for example, you specify the options to be an array of car objects, setting the `valueProperty` to 168 | be their "color" property, will result in a selection of color strings, instead of a selection of car objects. 169 | 170 | This version also removed the `multiselect-checkbox-option` component. This helper component was never intended to be 171 | part of the public API of this addon. If you were using it to create a customized checkbox list, stick on 0.2.x for a 172 | while; when Ember 1.13 is released this addon will be updated to provide better customization options with the help of 173 | block params. 174 | 175 | ## 0.2.0 176 | 177 | Upgraded to Ember CLI 0.2.0. 178 | 179 | ## 0.1.0 180 | 181 | Added option to disable a checkbox group, courtesy of @rafaelsales (see #6): 182 | 183 | ```hbs 184 | {{multiselect-checkboxes options=persons selection=selectedPersons labelProperty="name" disabled=personsDisabled}} 185 | ``` 186 | -------------------------------------------------------------------------------- /tests/integration/multiselect-checkboxes-test.js: -------------------------------------------------------------------------------- 1 | import { click, fillIn, render } from '@ember/test-helpers'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import { module, test } from 'qunit'; 4 | import { setupRenderingTest } from 'ember-qunit'; 5 | import { has } from 'require'; 6 | import { A } from '@ember/array'; 7 | import Object from '@ember/object'; 8 | import { run } from '@ember/runloop'; 9 | import $ from 'jquery'; 10 | 11 | module('Integration | Component | Multiselect-checkboxes', function(hooks) { 12 | setupRenderingTest(hooks); 13 | 14 | let fruits = A(['apple', 'orange', 'strawberry']); 15 | 16 | let cars = A([ 17 | { make: "BMW", color: "black"}, 18 | { make: "Ferari", color: "red"}, 19 | { make: "Volvo", color: "blue"} 20 | ]); 21 | 22 | let Person = Object.extend({ 23 | name: null, 24 | 25 | gender: null 26 | }); 27 | 28 | let persons = A([ 29 | Person.create({ name: "Lisa", gender: "Female" }), 30 | Person.create({ name: "Bob", gender: "Male" }), 31 | Person.create({ name: "John", gender: "Male"}) 32 | ]); 33 | 34 | test('uses the correct labels with primitive values and no label property', async function (assert) { 35 | this.set('options', fruits); 36 | 37 | await render(hbs` 38 | 39 | `); 40 | 41 | let labels = this.element.querySelectorAll('label'); 42 | 43 | assert.equal($(labels[0]).text().trim(), 'apple'); 44 | assert.equal($(labels[1]).text().trim(), 'orange'); 45 | assert.equal($(labels[2]).text().trim(), 'strawberry'); 46 | }); 47 | 48 | test('uses the correct labels with plain js values and a label property', async function (assert) { 49 | this.set('options', cars); 50 | 51 | await render(hbs` 52 | 53 | `); 54 | 55 | let labels = this.element.querySelectorAll('label'); 56 | 57 | assert.equal($(labels[0]).text().trim(), 'BMW'); 58 | assert.equal($(labels[1]).text().trim(), 'Ferari'); 59 | assert.equal($(labels[2]).text().trim(), 'Volvo'); 60 | }); 61 | 62 | if(has('ember-i18n')) { 63 | test('labels are translated when translate is true and i18n addon is present', async function (assert) { 64 | this.set('options', persons); 65 | 66 | await render(hbs` 67 | 68 | `); 69 | 70 | let labels = this.element.querySelectorAll('label'); 71 | 72 | assert.equal($(labels[0]).text().trim(), 'Luisa'); 73 | assert.equal($(labels[1]).text().trim(), 'Roberto'); 74 | assert.equal($(labels[2]).text().trim(), 'Juan'); 75 | }); 76 | 77 | test('labels are translated correctly when translate is true and i18n addon is present after switching locale', async function (assert) { 78 | this.set('options', persons); 79 | 80 | await render(hbs` 81 | 82 | `); 83 | 84 | run(() => this.owner.lookup('service:i18n').set('locale', 'fr')); 85 | 86 | let labels = this.element.querySelectorAll('label'); 87 | 88 | assert.equal($(labels[0]).text().trim(), 'Louise'); 89 | assert.equal($(labels[1]).text().trim(), 'Robert'); 90 | assert.equal($(labels[2]).text().trim(), 'Jean'); 91 | }); 92 | } 93 | 94 | test('labels are not translated when translate is true and i18n addon is not present', async function (assert) { 95 | this.set('options', persons); 96 | 97 | await render(hbs` 98 | 99 | `); 100 | 101 | let labels = this.element.querySelectorAll('label'); 102 | 103 | assert.equal($(labels[0]).text().trim(), 'Lisa'); 104 | assert.equal($(labels[1]).text().trim(), 'Bob'); 105 | assert.equal($(labels[2]).text().trim(), 'John'); 106 | }); 107 | 108 | test('uses the correct labels with Ember object values and a label property', async function (assert) { 109 | this.set('options', persons); 110 | 111 | await render(hbs` 112 | 113 | `); 114 | 115 | let labels = this.element.querySelectorAll('label'); 116 | 117 | assert.equal($(labels[0]).text().trim(), 'Lisa'); 118 | assert.equal($(labels[1]).text().trim(), 'Bob'); 119 | assert.equal($(labels[2]).text().trim(), 'John'); 120 | }); 121 | 122 | test('checks the checkboxes that represent a value currently in the selection', async function (assert) { 123 | this.setProperties({ 124 | 'options': persons, 125 | 'selection': A([persons[0], persons[2]]) 126 | }); 127 | 128 | await render(hbs` 129 | 130 | `); 131 | 132 | let checkboxes = this.element.querySelectorAll('input[type="checkbox"]'); 133 | 134 | assert.equal($(checkboxes[0]).prop('checked'), true); 135 | assert.equal($(checkboxes[1]).prop('checked'), false); 136 | assert.equal($(checkboxes[2]).prop('checked'), true); 137 | }); 138 | 139 | test('adds the value a checkbox represents to the selection when that checkbox is checked', async function (assert) { 140 | this.setProperties({ 141 | 'options': persons, 142 | 'selection': A([persons[0]]) 143 | }); 144 | 145 | await render(hbs` 146 | 147 | `); 148 | 149 | let checkboxes = this.element.querySelectorAll('input[type="checkbox"]'); 150 | 151 | assert.equal($(checkboxes[2]).prop('checked'), false); 152 | 153 | $(checkboxes[2]).click(); 154 | 155 | assert.equal($(checkboxes[2]).prop('checked'), true); 156 | assert.equal(this.get('selection.length'), 2); 157 | assert.equal(this.get('selection').includes(persons[2]), true); 158 | }); 159 | 160 | test('removes the value a checkbox represents from the selection when that checkbox is unchecked', async function (assert) { 161 | this.setProperties({ 162 | 'options': persons, 163 | 'selection': A([persons[0]]) 164 | }); 165 | 166 | await render(hbs` 167 | 168 | `); 169 | 170 | let checkboxes = this.element.querySelectorAll('input[type="checkbox"]'); 171 | 172 | assert.equal($(checkboxes[0]).prop('checked'), true); 173 | 174 | $(checkboxes[0]).click(); 175 | 176 | assert.equal($(checkboxes[0]).prop('checked'), false); 177 | assert.equal(this.get('selection.length'), 0); 178 | assert.equal(this.get('selection').includes(persons[0]), false); 179 | }); 180 | 181 | test('triggers the onchange action with the correct arguments when the selection changes', async function (assert) { 182 | this.setProperties({ 183 | 'options': persons, 184 | 'selection': A(), 185 | 'actions': { 186 | updateSelection: (newSelection, value, operation) => { 187 | assert.equal(newSelection.length, 1); 188 | assert.equal(newSelection.includes(persons[1]), true); 189 | assert.equal(value, persons[1]); 190 | assert.equal(operation, 'added'); 191 | } 192 | } 193 | }); 194 | 195 | await render(hbs` 196 | 197 | `); 198 | 199 | let checkboxes = this.element.querySelectorAll('input[type="checkbox"]'); 200 | 201 | $(checkboxes[1]).click(); 202 | }); 203 | 204 | test('does not update the bound selection value when updateSelectionValue is set to false', async function (assert) { 205 | this.setProperties({ 206 | 'options': persons, 207 | 'selection': A([persons[0]]) 208 | }); 209 | 210 | await render(hbs` 211 | 212 | `); 213 | 214 | let checkboxes = this.element.querySelectorAll('input[type="checkbox"]'); 215 | 216 | $(checkboxes[1]).click(); 217 | 218 | assert.equal(this.get('selection.length'), 1); 219 | assert.equal(this.get('selection').includes(persons[0]), true); 220 | assert.equal(this.get('selection').includes(persons[1]), false); 221 | }); 222 | 223 | test('checks the correct options with plain js values and a value property', async function (assert) { 224 | this.setProperties({ 225 | 'options': cars, 226 | 'selection': A(['red']) 227 | }); 228 | 229 | await render(hbs` 230 | 231 | `); 232 | 233 | let checkboxes = this.element.querySelectorAll('input[type="checkbox"]'); 234 | 235 | assert.equal($(checkboxes[0]).prop('checked'), false); 236 | assert.equal($(checkboxes[1]).prop('checked'), true); 237 | assert.equal($(checkboxes[2]).prop('checked'), false); 238 | }); 239 | 240 | test('updates the selection correctly with plain js values and a value property', async function (assert) { 241 | this.setProperties({ 242 | 'options': cars, 243 | 'selection': A(['red']) 244 | }); 245 | 246 | await render(hbs` 247 | 248 | `); 249 | 250 | let checkboxes = this.element.querySelectorAll('input[type="checkbox"]'); 251 | 252 | assert.equal($(checkboxes[0]).prop('checked'), false); 253 | 254 | $(checkboxes[0]).click(); 255 | 256 | assert.equal($(checkboxes[0]).prop('checked'), true); 257 | assert.equal(this.get('selection.length'), 2); 258 | assert.equal(this.get('selection').includes('black'), true); 259 | assert.equal(this.get('selection').includes('red'), true); 260 | }); 261 | 262 | test('checks the correct options with Ember object values and a value property', async function (assert) { 263 | this.setProperties({ 264 | 'options': persons, 265 | 'selection': A(['Bob']) 266 | }); 267 | 268 | await render(hbs` 269 | 270 | `); 271 | 272 | let checkboxes = this.element.querySelectorAll('input[type="checkbox"]'); 273 | 274 | assert.equal($(checkboxes[0]).prop('checked'), false); 275 | assert.equal($(checkboxes[1]).prop('checked'), true); 276 | assert.equal($(checkboxes[2]).prop('checked'), false); 277 | }); 278 | 279 | test('updates the selection correctly with Ember object values and a value property', async function (assert) { 280 | this.setProperties({ 281 | 'options': persons, 282 | 'selection': A(['Bob']) 283 | }); 284 | 285 | await render(hbs` 286 | 287 | `); 288 | 289 | let checkboxes = this.element.querySelectorAll('input[type="checkbox"]'); 290 | 291 | assert.equal($(checkboxes[0]).prop('checked'), false); 292 | 293 | $(checkboxes[0]).click(); 294 | 295 | assert.equal($(checkboxes[0]).prop('checked'), true); 296 | 297 | assert.equal(this.get('selection.length'), 2); 298 | assert.equal(this.get('selection').includes('Lisa'), true); 299 | assert.equal(this.get('selection').includes('Bob'), true); 300 | }); 301 | 302 | test('disables all checkboxes when disabled is set to true', async function (assert) { 303 | this.setProperties({ 304 | 'options': persons, 305 | 'selection': A([persons[0]]) 306 | }); 307 | 308 | await render(hbs` 309 | 310 | `); 311 | 312 | let checkboxes = this.element.querySelectorAll('input[type="checkbox"]'); 313 | 314 | $(checkboxes).each((index, checkbox) => { 315 | assert.equal($(checkbox).prop('disabled'), true); 316 | }); 317 | 318 | $(checkboxes).each((index, checkbox) => { 319 | $(checkbox).click(); 320 | }); 321 | 322 | assert.equal(this.get('selection.length'), 1); 323 | assert.equal(this.get('selection').includes(persons[0]), true); 324 | }); 325 | 326 | test('updates the displayed options when the bound options change', async function (assert) { 327 | this.set('options', fruits); 328 | 329 | await render(hbs` 330 | 331 | `); 332 | 333 | let labels = this.element.querySelectorAll('label'); 334 | 335 | assert.equal($(labels[0]).text().trim(), 'apple'); 336 | assert.equal($(labels[1]).text().trim(), 'orange'); 337 | assert.equal($(labels[2]).text().trim(), 'strawberry'); 338 | 339 | run(() => fruits.reverseObjects()); 340 | 341 | labels = this.element.querySelectorAll('label'); 342 | 343 | assert.equal($(labels[0]).text().trim(), 'strawberry'); 344 | assert.equal($(labels[1]).text().trim(), 'orange'); 345 | assert.equal($(labels[2]).text().trim(), 'apple'); 346 | }); 347 | 348 | test('updates checkboxes when the bound selection changes', async function (assert) { 349 | let selection = A([persons[0], persons[2]]); 350 | 351 | this.setProperties({ 352 | 'options': persons, 353 | 'selection': selection 354 | }); 355 | 356 | await render(hbs` 357 | 358 | `); 359 | 360 | let checkboxes = this.element.querySelectorAll('input[type="checkbox"]'); 361 | 362 | assert.equal($(checkboxes[0]).prop('checked'), true); 363 | assert.equal($(checkboxes[1]).prop('checked'), false); 364 | assert.equal($(checkboxes[2]).prop('checked'), true); 365 | 366 | run(() => selection.removeObject(persons[0])); 367 | 368 | checkboxes = this.element.querySelectorAll('input[type="checkbox"]'); 369 | 370 | assert.equal($(checkboxes[0]).prop('checked'), false); 371 | assert.equal($(checkboxes[1]).prop('checked'), false); 372 | assert.equal($(checkboxes[2]).prop('checked'), true); 373 | }); 374 | 375 | test('with a template block displays the correct custom labels for each person', async function (assert) { 376 | this.set('options', persons); 377 | 378 | await render(hbs` 379 | 380 |
  • 381 | 385 |
  • 386 |
    387 | `); 388 | 389 | let labels = this.element.querySelectorAll('label'); 390 | 391 | assert.equal($(labels[0]).text().trim(), '--Lisa--'); 392 | assert.equal($(labels[1]).text().trim(), '--Bob--'); 393 | assert.equal($(labels[2]).text().trim(), '--John--'); 394 | }); 395 | 396 | test('with a template block adds the value a checkbox represents to the selection when that checkbox is checked', async function (assert) { 397 | this.setProperties({ 398 | 'options': persons, 399 | 'selection': A() 400 | }); 401 | 402 | await render(hbs` 403 | 404 |
  • 405 | 409 |
  • 410 |
    411 | `); 412 | 413 | let checkboxes = this.element.querySelectorAll('input[type="checkbox"]'); 414 | 415 | assert.equal($(checkboxes[2]).prop('checked'), false); 416 | 417 | $(checkboxes[2]).click(); 418 | 419 | assert.equal($(checkboxes[2]).prop('checked'), true); 420 | assert.equal(this.get('selection.length'), 1); 421 | assert.equal(this.get('selection').includes(persons[2]), true); 422 | }); 423 | }); 424 | --------------------------------------------------------------------------------