├── .bowerrc ├── .editorconfig ├── .ember-cli ├── .eslintrc.js ├── .gitignore ├── .jscsrc ├── .npmignore ├── .travis.yml ├── .watchmanconfig ├── LICENSE ├── LICENSE.md ├── README.md ├── addon ├── .gitkeep ├── components │ ├── each-keys.js │ ├── em-data-value.js │ ├── sf-em-data.js │ ├── sf-filter.js │ ├── sf-headings.js │ ├── sf-rows.js │ ├── sf-table.js │ └── sort-filter-table.js ├── helpers │ ├── hash-contains.js │ ├── is-image.js │ ├── is-number.js │ ├── is-object.js │ ├── is-primitive.js │ └── is-private-key.js ├── templates │ └── components │ │ ├── each-keys.hbs │ │ ├── em-data-value.hbs │ │ ├── sf-em-data.hbs │ │ ├── sf-filter.hbs │ │ ├── sf-headings.hbs │ │ ├── sf-rows.hbs │ │ ├── sf-table.hbs │ │ └── sort-filter-table.hbs └── utils.js ├── app ├── .gitkeep ├── components │ ├── each-keys.js │ ├── em-data-value.js │ ├── sf-em-data.js │ ├── sf-filter.js │ ├── sf-headings.js │ ├── sf-rows.js │ ├── sf-table.js │ └── sort-filter-table.js └── helpers │ ├── hash-contains.js │ ├── is-image.js │ ├── is-number.js │ ├── is-object.js │ ├── is-primitive.js │ └── is-private-key.js ├── blueprints └── ember-sort-filter-table │ └── index.js ├── bower.json ├── config ├── deploy.js ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── package.json ├── testem.js ├── tests ├── .eslintrc.js ├── acceptance │ └── general-test.js ├── dummy │ ├── app │ │ ├── adapters │ │ │ └── application.js │ │ ├── app.js │ │ ├── components │ │ │ ├── .gitkeep │ │ │ ├── demo-block.js │ │ │ ├── demo-ember-data.js │ │ │ ├── demo-inline.js │ │ │ ├── demo-nav.js │ │ │ ├── ui-hero.js │ │ │ └── ui-tabs.js │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ └── application.js │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ ├── .gitkeep │ │ │ └── demo.js │ │ ├── resolver.js │ │ ├── router.js │ │ ├── routes │ │ │ ├── .gitkeep │ │ │ └── application.js │ │ ├── serializers │ │ │ └── application.js │ │ ├── styles │ │ │ └── app.scss │ │ └── templates │ │ │ ├── application.hbs │ │ │ └── components │ │ │ ├── .gitkeep │ │ │ ├── demo-block.hbs │ │ │ ├── demo-ember-data.hbs │ │ │ ├── demo-inline.hbs │ │ │ ├── demo-nav.hbs │ │ │ ├── ui-hero.hbs │ │ │ └── ui-tabs.hbs │ ├── config │ │ ├── environment.js │ │ └── targets.js │ └── public │ │ ├── crossdomain.xml │ │ ├── json │ │ └── demo.json │ │ └── robots.txt ├── helpers │ ├── destroy-app.js │ ├── module-for-acceptance.js │ ├── owner.js │ ├── resolver.js │ ├── start-app.js │ └── store.js ├── index.html ├── integration │ ├── .gitkeep │ └── components │ │ ├── each-keys-test.js │ │ ├── em-data-value-test.js │ │ ├── sf-em-data-test.js │ │ ├── sf-filter-test.js │ │ ├── sf-headings-test.js │ │ ├── sf-rows-test.js │ │ ├── sf-table-test.js │ │ └── sort-filter-table-test.js ├── pages │ ├── application.js │ └── components │ │ └── demo-nav.js ├── test-helper.js └── unit │ ├── .gitkeep │ └── helpers │ ├── hash-contains-test.js │ ├── is-image-test.js │ ├── is-number-test.js │ ├── is-object-test.js │ ├── is-primitive-test.js │ └── is-private-key-test.js ├── vendor └── .gitkeep └── yarn.lock /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.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 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | ecmaVersion: 2017, 6 | sourceType: 'module' 7 | }, 8 | extends: 'eslint:recommended', 9 | env: { 10 | browser: true 11 | }, 12 | rules: { 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.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 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log* 17 | yarn-error.log 18 | testem.log 19 | 20 | # ember-try 21 | .node_modules.ember-try/ 22 | bower.json.ember-try 23 | package.json.ember-try 24 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ember-suave" 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .gitignore 11 | .eslintrc.js 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | -------------------------------------------------------------------------------- /.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 | - "4" 7 | 8 | sudo: false 9 | 10 | cache: 11 | yarn: true 12 | 13 | env: 14 | # we recommend new addons test the current and previous LTS 15 | # as well as latest stable release (bonus points to beta/canary) 16 | - EMBER_TRY_SCENARIO=ember-lts-2.8 17 | - EMBER_TRY_SCENARIO=ember-lts-2.12 18 | - EMBER_TRY_SCENARIO=ember-release 19 | - EMBER_TRY_SCENARIO=ember-beta 20 | - EMBER_TRY_SCENARIO=ember-canary 21 | - EMBER_TRY_SCENARIO=ember-default 22 | 23 | matrix: 24 | fast_finish: true 25 | allow_failures: 26 | - env: EMBER_TRY_SCENARIO=ember-canary 27 | 28 | before_install: 29 | - curl -o- -L https://yarnpkg.com/install.sh | bash 30 | - export PATH=$HOME/.yarn/bin:$PATH 31 | - yarn global add phantomjs-prebuilt 32 | - phantomjs --version 33 | 34 | install: 35 | - yarn install --no-lockfile --non-interactive 36 | 37 | script: 38 | # Usually, it's ok to finish the test scenario without reverting 39 | # to the addon's original dependency state, skipping "cleanup". 40 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO --skip-cleanup 41 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/ember-sort-filter-table.svg)](https://badge.fury.io/js/ember-sort-filter-table) 2 | [![Build Status](https://travis-ci.org/crodriguez1a/ember-sort-filter-table.svg?branch=master)](https://travis-ci.org/crodriguez1a/ember-sort-filter-table) 3 | [![Ember Observer Score](http://emberobserver.com/badges/ember-sort-filter-table.svg)](https://emberobserver.com/addons/ember-sort-filter-table) 4 | 5 | # ember-sort-filter-table 6 | 7 | A table add-on for Ember-CLI with sorting and filtering. [Full Documentation & Demo](http://crodriguez1a.github.io/ember-sort-filter-table) 8 | 9 | ## Installation 10 | ember install ember-sort-filter-table 11 | 12 | ## Usage 13 | 14 | Simply pass an array of objects as the **table** parameter to the component. Use the key **rows** to define your array as follows: 15 | 16 | /** 17 | .js 18 | Defining a model that my table will display 19 | */ 20 | let model = { 21 | rows: [ 22 | { 23 | Tables: 'zebra stripes', 24 | Are: 'are neat', 25 | Cool: '$1' 26 | } 27 | ] 28 | }; 29 | 30 | ... 31 | 32 | {{! some-template.hbs }} 33 | {{component "sort-filter-table" table=model}} 34 | 35 | The addon will assemble the table headers from the object keys and display a table like this: 36 | 37 | | [Tables](#) | [Are](#) | [Cool](#) | 38 | | ------------- |:-------------:| -----:| 39 | | zebra stripes | are neat | $1 | 40 | 41 | If your model has properties that should not be displayed in the table, use an underscore to mark that property as private. 42 | 43 | let model = { 44 | rows:[ 45 | { 46 | name: 'Carlos Rodriguez', 47 | github_id: 'crodriguez1a', 48 | _writesCode: true 49 | } 50 | ] 51 | }; 52 | 53 | The model above would display like this: 54 | 55 | | [name](#) | [github id](#) | 56 | | ------------- |:-------------:| 57 | | Carlos Rodriguez | crodriguez1a | 58 | 59 | ## Contribute 60 | Collaboration is welcome and greatly appreciated. To collaborate on this project, follow the instructions that follow. 61 | 62 | ## Installation 63 | 64 | * `git clone` this repository 65 | * `npm install` 66 | * `bower install` 67 | 68 | ## Running 69 | 70 | * `ember server` 71 | * Visit your app at http://localhost:4200. 72 | 73 | ## Running Tests 74 | 75 | * `ember test` 76 | * `ember test --server` 77 | 78 | ## Building 79 | 80 | * `ember build` 81 | 82 | For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/). 83 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crodriguez1a/ember-sort-filter-table/21ab87c5e67d6fd4dd2ef55ac9b0bf9edc1fa18b/addon/.gitkeep -------------------------------------------------------------------------------- /addon/components/each-keys.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import layout from '../templates/components/each-keys'; 3 | import { computed } from 'ember-decorators/object'; 4 | 5 | /** 6 | Credit: rwjblue 7 | */ 8 | 9 | const { keys } = Object; 10 | 11 | /** 12 | Each Keys component 13 | 14 | @class EachKeys 15 | @extends Ember.Component 16 | */ 17 | export default Component.extend({ 18 | layout, 19 | tagName: '', 20 | @computed('object') 21 | items(object) { 22 | if (object) { 23 | let keysArr = keys(object); 24 | return keysArr.map((key) => { 25 | return { 26 | key, 27 | value: object[key] 28 | }; 29 | }); 30 | } 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /addon/components/em-data-value.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import layout from '../templates/components/em-data-value'; 3 | 4 | export default Component.extend({ 5 | layout 6 | }); 7 | -------------------------------------------------------------------------------- /addon/components/sf-em-data.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { get } from '@ember/object'; 3 | import layout from '../templates/components/sf-em-data'; 4 | import { computed } from 'ember-decorators/object'; 5 | import { assign } from '@ember/polyfills'; 6 | 7 | const { keys } = Object; 8 | 9 | export default Component.extend({ 10 | layout, 11 | tagName: '', 12 | 13 | /** 14 | Signal if nested arrays or objects should be displayed as a list 15 | 16 | @property shouldListNested 17 | @default true 18 | @type {Bool} 19 | @public 20 | */ 21 | shouldListNested: true, 22 | 23 | /** 24 | Extract plain object from ember data internal model 25 | 26 | @method _extractPojo 27 | @param store {Service} 28 | @private 29 | */ 30 | _extractPojo(store) { 31 | return store.toArray().map((item) => get(item, '_internalModel.__data')); 32 | }, 33 | 34 | /** 35 | Filter out nodes when a corresponding header was not provided 36 | headings ['name', 'address'] 37 | data [{ name, address, phone }, {...] 38 | result [ {name, address }, {... ] 39 | 40 | @method _filterByHeadings 41 | @param arr {Array} 42 | @param headings {Array} 43 | @returns {Array} 44 | @private 45 | */ 46 | _filterByHeadings(arr, headings) { 47 | return arr.map((obj) => { 48 | // prevent any mutation on the original object 49 | let _obj = assign({}, obj); 50 | // when headings and keys match, return early 51 | if (headings.join() === keys(_obj).join()) { 52 | return _obj 53 | } 54 | // otherwise, remove nodes where headings array does not include key 55 | for (let key in _obj) { 56 | if (!headings.includes(key)) { 57 | delete _obj[key] 58 | } 59 | } 60 | return _obj; 61 | }); 62 | }, 63 | /** 64 | Re-arranges records in the same order the headings suggest 65 | headings suggest ...["foo", "baz"] 66 | re-arrange records from... [{baz:"value", foo:"value"}] to [{foo:"value", baz:"value"}] 67 | 68 | @method _arrangeByHeadings 69 | @param arr {Array} POJA of ember data records 70 | @param headings {Array} List of headings 71 | @returns {Array} 72 | @private 73 | */ 74 | _arrangeByHeadings(arr, headings) { 75 | return arr.map((obj) => { 76 | return headings.reduce((hash, heading) => { 77 | hash[heading] = obj[heading]; 78 | return hash; 79 | }, {}); 80 | }); 81 | }, 82 | 83 | /** 84 | Convert ember data internal model into plain array 85 | 86 | @property listNested 87 | @default true 88 | @type {Bool} 89 | @public 90 | */ 91 | @computed('store') 92 | poja(store) { 93 | let objArr = []; 94 | let headings = get(this, 'group.groupHeadings'); 95 | 96 | if (store) { 97 | objArr = this._extractPojo(store); 98 | if (headings && headings.length) { 99 | objArr = this._filterByHeadings(objArr, headings); 100 | objArr = this._arrangeByHeadings(objArr, headings); 101 | } 102 | } 103 | return objArr.map((item) => assign({}, item)); 104 | } 105 | }); 106 | -------------------------------------------------------------------------------- /addon/components/sf-filter.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { set } from '@ember/object'; 3 | import layout from '../templates/components/sf-filter'; 4 | 5 | export default Component.extend({ 6 | layout, 7 | tagName: 'span', 8 | classNames: ['sf-table--filter'], 9 | placeholder: 'Filter', 10 | actions: { 11 | /** 12 | * Clear the current filter query 13 | * 14 | * @method clearField 15 | * @private 16 | */ 17 | clearField() { 18 | set(this, 'group.groupQuery', null); 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /addon/components/sf-headings.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { get, set } from '@ember/object'; 3 | import layout from '../templates/components/sf-headings'; 4 | 5 | export default Component.extend({ 6 | layout, 7 | tagName: '', 8 | init() { 9 | this._super(...arguments); 10 | this._syncWithGroup(); 11 | }, 12 | 13 | actions: { 14 | /** 15 | * Updates sort key and direction 16 | * 17 | * @method sort 18 | * @param key {String} 19 | * @public 20 | */ 21 | sort(key) { 22 | // Reference current direction 23 | let dir = get(this, 'group.groupSortDirection'); 24 | // Toggle direction 25 | set(this, 'group.groupSortDirection', dir === 'asc' ? 'desc' : 'asc'); 26 | // Update sort key 27 | set(this, 'group.groupSortKey', key); 28 | } 29 | }, 30 | 31 | /** 32 | Provide headings to group 33 | 34 | @method _syncWithGroup 35 | @private 36 | */ 37 | _syncWithGroup() { 38 | if (get(this, 'headings')) { 39 | set(this, 'group.groupHeadings', get(this, 'headings').getEach('key')); 40 | } 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /addon/components/sf-rows.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import layout from '../templates/components/sf-rows'; 3 | import { computed } from 'ember-decorators/object'; 4 | import { assign } from '@ember/polyfills'; 5 | 6 | export default Component.extend({ 7 | layout, 8 | tagName: '', 9 | 10 | /** 11 | Create a safe copy of the hash provided 12 | 13 | @property _rows 14 | @private 15 | @returns {Array} 16 | */ 17 | @computed('rows') 18 | _rows(rows) { 19 | if (rows && rows.length) { 20 | return rows.map((item) => assign({}, item)); 21 | } else { 22 | return []; 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /addon/components/sf-table.js: -------------------------------------------------------------------------------- 1 | import SortFilterTable from 'ember-sort-filter-table/components/sort-filter-table'; 2 | 3 | /** 4 | * Less verbose alias for 'sort-filter-table' component 5 | * 6 | * @class sfTable 7 | * @extends SortFilterTable 8 | */ 9 | 10 | export default SortFilterTable.extend({ 11 | layoutName: 'components/sort-filter-table' 12 | }); 13 | -------------------------------------------------------------------------------- /addon/components/sort-filter-table.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import EmberObject, { get, set } from '@ember/object'; 3 | import { A } from '@ember/array' 4 | import layout from '../templates/components/sort-filter-table'; 5 | import { computed } from 'ember-decorators/object'; 6 | import { alias } from 'ember-decorators/object/computed'; 7 | import { isPrivateKey, primitiveKeys } from '../utils'; 8 | 9 | const { values } = Object; 10 | 11 | /** 12 | Sortable Filterable Table Component 13 | 14 | @class SortFilterTable 15 | @extends Ember.Component 16 | */ 17 | export default Component.extend({ 18 | layout, 19 | tagName: 'table', 20 | classNames: ['sort-filter-table table'], 21 | 22 | /** 23 | Config: Signal if filter input field should be included 24 | 25 | @property filterable 26 | @type Bool 27 | @public 28 | */ 29 | filterable: true, 30 | 31 | /** 32 | A placeholder to display in the filter input field 33 | 34 | @property filterPlaceholder 35 | @public 36 | @type String 37 | */ 38 | filterPlaceholder: 'filter', 39 | 40 | /** 41 | A query or filter provided by input 42 | 43 | @property filter 44 | @public 45 | @type String 46 | */ 47 | filter: '', 48 | 49 | /** 50 | Only display rows with primitive values and public keys 51 | 52 | @property rows 53 | @private 54 | @type Array 55 | */ 56 | @computed('table.rows', 'filter') 57 | rows(rows, filterQuery) { 58 | return A(rows.filter((row) => { 59 | let rowValues = values(row); 60 | let filterExp = new RegExp(filterQuery, 'ig'); 61 | set(row, '_filtered', !(filterExp).test((rowValues.join(',')).toLowerCase())); 62 | return row; 63 | })); 64 | }, 65 | 66 | /** 67 | Extract column headings from keys 68 | 69 | @property headings 70 | @type Array 71 | @public 72 | */ 73 | @computed('rows') 74 | headings(rows) { 75 | return A(primitiveKeys(rows[0])); 76 | }, 77 | 78 | /** 79 | Assemble pretty labels from column headings/keys 80 | 81 | @property labels 82 | @type Array 83 | @public 84 | 85 | */ 86 | @computed('headings') 87 | labels(headings) { 88 | return A(headings.map((item) => { 89 | return EmberObject.create({ 90 | _key: item, 91 | name: this._handleSeparators(item), 92 | _sortClass: 'asc', 93 | _isPrivate: isPrivateKey(item) 94 | }); 95 | })); 96 | }, 97 | 98 | /** 99 | Store separator for headings 100 | 101 | @property _separator 102 | @type String 103 | @private 104 | */ 105 | _separator: null, 106 | 107 | /** 108 | Replaces hyphens or underscores with spaces (used to prettify headings) 109 | 110 | @method _handleSeparators 111 | @private 112 | */ 113 | _handleSeparators(str) { 114 | let isPrivate = str[0] === '_'; 115 | 116 | if (!isPrivate) { 117 | let separator = str.match(/[-\s_]/g); 118 | let camelCase = str.match(/[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*/); 119 | separator = camelCase || separator; 120 | 121 | if (separator && separator.length) { 122 | // save current separator 123 | set(this, '_separator', separator[0]); 124 | 125 | // return label without separator for display 126 | return str.replace(new RegExp(separator[0], 'g'), ' '); 127 | } else { 128 | // pass through 129 | return str; 130 | } 131 | } 132 | 133 | }, 134 | 135 | /** 136 | Label to determine sort order 137 | 138 | @property sortLabel 139 | @type String 140 | @private 141 | */ 142 | @computed('headings') 143 | sortLabel(headings) { 144 | return headings[0]; 145 | }, 146 | 147 | /** 148 | Signal wether current sort param is asc or desc 149 | 150 | @property isAscending 151 | @type Bool 152 | @private 153 | */ 154 | isAscending: true, 155 | 156 | /** 157 | Compute when sort should reverse order 158 | 159 | @property _direction 160 | @private 161 | @type String 162 | */ 163 | @computed('isAscending') 164 | _direction(isAscending) { 165 | return isAscending ? '' : ':desc'; 166 | }, 167 | 168 | /** 169 | Calculated total number of columns (colspan) 170 | 171 | @property totalColumns 172 | @type Number 173 | @public 174 | */ 175 | @alias('labels.length') totalColumns: 0, 176 | 177 | /** 178 | * Key to sorty by 179 | * 180 | * @property groupSortKey 181 | * @type String 182 | * @public 183 | */ 184 | groupSortKey: 'none', 185 | 186 | /** 187 | * Sort direction (asc/desc) 188 | * 189 | * @property groupSortDirection 190 | * @type String 191 | * @public 192 | */ 193 | groupSortDirection: 'desc', 194 | 195 | actions: { 196 | 197 | /** 198 | Apply label as sort 199 | 200 | @method sortByLabel 201 | @private 202 | */ 203 | sortByLabel(label) { 204 | let sortName = get(label, 'name').replace(/ /g, get(this, '_separator')); 205 | 206 | // toggle sort direction 207 | this.toggleProperty('isAscending'); 208 | 209 | // provide direction class 210 | set(label, '_sortClass', get(this, 'isAscending') ? 'asc' : 'desc'); 211 | 212 | // update sort label prop 213 | set(this, 'sortLabel', sortName); 214 | }, 215 | 216 | /** 217 | Send values up to actions 218 | 219 | @method sendEditAction 220 | @private 221 | */ 222 | sendEditAction(row, key, value, actionType) { 223 | let action = get(this, actionType); 224 | 225 | set(row, '_editingRow', null); 226 | if (action) { 227 | action({ 228 | row, 229 | key, 230 | value 231 | }); 232 | } 233 | }, 234 | 235 | /** 236 | Display input field for editing 237 | 238 | @method 239 | @private 240 | */ 241 | editValue(row, key) { 242 | get(this, 'rows').setEach('_editingRow', null); 243 | set(row, '_editingRow', key); 244 | }, 245 | } 246 | }); 247 | -------------------------------------------------------------------------------- /addon/helpers/hash-contains.js: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | 3 | const { 4 | stringify 5 | } = JSON; 6 | 7 | export function hashContains(params) { 8 | // Stringify the hash 9 | let _hash = params[0]; 10 | let str = stringify(_hash); 11 | 12 | // Convert the query to a regex 13 | let query = params[1] || ''; 14 | let rgx = new RegExp(query, 'i'); 15 | 16 | // Returns truthy when str was found in hash 17 | return (rgx).test(str); 18 | } 19 | 20 | export default helper(hashContains); 21 | -------------------------------------------------------------------------------- /addon/helpers/is-image.js: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | 3 | export function isImage(params/*, hash*/) { 4 | let img = params[0]; 5 | return img && (/([^\s]+(\.(jpg|png|gif|bmp))$)/gm).test(img); 6 | } 7 | 8 | export default helper(isImage); 9 | -------------------------------------------------------------------------------- /addon/helpers/is-number.js: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | 3 | export function isNumber(params/*, hash*/) { 4 | let val = params[0]; 5 | // See if this value only contains numbers (e.g., "12" but not "12 things") 6 | return val && (/^[0-9]*$/).test(val); 7 | } 8 | 9 | export default helper(isNumber); 10 | -------------------------------------------------------------------------------- /addon/helpers/is-object.js: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | 3 | export function isObject(params/*, hash*/) { 4 | let obj = params[0]; 5 | return obj && obj.toString() === '[object Object]'; 6 | } 7 | 8 | export default helper(isObject); 9 | -------------------------------------------------------------------------------- /addon/helpers/is-primitive.js: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | 3 | /** 4 | Signal if a value is primitive type (exluding null and undefined) 5 | 6 | @helper isPrimitive 7 | @private 8 | @returns Bool 9 | */ 10 | export function isPrimitive(params/*, hash*/) { 11 | let val = params[0]; 12 | return typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number'; 13 | } 14 | 15 | export default helper(isPrimitive); 16 | -------------------------------------------------------------------------------- /addon/helpers/is-private-key.js: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | import { isPrivateKey as _isPrivate } from '../utils'; 3 | 4 | /** 5 | Object keys with a leading underscore should be designated as private 6 | 7 | @helper isPrivateKey 8 | @private 9 | @returns Bool 10 | */ 11 | export function isPrivateKey(params/*, hash*/) { 12 | let key = params[0]; 13 | return key ? _isPrivate(key) : false ; 14 | } 15 | 16 | export default helper(isPrivateKey); 17 | -------------------------------------------------------------------------------- /addon/templates/components/each-keys.hbs: -------------------------------------------------------------------------------- 1 | {{#if items}} 2 | {{#each items as |item|}} 3 | {{yield item.key item.value}} 4 | {{/each}} 5 | {{else}} 6 | {{yield}} 7 | {{/if}} 8 | -------------------------------------------------------------------------------- /addon/templates/components/em-data-value.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasBlock}} 2 | {{yield}} 3 | {{else}} 4 | {{#if (is-image value)}} 5 | 6 | {{else}} 7 | {{html-safe value}} 8 | {{/if}} 9 | {{/if}} 10 | -------------------------------------------------------------------------------- /addon/templates/components/sf-em-data.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasBlock}} 2 | {{yield this}} 3 | {{else}} 4 | {{#each (sort-by (concat group.groupSortKey ':' group.groupSortDirection) poja) as |record|}} 5 | {{#if (hash-contains record group.groupQuery)}} 6 | 7 | {{#each-keys object=record as | key value|}} 8 | {{#unless (is-private-key key)}} 9 | 10 | {{#if shouldListNested}} 11 | {{#if (is-array value)}} 12 | 17 | {{else if (is-object value)}} 18 | 23 | {{else}} 24 | {{em-data-value value=value}} 25 | {{/if}} 26 | {{else}} 27 | {{em-data-value value=value}} 28 | {{/if}} 29 | 30 | {{/unless}} 31 | {{/each-keys}} 32 | 33 | {{/if}} 34 | {{/each}} 35 | {{/if}} 36 | -------------------------------------------------------------------------------- /addon/templates/components/sf-filter.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasBlock}} 2 | {{yield}} 3 | {{else}} 4 | 5 | {{#if (and withClearField group.groupQuery)}} 6 | 7 | {{/if}} 8 | {{/if}} 9 | -------------------------------------------------------------------------------- /addon/templates/components/sf-headings.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasBlock}} 2 | {{yield}} 3 | {{else}} 4 | {{#each headings as |heading|}} 5 | 6 | 7 | {{html-safe heading.display}} 8 | 9 | 10 | {{/each}} 11 | {{/if}} 12 | -------------------------------------------------------------------------------- /addon/templates/components/sf-rows.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasBlock}} 2 | {{yield}} 3 | {{else}} 4 | {{#each (sort-by (concat group.groupSortKey ':' group.groupSortDirection) _rows) as |item|}} 5 | {{#if (hash-contains item group.groupQuery)}} 6 | 7 | {{#each-keys object=item as | key value|}} 8 | {{#unless (is-private-key key)}} 9 | {{html-safe value}} 10 | {{/unless}} 11 | {{/each-keys}} 12 | 13 | {{/if}} 14 | {{/each}} 15 | {{/if}} 16 | -------------------------------------------------------------------------------- /addon/templates/components/sf-table.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} 2 | -------------------------------------------------------------------------------- /addon/templates/components/sort-filter-table.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasBlock}} 2 | {{!Block syntax}} 3 | {{yield (hash 4 | headings=(component 'sf-headings' group=this) 5 | filter=(component 'sf-filter' group=this) 6 | rows=(component 'sf-rows' group=this) 7 | data=(component 'sf-em-data' group=this) 8 | ) 9 | }} 10 | {{else}} 11 | {{!ember data}} 12 | {{#if store}} 13 | {{!TODO Inline ember data support}} 14 | {{else if table}} 15 | 16 | {{!Labels}} 17 | 18 | {{#each (filter-by "_isPrivate" false labels) as |label|}} 19 | 20 | {{html-safe label.name}} 21 | 22 | {{/each}} 23 | 24 | 25 | {{!Filterable}} 26 | {{#if filterable}} 27 | 28 | 29 | {{input class="input" value=filter placeholder=filterPlaceholder}} 30 | 31 | 32 | {{/if}} 33 | 34 | 35 | 36 | {{!Sort by label }} 37 | {{#each (sort-by (concat sortLabel _direction) rows) as |row|}} 38 | {{#unless row._filtered}} 39 | 40 | {{#each-keys object=row as |key value|}} 41 | {{#unless (is-private-key key)}} 42 | {{#if (is-primitive value)}} 43 | 44 | {{#if editable}} 45 | {{!Can edit}} 46 | {{#if (eq row._editingRow key)}} 47 | 56 | {{else}} 57 | {{html-safe value}} 58 | {{/if}} 59 | {{else}} 60 | {{!Cannot edit}} 61 | {{html-safe value}} 62 | {{/if}} 63 | 64 | {{/if}} 65 | {{/unless}} 66 | {{/each-keys}} 67 | 68 | {{/unless}} 69 | {{/each}} 70 | 71 | {{else}} 72 | {{!No model was provided}} 73 | 74 | 75 | Empty 76 | 77 | 78 | 79 | 80 | n/a 81 | 82 | 83 | {{/if}} 84 | {{/if}} 85 | -------------------------------------------------------------------------------- /addon/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | Object keys with a leading underscore should be designated as private 3 | 4 | @method isPrivateKey 5 | @private 6 | @returns Bool 7 | */ 8 | export const isPrivateKey = (key) => { 9 | return (/_/).test(key[0]); 10 | }; 11 | 12 | /** 13 | Extract only keys that point to primitive types 14 | (exluding null and undefined) 15 | 16 | @method primitiveKeys 17 | @private 18 | @returns Array 19 | */ 20 | export const primitiveKeys = (obj) => { 21 | let arr = []; 22 | for (let i in obj) { 23 | if (obj.hasOwnProperty(i)) { 24 | let val = obj[i]; 25 | if (typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number') { 26 | arr.push(i); 27 | } 28 | } 29 | } 30 | return arr; 31 | }; 32 | 33 | /** 34 | Object values polyfill 35 | https://github.com/tc39/proposal-object-values-entries/blob/master/polyfill.js 36 | */ 37 | const reduce = Function.bind.call(Function.call, Array.prototype.reduce); 38 | const isEnumerable = Function.bind.call(Function.call, Object.prototype.propertyIsEnumerable); 39 | const concat = Function.bind.call(Function.call, Array.prototype.concat); 40 | const keys = Object.keys; 41 | 42 | if (!Object.values) { 43 | Object.values = function values(O) { 44 | return reduce(keys(O), (v, k) => concat(v, typeof k === 'string' && isEnumerable(O, k) ? [O[k]] : []), []); 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crodriguez1a/ember-sort-filter-table/21ab87c5e67d6fd4dd2ef55ac9b0bf9edc1fa18b/app/.gitkeep -------------------------------------------------------------------------------- /app/components/each-keys.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-sort-filter-table/components/each-keys'; 2 | -------------------------------------------------------------------------------- /app/components/em-data-value.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-sort-filter-table/components/em-data-value'; -------------------------------------------------------------------------------- /app/components/sf-em-data.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-sort-filter-table/components/sf-em-data'; -------------------------------------------------------------------------------- /app/components/sf-filter.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-sort-filter-table/components/sf-filter'; -------------------------------------------------------------------------------- /app/components/sf-headings.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-sort-filter-table/components/sf-headings'; 2 | -------------------------------------------------------------------------------- /app/components/sf-rows.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-sort-filter-table/components/sf-rows'; -------------------------------------------------------------------------------- /app/components/sf-table.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-sort-filter-table/components/sf-table'; -------------------------------------------------------------------------------- /app/components/sort-filter-table.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-sort-filter-table/components/sort-filter-table'; 2 | -------------------------------------------------------------------------------- /app/helpers/hash-contains.js: -------------------------------------------------------------------------------- 1 | export { default, hashContains } from 'ember-sort-filter-table/helpers/hash-contains'; 2 | -------------------------------------------------------------------------------- /app/helpers/is-image.js: -------------------------------------------------------------------------------- 1 | export { default, isImage } from 'ember-sort-filter-table/helpers/is-image'; 2 | -------------------------------------------------------------------------------- /app/helpers/is-number.js: -------------------------------------------------------------------------------- 1 | export { default, isNumber } from 'ember-sort-filter-table/helpers/is-number'; 2 | -------------------------------------------------------------------------------- /app/helpers/is-object.js: -------------------------------------------------------------------------------- 1 | export { default, isObject } from 'ember-sort-filter-table/helpers/is-object'; 2 | -------------------------------------------------------------------------------- /app/helpers/is-primitive.js: -------------------------------------------------------------------------------- 1 | export { default, isPrimitive } from 'ember-sort-filter-table/helpers/is-primitive'; 2 | -------------------------------------------------------------------------------- /app/helpers/is-private-key.js: -------------------------------------------------------------------------------- 1 | export { default, isPrivateKey } from 'ember-sort-filter-table/helpers/is-private-key'; 2 | -------------------------------------------------------------------------------- /blueprints/ember-sort-filter-table/index.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | description: 'Install dependencies', 4 | 5 | normalizeEntityName: function(entityName) { 6 | return entityName; 7 | }, 8 | 9 | /* 10 | afterInstall: function(options) { 11 | 12 | } 13 | */ 14 | }; 15 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-sort-filter-table" 3 | } 4 | -------------------------------------------------------------------------------- /config/deploy.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(deployTarget) { 4 | var ENV = { 5 | build: {} 6 | // include other plugin configuration that applies to all deploy targets here 7 | }; 8 | 9 | if (deployTarget === 'development') { 10 | ENV.build.environment = 'development'; 11 | // configure other plugins for development deploy target here 12 | } 13 | 14 | if (deployTarget === 'staging') { 15 | ENV.build.environment = 'production'; 16 | // configure other plugins for staging deploy target here 17 | } 18 | 19 | if (deployTarget === 'production') { 20 | ENV.build.environment = 'production'; 21 | // configure other plugins for production deploy target here 22 | } 23 | 24 | // Note: if you need to build some configuration asynchronously, you can return 25 | // a promise that resolves with the ENV object instead of returning the 26 | // ENV object synchronously. 27 | return ENV; 28 | }; 29 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | useYarn: true, 4 | scenarios: [ 5 | { 6 | name: 'ember-lts-2.8', 7 | bower: { 8 | dependencies: { 9 | 'ember': 'components/ember#lts-2-8' 10 | }, 11 | resolutions: { 12 | 'ember': 'lts-2-8' 13 | } 14 | }, 15 | npm: { 16 | devDependencies: { 17 | 'ember-source': null 18 | } 19 | } 20 | }, 21 | { 22 | name: 'ember-lts-2.12', 23 | npm: { 24 | devDependencies: { 25 | 'ember-source': '~2.12.0' 26 | } 27 | } 28 | }, 29 | { 30 | name: 'ember-release', 31 | bower: { 32 | dependencies: { 33 | 'ember': 'components/ember#release' 34 | }, 35 | resolutions: { 36 | 'ember': 'release' 37 | } 38 | }, 39 | npm: { 40 | devDependencies: { 41 | 'ember-source': null 42 | } 43 | } 44 | }, 45 | { 46 | name: 'ember-beta', 47 | bower: { 48 | dependencies: { 49 | 'ember': 'components/ember#beta' 50 | }, 51 | resolutions: { 52 | 'ember': 'beta' 53 | } 54 | }, 55 | npm: { 56 | devDependencies: { 57 | 'ember-source': null 58 | } 59 | } 60 | }, 61 | { 62 | name: 'ember-canary', 63 | bower: { 64 | dependencies: { 65 | 'ember': 'components/ember#canary' 66 | }, 67 | resolutions: { 68 | 'ember': 'canary' 69 | } 70 | }, 71 | npm: { 72 | devDependencies: { 73 | 'ember-source': null 74 | } 75 | } 76 | }, 77 | { 78 | name: 'ember-default', 79 | npm: { 80 | devDependencies: {} 81 | } 82 | } 83 | ] 84 | }; 85 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = function(/* environment, appConfig */) { 5 | return { }; 6 | }; 7 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 5 | 6 | module.exports = function(defaults) { 7 | let app = new EmberAddon(defaults, { 8 | 'ember-cli-babel': { 9 | includePolyfill: true 10 | }, 11 | sassOptions: { 12 | includePaths: [ 13 | 'node_modules/bulma' 14 | ] 15 | } 16 | }); 17 | 18 | /* 19 | This build file specifies the options for the dummy test app of this 20 | addon, located in `/tests/dummy` 21 | This build file does *not* influence how the addon or the app using it 22 | behave. You most likely want to be modifying `./index.js` or app's build file 23 | */ 24 | 25 | return app.toTree(); 26 | }; 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = { 5 | name: 'ember-sort-filter-table', 6 | init: function(/*(app*/) { 7 | this._super.init && this._super.init.apply(this, arguments); 8 | 9 | this.options = this.options || {}; 10 | this.options.babel = this.options.babel || {}; 11 | this.options.babel.plugins = this.options.babel.plugins || []; 12 | 13 | if (this.options.babel.plugins.indexOf('transform-decorators-legacy') === -1) { 14 | this.options.babel.plugins.push('transform-decorators-legacy'); 15 | } 16 | 17 | if (this.options.babel.plugins.indexOf('transform-class-properties') === -1) { 18 | this.options.babel.plugins.push('transform-class-properties'); 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-sort-filter-table", 3 | "version": "0.3.4", 4 | "description": "A sortable/filterable table add-on for your ember application", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "license": "MIT", 9 | "author": "@crodriguez1a", 10 | "directories": { 11 | "doc": "doc", 12 | "test": "tests" 13 | }, 14 | "repository": "https://github.com/crodriguez1a/ember-sort-filter-table", 15 | "scripts": { 16 | "build": "ember build", 17 | "start": "ember server", 18 | "test": "ember try:each" 19 | }, 20 | "dependencies": { 21 | "babel-eslint": "^7.2.3", 22 | "babel-plugin-transform-class-properties": "^6.24.1", 23 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 24 | "ember-cli-babel": "^6.3.0", 25 | "ember-cli-htmlbars": "^2.0.1", 26 | "ember-cli-page-object": "^1.11.0", 27 | "ember-cli-string-helpers": "^1.4.0", 28 | "ember-composable-helpers": "^2.0.3", 29 | "ember-decorators": "^1.2.1", 30 | "ember-truth-helpers": "^1.3.0" 31 | }, 32 | "devDependencies": { 33 | "broccoli-asset-rev": "^2.4.5", 34 | "bulma": "^0.5.1", 35 | "code-highlight-linenums": "^0.2.1", 36 | "ember-ajax": "^3.0.0", 37 | "ember-cli": "~2.14.1", 38 | "ember-cli-dependency-checker": "^1.3.0", 39 | "ember-cli-deploy": "^1.0.1", 40 | "ember-cli-deploy-build": "^1.1.1", 41 | "ember-cli-deploy-git": "^1.3.0", 42 | "ember-cli-eslint": "^3.0.0", 43 | "ember-cli-htmlbars-inline-precompile": "^0.4.3", 44 | "ember-cli-inject-live-reload": "^1.4.1", 45 | "ember-cli-qunit": "^4.0.0", 46 | "ember-cli-sass": "~6.1.2", 47 | "ember-cli-shims": "^1.1.0", 48 | "ember-cli-sri": "^2.1.0", 49 | "ember-cli-uglify": "^1.2.0", 50 | "ember-computed-decorators": "^0.3.0", 51 | "ember-data": "^2.14.9", 52 | "ember-disable-prototype-extensions": "^1.1.2", 53 | "ember-export-application-global": "^2.0.0", 54 | "ember-font-awesome": "^3.0.5", 55 | "ember-load-initializers": "^1.0.0", 56 | "ember-macro-helpers": "^0.16.0", 57 | "ember-resolver": "^4.0.0", 58 | "ember-source": "~2.14.1", 59 | "ember-themed-syntax": "0.1.1", 60 | "eslint-plugin-ember-suave": "~1.0.0", 61 | "highlightjs": "^9.10.0", 62 | "hljs-themes": "^1.0.0", 63 | "loader.js": "^4.2.3" 64 | }, 65 | "engines": { 66 | "node": "^4.5 || 6.* || >= 7.*" 67 | }, 68 | "ember-addon": { 69 | "configPath": "tests/dummy/config" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | test_page: 'tests/index.html?hidepassed', 4 | disable_watching: true, 5 | launch_in_ci: [ 6 | 'Chrome' 7 | ], 8 | launch_in_dev: [ 9 | 'Chrome' 10 | ], 11 | browser_args: { 12 | Chrome: [ 13 | '--disable-gpu', 14 | '--headless', 15 | '--remote-debugging-port=9222', 16 | '--window-size=1440,900' 17 | ] 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | embertest: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/acceptance/general-test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'qunit'; 2 | import moduleForAcceptance from '../../tests/helpers/module-for-acceptance'; 3 | import page from '../../tests/pages/application'; 4 | 5 | moduleForAcceptance('Acceptance | general'); 6 | 7 | test('visiting various tabs', async function(assert) { 8 | await page.visit('/'); 9 | await page.demoNav.tabInline(); 10 | assert.equal((/(block|emberData)/).test(currentURL()), false, `navigated to inline (default) tab ${currentURL()}`); 11 | 12 | await page.demoNav.tabBlock(); 13 | assert.equal(currentURL(), '/?tab=block', 'navigated to block tab'); 14 | 15 | await page.demoNav.tabData(); 16 | assert.equal(currentURL(), '/?tab=emberData', 'navigated to emberData tab'); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/dummy/app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import config from '../config/environment'; 3 | 4 | const PREFIX = config.APP.NAMESPACE || ''; 5 | 6 | const { 7 | JSONAPIAdapter 8 | } = DS; 9 | 10 | export default JSONAPIAdapter.extend({ 11 | namespace: `${PREFIX}/json`, 12 | pathForType (modelName) { 13 | return `${modelName}.json`; 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | const App = Ember.Application.extend({ 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | Resolver 10 | }); 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crodriguez1a/ember-sort-filter-table/21ab87c5e67d6fd4dd2ef55ac9b0bf9edc1fa18b/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/components/demo-block.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend(); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/components/demo-ember-data.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend(); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/components/demo-inline.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { computed } from 'ember-decorators/object'; 3 | 4 | export default Component.extend({ 5 | /** 6 | * @private 7 | */ 8 | @computed() 9 | devs() { 10 | return { 11 | rows: [ 12 | { 13 | name: 'Carlos Rodriguez', 14 | 'github_ID': 'crodriguez1a', 15 | _tags: 'developer javascript ember ios' 16 | }, 17 | { 18 | name: 'Alex DiLiberto', 19 | 'github_ID': 'alexdiliberto', 20 | _tags: 'developer javascript ember android' 21 | } 22 | ] 23 | }; 24 | }, 25 | 26 | /** 27 | * @private 28 | */ 29 | @computed() 30 | small() { 31 | return { 32 | rows: [ 33 | { 34 | 'browser': ' Chrome', 35 | 'november': '48.15%', 36 | 'december': '46.22%', 37 | 'change': '-1.93%', 38 | 'relative': '-4.00%', 39 | '_private': 'foo', 40 | hello() { 41 | return 'foo'; 42 | } 43 | }, 44 | { 45 | 'browser': ' Firefox', 46 | 'november': '16.76%', 47 | 'december': '16.34%', 48 | 'change': '-0.42%', 49 | 'relative': '-2.50%', 50 | '_private': 'bar', 51 | hello() { 52 | return 'foo'; 53 | } 54 | }, 55 | { 56 | 'browser': ' Safari', 57 | 'november': '4.45%', 58 | 'december': '4.24%', 59 | 'change': '-0.21%', 60 | 'relative': '-4.70%', 61 | '_private': 'foo', 62 | hello() { 63 | return 'foo'; 64 | } 65 | } 66 | ] 67 | }; 68 | }, 69 | 70 | /** 71 | * @private 72 | */ 73 | @computed() 74 | big() { 75 | return { 76 | rows: [ 77 | { 78 | 'No.': 9, 79 | 'Player': 'Ron Harper', 80 | 'Pos': 'PG', 81 | 'Ht': '6-6', 82 | 'Wt': 185, 83 | 'Birth-Date': 'January 20 1964', 84 | 'Exp': 9, 85 | 'College': 'Miami University' 86 | }, 87 | { 88 | 'No.': 23, 89 | 'Player': 'Michael Jordan', 90 | 'Pos': 'SG', 91 | 'Ht': '6-6', 92 | 'Wt': 195, 93 | 'Birth-Date': 'February 17 1963', 94 | 'Exp': 10, 95 | 'College': 'University of North Carolina' 96 | }, 97 | 98 | { 99 | 'No.': 13, 100 | 'Player': 'Luc Longley', 101 | 'Pos': 'C', 102 | 'Ht': '7-2', 103 | 'Wt': 265, 104 | 'Birth-Date': 'January 19 1969', 105 | 'Exp': 4, 106 | 'College': 'University of New Mexico' 107 | }, 108 | { 109 | 'No.': 33, 110 | 'Player': 'Scottie Pippen', 111 | 'Pos': 'SF', 112 | 'Ht': '6-8', 113 | 'Wt': 210, 114 | 'Birth-Date': 'September 25 1965', 115 | 'Exp': 8, 116 | 'College': 'University of Central Arkansas' 117 | }, 118 | { 119 | 'No.': 91, 120 | 'Player': 'Dennis Rodman', 121 | 'Pos': 'PF', 122 | 'Ht': '6-7', 123 | 'Wt': 210, 124 | 'Birth-Date': 'May 13 1961', 125 | 'Exp': 9, 126 | 'College': 'Southeastern Oklahoma State University' 127 | } 128 | ] 129 | }; 130 | }, 131 | actions: { 132 | /** 133 | @public 134 | */ 135 | editField(row, key, value) { 136 | if (row && key && value) { 137 | return; 138 | } 139 | }, 140 | 141 | /** 142 | @public 143 | */ 144 | cancelEditField(row, key, value) { 145 | if (row && key && value) { 146 | return; 147 | } 148 | }, 149 | } 150 | }); 151 | -------------------------------------------------------------------------------- /tests/dummy/app/components/demo-nav.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { get } from '@ember/object'; 3 | 4 | export default Component.extend({ 5 | /** 6 | @property Name of currently active section 7 | @type Sting 8 | */ 9 | tab: null, 10 | 11 | /** 12 | @property Action to perform to show active section 13 | @type Function 14 | */ 15 | show: null, 16 | 17 | actions: { 18 | show() { 19 | if (get(this, 'show')) { 20 | get(this, 'show')(...arguments); 21 | } 22 | } 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /tests/dummy/app/components/ui-hero.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend({ 4 | classNames: ['hero'], 5 | classNameBindings: ['isWarning:is-warning'] 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/components/ui-tabs.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend({ 4 | classNames: ['tabs'], 5 | classNameBindings: ['isBoxed:is-boxed', 'isRight:is-right'] 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crodriguez1a/ember-sort-filter-table/21ab87c5e67d6fd4dd2ef55ac9b0bf9edc1fa18b/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { set } from '@ember/object'; 3 | 4 | export default Controller.extend({ 5 | /** 6 | Active tab rendered 7 | 8 | @property tab 9 | @default inline 10 | @type String 11 | */ 12 | tab: 'inline', 13 | 14 | actions: { 15 | show(section) { 16 | set(this, 'tab', section); 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crodriguez1a/ember-sort-filter-table/21ab87c5e67d6fd4dd2ef55ac9b0bf9edc1fa18b/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ember Sort Filter Table 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/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crodriguez1a/ember-sort-filter-table/21ab87c5e67d6fd4dd2ef55ac9b0bf9edc1fa18b/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/models/demo.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { 4 | Model, 5 | attr 6 | } = DS; 7 | 8 | export default Model.extend({ 9 | title: attr('string'), 10 | score: attr('string'), 11 | publisher: attr('string'), 12 | short_description: attr('string'), 13 | platforms: attr(), 14 | thumb: attr('string') 15 | }); 16 | -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | 4 | const Router = Ember.Router.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | this.route("/", { path: "*path"}); 11 | }); 12 | 13 | export default Router; 14 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crodriguez1a/ember-sort-filter-table/21ab87c5e67d6fd4dd2ef55ac9b0bf9edc1fa18b/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/routes/application.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { get } from '@ember/object'; 3 | import { inject as service } from '@ember/service'; 4 | 5 | export default Route.extend({ 6 | store: service(), 7 | queryParams: { 8 | tab: { 9 | replace: true 10 | } 11 | }, 12 | 13 | model() { 14 | return get(this, 'store').findAll('demo'); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /tests/dummy/app/serializers/application.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { 4 | JSONAPISerializer 5 | } = DS; 6 | 7 | export default JSONAPISerializer.extend({ 8 | normalizeResponse(status, headers, payload, /*requestData*/) { 9 | return { 10 | data: payload.map((item, index) => { 11 | return { 12 | type: 'demo', 13 | id: index, 14 | attributes: item 15 | }; 16 | }) 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.scss: -------------------------------------------------------------------------------- 1 | $fontFamily: monospace; 2 | 3 | @import 'bulma'; 4 | 5 | * { 6 | .tab.is-active { 7 | i.fa { 8 | border-color: $primary; 9 | color: $primary; 10 | } 11 | } 12 | i.fa { 13 | display: inline-block; 14 | vertical-align: middle; 15 | position: relative; 16 | margin-top: -0.5%; 17 | 18 | &.expiremental { 19 | font-size: 95%; 20 | padding: 0.25rem; 21 | border-radius: 100%; 22 | border: 1px dotted $grey; 23 | margin: 0.2rem 0 0 0.5rem; 24 | vertical-align: bottom; 25 | color: $grey-dark; 26 | } 27 | } 28 | 29 | .button.cancel { 30 | @extend .is-danger; 31 | } 32 | 33 | button.edit { 34 | @extend .is-primary; 35 | } 36 | 37 | .edit-field { 38 | li { 39 | list-style: none; 40 | } 41 | } 42 | 43 | section.section { 44 | padding: 1.5rem; 45 | } 46 | 47 | // ember data 48 | .sftable-em-img { 49 | width: 100%; 50 | height: auto; 51 | } 52 | 53 | .sf-table--filter { 54 | position: relative; 55 | } 56 | 57 | a.sf-table--clear-field { 58 | &:after { 59 | display: block; 60 | vertical-align: middle; 61 | position: absolute; 62 | content: '\f00d'; 63 | font-family: 'FontAwesome', sans-serif; 64 | top: 3px; 65 | right: 0.5rem; 66 | width: 1rem; 67 | height: 100%; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#ui-hero isWarning=true}} 3 |
4 |

5 | Ember Sort Filter Table 6 |

7 |
8 |
9 | {{#ui-tabs isRight=true isBoxed=true}} 10 | 18 | {{/ui-tabs}} 19 |
20 | {{/ui-hero}} 21 |
22 |
23 |

Version Notes

24 |
    25 |
  • 26 | 0.3.3 27 |
      28 |
    • Fixes #39
    • 29 |
    • Fixes #37
    • 30 |
    • Fixes #35
    • 31 |
    32 |
  • 33 |
  • 34 | 0.3.0 35 |
      36 |
    • Fixes #29
    • 37 |
    • Updates to Ember 2.14.x
    • 38 |
    • Contextual component approach (backwards compatible)
    • 39 |
    40 |
  • 41 | {{#if older}} 42 |
  • 43 | 0.2.6 44 |
      45 |
    • Block syntax support
    • 46 |
    • Ember Data support
    • 47 |
    48 |
  • 49 |
  • 50 | 0.2.5 51 |
      52 |
    • Fixes issue #27
    • 53 |
    54 |
  • 55 |
  • 56 | 0.2.4 57 |
      58 |
    • Update to Ember 2.6 - issue #25
    • 59 |
    • Replace lodash dependency with Object.values polyfill
    • 60 |
    • Fixes issue #26
    • 61 |
    62 |
  • 63 |
  • 64 | 0.2.1 65 |
      66 |
    • 67 | Remove observer - issue #21 68 |
    • 69 |
    • 70 | Replace sendAction with closure actions - issue #22 71 |
    • 72 |
    73 |
  • 74 |
  • 75 | 0.2.0 76 |
      77 |
    • Ember 2.x optimization
    • 78 |
    • Template update to remove outer div. Base of component is now a table tag.
    • 79 |
    • Addition of some semantic class names
    • 80 |
    • Prevent non-primitives in model from rendering (e.g., functions, objects)
    • 81 |
    • Fix issue #16
    • 82 |
    • Fix issue #17
    • 83 |
    84 |
  • 85 |
  • 86 | 0.1.9 87 |
      88 |
    • Fix issue #15
    • 89 |
    90 |
  • 91 |
  • 92 | 0.1.8 93 |
      94 |
    • HTML markup support in model
    • 95 |
    • Component short name alias sf-table
    • 96 |
    97 |
  • 98 |
  • 99 | 0.1.7 100 |
      101 |
    • Camel case row header support
    • 102 |
    • Private keys bugfix
    • 103 |
    104 |
  • 105 | {{/if}} 106 |
107 | {{#unless older}} 108 |

109 | 110 | Older 111 | 112 |

113 | {{/unless}} 114 |
115 |
116 | 117 |
118 |
119 |

Installation

120 | {{#themed-syntax lang="bash" theme="dark"}} 121 | ember install ember-sort-filter-table 122 | {{/themed-syntax}} 123 |
124 |
125 | 126 |
127 |

Usage

128 | {{demo-nav tab=tab show=(action 'show')}} 129 |
130 | 131 | {{component (concat 'demo-' tab) model=model}} 132 | 133 | 150 |
151 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crodriguez1a/ember-sort-filter-table/21ab87c5e67d6fd4dd2ef55ac9b0bf9edc1fa18b/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/demo-block.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | A table can be composed using the following components: 5 |

29 |

30 |

31 | Components within a table communicate via the yielded group parameter. 32 |

33 |

34 | {{#themed-syntax lang="handlebars" theme="dark"}} 35 | {{#sf-table as | sf |}} 36 | <thead> 37 | <tr> 38 | {{sf.headings 39 | headings=(array 40 | (hash key="firstName" display="First Name") 41 | (hash key="lastName" display="Last Name") 42 | (hash key="number" display="No.") 43 | (hash key="position" display="Pos.") 44 | )}} 45 | </tr> 46 | </thead> 47 | <tr><td colspan="99">{{sf.filter placeholder="Search"}}</td></tr> 48 | <tbody> 49 | {{sf.rows rows=(array 50 | (hash firstName='Michael' lastName='Jordan' number='23' position='G' _tags='guard north carolina') 51 | (hash firstName='Scottie' lastName='Pippen' number='33' position='SF' _tags='small forward central arkansas') 52 | (hash firstName='Horace' lastName='Grant' number='54' position='F' _tags='power forward clemson') 53 | )}} 54 | </tbody> 55 | {{/sf-table}} 56 | {{/themed-syntax}} 57 |

58 | 59 |

60 | As with inline usage, you can still designate properties that will not be displayed, using the an underscore (_tags="small forward marquette"). 61 |

62 | 63 | {{#sf-table as | sf |}} 64 | 65 | 66 | {{sf.headings 67 | headings=(array 68 | (hash key="firstName" display="First Name") 69 | (hash key="lastName" display="Last Name") 70 | (hash key="number" display="No.") 71 | (hash key="position" display="Pos.") 72 | )}} 73 | 74 | 75 | 76 | {{sf.filter placeholder="Search"}} 77 | 78 | 79 | 1990s 80 | 81 | 82 | {{sf.rows rows=(array 83 | (hash firstName='Michael' lastName='Jordan' number='23' position='G' _tags='guard north carolina') 84 | (hash firstName='Scottie' lastName='Pippen' number='33' position='SF' _tags='small forward central arkansas') 85 | (hash firstName='Horace' lastName='Grant' number='54' position='F' _tags='power forward clemson') 86 | )}} 87 | 88 | 89 | Today 90 | 91 | 92 | {{sf.rows rows=(array 93 | (hash firstName='Dwyane' lastName='Wade' number='3' position='G' _tags='guard marquette') 94 | (hash firstName='Jimmy' lastName='Butler' number='21' position='SF' _tags='small forward marquette') 95 | (hash firstName='Rajan' lastName='Rondo' number='9' position='G' _tags='guard kentucky') 96 | )}} 97 | 98 | {{/sf-table}} 99 |
100 | 101 |
102 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/demo-ember-data.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | A table that relies on Ember Data can be composed using the following components: 5 |

27 |

28 |

29 | Components within a table communicate via the yielded group parameter. 30 |

31 |

32 | {{#themed-syntax lang="handlebars" theme="dark"}} 33 | {{#sf-table as | sf |}} 34 | <thead> 35 | <tr> 36 | {{sf.headings 37 | headings=(array 38 | (hash key="score" display="Score") 39 | (hash key="title" display="Title") 40 | (hash key="publisher" display="Publisher") 41 | (hash key="short_description" display="Description") 42 | (hash key="platforms" display="Platforms") 43 | )}} 44 | </tr> 45 | </thead> 46 | <tr><td colspan="99">{{sf.filter placeholder="Search" withClearField=true}}</td></tr> 47 | <tbody> 48 | {{sf.data store=model}} 49 | </tbody> 50 | {{/sf-table}} 51 | {{/themed-syntax}} 52 |

53 | 54 | {{#sf-table class="is-striped" as | sf |}} 55 | 56 | 57 | {{sf.headings 58 | headings=(array 59 | (hash key="score" display="Score") 60 | (hash key="title" display="Title") 61 | (hash key="publisher" display="Publisher") 62 | (hash key="short_description" display="Description") 63 | (hash key="platforms" display="Platforms") 64 | )}} 65 | 66 | 67 | 68 | 69 | {{sf.filter placeholder="Search" withClearField=true}} 70 | 71 | 72 | 73 | {{sf.data store=model}} 74 | 75 | {{/sf-table}} 76 |
77 |
78 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/demo-inline.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{#themed-syntax lang="handlebars" theme="dark"}} 5 | {{sf-table table=myTableModel}} 6 | {{/themed-syntax}} 7 |

8 | 9 | {{sf-table table=small isBordered=true isStriped=true}} 10 | 11 |

12 | Provide an object that contains an array called rows: 13 |

14 |

15 | {{#themed-syntax lang="javascript" theme="dark" withBuffers=false}} 16 | /* Using font awesome icons (not included) */ 17 | myTableModel: { 18 | rows: [ 19 | { 20 | 'Browser': '<i class="fa fa-chrome"></i> Chrome', 21 | 'November': '48.15%', 22 | 'December': '46.22%', 23 | 'change': '-1.93%', 24 | 'relative': '-4.00%' 25 | }, 26 | { 27 | 'Browser': '<i class="fa fa-firefox"></i> Firefox', 28 | 'November': '16.76%', 29 | 'December': '16.34%', 30 | 'change': '-0.42%', 31 | 'relative': '-2.50%' 32 | }, 33 | { 34 | 'Browser': '<i class="fa fa-safari"></i> Safari', 35 | 'November': '4.45%', 36 | 'December': '4.24%', 37 | 'change': '-0.21%', 38 | 'relative': '-4.70%' 39 | } 40 | ] 41 | }; 42 | {{/themed-syntax}} 43 |

44 | 45 |

46 | The addon will assemble the table headers from the object keys. If your model has properties that should not be displayed in the table, use an underscore to designate that property as private. 47 |

48 | 49 |

50 | {{#themed-syntax lang="javascript" theme="dark" withBuffers=false}} 51 | let devs = { 52 | rows:[ 53 | { 54 | name: 'Carlos Rodriguez', 55 | github_ID: 'crodriguez1a', 56 | _tags: 'developer javascript ember ios' 57 | }, 58 | { 59 | name: 'Alex DiLiberto', 60 | github_ID: 'alexdiliberto', 61 | _tags: 'developer javascript ember android' 62 | } 63 | ] 64 | }; 65 | {{/themed-syntax}} 66 |

67 | 68 |

69 | The model above would display as below. Although the _tags key is designated private and does not display, its value is still considered. 70 |

71 | 72 | {{sf-table isBordered=true table=devs filterPlaceholder="Type android or ios"}} 73 |
74 |
75 | 76 |
77 |
78 |

Configuration

79 | 90 |
91 |
92 | 93 |
94 |
95 |

Editable Example

96 | {{sf-table isStriped=true isBordered=true table=big editable=true edit=(action "editField") cancel=(action "cancelEditField")}} 97 | 98 | {{#themed-syntax lang="htmlbars" theme="dark"}} 99 | {{sf-table table=model editable=true edit=(action "myEditAction") cancel=(action "myCancelAction") }} 100 | {{/themed-syntax}} 101 | 102 |
103 |

104 | The edit and cancel actions will return the following hash: 105 |

110 |

111 | 112 | {{#themed-syntax lang="javascript" theme="dark" withBuffers=false}} 113 | myEditAction(hash) { 114 | //=> { row: 'Class', key: 'String', value: 'String' } 115 | }, 116 | 117 | myCancelAction(hash) { 118 | //=> { row: 'Class', key: 'String', value: 'String' } 119 | } 120 | {{/themed-syntax}} 121 |
122 |
123 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/demo-nav.hbs: -------------------------------------------------------------------------------- 1 | {{#ui-tabs isBoxed=true}} 2 | 13 | {{/ui-tabs}} 14 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/ui-hero.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} 2 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/ui-tabs.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} 2 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = function(environment) { 5 | let ENV = { 6 | modulePrefix: 'dummy', 7 | environment, 8 | rootURL: '/', 9 | locationType: 'auto', 10 | EmberENV: { 11 | FEATURES: { 12 | // Here you can enable experimental features on an ember canary build 13 | // e.g. 'with-controller': true 14 | }, 15 | EXTEND_PROTOTYPES: { 16 | // Prevent Ember Data from overriding Date.parse. 17 | Date: false 18 | } 19 | }, 20 | 21 | APP: { 22 | // Here you can pass flags/options to your application instance 23 | // when it is created 24 | }, 25 | 26 | // 'ember-bulma': { 27 | // except: ['ui-container'] // excludes `ui-container` https://github.com/open-tux/ember-bulma/issues/78 28 | // } 29 | }; 30 | 31 | if (environment === 'development') { 32 | // ENV.APP.LOG_RESOLVER = true; 33 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 34 | // ENV.APP.LOG_TRANSITIONS = true; 35 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 36 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 37 | } 38 | 39 | if (environment === 'test') { 40 | // Testem prefers this... 41 | ENV.locationType = 'none'; 42 | 43 | // keep test console output quieter 44 | ENV.APP.LOG_ACTIVE_GENERATION = false; 45 | ENV.APP.LOG_VIEW_LOOKUPS = false; 46 | 47 | ENV.APP.rootElement = '#ember-testing'; 48 | } 49 | 50 | if (environment === 'production') { 51 | ENV.rootURL = '/ember-sort-filter-table'; 52 | ENV.APP.NAMESPACE = 'ember-sort-filter-table'; 53 | } 54 | 55 | return ENV; 56 | }; 57 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | browsers: [ 4 | 'ie 9', 5 | 'last 1 Chrome versions', 6 | 'last 1 Firefox versions', 7 | 'last 1 Safari versions' 8 | ] 9 | }; 10 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tests/dummy/public/json/demo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title":"Metal Gear Solid V: The Definitive Experience", 4 | "score":"10", 5 | "publisher":"Konami", 6 | "short_description":"October 11, 2016 - Metal Gear Solid V: The Definitive Experience is the ultimate collection of the MGS 5 story which includes the critically acclaimed Metal Gear Solid V:...", 7 | "platforms":{ 8 | "1":"PS4", 9 | "2":"PC", 10 | "3":"Xbox One" 11 | }, 12 | "thumb":"http:\/\/assets2.ignimgs.com\/2016\/08\/30\/metal-gear-solid-definitive-button-00jpg-75869c_160w.jpg" 13 | }, 14 | { 15 | "title":"Metal Gear", 16 | "score":"", 17 | "publisher":"Ultra Games", 18 | "short_description":"June 01, 1988 - Outer Heaven leader Ca Taffy has activated the ultimate weapon: Metal Gear. It is up to Solid Snake to find and destroy the weapon and end Ca Taffy's reign of terror.", 19 | "platforms":{ 20 | "1":"NES" 21 | }, 22 | "thumb":"http:\/\/media.gamestats.com\/gg\/image\/object\/007\/007055\/MetalGeaR_f_NESBOXboxart_160w.jpg" 23 | }, 24 | { 25 | "title":"Metal Gear Survive", 26 | "score":"", 27 | "publisher":"Konami", 28 | "short_description":"TBA 2017", 29 | "platforms":{ 30 | "1":"Xbox One", 31 | "2":"PC", 32 | "3":"PS4" 33 | }, 34 | "thumb":"http:\/\/assets1.ignimgs.com\/2016\/08\/17\/metal-gear-survive-buttonjpg-a7680a_160w.jpg" 35 | }, 36 | { 37 | "title":"Metal Gear Rising: Revengeance", 38 | "score":"8.5", 39 | "publisher":"Konami", 40 | "short_description":"February 19, 2013 - Productions, Metal Gear Rising: Revengeance pits cyborg against cyborg in battle that blazes past every human limit! The main character, Raiden, was once feared...", 41 | "platforms":{ 42 | "1":"PS3", 43 | "2":"Xbox 360", 44 | "3":"PC", 45 | "4":"Android" 46 | }, 47 | "thumb":"http:\/\/media.ign.com\/games\/image\/object\/142\/14276686\/metalgearrisingrev1boxart_160w.jpg" 48 | }, 49 | { 50 | "title":"Metal Gear Online [2008]", 51 | "score":"", 52 | "publisher":"Konami", 53 | "short_description":"June 12, 2008 - ...in the Metal Gear franchise, following up on the success of the online portion of MGS3 Subsistence on PS2 and MGS Portable Ops on PSP. Metal Gear Online...", 54 | "platforms":{ 55 | "1":"PS3", 56 | "2":"Arcade" 57 | }, 58 | "thumb":"" 59 | }, 60 | { 61 | "title":"Metal Gear Acid", 62 | "score":"6.5", 63 | "publisher":"Konami", 64 | "short_description":"PSP Launch - 03\/24\/2005 - ...blockbuster Metal Gear franchise gives players a new approach to their espionage. Metal Gear Acid (also lovingly known as Acid Metal by its creators for the...", 65 | "platforms":{ 66 | "1":"PSP" 67 | }, 68 | "thumb":"http:\/\/media.gamestats.com\/gg\/image\/object\/664\/664947\/MGSAcid_PSP_BoxFront_ESRBed02202005boxart_160w.jpg" 69 | }, 70 | { 71 | "title":"Metal Gear Acid 2", 72 | "score":"7.7", 73 | "publisher":"Konami", 74 | "short_description":"March 21, 2006 - ...previous Metal Gear Acid game, Metal Gear Acid 2 features an improved card-based strategic gameplay system with card weapon upgrades and over 500 cards to...", 75 | "platforms":{ 76 | "1":"PSP" 77 | }, 78 | "thumb":"http:\/\/pspmedia.ign.com\/psp\/image\/object\/748\/748621\/metalgearacid2_pspboxboxart_160w.jpg" 79 | }, 80 | { 81 | "title":"Metal Gear Solid [2000]", 82 | "score":"10", 83 | "publisher":"Konami", 84 | "short_description":"May 05, 2000 - ...bipedal tank Metal Gear has fallen into the hands of terrorists. Seven years after the fiasco at Outer Heaven involving the original Metal Gear, the US government...", 85 | "platforms":{ 86 | "1":"GBC" 87 | }, 88 | "thumb":"http:\/\/media.gamestats.com\/gg\/image\/object\/013\/013458\/metalgear_ghostbabel_GBCBOXboxart_160w.jpg" 89 | }, 90 | { 91 | "title":"Metal Gear Online [MGS V]", 92 | "score":"", 93 | "publisher":"Konami", 94 | "short_description":"October 06, 2015 - Metal Gear Online is the multiplayer component for Metal Gear Solid V: The Phantom Pain.", 95 | "platforms":{ 96 | "1":"Xbox 360", 97 | "2":"PS3", 98 | "3":"PS4", 99 | "4":"Xbox One", 100 | "5":"PC" 101 | }, 102 | "thumb":"http:\/\/assets1.ignimgs.com\/2014\/12\/06\/metal-gear-online-placeholderjpg-d18fc9_160w.jpg" 103 | }, 104 | { 105 | "title":"Metal Gear Solid 2: Substance", 106 | "score":"9", 107 | "publisher":"Konami", 108 | "short_description":"November 05, 2002 - ...popular Metal Gear Solid 2 title, on PlayStation 2, Xbox, and PC, expands gameplay with added options, and features. The largest addition to gameplay are...", 109 | "platforms":{ 110 | "1":"Xbox", 111 | "2":"PS2", 112 | "3":"PC" 113 | }, 114 | "thumb":"http:\/\/media.gamestats.com\/gg\/image\/mgs2substance_xboxbox_orgboxart_160w.jpg" 115 | }, 116 | { 117 | "title":"Metal Gear Solid Touch", 118 | "score":"6", 119 | "publisher":"Konami", 120 | "short_description":"March 19, 2009 - ...in the Metal Gear Solid franchise, Metal Gear Solid Touch is a shooting game that makes use of the iPad's touchscreen control mechanisms to retell gamers...", 121 | "platforms":{ 122 | "1":"iPhone", 123 | "2":"iPad" 124 | }, 125 | "thumb":"http:\/\/wiimedia.ign.com\/wii\/image\/object\/143\/14306701\/METAL-GEAR-SOLID-TOUCH_dl10_iPhoneboxart_160w.jpg" 126 | }, 127 | { 128 | "title":"Metal Gear Solid HD Collection", 129 | "score":"9", 130 | "publisher":"Konami", 131 | "short_description":"November 08, 2011 - ...in the Metal Gear franchise with the Metal Gear Solid HD Collection. Included are Metal Gear Solid 2: Sons of Liberty, Metal Gear Solid 3: Snake Eater and...", 132 | "platforms":{ 133 | "1":"PS3", 134 | "2":"Xbox 360", 135 | "3":"Vita" 136 | }, 137 | "thumb":"http:\/\/media.ign.com\/games\/image\/object\/110\/110044\/Metal-Gear-Solid-HD_STANDARD_US_ESRB_PS3boxart_160w.jpg" 138 | }, 139 | { 140 | "title":"Metal Gear Solid: The Essential Collection", 141 | "score":"8.8", 142 | "publisher":"Konami", 143 | "short_description":"March 18, 2008 - Metal Gear Solid: The Essential Collection includes three classic games in the groundbreaking Metal gear franchise. Included are the original Metal Gear...", 144 | "platforms":{ 145 | "1":"PS2" 146 | }, 147 | "thumb":"http:\/\/media.ign.com\/games\/image\/object\/142\/14233286\/metalgearsolidecollectionpsboxart_160w.jpg" 148 | }, 149 | { 150 | "title":"Metal Gear Solid 3: Subsistence", 151 | "score":"9.8", 152 | "publisher":"Konami", 153 | "short_description":"March 14, 2006 - ...dazzling Metal Gear Solid 3: Snake Eater . For Subsistence , a new 3rd Person View Camera\" gives a controllable, low-angle view of the action for a more intense...", 154 | "platforms":{ 155 | "1":"PS2" 156 | }, 157 | "thumb":"http:\/\/ps2media.ign.com\/ps2\/image\/object\/748\/748590\/mgs3subsistence_ps2box_usa_org_000boxart_160w.jpg" 158 | }, 159 | { 160 | "title":"Metal Gear Solid: The Twin Snakes", 161 | "score":"8.5", 162 | "publisher":"Konami", 163 | "short_description":"March 09, 2004 - PlayStation hit Metal Gear Solid is recreated with updated graphics and an expanded story on the Nintendo GameCube. This overhauled version of the PS One best-seller...", 164 | "platforms":{ 165 | "1":"GCN" 166 | }, 167 | "thumb":"http:\/\/media.gamestats.com\/gg\/image\/mgstwin_gcn_boxboxart_160w.jpg" 168 | }, 169 | { 170 | "title":"Metal Gear Solid: Portable Ops", 171 | "score":"9", 172 | "publisher":"Konami", 173 | "short_description":"December 05, 2006 - ...ongoing Metal Gear Solid narrative, as players learn more about returning characters such as Para-Medic, Major Zero and Sigint and also witness Big Boss'...", 174 | "platforms":{ 175 | "1":"PSP" 176 | }, 177 | "thumb":"http:\/\/pocketmedia.ign.com\/pocket\/image\/object\/783\/783630\/metalgearsolidportops_pspboxboxart_160w.jpg" 178 | }, 179 | { 180 | "title":"Metal Gear 2: Solid Snake", 181 | "score":"", 182 | "publisher":"Konami", 183 | "short_description":"TBD", 184 | "platforms":{ 185 | "1":"MSX2", 186 | "2":"Wii" 187 | }, 188 | "thumb":"http:\/\/media.gamestats.com\/gg\/image\/object\/498\/498104\/mgs2solidsnake_msxboxboxart_160w.jpg" 189 | }, 190 | { 191 | "title":"Metal Gear Solid V: The Phantom Pain", 192 | "score":"10", 193 | "publisher":"Konami", 194 | "short_description":"September 01, 2015 - ...in the Metal Gear franchise, Metal Gear Solid V: The Phantom Pain continues the complex saga of the Snake soldier line, connecting storylines from Metal...", 195 | "platforms":{ 196 | "1":"Xbox One", 197 | "2":"PS4", 198 | "3":"PS3", 199 | "4":"Xbox 360", 200 | "5":"PC" 201 | }, 202 | "thumb":"http:\/\/assets2.ignimgs.com\/2015\/07\/16\/mgs-5-us-esrb-std-xonejpg-1783a7_160w.jpg" 203 | }, 204 | { 205 | "title":"Metal Gear Solid V: Ground Zeroes", 206 | "score":"8", 207 | "publisher":"Konami", 208 | "short_description":"March 18, 2014 - Metal Gear Solid V: Ground Zeroes acts as a prologue to Metal Gear Solid V: The Phantom Pain and takes place one year after the events of Metal Gear Solid:...", 209 | "platforms":{ 210 | "1":"Xbox 360", 211 | "2":"Xbox One", 212 | "3":"PS3", 213 | "4":"PS4", 214 | "5":"PC" 215 | }, 216 | "thumb":"http:\/\/assets2.ignimgs.com\/2014\/03\/18\/metal-gear-solid-v-ground-zeroes-us-esrb-x360jpg-e31ae2_160w.jpg" 217 | }, 218 | { 219 | "title":"Metal Gear Solid 3D Snake Eater", 220 | "score":"8.5", 221 | "publisher":"Konami", 222 | "short_description":"February 21, 2012 - Metal Gear Solid 3D Snake Eater is set during the Cold War of the 60s, and sees series hero Snake infiltrating the Soviet jungle to bring back a scientist...", 223 | "platforms":{ 224 | "1":"3DS" 225 | }, 226 | "thumb":"http:\/\/xbox360media.ign.com\/xbox360\/image\/object\/077\/077733\/MGS_3DS_2D_Final1boxart_160w.jpg" 227 | } 228 | ] 229 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default function destroyApp(application) { 4 | Ember.run(application, 'destroy'); 5 | } 6 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import Ember from 'ember'; 3 | import startApp from '../helpers/start-app'; 4 | import destroyApp from '../helpers/destroy-app'; 5 | 6 | const { RSVP: { resolve } } = Ember; 7 | 8 | export default function(name, options = {}) { 9 | module(name, { 10 | beforeEach() { 11 | this.application = startApp(); 12 | 13 | if (options.beforeEach) { 14 | return options.beforeEach.apply(this, arguments); 15 | } 16 | }, 17 | 18 | afterEach() { 19 | let afterEach = options.afterEach && options.afterEach.apply(this, arguments); 20 | return resolve(afterEach).then(() => destroyApp(this.application)); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /tests/helpers/owner.js: -------------------------------------------------------------------------------- 1 | // https://github.com/emberjs/data/blob/master/tests/helpers/owner.js 2 | 3 | import Ember from 'ember'; 4 | 5 | let Owner; 6 | 7 | if (Ember._RegistryProxyMixin && Ember._ContainerProxyMixin) { 8 | Owner = Ember.Object.extend(Ember._RegistryProxyMixin, Ember._ContainerProxyMixin); 9 | } else { 10 | Owner = Ember.Object.extend(); 11 | } 12 | 13 | export default Owner; 14 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Application from '../../app'; 3 | import config from '../../config/environment'; 4 | 5 | export default function startApp(attrs) { 6 | let attributes = Ember.merge({}, config.APP); 7 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 8 | 9 | return Ember.run(() => { 10 | let application = Application.create(attributes); 11 | application.setupForTesting(); 12 | application.injectTestHelpers(); 13 | return application; 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /tests/helpers/store.js: -------------------------------------------------------------------------------- 1 | // https://raw.githubusercontent.com/emberjs/data/master/tests/helpers/store.js 2 | 3 | import Ember from 'ember'; 4 | import DS from 'ember-data'; 5 | import Owner from './owner'; 6 | 7 | export default function setupStore(options) { 8 | let container, registry, owner; 9 | let env = {}; 10 | options = options || {}; 11 | 12 | if (Ember.Registry) { 13 | registry = env.registry = new Ember.Registry(); 14 | owner = Owner.create({ 15 | __registry__: registry 16 | }); 17 | container = env.container = registry.container({ 18 | owner: owner 19 | }); 20 | owner.__container__ = container; 21 | } else { 22 | container = env.container = new Ember.Container(); 23 | registry = env.registry = container; 24 | } 25 | 26 | env.replaceContainerNormalize = function replaceContainerNormalize(fn) { 27 | if (env.registry) { 28 | env.registry.normalize = fn; 29 | } else { 30 | env.container.normalize = fn; 31 | } 32 | }; 33 | 34 | let adapter = env.adapter = (options.adapter || '-default'); 35 | delete options.adapter; 36 | 37 | if (typeof adapter !== 'string') { 38 | env.registry.register('adapter:-ember-data-test-custom', adapter); 39 | adapter = '-ember-data-test-custom'; 40 | } 41 | 42 | for (let prop in options) { 43 | registry.register('model:' + Ember.String.dasherize(prop), options[prop]); 44 | } 45 | 46 | registry.register('service:store', DS.Store.extend({ 47 | adapter: adapter 48 | })); 49 | 50 | registry.optionsForType('serializer', { singleton: false }); 51 | registry.optionsForType('adapter', { singleton: false }); 52 | registry.register('adapter:-default', DS.Adapter); 53 | 54 | registry.register('serializer:-default', DS.JSONAPISerializer); 55 | registry.register('serializer:-json', DS.JSONSerializer); 56 | registry.register('serializer:-rest', DS.RESTSerializer); 57 | 58 | registry.register('adapter:-rest', DS.RESTAdapter); 59 | registry.register('adapter:-json-api', DS.JSONAPIAdapter); 60 | 61 | registry.injection('serializer', 'store', 'service:store'); 62 | 63 | env.store = container.lookup('service:store'); 64 | env.restSerializer = container.lookup('serializer:-rest'); 65 | env.restSerializer.store = env.store; 66 | env.serializer = env.store.serializerFor('-default'); 67 | env.serializer.store = env.store; 68 | env.adapter = env.store.get('defaultAdapter'); 69 | 70 | return env; 71 | } 72 | 73 | export { setupStore }; 74 | 75 | export function createStore(options) { 76 | return setupStore(options).store; 77 | } 78 | -------------------------------------------------------------------------------- /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 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crodriguez1a/ember-sort-filter-table/21ab87c5e67d6fd4dd2ef55ac9b0bf9edc1fa18b/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/integration/components/each-keys-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('each-keys', 'Integration | Component | each keys', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Set any properties with this.set('myProperty', 'value'); 10 | // Handle any actions with this.on('myAction', function(val) { ... }); 11 | 12 | this.render(hbs`{{each-keys}}`); 13 | 14 | assert.equal(this.$().text().trim(), ''); 15 | 16 | // Template block usage: 17 | this.render(hbs` 18 | {{#each-keys}} 19 | template block text 20 | {{/each-keys}} 21 | `); 22 | 23 | assert.equal(this.$().text().trim(), 'template block text'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/integration/components/em-data-value-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('em-data-value', 'Integration | Component | em data value', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Set any properties with this.set('myProperty', 'value'); 10 | // Handle any actions with this.on('myAction', function(val) { ... }); 11 | 12 | this.render(hbs`{{em-data-value}}`); 13 | 14 | assert.equal(this.$().text().trim(), ''); 15 | 16 | // Template block usage: 17 | this.render(hbs` 18 | {{#em-data-value}} 19 | template block text 20 | {{/em-data-value}} 21 | `); 22 | 23 | assert.equal(this.$().text().trim(), 'template block text'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/integration/components/sf-em-data-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test/*, skip*/ } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import { create, text, count } from 'ember-cli-page-object'; 4 | import RSVP from 'rsvp'; 5 | import {/*setupStore,*/ createStore} from 'dummy/tests/helpers/store'; 6 | import DS from 'ember-data'; 7 | import { run } from '@ember/runloop'; 8 | 9 | let store; 10 | 11 | const person = DS.Model.extend({ 12 | name: DS.attr('string'), 13 | nickName: DS.attr('string') 14 | }); 15 | 16 | const adapter = DS.Adapter.extend({ 17 | deleteRecord() { 18 | return RSVP.Promise.resolve(); 19 | } 20 | }); 21 | 22 | const component = create({ 23 | table: text('.table'), 24 | headings: { count: count(), scope: '.table th', text: text('', {multiple: true }) }, 25 | rows: { count: count(), scope: '.table td', text: text('', {multiple: true }) } 26 | }) 27 | 28 | moduleForComponent('sf-em-data', 'Integration | Component | sf em data', { 29 | integration: true, 30 | beforeEach() { 31 | component.setContext(this); 32 | store = createStore({ adapter, person }); 33 | }, 34 | afterEach() { 35 | component.removeContext(); 36 | } 37 | }); 38 | 39 | test('it renders', function(assert) { 40 | this.render(hbs`{{sf-em-data}}`); 41 | assert.equal(this.$().text().trim(), ''); 42 | 43 | // Template block usage: 44 | this.render(hbs` 45 | {{#sf-em-data}} 46 | template block text 47 | {{/sf-em-data}} 48 | `); 49 | 50 | assert.equal(this.$().text().trim(), 'template block text'); 51 | }); 52 | 53 | test('it renders only the number of rows and columns corresponding with the headings hash: Single', async function(assert) { 54 | let recordArray = store.recordArrayManager 55 | .createAdapterPopulatedRecordArray('person', null); 56 | 57 | let payload = { 58 | data: [ 59 | { 60 | type: 'person', 61 | id: '1', 62 | attributes: { 63 | name: 'Someone MaJoe', 64 | nickName: 'Super MaJoe' 65 | } 66 | } 67 | ] 68 | }; 69 | 70 | await run(() => { 71 | recordArray._setInternalModels(store._push(payload), payload); 72 | }); 73 | 74 | this.model = recordArray; 75 | 76 | this.render(hbs` 77 | {{#sf-table class="is-striped" as | sf |}} 78 | {{sf.headings 79 | headings=(array 80 | (hash key="name" display="Name") 81 | )}} 82 | {{sf.data store=model}} 83 | {{/sf-table}} 84 | `); 85 | assert.equal(component.headings.count, 1, 'A single heading was rendered'); 86 | assert.equal(component.rows.count, 1, 'A single row rendered'); 87 | }); 88 | 89 | test('it renders data in the same order the headings suggest', async function(assert) { 90 | let recordArray = store.recordArrayManager 91 | .createAdapterPopulatedRecordArray('person', null); 92 | 93 | let payload = { 94 | data: [ 95 | { 96 | type: 'person', 97 | id: '1', 98 | attributes: { 99 | name: 'Someone MaJoe', 100 | address: '123 hero lane', 101 | nickName: 'Super MaJoe' 102 | } 103 | } 104 | ] 105 | }; 106 | 107 | await run(() => { 108 | recordArray._setInternalModels(store._push(payload), payload); 109 | }); 110 | 111 | this.model = recordArray; 112 | 113 | this.render(hbs` 114 | {{#sf-table class="is-striped" as | sf |}} 115 | {{sf.headings 116 | headings=(array 117 | (hash key="address" display="Address") 118 | (hash key="nickName" display="Nickety Name") 119 | (hash key="name" display="Name") 120 | )}} 121 | {{sf.data store=model}} 122 | {{/sf-table}} 123 | `); 124 | assert.equal(component.rows.text.join(), "123 hero lane,Super MaJoe,Someone MaJoe", 'It rendered the records arranged in the correct order'); 125 | 126 | this.render(hbs` 127 | {{#sf-table class="is-striped" as | sf |}} 128 | {{sf.headings 129 | headings=(array 130 | (hash key="nickName" display="Nickety Name") 131 | (hash key="name" display="Name") 132 | (hash key="address" display="Address") 133 | )}} 134 | {{sf.data store=model}} 135 | {{/sf-table}} 136 | `); 137 | assert.equal(component.rows.text.join(), "Super MaJoe,Someone MaJoe,123 hero lane", 'It rendered the records arranged in the correct order'); 138 | }); 139 | 140 | test('it renders only the number of rows and columns corresponding with the headings hash: Multiple', async function(assert) { 141 | let recordArray = store.recordArrayManager 142 | .createAdapterPopulatedRecordArray('person', null); 143 | 144 | let payload = { 145 | data: [ 146 | { 147 | type: 'person', 148 | id: '1', 149 | attributes: { 150 | name: 'Someone MaJoe', 151 | nickName: 'Super MaJoe', 152 | occupation: 'super hero', 153 | address: '123 hero lane' 154 | } 155 | } 156 | ] 157 | }; 158 | 159 | await run(() => { 160 | recordArray._setInternalModels(store._push(payload), payload); 161 | }); 162 | 163 | this.model = recordArray; 164 | 165 | this.render(hbs` 166 | {{#sf-table class="is-striped" as | sf |}} 167 | {{sf.headings 168 | headings=(array 169 | (hash key="name" display="Name") 170 | (hash key="nickName" display="Nick Name") 171 | )}} 172 | {{sf.data store=model}} 173 | {{/sf-table}} 174 | `); 175 | // assert.async(); 176 | assert.equal(component.headings.count, 2, 'Multiple headings were rendered'); 177 | assert.equal(component.rows.count, 2, 'Multiple rows were rendered'); 178 | }); 179 | 180 | // TODO unit test POJO extractions 181 | -------------------------------------------------------------------------------- /tests/integration/components/sf-filter-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('sf-filter', 'Integration | Component | sf filter', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Set any properties with this.set('myProperty', 'value'); 10 | // Handle any actions with this.on('myAction', function(val) { ... }); 11 | 12 | this.render(hbs`{{sf-filter}}`); 13 | 14 | assert.equal(this.$().text().trim(), ''); 15 | 16 | // Template block usage: 17 | this.render(hbs` 18 | {{#sf-filter}} 19 | template block text 20 | {{/sf-filter}} 21 | `); 22 | 23 | assert.equal(this.$().text().trim(), 'template block text'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/integration/components/sf-headings-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('sf-headings', 'Integration | Component | sf headings', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Set any properties with this.set('myProperty', 'value'); 10 | // Handle any actions with this.on('myAction', function(val) { ... }); 11 | 12 | this.render(hbs`{{sf-headings}}`); 13 | 14 | assert.equal(this.$().text().trim(), ''); 15 | 16 | // Template block usage: 17 | this.render(hbs` 18 | {{#sf-headings}} 19 | template block text 20 | {{/sf-headings}} 21 | `); 22 | 23 | assert.equal(this.$().text().trim(), 'template block text'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/integration/components/sf-rows-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('sf-rows', 'Integration | Component | sf rows', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Set any properties with this.set('myProperty', 'value'); 10 | // Handle any actions with this.on('myAction', function(val) { ... }); 11 | 12 | this.render(hbs`{{sf-rows}}`); 13 | 14 | assert.equal(this.$().text().trim(), ''); 15 | 16 | // Template block usage: 17 | this.render(hbs` 18 | {{#sf-rows}} 19 | template block text 20 | {{/sf-rows}} 21 | `); 22 | 23 | assert.equal(this.$().text().trim(), 'template block text'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/integration/components/sf-table-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('sf-table', 'Integration | Component | sf table', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Set any properties with this.set('myProperty', 'value'); 10 | // Handle any actions with this.on('myAction', function(val) { ... }); 11 | 12 | this.render(hbs`{{sf-table}}`); 13 | 14 | assert.equal(this.$('.no-data-provided').length, 1); 15 | 16 | // Template block usage: 17 | this.render(hbs` 18 | {{#sf-table}} 19 | template block text 20 | {{/sf-table}} 21 | `); 22 | 23 | assert.equal(this.$().text().trim(), 'template block text'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/integration/components/sort-filter-table-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { moduleForComponent, test, skip } from 'ember-qunit'; 3 | import hbs from 'htmlbars-inline-precompile'; 4 | 5 | // since run-loop is disabled, wrap any code with asynchronous side-effects in a run 6 | const { 7 | run, 8 | set 9 | } = Ember; 10 | 11 | let sample = { 12 | rows: [ 13 | { 14 | 'last_name': 'Billups', 15 | 'first_name': 'Chauncey', 16 | 'display_name': 'Chauncey Billups', 17 | 'birthdate': '1976-09-25', 18 | 'age': 37, 19 | 'birthplace': 'Denver, Colorado, USA', 20 | 'height_in': 75, 21 | 'height_cm': 190.5, 22 | 'height_m': 1.9, 23 | 'height_formatted': '6\'3\'', 24 | 'weight_lb': 202, 25 | 'weight_kg': 91.8, 26 | 'position': 'PG', 27 | 'uniform_number': 1 28 | }, 29 | { 30 | 'last_name': 'Bynum', 31 | 'first_name': 'William', 32 | 'display_name': 'Will Bynum', 33 | 'birthdate': '1983-01-04', 34 | 'age': 30, 35 | 'birthplace': 'Chicago, Illinois, USA', 36 | 'height_in': 72, 37 | 'height_cm': 182.9, 38 | 'height_m': 1.8, 39 | 'height_formatted': '6\'0\'', 40 | 'weight_lb': 185, 41 | 'weight_kg': 84.1, 42 | 'position': 'PG', 43 | 'uniform_number': 12 44 | } 45 | ] 46 | }; 47 | 48 | let hyphen = { 49 | rows: [ 50 | { 51 | 'hyphen-ated': true 52 | } 53 | ] 54 | }; 55 | 56 | let underscore = { 57 | rows: [ 58 | { 59 | 'under_scored_underscored': true 60 | } 61 | ] 62 | }; 63 | 64 | let alphaSort = { 65 | rows: [ 66 | { 67 | name: 'alpha' 68 | }, 69 | { 70 | name: 'zeta' 71 | } 72 | ] 73 | }; 74 | 75 | let alphaSortHypen = { 76 | rows: [ 77 | { 78 | 'the-name': 'alpha' 79 | }, 80 | { 81 | 'the-name': 'zeta' 82 | } 83 | ] 84 | }; 85 | 86 | let alphaSortUnderscore = { 87 | rows: [ 88 | { 89 | 'the_name': 'alpha' 90 | }, 91 | { 92 | 'the_name': 'zeta' 93 | } 94 | ] 95 | }; 96 | 97 | let alphaSortCamelCase = { 98 | rows: [ 99 | { 100 | 'theName': 'alpha' 101 | }, 102 | { 103 | 'theName': 'zeta' 104 | } 105 | ] 106 | }; 107 | 108 | let numericSort = { 109 | rows: [ 110 | { 111 | number: "0" 112 | }, 113 | { 114 | number: "1" 115 | } 116 | ] 117 | }; 118 | 119 | let camelCase = { 120 | rows: [ 121 | { 122 | camelCase: true, 123 | caseCamel: true 124 | } 125 | ] 126 | }; 127 | 128 | let emberObject = Ember.Object.create({ 129 | rows: Ember.A([ 130 | Ember.Object.create({ 131 | foo: 'bar' 132 | }) 133 | ]) 134 | }); 135 | 136 | let privateLabel = { 137 | rows: [ 138 | { 139 | _private: 'foo' 140 | } 141 | ] 142 | }; 143 | 144 | let notAllPrimitive = { 145 | rows: [ 146 | { 147 | hello: 'world', 148 | yo: true, 149 | mtvraps: 1 150 | } 151 | ] 152 | }; 153 | 154 | moduleForComponent('sort-filter-table', 'Integration | Component | sort filter table', { 155 | integration: true 156 | }); 157 | 158 | test('it renders', function(assert) { 159 | // Set any properties with set(this, 'myProperty', 'value'); 160 | // Handle any actions with this.on('myAction', function(val) { ... }); 161 | 162 | this.render(hbs`{{sort-filter-table table=table}}`); 163 | 164 | assert.equal(this.$('.no-data-provided').length, 1); 165 | 166 | // Template block usage: 167 | this.render(hbs` 168 | {{#sort-filter-table}} 169 | template block text 170 | {{/sort-filter-table}} 171 | `); 172 | 173 | assert.equal(this.$().text().trim(), 'template block text'); 174 | }); 175 | 176 | skip('contextual component implementation is backwards compatible', function(assert) { 177 | assert.ok(false); 178 | }); 179 | 180 | test('it assembles header labels', function(assert) { 181 | set(this, 'table', sample); 182 | this.render(hbs`{{sort-filter-table table=table}}`); 183 | 184 | assert.equal(this.$('.table-header').length, 14, 'Correct number of labels are in DOM and in sync with model'); 185 | }); 186 | 187 | test('it excludes headers marked as private with a leading underscore', function(assert) { 188 | set(this, 'table', privateLabel); 189 | this.render(hbs`{{sort-filter-table table=table}}`); 190 | 191 | assert.equal(this.$('.table-header').length, 0, 'When an object key has a leading underscore (private), exclude from DOM'); 192 | }); 193 | 194 | test('it handles headers with underscores, hyphens, spaces, or camel case', function(assert) { 195 | // hyphenated keys 196 | set(this, 'table', hyphen); 197 | this.render(hbs`{{sort-filter-table table=table}}`); 198 | 199 | assert.equal((/-/g).test(this.$('.table-header').text().trim()), false, 'When object keys use hyphens, labels are displayed DOM without hyphens'); 200 | 201 | // keys with underscores 202 | run(() => { 203 | set(this, 'table', underscore); 204 | this.render(hbs`{{sort-filter-table table=table}}`); 205 | 206 | assert.equal((/_/g).test(this.$('.table-header').text().trim()), false, 'When object keys use underscores, labels are displayed DOM without underscores'); 207 | }); 208 | 209 | // keys with camel case 210 | run(() => { 211 | set(this, 'table', camelCase); 212 | this.render(hbs`{{sort-filter-table table=table}}`); 213 | 214 | assert.equal((/[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*/).test(this.$('.table-header').text().trim()), false, 'When object keys use camel case, labels are displayed DOM with no camel casing'); 215 | }); 216 | 217 | }); 218 | 219 | test('it sorts alphabetically', function(assert) { 220 | set(this, 'table', alphaSort); 221 | this.render(hbs`{{sort-filter-table table=table}}`); 222 | 223 | let $sortLabel = this.$('.sort-labels'); 224 | 225 | $sortLabel.click(); 226 | assert.equal(this.$().find('tbody td').first().text().replace(/\n/g, '').replace(/ /g, ''), 'zeta', 'Table was sorted alphabetically'); 227 | 228 | $sortLabel.click(); 229 | assert.equal(this.$().find('tbody td').first().text().replace(/\n/g, '').replace(/ /g, ''), 'alpha', 'Table was sorted again in the reverse'); 230 | 231 | }); 232 | 233 | test('it sorts alphabetically with hyphen separator', function(assert) { 234 | set(this, 'table', alphaSortHypen); 235 | this.render(hbs`{{sort-filter-table table=table}}`); 236 | 237 | let $sortLabel = this.$('.sort-labels'); 238 | 239 | $sortLabel.click(); 240 | assert.equal(this.$().find('tbody td').first().text().replace(/\n/g, '').replace(/ /g, ''), 'zeta', 'Table was sorted alphabetically'); 241 | 242 | $sortLabel.click(); 243 | assert.equal(this.$().find('tbody td').first().text().replace(/\n/g, '').replace(/ /g, ''), 'alpha', 'Table was sorted again in the reverse'); 244 | 245 | }); 246 | 247 | test('it sorts alphabetically with underscore separator', function(assert) { 248 | set(this, 'table', alphaSortUnderscore); 249 | this.render(hbs`{{sort-filter-table table=table}}`); 250 | 251 | let $sortLabel = this.$('.sort-labels'); 252 | 253 | $sortLabel.click(); 254 | assert.equal(this.$().find('tbody td').first().text().replace(/\n/g, '').replace(/ /g, ''), 'zeta', 'Table was sorted alphabetically'); 255 | 256 | $sortLabel.click(); 257 | assert.equal(this.$().find('tbody td').first().text().replace(/\n/g, '').replace(/ /g, ''), 'alpha', 'Table was sorted again in the reverse'); 258 | 259 | }); 260 | 261 | test('it sorts alphabetically with camelCase separator', function(assert) { 262 | set(this, 'table', alphaSortCamelCase); 263 | this.render(hbs`{{sort-filter-table table=table}}`); 264 | 265 | let $sortLabel = this.$('.sort-labels'); 266 | 267 | $sortLabel.click(); 268 | assert.equal(this.$().find('tbody td').first().text().replace(/\n/g, '').replace(/ /g, ''), 'zeta', 'Table was sorted alphabetically'); 269 | 270 | $sortLabel.click(); 271 | assert.equal(this.$().find('tbody td').first().text().replace(/\n/g, '').replace(/ /g, ''), 'alpha', 'Table was sorted again in the reverse'); 272 | 273 | }); 274 | 275 | test('it sorts numerically', function(assert) { 276 | set(this, 'table', numericSort); 277 | this.render(hbs`{{sort-filter-table table=table}}`); 278 | 279 | let $sortLabel = this.$('.sort-labels'); 280 | 281 | $sortLabel.click(); 282 | assert.equal(this.$().find('tbody td').first().text().replace(/\n/g, '').replace(/ /g, ''), '1', 'Table was sorted numerically'); 283 | $sortLabel.click(); 284 | assert.equal(this.$().find('tbody td').first().text().replace(/\n/g, '').replace(/ /g, ''), '0', 'Table was sorted again in the reverse'); 285 | }); 286 | 287 | test('it filters appropriately with multiple filter tems', function(assert) { 288 | set(this, 'table', sample); 289 | this.render(hbs`{{sort-filter-table filter="Chauncey Billups" table=table}}`); 290 | 291 | assert.ok(this.$().find('tbody tr').length > 0, 'When a filter using two query terms (eg., John Doe) is applied, a match is found'); 292 | }); 293 | 294 | test('it handles both POJOs and Ember Objects in the model', function(assert) { 295 | set(this, 'table', emberObject); 296 | this.render(hbs`{{sort-filter-table table=table}}`); 297 | 298 | assert.equal(this.$().find('tbody tr').length, 1, 'When an Ember Object is passed, DOM is populated accordingly'); 299 | }); 300 | 301 | test('it toggles to edit mode', function(assert) { 302 | this.setProperties({ 303 | table: sample, 304 | editable: true, 305 | edit: 'myEditAction', 306 | cancel: 'myCancelAction' 307 | }); 308 | this.render(hbs`{{sort-filter-table editable=true edit=edit cancel=cancel table=table}}`); 309 | 310 | let $editValue = this.$('.edit-value:first'); 311 | $editValue.click(); 312 | 313 | assert.ok(this.$().find('.edit-field:first').length > 0, 'An editable value was clicked, its corresponding form is displayed'); 314 | assert.ok(this.$('.send-edit:first').length > 0, 'An edit button is available'); 315 | assert.ok(this.$('.cancel-edit:first').length > 0, 'A cancel button is available'); 316 | 317 | }); 318 | 319 | test('it sends up the params up to the controller', function(assert) { 320 | let editValues; 321 | let cancelValues; 322 | 323 | // Simulate controller 324 | let myEditAction = (params) => { 325 | editValues = params; 326 | }; 327 | 328 | let myCancelAction = (params) => { 329 | cancelValues = params; 330 | }; 331 | 332 | this.setProperties({ 333 | table: sample, 334 | editable: true, 335 | edit: myEditAction, 336 | cancel: myCancelAction 337 | }); 338 | this.render(hbs`{{sort-filter-table editable=true edit=edit cancel=cancel table=table}}`); 339 | 340 | this.$('.edit-value:first').click(); 341 | this.$('.send-edit:first').click(); 342 | assert.ok(!!editValues, 'The controller\'s edit action received parameters'); 343 | 344 | run(() => { 345 | this.$('.edit-value:first').click(); 346 | this.$('.cancel-edit:first').click(); 347 | assert.ok(!!cancelValues, 'The controller\'s cancel action received parameters'); 348 | }); 349 | 350 | }); 351 | 352 | test('it only allows primitive types in its row headers', function(assert) { 353 | set(this, 'table', notAllPrimitive); 354 | this.render(hbs`{{sort-filter-table table=table}}`); 355 | 356 | assert.equal(this.$('.table-header').length, 3, 'Keys pointing to non primitive types were not included'); 357 | }); 358 | -------------------------------------------------------------------------------- /tests/pages/application.js: -------------------------------------------------------------------------------- 1 | import { 2 | create, 3 | visitable 4 | } from 'ember-cli-page-object'; 5 | import demoNav from '../../tests/pages/components/demo-nav'; 6 | 7 | export default create({ 8 | visit: visitable('/'), 9 | demoNav 10 | }); 11 | -------------------------------------------------------------------------------- /tests/pages/components/demo-nav.js: -------------------------------------------------------------------------------- 1 | import { 2 | clickable, 3 | /* text */ 4 | } from 'ember-cli-page-object'; 5 | 6 | export default { 7 | tabInline: clickable('.t-tab-inline'), 8 | tabBlock: clickable('.t-tab-block'), 9 | tabData: clickable('.t-tab-data') 10 | }; 11 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | import { start } from 'ember-cli-qunit'; 6 | 7 | setResolver(resolver); 8 | start(); 9 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crodriguez1a/ember-sort-filter-table/21ab87c5e67d6fd4dd2ef55ac9b0bf9edc1fa18b/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/helpers/hash-contains-test.js: -------------------------------------------------------------------------------- 1 | import { hashContains } from 'dummy/helpers/hash-contains'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | hash contains'); 5 | 6 | // Replace this with your real tests. 7 | test('it works', function(assert) { 8 | let result = hashContains([{foo:'bax'}, 'bax']); 9 | assert.ok(result); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/unit/helpers/is-image-test.js: -------------------------------------------------------------------------------- 1 | import { isImage } from 'dummy/helpers/is-image'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | is image'); 5 | 6 | // Replace this with your real tests. 7 | test('it works', function(assert) { 8 | let result = isImage(['foo.jpg']); 9 | assert.ok(result); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/unit/helpers/is-number-test.js: -------------------------------------------------------------------------------- 1 | import { isNumber } from 'dummy/helpers/is-number'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | is number'); 5 | 6 | // Replace this with your real tests. 7 | test('it works', function(assert) { 8 | let result = isNumber([42]); 9 | assert.ok(result); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/unit/helpers/is-object-test.js: -------------------------------------------------------------------------------- 1 | import { isObject } from 'dummy/helpers/is-object'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | is object'); 5 | 6 | // Replace this with your real tests. 7 | test('it works', function(assert) { 8 | let result = isObject([{'blue':42}]); 9 | assert.ok(result); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/unit/helpers/is-primitive-test.js: -------------------------------------------------------------------------------- 1 | import { isPrimitive } from 'dummy/helpers/is-primitive'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | is primitive'); 5 | 6 | // Replace this with your real tests. 7 | test('it works', function(assert) { 8 | let result = isPrimitive([42]); 9 | assert.ok(result); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/unit/helpers/is-private-key-test.js: -------------------------------------------------------------------------------- 1 | import { isPrivateKey } from 'dummy/helpers/is-private-key'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | is private key'); 5 | 6 | // Replace this with your real tests. 7 | test('it works', function(assert) { 8 | let result = isPrivateKey(['_foo']); 9 | assert.ok(result); 10 | }); 11 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crodriguez1a/ember-sort-filter-table/21ab87c5e67d6fd4dd2ef55ac9b0bf9edc1fa18b/vendor/.gitkeep --------------------------------------------------------------------------------