├── app
├── .gitkeep
├── templates
│ └── components
│ │ └── fsg-list.hbs
└── components
│ └── fsg-list.js
├── addon
├── .gitkeep
└── components
│ └── fsg-list.js
├── vendor
└── .gitkeep
├── tests
├── unit
│ ├── .gitkeep
│ └── components
│ │ └── fsg-list-test.js
├── dummy
│ ├── public
│ │ ├── .gitkeep
│ │ ├── robots.txt
│ │ └── crossdomain.xml
│ ├── app
│ │ ├── helpers
│ │ │ └── .gitkeep
│ │ ├── models
│ │ │ └── .gitkeep
│ │ ├── routes
│ │ │ └── .gitkeep
│ │ ├── styles
│ │ │ ├── .gitkeep
│ │ │ └── app.css
│ │ ├── views
│ │ │ └── .gitkeep
│ │ ├── components
│ │ │ └── .gitkeep
│ │ ├── controllers
│ │ │ ├── .gitkeep
│ │ │ └── main.js
│ │ ├── templates
│ │ │ ├── .gitkeep
│ │ │ ├── components
│ │ │ │ └── .gitkeep
│ │ │ ├── application.hbs
│ │ │ ├── person.hbs
│ │ │ └── main.hbs
│ │ ├── router.js
│ │ ├── app.js
│ │ └── index.html
│ ├── .jshintrc
│ └── config
│ │ └── environment.js
├── helpers
│ ├── resolver.js
│ └── start-app.js
├── test-helper.js
├── index.html
└── .jshintrc
├── index.js
├── .bowerrc
├── testem.json
├── .travis.yml
├── .gitignore
├── bower.json
├── .editorconfig
├── Brocfile.js
├── package.json
└── README.md
/app/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/addon/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/unit/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/public/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/helpers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/styles/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/views/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/components/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/controllers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/components/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/application.hbs:
--------------------------------------------------------------------------------
1 | {{outlet}}
2 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'ember-cli-fsg-list'
3 | };
4 |
--------------------------------------------------------------------------------
/tests/dummy/public/robots.txt:
--------------------------------------------------------------------------------
1 | # robotstxt.org/
2 |
3 | User-agent: *
4 |
--------------------------------------------------------------------------------
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower_components",
3 | "analytics": false
4 | }
5 |
--------------------------------------------------------------------------------
/app/templates/components/fsg-list.hbs:
--------------------------------------------------------------------------------
1 | {{#each item in fsgList}}
2 | {{partial itemPartial}}
3 | {{/each}}
4 |
--------------------------------------------------------------------------------
/app/components/fsg-list.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import FSGList from 'ember-cli-fsg-list/components/fsg-list';
3 |
4 | export default FSGList;
5 |
--------------------------------------------------------------------------------
/tests/helpers/resolver.js:
--------------------------------------------------------------------------------
1 | import Resolver from 'ember/resolver';
2 |
3 | var resolver = Resolver.create();
4 |
5 | resolver.namespace = {
6 | modulePrefix: 'dummy'
7 | };
8 |
9 | export default resolver;
10 |
--------------------------------------------------------------------------------
/testem.json:
--------------------------------------------------------------------------------
1 | {
2 | "framework": "qunit",
3 | "test_page": "tests/index.html",
4 | "launch_in_ci": [
5 | "PhantomJS"
6 | ],
7 | "launch_in_dev": [
8 | "PhantomJS",
9 | "Chrome"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/tests/dummy/app/router.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | var Router = Ember.Router.extend({
4 | location: DummyENV.locationType
5 | });
6 |
7 | Router.map(function() {
8 | this.route('main');
9 | });
10 |
11 | export default Router;
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: node_js
3 |
4 | sudo: false
5 |
6 | cache:
7 | directories:
8 | - node_modules
9 |
10 | install:
11 | - npm install -g bower
12 | - npm install
13 | - bower install
14 |
15 | script:
16 | - npm test
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://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 | testem.log
18 |
--------------------------------------------------------------------------------
/tests/dummy/app/styles/app.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | margin: 20px;
3 | }
4 | span.id {
5 | display: inline-block;
6 | width: 20px;
7 | }
8 | span.name {
9 | display: inline-block;
10 | width: 150px;
11 | }
12 |
13 | button {
14 | font-size: 15px;
15 | width: 200px;
16 | height: 40px;
17 | }
18 |
19 | .active {
20 | background: #337ab7;
21 | }
22 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/person.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{#if item._isTitle}}
3 | {{item._title}}
4 | {{else}}
5 |
6 | {{item.id}}
7 | {{item.name}}
8 | {{item.occupation}}
9 |
10 | {{/if}}
11 |
12 |
--------------------------------------------------------------------------------
/tests/dummy/app/app.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import Resolver from 'ember/resolver';
3 | import loadInitializers from 'ember/load-initializers';
4 |
5 | Ember.MODEL_FACTORY_INJECTIONS = true;
6 |
7 | var App = Ember.Application.extend({
8 | modulePrefix: 'dummy', // TODO: loaded via config
9 | Resolver: Resolver
10 | });
11 |
12 | loadInitializers(App, 'dummy');
13 |
14 | export default App;
15 |
--------------------------------------------------------------------------------
/tests/test-helper.js:
--------------------------------------------------------------------------------
1 | import resolver from './helpers/resolver';
2 | import {
3 | setResolver
4 | } from 'ember-qunit';
5 |
6 | setResolver(resolver);
7 |
8 | document.write('');
9 |
10 | QUnit.config.urlConfig.push({ id: 'nocontainer', label: 'Hide container'});
11 | if (QUnit.urlParams.nocontainer) {
12 | document.getElementById('ember-testing-container').style.visibility = 'hidden';
13 | } else {
14 | document.getElementById('ember-testing-container').style.visibility = 'visible';
15 | }
16 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dummy",
3 | "dependencies": {
4 | "handlebars": "~1.3.0",
5 | "jquery": "^1.11.1",
6 | "ember": "1.7.0",
7 | "ember-resolver": "~0.1.7",
8 | "loader": "stefanpenner/loader.js#1.0.1",
9 | "ember-cli-shims": "stefanpenner/ember-cli-shims#0.0.3",
10 | "ember-cli-test-loader": "rwjblue/ember-cli-test-loader#0.0.4",
11 | "ember-load-initializers": "stefanpenner/ember-load-initializers#0.0.2",
12 | "ember-qunit": "0.1.8",
13 | "ember-qunit-notifications": "0.0.4",
14 | "qunit": "~1.15.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/main.hbs:
--------------------------------------------------------------------------------
1 | Main
2 | Filter: {{input value=filterTerm}}
3 |
4 | Order:
5 |
6 |
7 |
8 |
9 | {{fsg-list
10 | list = list
11 | itemPartial = itemPartial
12 | filterTerm = filterTerm
13 | filterBy = filterKeys
14 | sortBy = sortKeys
15 | groupBy = groupFn
16 | actionName = 'clickItem'
17 | }}
18 |
19 |
--------------------------------------------------------------------------------
/.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 | [*.js]
17 | indent_style = space
18 | indent_size = 2
19 |
20 | [*.hbs]
21 | indent_style = space
22 | indent_size = 2
23 |
24 | [*.css]
25 | indent_style = space
26 | indent_size = 2
27 |
28 | [*.html]
29 | indent_style = space
30 | indent_size = 2
31 |
32 | [*.md]
33 | trim_trailing_whitespace = false
34 |
--------------------------------------------------------------------------------
/tests/dummy/public/crossdomain.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/dummy/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "predef": {
3 | "document": true,
4 | "window": true,
5 | "-Promise": true,
6 | "DummyENV": true
7 | },
8 | "browser" : true,
9 | "boss" : true,
10 | "curly": true,
11 | "debug": false,
12 | "devel": true,
13 | "eqeqeq": true,
14 | "evil": true,
15 | "forin": false,
16 | "immed": false,
17 | "laxbreak": false,
18 | "newcap": true,
19 | "noarg": true,
20 | "noempty": false,
21 | "nonew": false,
22 | "nomen": false,
23 | "onevar": false,
24 | "plusplus": false,
25 | "regexp": false,
26 | "undef": true,
27 | "sub": true,
28 | "strict": false,
29 | "white": false,
30 | "eqnull": true,
31 | "esnext": true,
32 | "unused": true
33 | }
34 |
--------------------------------------------------------------------------------
/tests/helpers/start-app.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import Application from 'dummy/app';
3 | import Router from 'dummy/router';
4 |
5 | export default function startApp(attrs) {
6 | var App;
7 |
8 | var attributes = Ember.merge({
9 | // useful Test defaults
10 | rootElement: '#ember-testing',
11 | LOG_ACTIVE_GENERATION: false,
12 | LOG_VIEW_LOOKUPS: false
13 | }, attrs); // but you can override;
14 |
15 | Router.reopen({
16 | location: 'none'
17 | });
18 |
19 | Ember.run(function() {
20 | App = Application.create(attributes);
21 | App.setupForTesting();
22 | App.injectTestHelpers();
23 | });
24 |
25 | App.reset(); // this shouldn't be needed, i want to be able to "start an app at a specific URL"
26 |
27 | return App;
28 | }
29 |
--------------------------------------------------------------------------------
/Brocfile.js:
--------------------------------------------------------------------------------
1 | /* global require, module */
2 |
3 | var EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
4 |
5 | var app = new EmberAddon();
6 |
7 | // Use `app.import` to add additional libraries to the generated
8 | // output files.
9 | //
10 | // If you need to use different assets in different
11 | // environments, specify an object as the first parameter. That
12 | // object's keys should be the environment name and the values
13 | // should be the asset to use in that environment.
14 | //
15 | // If the library that you are including contains AMD or ES6
16 | // modules that you would like to import into your application
17 | // please specify an object with the list of modules as keys
18 | // along with the exports of each module as its value.
19 |
20 | module.exports = app.toTree();
21 |
--------------------------------------------------------------------------------
/tests/dummy/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Dummy
7 |
8 |
9 |
10 | {{BASE_TAG}}
11 |
12 |
13 |
14 |
15 |
16 |
20 |
21 |
22 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/tests/dummy/config/environment.js:
--------------------------------------------------------------------------------
1 | /* jshint node: true */
2 |
3 | module.exports = function(environment) {
4 | var ENV = {
5 | environment: environment,
6 | baseURL: '/',
7 | locationType: 'auto',
8 | EmberENV: {
9 | FEATURES: {
10 | // Here you can enable experimental features on an ember canary build
11 | // e.g. 'with-controller': true
12 | }
13 | },
14 |
15 | APP: {
16 | // Here you can pass flags/options to your application instance
17 | // when it is created
18 | }
19 | };
20 |
21 | if (environment === 'development') {
22 | // ENV.APP.LOG_RESOLVER = true;
23 | ENV.APP.LOG_ACTIVE_GENERATION = true;
24 | // ENV.APP.LOG_TRANSITIONS = true;
25 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
26 | ENV.APP.LOG_VIEW_LOOKUPS = true;
27 | }
28 |
29 | if (environment === 'test') {
30 | ENV.baseURL = '/'; // Testem prefers this...
31 | }
32 |
33 | if (environment === 'production') {
34 |
35 | }
36 |
37 | return ENV;
38 | };
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ember-cli-fsg-list",
3 | "version": "0.0.4",
4 | "directories": {
5 | "doc": "doc",
6 | "test": "test"
7 | },
8 | "scripts": {
9 | "start": "ember server",
10 | "build": "ember build",
11 | "test": "ember test"
12 | },
13 | "repository": "https://github.com/poetic/ember-cli-fsg-list",
14 | "engines": {
15 | "node": ">= 0.10.0"
16 | },
17 | "keywords": [
18 | "ember-addon",
19 | "filtered sorted grouped list component",
20 | "list component",
21 | "component"
22 | ],
23 | "ember-addon": {
24 | "configPath": "tests/dummy/config"
25 | },
26 | "author": "Chun Yang",
27 | "license": "MIT",
28 | "devDependencies": {
29 | "body-parser": "^1.2.0",
30 | "broccoli-asset-rev": "0.0.17",
31 | "broccoli-ember-hbs-template-compiler": "^1.6.1",
32 | "ember-cli": "0.0.44",
33 | "ember-cli-ember-data": "0.1.0",
34 | "ember-cli-ic-ajax": "0.1.1",
35 | "ember-cli-qunit": "0.0.5",
36 | "express": "^4.8.5",
37 | "glob": "^4.0.5"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Dummy Tests
7 |
8 |
9 |
10 | {{BASE_TAG}}
11 |
12 |
13 |
14 |
15 |
31 |
32 |
33 |
34 |
35 |
36 |
40 |
41 |
42 |
43 |
44 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/tests/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "predef": [
3 | "document",
4 | "window",
5 | "location",
6 | "setTimeout",
7 | "$",
8 | "-Promise",
9 | "QUnit",
10 | "define",
11 | "console",
12 | "equal",
13 | "notEqual",
14 | "notStrictEqual",
15 | "test",
16 | "asyncTest",
17 | "testBoth",
18 | "testWithDefault",
19 | "raises",
20 | "throws",
21 | "deepEqual",
22 | "start",
23 | "stop",
24 | "ok",
25 | "strictEqual",
26 | "module",
27 | "moduleFor",
28 | "moduleForComponent",
29 | "moduleForModel",
30 | "process",
31 | "expect",
32 | "visit",
33 | "exists",
34 | "fillIn",
35 | "click",
36 | "keyEvent",
37 | "triggerEvent",
38 | "find",
39 | "findWithAssert",
40 | "wait",
41 | "DS",
42 | "keyEvent",
43 | "isolatedContainer",
44 | "startApp",
45 | "andThen",
46 | "currentURL",
47 | "currentPath",
48 | "currentRouteName"
49 | ],
50 | "node": false,
51 | "browser": false,
52 | "boss": true,
53 | "curly": false,
54 | "debug": false,
55 | "devel": false,
56 | "eqeqeq": true,
57 | "evil": true,
58 | "forin": false,
59 | "immed": false,
60 | "laxbreak": false,
61 | "newcap": true,
62 | "noarg": true,
63 | "noempty": false,
64 | "nonew": false,
65 | "nomen": false,
66 | "onevar": false,
67 | "plusplus": false,
68 | "regexp": false,
69 | "undef": true,
70 | "sub": true,
71 | "strict": false,
72 | "white": false,
73 | "eqnull": true,
74 | "esnext": true
75 | }
76 |
--------------------------------------------------------------------------------
/tests/dummy/app/controllers/main.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | var MainController = Ember.Controller.extend({
4 | list: [
5 | {id: 1 , name: 'Chun Yang' , occupation: 'Web Developer'} ,
6 | {id: 2 , name: 'Dummy A' , occupation: 'Web Developer'} ,
7 | {id: 3 , name: 'Dummy B' , occupation: 'QA'} ,
8 | {id: 4 , name: 'Ellen Degeneres' , occupation: 'QA'} ,
9 | {id: 5 , name: 'Jackie Chan' , occupation: 'Manager'} ,
10 | ].map(function(obj){
11 | return Ember.Object.create(obj);
12 | }),
13 | itemPartial: 'person',
14 |
15 | // ---------- Filter
16 | filterTerm: null,
17 | filterKeys: ['name'],
18 | filterFn: function(item){
19 | return item.get('id') > this.get('filterTerm');
20 | },
21 |
22 | // ---------- Sort
23 | sortKeys: function(){
24 | var orders = [];
25 |
26 | ['occupationOrder', 'nameOrder'].forEach(function(orderKey){
27 | if(this.get(orderKey)){
28 | orders.pushObject(orderKey.replace(/Order$/, ':') + this.get(orderKey));
29 | }
30 | }.bind(this));
31 |
32 | return orders;
33 | }.property('occupationOrder', 'nameOrder'),
34 | sortFn: function(a, b){
35 | return b.get('id') - a.get('id');
36 | },
37 |
38 | // ---------- Group
39 | groupFn: function(item){
40 | return item.occupation;
41 | },
42 |
43 | actions: {
44 | toggleOrder: function(key){
45 | if(this.get(key) === 'asc'){
46 | this.set(key, 'desc');
47 | } else {
48 | this.set(key, 'asc');
49 | }
50 | },
51 | clickItem: function(item){
52 | var element = Ember.$('.id:contains("' + item.get('id') + '")');
53 | element.parent().toggleClass('active');
54 | },
55 | }
56 | });
57 |
58 | export default MainController;
59 |
--------------------------------------------------------------------------------
/addon/components/fsg-list.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | // helpers
4 | var computed = {
5 | keys: function(sourceKey){
6 | return function(){
7 | var source = this.get(sourceKey);
8 | if(Ember.isArray(source) && source.length > 0) {
9 | return source;
10 | }
11 | }.property(sourceKey + '.[]');
12 | },
13 |
14 | fn: function(sourceKey){
15 | return function(){
16 | var source = this.get(sourceKey);
17 | if(typeof source === 'function') {
18 | return source;
19 | }
20 | }.property(sourceKey);
21 | },
22 | };
23 |
24 | var FilteredSortedGroupedListComponent = Ember.Component.extend({
25 | list: [],
26 |
27 | // itemPartial
28 | itemPartial: '',
29 |
30 | // itemAction
31 | itemAction: null,
32 | actions: {
33 | _selectItem: function(){
34 | // Bubble action up to the controller
35 | if(this.get('actionName')){
36 | // Only for testing
37 | if(typeof DummyENV !== 'undefined') {
38 | if(DummyENV.environment === 'test' && window._counter >=0 ) {
39 | window._counter += 1;
40 | }
41 | }
42 | this.sendAction.bind(this, 'actionName').apply(this, arguments);
43 | }
44 | }
45 | },
46 |
47 | // ---------- filter
48 | filterTerm: null,
49 | // filter can be a function of an array of list item keys
50 | // function : function(item, index, list)
51 | // keys : ['id', 'name']
52 | filterBy: [],
53 |
54 | _filterKeys: computed.keys('filterBy'),
55 | _filterFn: computed.fn('filterBy'),
56 | _defaultFilterFn: function(item){
57 | var purify = function(dirtyStr){
58 | return dirtyStr.toLowerCase().replace(/\s+/g, '');
59 | };
60 | var getValue = function(key){
61 | return item.get(key);
62 | };
63 | var stack = purify(this.get('_filterKeys').map(getValue).join(''));
64 | var needle = purify(this.get('filterTerm'));
65 | return stack.indexOf(needle) > -1;
66 | },
67 |
68 | _fList: function(){
69 | var list = this.get('list').toArray();
70 |
71 | if(!this.get('filterTerm')){
72 | return list;
73 | }
74 | if(this.get('_filterKeys')) {
75 | return list.filter(this.get('_defaultFilterFn').bind(this));
76 | }
77 | if(this.get('_filterFn')) {
78 | return list.filter(this.get('_filterFn').bind(this));
79 | }
80 | return list;
81 | }.property('list.[]', 'filterTerm', '_filterKeys', '_filterFn'),
82 |
83 | // ---------- sort
84 | sortBy: [],
85 |
86 | _sortKeys: computed.keys('sortBy'),
87 | _sortFn: computed.fn('sortBy'),
88 | _defaultSortFn: function(a, b){
89 | var compareValue;
90 | this.get('_sortKeys').some(function(metaKey){
91 | var keys = metaKey.split(':');
92 | var key = keys[0];
93 | var asc = keys[1] === 'desc' ? -1 : 1;
94 | var propA = a.get(key);
95 | var propB = b.get(key);
96 | compareValue = Ember.compare(propA, propB) * asc;
97 | return compareValue;
98 | });
99 | return compareValue || 0;
100 | },
101 |
102 | _fsList: function(){
103 | var list = this.get('_fList').toArray();
104 |
105 | if(this.get('_sortKeys')) {
106 | return list.sort(this.get('_defaultSortFn').bind(this));
107 | }
108 | if(this.get('_sortFn')) {
109 | return list.sort(this.get('_sortFn').bind(this));
110 | }
111 | return list;
112 | }.property('_fList', '_sortKeys', '_sortFn'),
113 |
114 | // ---------- group
115 | groupBy: null,
116 | groupTitleAttrs: [],
117 |
118 | _fsgList: function(){
119 | var groupBy = this.get('groupBy');
120 | var list = this.get('_fsList').toArray();
121 |
122 | if(!groupBy){
123 | return list;
124 | }
125 |
126 | var groups = this._groupsFromList(list, groupBy);
127 | var fsgList = this._listFromGroups(groups);
128 |
129 | return fsgList;
130 | }.property('_fsList'),
131 |
132 | _groupsFromList: function(list, groupBy){
133 | var addItemToGroup = function(groups, item){
134 | var key = groupBy(item);
135 | if(groups[key]){
136 | groups[key].push(item);
137 | } else {
138 | groups[key] = [item];
139 | }
140 | return groups;
141 | };
142 |
143 | return list.reduce(addItemToGroup, {});
144 | },
145 |
146 | _listFromGroups: function(groups){
147 | var component = this;
148 | var list = [];
149 | for(var key in groups){
150 | var titleObj = {_isTitle: true, _title: key};
151 | component.get('groupTitleAttrs').forEach(function(attr){
152 | var fn = component.get(attr);
153 | titleObj[attr] = fn(key);
154 | });
155 | list.pushObject(titleObj);
156 | list.pushObjects(groups[key]);
157 | }
158 | return list;
159 | },
160 |
161 | fsgList: Ember.computed.alias('_fsgList'),
162 | });
163 |
164 | export default FilteredSortedGroupedListComponent;
165 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ember-cli-fsg-list
2 | ember-cli-fsg-list is an EmberCLI addon.
3 | It is a filterable, sortable, and groupable Ember component.
4 | You can also assign custom action to the items in the list.
5 |
6 | It is really helpful when you want to create something like this:
7 |
8 | 
9 |
10 | # Example:
11 | - In your ember app, run:
12 | ```bash
13 | npm install ember-cli-fsg-list --save-dev
14 | ```
15 |
16 | - In your controller 'app/controllers/demo.js':
17 | ```javascript
18 | var DemoController = Ember.Controller.extend({
19 | // you MUST provide an Ember array of Ember object, E.G. a model of list
20 | list: [
21 | {id: 1 , name: 'Chun Yang' , occupation: 'Web Developer'} ,
22 | {id: 2 , name: 'Dummy A' , occupation: 'Web Developer'} ,
23 | {id: 3 , name: 'Dummy B' , occupation: 'QA'} ,
24 | {id: 4 , name: 'Ellen Degeneres' , occupation: 'QA'} ,
25 | {id: 5 , name: 'Jackie Chan' , occupation: 'Manager'} ,
26 | ].map(function(obj){
27 | return Ember.Object.create(obj);
28 | }),
29 |
30 | // you MUST provide a partial for each item in the list
31 | itemPartial: 'person',
32 |
33 | // ---------- Filter(optional)
34 | filterTerm: null, // this is bounded to a input element in the template
35 | // you can assign an array OR a function to filterBy
36 | filterKeys: ['name'],
37 | filterFn: function(item){
38 | return item.get('id') > this.get('filterTerm');
39 | },
40 | // if you do not provide a filterFn, the _defaultFilterFn is used:
41 | // E.G. 'a b' will match 'Abc', 'Dabc', 'De a b f' and so on.
42 | _defaultFilterFn: function(item){
43 | var purify = function(dirtyStr){
44 | return dirtyStr.toLowerCase().replace(/\s+/g, '');
45 | };
46 | var getValue = function(key){
47 | return item.get(key);
48 | };
49 | var stack = purify(this.get('_filterKeys').map(getValue).join(''));
50 | var needle = purify(this.get('filterTerm'));
51 | return stack.indexOf(needle) > -1;
52 | },
53 |
54 | // ---------- Sort(optional)
55 | // you can assign an array OR a function to sortBy
56 | sortKeys: ['occupation', 'id:asc', 'name:desc'],
57 | sortFn: function(a, b){
58 | return a.get('id') - b.get('id');
59 | },
60 |
61 | // ---------- Group(optional)
62 | // you can provide a function to groupBy
63 | groupFn: function(item){
64 | return item.occupation;
65 | },
66 |
67 | // you can provide custom attributes to the title item in a group
68 | // each attribute is a function with the group title obtained from groupFn
69 | // as argument
70 | groupTitleAttrs: ['titleImageUrl'],
71 | titleImageUrl: function(title){
72 | return 'http://' + title + '.com';
73 | },
74 |
75 | // ---------- Actions(optional)
76 | actions: {
77 | logToConsole: function(item){
78 | console.log('The item you clicked is: ', item);
79 | }
80 | }
81 | });
82 | ```
83 |
84 | - In your template 'app/templates/demo.hbs'
85 | ```javascript
86 | {{input value=filterTerm placeholder="name"}}
87 |
88 | {{fsg-list
89 | list = list
90 | itemPartial = itemPartial
91 | filterTerm = filterTerm
92 | filterBy = filterKeys
93 | sortBy = sortKeys
94 | groupBy = groupFn
95 | groupTitleAttrs = groupTitleAttrs
96 | actionName = 'logToConsole'
97 | titleImageUrl = titleImageUrl
98 | }}
99 |
100 | ```
101 |
102 | - In your partial template 'app/template/person.hbs'
103 | ```html
104 |
105 |
106 | {{#if item._isTitle}}
107 | {{item._title}}
108 |
109 | {{else}}
110 |
111 |
112 | {{item.id}}
113 | ({{item.occupation}})
114 | {{item.name}}
115 |
116 | {{/if}}
117 |
118 | ```
119 |
120 | # Component variables:
121 | - list: Ember array of Ember objects
122 | - itemPartial: string, template name
123 | - filterTerm: string
124 | - filterBy: array of strings OR function
125 | - sortBy: array of strings OR function
126 | - groupBy: a function
127 | - groupTitleAttrs: array of strings
128 | - actionName: string, action name in your controller
129 |
130 | # Emitted variables from the component to partial template:
131 | - item: Ember object, an object from the input list
132 | - item.\_isTitle: boolean, if this item is a group title
133 | - item.\_title: string, output of the group function
134 | - \_selectItem: function, used to bubble up the action to your controller
135 |
--------------------------------------------------------------------------------
/tests/unit/components/fsg-list-test.js:
--------------------------------------------------------------------------------
1 | import {
2 | moduleForComponent,
3 | test
4 | } from 'ember-qunit';
5 | import Ember from 'ember';
6 |
7 | // list data used for testing;
8 | var list = [
9 | {id: 1 , name: 'Chun Yang' , occupation: 'Web Developer'} ,
10 | {id: 2 , name: 'Dummy A' , occupation: 'Web Developer'} ,
11 | {id: 3 , name: 'Dummy B' , occupation: 'QA'} ,
12 | {id: 4 , name: 'Ellen Degeneres' , occupation: 'QA'} ,
13 | {id: 5 , name: 'Jackie Chan' , occupation: 'Manager'} ,
14 | ].map(function(obj){
15 | return Ember.Object.create(obj);
16 | });
17 | list = Ember.A(list);
18 |
19 | moduleForComponent('fsg-list', 'FsgListComponent', {
20 | // specify the other units that are required for this test
21 | needs: ['template:person'],
22 | setup: function(){
23 | var component = this.subject();
24 | component.set('list', list);
25 | component.set('itemPartial', 'person');
26 | },
27 | });
28 |
29 | test('it renders', function() {
30 | expect(2);
31 |
32 | // creates the component instance
33 | var component = this.subject();
34 | equal(component._state, 'preRender');
35 |
36 | // appends the component to the page
37 | this.append();
38 | equal(component._state, 'inDOM');
39 | });
40 |
41 | test('it shows a list of partials', function(){
42 | expect(5);
43 |
44 | this.append();
45 |
46 | var items = $('.item');
47 | list.forEach(function(item, index){
48 | var name = items.eq(index).find('.name').text();
49 | equal(name, item.get('name'));
50 | });
51 | });
52 |
53 | test('it should show new item when we add one', function(){
54 | expect(2);
55 |
56 | var listCopy = Ember.copy(list);
57 |
58 | var component = this.subject();
59 | component.set('list', listCopy);
60 | component.set('itemPartial', 'person');
61 | this.append();
62 |
63 | equal($('.item').length, 5);
64 |
65 | Ember.run(function(){
66 | listCopy.pushObject(Ember.Object.create({
67 | id: 100, name: 'Mike Chun', occupation: 'Web Designer'
68 | }));
69 | });
70 |
71 | equal($('.item').length, 6);
72 | });
73 |
74 | test('it should filter the list by filterTerm and filter(filterKeys)', function(){
75 | expect(5);
76 |
77 | var component = this.subject();
78 | component.set('filterTerm', 'Chun');
79 | component.set('filterBy', ['name']);
80 | this.append();
81 |
82 | var items = $('.item');
83 | list.forEach(function(item){
84 | var selector = '.name:contains("' + item.get('name') + '")';
85 | var exist = !!items.find(selector).length;
86 | if(item.get('name') === 'Chun Yang'){
87 | equal(exist, true);
88 | } else {
89 | equal(exist, false);
90 | }
91 | });
92 | });
93 |
94 | test('it should filter the list by filterTerm and filter(filterFn)', function(){
95 | expect(5);
96 |
97 | var component = this.subject();
98 | component.set('filterTerm', 'Chun');
99 | component.set('filterBy', function(item){
100 | return item.get('id') > 3;
101 | });
102 | this.append();
103 |
104 | var items = $('.item');
105 | list.forEach(function(item){
106 | var selector = '.id:contains("' + item.get('id') + '")';
107 | var exist = !!items.find(selector).length;
108 | if(item.get('id') > 3){
109 | equal(exist, true);
110 | } else {
111 | equal(exist, false);
112 | }
113 | });
114 | });
115 |
116 | test('it can be sorted by an array of strings', function(){
117 | expect(2);
118 |
119 | var component = this.subject();
120 | component.set('sortBy', ['occupation:desc', 'name:desc']);
121 | this.append();
122 |
123 | var sequence = $('.item .id').text();
124 | deepEqual(sequence, '21435');
125 |
126 | Ember.run(function(){
127 | component.set('sortBy', ['id']);
128 | });
129 |
130 | sequence = $('.item .id').text();
131 | deepEqual(sequence, '12345');
132 | });
133 |
134 | test('it can be sorted by a function', function(){
135 | expect(1);
136 |
137 | var component = this.subject();
138 | component.set('sortBy', function(a, b){
139 | return b.get('id') - a.get('id');
140 | });
141 | this.append();
142 |
143 | var sequence = $('.item .id').text();
144 | deepEqual(sequence, '54321');
145 | });
146 |
147 | test('it can be grouped by a function', function(){
148 | expect(3);
149 |
150 | var component = this.subject();
151 | component.set('groupBy', function(item){
152 | return item.get('occupation');
153 | });
154 | this.append();
155 |
156 | equal($('.item').length, 8);
157 | equal($('.item .id').length, 5);
158 | equal($('.item .title').length, 3);
159 | });
160 |
161 | test('it can be filtered, sorted and grouped', function(){
162 | expect(2);
163 |
164 | var component = this.subject();
165 | component.set('sortBy', ['name:desc']);
166 | component.set('filterBy', ['name', 'occupation']);
167 | component.set('filterTerm', 'm');
168 | component.set('groupBy', function(item){
169 | return item.get('occupation');
170 | });
171 | this.append();
172 |
173 | equal($('.item .id').text(), '532', 'it is filtered and sorted');
174 | equal($('.item .title').text().replace(/\s+/g, ''),
175 | 'ManagerQAWebDeveloper',
176 | 'it is grouped');
177 | });
178 |
179 | test('it can trigger an action when clicked', function(){
180 | expect(1);
181 |
182 | window._counter = 0;
183 | var component = this.subject();
184 | component.set('actionName', 'clickItem');
185 | this.append();
186 |
187 | $('.item div').first().click();
188 | equal(window._counter, 1);
189 | });
190 |
--------------------------------------------------------------------------------