├── rollup ├── cdn.js └── build.js ├── src ├── slice.ts ├── directives │ ├── summary.ts │ ├── working-indicator.ts │ ├── search.ts │ ├── filter.ts │ ├── sort.ts │ ├── pagination.ts │ └── table.ts └── index.ts ├── README.md ├── tsconfig.json ├── test ├── directives │ ├── summary.js │ ├── workingIndicator.js │ ├── search.js │ ├── filter.js │ ├── pagination.js │ ├── sort.js │ └── table.js ├── slice.js └── table.js ├── .gitignore ├── .circleci └── config.yml ├── LICENSE └── package.json /rollup/cdn.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import pkg from '../package.json'; 3 | 4 | const name = pkg.name.replace(/-/g, '_'); 5 | 6 | export default { 7 | input: './dist/src/index.js', 8 | output: [{ 9 | format: 'es', 10 | file: `./dist/bundle/${name}.js`, 11 | sourcemap: true 12 | }], 13 | plugins: [resolve()] 14 | }; 15 | -------------------------------------------------------------------------------- /src/slice.ts: -------------------------------------------------------------------------------- 1 | export interface SliceConfiguration { 2 | page?: number; 3 | size?: number; 4 | } 5 | 6 | export const sliceFactory = ({page = 1, size}: SliceConfiguration = {page: 1}) => (array: T[] = []): T[] => { 7 | const actualSize = size || array.length; 8 | const offset = (page - 1) * actualSize; 9 | return array.slice(offset, offset + actualSize); 10 | }; 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smart-table-core 2 | 3 | [![CircleCI](https://circleci.com/gh/smart-table/smart-table-core.svg?style=svg)](https://circleci.com/gh/smart-table/smart-table-core) 4 | [![Gitter](https://badges.gitter.im/join_chat.svg)](https://gitter.im/smart-table/Lobby) 5 | 6 | smart table core programmatic behaviors (no view related) 7 | 8 | see [documentation](https://smart-table.github.io/www/dist/) 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2017", 5 | "dom" 6 | ], 7 | "module": "es2015", 8 | "moduleResolution": "node", 9 | "noImplicitAny": false, 10 | "preserveConstEnums": true, 11 | "declaration": true, 12 | "declarationDir": "./dist/declarations", 13 | "target": "es2017" 14 | }, 15 | "include": [ 16 | "./dist/src/*" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test/directives/summary.js: -------------------------------------------------------------------------------- 1 | import {emitter} from 'smart-table-events'; 2 | import {SmartTableEvents as evts, summaryDirective as summary} from '../../dist/bundle/module.js'; 3 | 4 | export default ({test}) => { 5 | test('summary directive should be able to register listener', (t) => { 6 | let counter = 0; 7 | const table = emitter(); 8 | const s = summary({table}); 9 | s.onSummaryChange(() => counter++); 10 | table.dispatch(evts.SUMMARY_CHANGED); 11 | t.equal(counter, 1, 'should have updated the counter'); 12 | }); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /test/directives/workingIndicator.js: -------------------------------------------------------------------------------- 1 | import {SmartTableEvents as evts, workingIndicatorDirective as workingIndicator} from '../../dist/bundle/module.js'; 2 | import {emitter} from 'smart-table-events'; 3 | 4 | export default ({test}) => { 5 | test('summary directive should be able to register listener', t => { 6 | let counter = 0; 7 | const table = emitter(); 8 | const s = workingIndicator({table}); 9 | s.onExecutionChange(() => counter++); 10 | table.dispatch(evts.EXEC_CHANGED); 11 | t.equal(counter, 1, 'should have updated the counter'); 12 | }); 13 | } -------------------------------------------------------------------------------- /rollup/build.js: -------------------------------------------------------------------------------- 1 | import pkg from '../package.json'; 2 | 3 | const main = pkg.main; 4 | const module = pkg.module; 5 | 6 | export default { 7 | input: './dist/src/index.js', 8 | output: [{ 9 | format: 'es', 10 | file: `${main + '.mjs'}` 11 | }, { 12 | format: 'es', 13 | file: `${module}` 14 | }, { 15 | format: 'cjs', 16 | file: `${main + '.js'}` 17 | }], 18 | external: [ 19 | 'smart-table-events', 20 | 'smart-table-filter', 21 | 'smart-table-json-pointer', 22 | 'smart-table-operators', 23 | 'smart-table-search', 24 | 'smart-table-sort' 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .idea 40 | .DS_Store 41 | dist 42 | -------------------------------------------------------------------------------- /test/slice.js: -------------------------------------------------------------------------------- 1 | import {sliceFactory as slicer} from '../dist/bundle/module.js'; 2 | 3 | export default ({test}) => { 4 | test('slice: get a page with specified size', (t) => { 5 | const input = [1, 2, 3, 4, 5, 6, 7]; 6 | const output = slicer({page: 1, size: 5})(input); 7 | t.deepEqual(output, [1, 2, 3, 4, 5]); 8 | }); 9 | test('slice: get a partial page if size is too big', (t) => { 10 | const input = [1, 2, 3, 4, 5, 6, 7]; 11 | const output = slicer({page: 2, size: 5})(input); 12 | t.deepEqual(output, [6, 7]); 13 | }); 14 | test('slice: get all the asset if no param is provided', (t) => { 15 | const input = [1, 2, 3, 4, 5, 6, 7]; 16 | const output = slicer()(input); 17 | t.deepEqual(output, input); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | - image: circleci/node:latest 10 | steps: 11 | - checkout 12 | 13 | # Download and cache dependencies 14 | - restore_cache: 15 | keys: 16 | - v1-dependencies-{{ checksum "package.json" }} 17 | # fallback to using the latest cache if no exact match is found 18 | - v1-dependencies- 19 | 20 | - run: npm install && npm run build 21 | 22 | - save_cache: 23 | paths: 24 | - node_modules 25 | key: v1-dependencies-{{ checksum "package.json" }} 26 | 27 | # run tests! 28 | - run: npm test 29 | 30 | -------------------------------------------------------------------------------- /src/directives/summary.ts: -------------------------------------------------------------------------------- 1 | import {proxyListener, ProxyEmitter} from 'smart-table-events'; 2 | import {SmartTable, SmartTableEvents} from './table'; 3 | 4 | export interface SummaryDirective extends ProxyEmitter { 5 | onSummaryChange(listener: SummaryChangeCallback): SummaryDirective; 6 | } 7 | 8 | export interface Summary { 9 | page: number; 10 | size: number; 11 | filteredCount: number; 12 | } 13 | 14 | export interface SummaryChangeCallback { 15 | (summary: Summary): void; 16 | } 17 | 18 | export interface SummaryDirectiveConfiguration { 19 | table: SmartTable 20 | } 21 | 22 | const summaryListener = proxyListener({[SmartTableEvents.SUMMARY_CHANGED]: 'onSummaryChange'}); 23 | 24 | export const summaryDirective = ({table}: SummaryDirectiveConfiguration) => summaryListener({emitter: table}); 25 | -------------------------------------------------------------------------------- /src/directives/working-indicator.ts: -------------------------------------------------------------------------------- 1 | import {ProxyEmitter, proxyListener} from 'smart-table-events'; 2 | import {SmartTable, SmartTableEvents} from './table'; 3 | 4 | export interface WorkingIndicator { 5 | working: boolean 6 | } 7 | 8 | export interface WorkingIndicatorChangeCallback { 9 | (state: WorkingIndicator): void; 10 | } 11 | 12 | export interface WorkingIndicatorDirective extends ProxyEmitter { 13 | onExecutionChange(listener: WorkingIndicatorChangeCallback): void; 14 | } 15 | 16 | export interface WorkingIndicatorDirectiveConfiguration { 17 | table: SmartTable; 18 | } 19 | 20 | const executionListener = proxyListener({[SmartTableEvents.EXEC_CHANGED]: 'onExecutionChange'}); 21 | 22 | export const workingIndicatorDirective = ({table}: WorkingIndicatorDirectiveConfiguration) => executionListener({emitter: table}); 23 | -------------------------------------------------------------------------------- /test/table.js: -------------------------------------------------------------------------------- 1 | import {smartTable} from '../dist/bundle/module.js'; 2 | 3 | export default ({test}) => { 4 | test('compose table factory', (t) => { 5 | const data = []; 6 | const tableState = {}; 7 | const tableInstance = smartTable({data, tableState}, function ({data: d, tableState: ts}) { 8 | return { 9 | getData() { 10 | return d; 11 | }, 12 | getTableState() { 13 | return ts; 14 | } 15 | }; 16 | }); 17 | 18 | t.ok(tableInstance.getData !== undefined && tableInstance.getTableState !== undefined, 'table instance should have extended behaviour'); 19 | t.ok(tableInstance.exec !== undefined, 'table instance should have regular behaviour'); 20 | t.equal(tableInstance.getData(), data, 'all factories should have the same data reference'); 21 | t.equal(tableInstance.getTableState(), tableState, 'all factories should have the same table state reference'); 22 | }); 23 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 smart-table 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/directives/search.ts: -------------------------------------------------------------------------------- 1 | import {proxyListener, ProxyEmitter} from 'smart-table-events'; 2 | import {SmartTable, SmartTableEvents} from './table'; 3 | import {SearchConfiguration} from 'smart-table-search'; 4 | 5 | interface SearchProxy extends ProxyEmitter { 6 | onSearchChange(listener: SearchChangeCallback): SearchDirective; 7 | } 8 | 9 | export {SearchConfiguration} from 'smart-table-search'; 10 | 11 | export interface SearchChangeCallback { 12 | (searchState: SearchConfiguration): void; 13 | } 14 | 15 | export interface SearchDirective extends SearchProxy { 16 | search(input: string, opts?: object): void; 17 | 18 | state(): SearchConfiguration; 19 | } 20 | 21 | export interface SearchDirectiveConfiguration { 22 | table: SmartTable; 23 | scope: string[] 24 | } 25 | 26 | const searchListener = proxyListener({[SmartTableEvents.SEARCH_CHANGED]: 'onSearchChange'}); 27 | 28 | export const searchDirective = ({table, scope = []}: SearchDirectiveConfiguration): SearchDirective => { 29 | const proxy = searchListener({emitter: table}); 30 | return Object.assign(proxy, { 31 | search(input, opts = {}) { 32 | return table.search(Object.assign({}, {value: input, scope}, opts)); 33 | }, 34 | state() { 35 | return table.getTableState().search; 36 | } 37 | }, proxy); 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smart-table-core", 3 | "version": "3.0.7", 4 | "description": "smart-core directives", 5 | "main": "./dist/bundle/index", 6 | "types": "./dist/declarations/index.d.ts", 7 | "module": "./dist/bundle/module.js", 8 | "directories": { 9 | "test": "test" 10 | }, 11 | "scripts": { 12 | "test": "pta", 13 | "build:clean": "rm -rf ./dist && mkdir -p ./dist/bundle && cp -r ./src ./dist/src", 14 | "build:compile": "tsc", 15 | "build:bundle": "rollup -c ./rollup/build.js && rollup -c ./rollup/cdn.js", 16 | "build": "npm run build:clean && npm run build:compile && npm run build:bundle && rm -rf dist/src" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/smart-table/smart-table-core.git" 21 | }, 22 | "keywords": [ 23 | "smart-table", 24 | "datatable", 25 | "table", 26 | "grid" 27 | ], 28 | "author": "Laurent Renard", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/smart-table/smart-table-core/issues" 32 | }, 33 | "files": [ 34 | "dist" 35 | ], 36 | "homepage": "https://github.com/smart-table/smart-table-core#readme", 37 | "devDependencies": { 38 | "pta": "^0.1.0", 39 | "rollup": "^1.21.2", 40 | "rollup-plugin-node-resolve": "^5.2.0", 41 | "typescript": "^3.6.3" 42 | }, 43 | "dependencies": { 44 | "smart-table-events": "^1.0.10", 45 | "smart-table-filter": "^2.0.5", 46 | "smart-table-json-pointer": "^3.0.0", 47 | "smart-table-operators": "^2.0.10", 48 | "smart-table-search": "^2.0.8", 49 | "smart-table-sort": "^2.0.5" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/directives/search.js: -------------------------------------------------------------------------------- 1 | import {emitter} from 'smart-table-events'; 2 | import {searchDirective as search, SmartTableEvents as evs} from '../../dist/bundle/module.js'; 3 | 4 | const fakeTable = () => { 5 | const table = emitter(); 6 | table.search = input => input; 7 | table.getTableState = () => ({search: {foo: 'bar'}}); 8 | return table; 9 | }; 10 | 11 | export default ({test}) => { 12 | test('search directive should be able to register listener', t => { 13 | let counter = 0; 14 | const table = fakeTable(); 15 | const dir = search({table}); 16 | dir.onSearchChange(() => counter++); 17 | table.dispatch(evs.SEARCH_CHANGED); 18 | t.equal(counter, 1, 'should have updated the counter'); 19 | }); 20 | test('search directive should call table search method passing the appropriate argument', t => { 21 | const table = fakeTable(); 22 | const dir = search({table, scope: ['foo', 'bar.woot']}); 23 | const arg = dir.search(42); 24 | t.deepEqual(arg, {value: 42, scope: ['foo', 'bar.woot']}); 25 | }); 26 | test('search directive should be able to pass extra options', t => { 27 | const table = fakeTable(); 28 | const dir = search({table, scope: ['foo', 'bar.woot']}); 29 | const arg = dir.search(42, {flags: 'i'}); 30 | t.deepEqual(arg, {value: 42, scope: ['foo', 'bar.woot'], flags: 'i'}); 31 | }); 32 | test('search directive should return the search part of the table state', t => { 33 | const table = fakeTable(); 34 | const dir = search({table, scope: ['foo']}); 35 | t.deepEqual(dir.state(), {foo: 'bar'}); 36 | }); 37 | } -------------------------------------------------------------------------------- /src/directives/filter.ts: -------------------------------------------------------------------------------- 1 | import {ProxyEmitter, proxyListener} from 'smart-table-events'; 2 | import {FilterConfiguration, FilterOperator} from 'smart-table-filter'; 3 | import {SmartTable, SmartTableEvents} from './table'; 4 | 5 | interface FilterProxy extends ProxyEmitter { 6 | onFilterChange(listener: FilterChangeCallback): FilterDirective; 7 | } 8 | 9 | export {FilterConfiguration, FilterOperator} from 'smart-table-filter'; 10 | 11 | export interface FilterChangeCallback { 12 | (filterState: FilterConfiguration): void; 13 | } 14 | 15 | export interface FilterDirective extends FilterProxy { 16 | 17 | filter(input?: K): void; 18 | 19 | state(): FilterConfiguration; 20 | } 21 | 22 | const filterListener = proxyListener({[SmartTableEvents.FILTER_CHANGED]: 'onFilterChange'}); 23 | 24 | // todo expose and re-export from smart-table-filter 25 | export const enum FilterType { 26 | BOOLEAN = 'boolean', 27 | NUMBER = 'number', 28 | DATE = 'date', 29 | STRING = 'string' 30 | } 31 | 32 | export interface FilterDirectiveConfiguration { 33 | table: SmartTable; 34 | pointer: string; 35 | operator?: FilterOperator; 36 | type?: FilterType; 37 | } 38 | 39 | export const filterDirective = ({table, pointer, operator = FilterOperator.INCLUDES, type = FilterType.STRING}: FilterDirectiveConfiguration): FilterDirective => { 40 | const proxy = filterListener({emitter: table}); 41 | return Object.assign({ 42 | filter(input?: K) { 43 | 44 | const newState = this.state(); 45 | if (input === void 0) { 46 | delete newState[pointer]; 47 | } else { 48 | Object.assign(newState, { 49 | [pointer]: [{ 50 | value: input, 51 | operator, 52 | type 53 | }] 54 | }); 55 | } 56 | 57 | return table.filter(newState); 58 | }, 59 | state() { 60 | return table.getTableState().filter || {}; 61 | } 62 | }, proxy); 63 | }; 64 | -------------------------------------------------------------------------------- /test/directives/filter.js: -------------------------------------------------------------------------------- 1 | import {filterDirective as filter, SmartTableEvents as evs} from '../../dist/bundle/module.js'; 2 | import {emitter} from 'smart-table-events'; 3 | 4 | const fakeTable = (initialState = {}) => { 5 | const table = emitter(); 6 | table.filter = input => input; 7 | table.getTableState = () => initialState; 8 | return table; 9 | }; 10 | 11 | export default ({test}) => { 12 | test('filter directive should be able to register listener', (t) => { 13 | let counter = 0; 14 | const table = fakeTable(); 15 | const fd = filter({table, pointer: 'foo'}); 16 | fd.onFilterChange(() => counter++); 17 | table.dispatch(evs.FILTER_CHANGED); 18 | t.equal(counter, 1, 'should have updated the counter'); 19 | }); 20 | 21 | test('filter directive should call table filter method passing a clause argument', t => { 22 | const table = fakeTable(); 23 | const fd = filter({table, pointer: 'foo.bar', operator: 'is', type: 'number'}); 24 | const arg = fd.filter(42); 25 | t.deepEqual(arg, {'foo.bar': [{value: 42, operator: 'is', type: 'number'}]}); 26 | }); 27 | 28 | test('filter directive should not overwrite other part of the filter state', t => { 29 | const table = fakeTable({filter: {woot: [{value: 'blah'}]}}); 30 | const fd = filter({table, pointer: 'foo.bar', operator: 'is', type: 'number'}); 31 | const arg = fd.filter(42); 32 | t.deepEqual(arg, {'foo.bar': [{value: 42, operator: 'is', type: 'number'}], woot: [{value: 'blah'}]}); 33 | }); 34 | 35 | test('filter directive should reset the clauses for the prop when no argument is provided', t => { 36 | const table = fakeTable({ 37 | filter: {woot: [{value: 'blah'}]} 38 | }); 39 | const fd = filter({table, pointer: 'woot'}); 40 | const arg = fd.filter(); 41 | t.deepEqual(arg, {}); 42 | }); 43 | 44 | test('filter directive should return the filter part of the table state', t => { 45 | const table = fakeTable({ 46 | filter: {woot: [{value: 'blah'}]}, 47 | sort: { 48 | pointer: 'woot', 49 | direction: 'asc' 50 | } 51 | }); 52 | const fd = filter({table, pointer: 'foo.bar', operator: 'is', type: 'number'}); 53 | t.deepEqual(fd.state(), {woot: [{value: 'blah'}]}); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/directives/sort.ts: -------------------------------------------------------------------------------- 1 | import {ProxyEmitter, proxyListener} from 'smart-table-events'; 2 | import {SmartTable, SmartTableEvents} from './table'; 3 | import {SortDirection} from 'smart-table-sort'; 4 | 5 | export {SortDirection} from 'smart-table-sort'; 6 | 7 | export interface SortConfiguration { 8 | pointer?: string; 9 | direction?: SortDirection; 10 | } 11 | 12 | export interface SortChangeCallback { 13 | (state: SortConfiguration): void; 14 | } 15 | 16 | interface SortProxy extends ProxyEmitter { 17 | onSortToggle(listener: SortChangeCallback): SortDirective; 18 | } 19 | 20 | export interface SortDirective extends SortProxy { 21 | 22 | toggle(): void; 23 | 24 | state(): SortConfiguration; 25 | } 26 | 27 | 28 | const debounce = (fn: Function, time: number) => { 29 | let timer = null; 30 | return (...args) => { 31 | if (timer !== null) { 32 | clearTimeout(timer); 33 | } 34 | timer = setTimeout(() => fn(...args), time); 35 | }; 36 | }; 37 | 38 | const sortListeners = proxyListener({[SmartTableEvents.TOGGLE_SORT]: 'onSortToggle'}); 39 | const directions = [SortDirection.ASC, SortDirection.DESC]; 40 | 41 | export interface SortDirectiveConfiguration { 42 | table: SmartTable; 43 | pointer: string; 44 | cycle?: boolean; 45 | debounceTime?: number; 46 | } 47 | 48 | export const sortDirective = ({pointer, table, cycle = false, debounceTime = 0}: SortDirectiveConfiguration): SortDirective => { 49 | const cycleDirections = cycle === true ? [SortDirection.NONE].concat(directions) : [...directions].reverse(); 50 | const commit = debounce(table.sort, debounceTime); 51 | let hit = 0; 52 | 53 | const proxy = sortListeners({emitter: table}); 54 | const directive = Object.assign({ 55 | toggle() { 56 | hit++; 57 | const direction = cycleDirections[hit % cycleDirections.length]; 58 | return commit({pointer, direction}); 59 | }, 60 | state() { 61 | return table.getTableState().sort; 62 | } 63 | }, proxy); 64 | 65 | directive.onSortToggle(({pointer: p}) => { 66 | hit = pointer !== p ? 0 : hit; 67 | }); 68 | 69 | const {pointer: statePointer, direction = SortDirection.ASC} = directive.state(); 70 | hit = statePointer === pointer ? (direction === SortDirection.ASC ? 1 : 2) : 0; 71 | return directive; 72 | }; 73 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {defaultSortFactory} from 'smart-table-sort'; 2 | import {filter, FilterConfiguration} from 'smart-table-filter'; 3 | import {regexp} from 'smart-table-search'; 4 | import {tableDirective, SmartTable, TableState} from './directives/table'; 5 | import {SearchConfiguration} from 'smart-table-search'; 6 | import {SortConfiguration} from './directives/sort'; 7 | 8 | export interface SmartTableInput { 9 | sortFactory?: (conf: SortConfiguration) => (items: T[]) => T[]; 10 | filterFactory?: (conf: FilterConfiguration) => (items: T[]) => T[]; 11 | searchFactory?: (conf: SearchConfiguration) => (items: T[]) => T[]; 12 | tableState?: TableState; 13 | data?: T[] 14 | } 15 | 16 | export interface SmartTableExtensionInput extends SmartTableInput { 17 | table: SmartTable 18 | } 19 | 20 | export interface SmartTableExtension { 21 | (input: SmartTableExtensionInput): any; 22 | } 23 | 24 | const defaultTableState = () => ({sort: {}, slice: {page: 1}, filter: {}, search: {}}); 25 | export const smartTable = ({ 26 | sortFactory = defaultSortFactory, 27 | filterFactory = filter, 28 | searchFactory = regexp, 29 | tableState = defaultTableState(), 30 | data = [] 31 | }: SmartTableInput = { 32 | sortFactory: defaultSortFactory, 33 | filterFactory: filter, 34 | searchFactory: regexp, 35 | tableState: defaultTableState(), 36 | data: [] 37 | }, ...tableExtensions: SmartTableExtension[]): SmartTable => { 38 | 39 | const coreTable = tableDirective({sortFactory, filterFactory, tableState, data, searchFactory}); 40 | 41 | return tableExtensions.reduce((accumulator, newdir) => Object.assign(accumulator, newdir({ 42 | sortFactory, 43 | filterFactory, 44 | searchFactory, 45 | tableState, 46 | data, 47 | table: coreTable 48 | })) 49 | , coreTable); 50 | }; 51 | 52 | export * from './directives/filter'; 53 | export * from './directives/search'; 54 | export * from './directives/pagination'; 55 | export * from './directives/sort'; 56 | export * from './directives/summary'; 57 | export * from './directives/table'; 58 | export * from './directives/working-indicator'; 59 | export * from './slice'; 60 | -------------------------------------------------------------------------------- /src/directives/pagination.ts: -------------------------------------------------------------------------------- 1 | import {ProxyEmitter, proxyListener} from 'smart-table-events'; 2 | import {SmartTable, SmartTableEvents} from './table'; 3 | import {Summary, SummaryDirective} from './summary'; 4 | import {SliceConfiguration} from '../slice'; 5 | 6 | export interface PageChangeCallback { 7 | (sliceState: SliceConfiguration): void; 8 | } 9 | 10 | interface PaginationProxy extends SummaryDirective, ProxyEmitter { 11 | onPageChange(listener: PageChangeCallback); 12 | } 13 | 14 | export interface PaginationDirective extends PaginationProxy { 15 | selectPage(p: number): void; 16 | 17 | selectNextPage(): void; 18 | 19 | selectPreviousPage(): void; 20 | 21 | isPreviousPageEnabled(): boolean; 22 | 23 | isNextPageEnabled(): boolean; 24 | 25 | changePageSize(size: number): void; 26 | 27 | state(): SliceConfiguration; 28 | } 29 | 30 | const sliceListener = proxyListener({ 31 | [SmartTableEvents.PAGE_CHANGED]: 'onPageChange', 32 | [SmartTableEvents.SUMMARY_CHANGED]: 'onSummaryChange' 33 | }); 34 | 35 | export interface PaginationDirectiveConfiguration { 36 | table: SmartTable; 37 | } 38 | 39 | 40 | export const paginationDirective = ({table}: PaginationDirectiveConfiguration): PaginationDirective => { 41 | let {slice: {page: currentPage, size: currentSize}} = table.getTableState(); 42 | let itemListLength = table.filteredCount; 43 | let pageCount = currentSize ? Math.ceil(itemListLength / currentSize) : 1; 44 | 45 | const proxy = sliceListener({emitter: table}); 46 | 47 | const api = { 48 | selectPage(p) { 49 | return table.slice({page: p, size: currentSize}); 50 | }, 51 | selectNextPage() { 52 | return api.selectPage(currentPage + 1); 53 | }, 54 | selectPreviousPage() { 55 | return api.selectPage(currentPage - 1); 56 | }, 57 | changePageSize(size) { 58 | return table.slice({page: 1, size}); 59 | }, 60 | isPreviousPageEnabled() { 61 | return currentPage > 1; 62 | }, 63 | isNextPageEnabled() { 64 | return pageCount > currentPage; 65 | }, 66 | state() { 67 | return Object.assign(table.getTableState().slice, {filteredCount: itemListLength, pageCount}); 68 | } 69 | }; 70 | const directive = Object.assign(api, proxy); 71 | 72 | directive.onSummaryChange(({page: p, size: s, filteredCount}: Summary) => { 73 | currentPage = p; 74 | currentSize = s; 75 | itemListLength = filteredCount; 76 | pageCount = currentSize ? Math.ceil(itemListLength / currentSize) : 1; 77 | }); 78 | 79 | return directive; 80 | }; 81 | -------------------------------------------------------------------------------- /test/directives/pagination.js: -------------------------------------------------------------------------------- 1 | import {paginationDirective as pagination, SmartTableEvents as evts} from '../../dist/bundle/module.js'; 2 | import {emitter} from 'smart-table-events'; 3 | 4 | const fakeTable = (slice = {}) => { 5 | const table = emitter(); 6 | table.getTableState = () => ({ 7 | slice 8 | }); 9 | table.slice = input => input; 10 | return table; 11 | }; 12 | 13 | export default ({test}) => { 14 | test('pagination directive should be able to register listener to PAGE_CHANGED event', t => { 15 | let counter = 0; 16 | const table = fakeTable(); 17 | const dir = pagination({table}); 18 | dir.onPageChange(() => counter++); 19 | table.dispatch(evts.PAGE_CHANGED, {size: 25, page: 1}); 20 | t.equal(counter, 1, 'should have updated the counter'); 21 | }); 22 | test('pagination directive should be able to register listener to SUMMARY_CHANGED event', t => { 23 | let counter = 0; 24 | const table = fakeTable(); 25 | const dir = pagination({table}); 26 | dir.onSummaryChange(() => counter++); 27 | table.dispatch(evts.SUMMARY_CHANGED, {size: 25, page: 1}); 28 | t.equal(counter, 1, 'should have updated the counter'); 29 | }); 30 | test('pagination directive should call table pagination method with the given page', t => { 31 | const table = fakeTable({size: 25, page: 4}); 32 | const dir = pagination({table}); 33 | const arg = dir.selectPage(2); 34 | t.deepEqual(arg, {page: 2, size: 25}); 35 | }); 36 | test('pagination directive should call table pagination method with the next page arguments', t => { 37 | const table = fakeTable({size: 21, page: 4}); 38 | const dir = pagination({table}); 39 | const {page, size} = dir.selectNextPage(); 40 | t.equal(page, 5, 'should be the next page'); 41 | t.equal(size, 21, 'should keep the current page size'); 42 | }); 43 | test('pagination directive should call table pagination method with the previous page arguments', t => { 44 | const table = fakeTable({size: 26, page: 9}); 45 | const dir = pagination({table}); 46 | const {page, size} = dir.selectPreviousPage(); 47 | t.equal(page, 8, 'should be the previous page'); 48 | t.equal(size, 26, 'should keep the current page size'); 49 | }); 50 | test('pagination directive should call table pagination method with the page size, returning to page one', t => { 51 | const table = fakeTable(); 52 | const dir = pagination({table, size: 100, page: 3}); 53 | const {page, size} = dir.changePageSize(42); 54 | t.equal(page, 1, 'should have returned to the first page'); 55 | t.equal(size, 42, 'should have change the page size'); 56 | }); 57 | test('pagination directive should tell whether previous page is enabled', t => { 58 | const table = fakeTable(); 59 | const dir = pagination({table}); 60 | table.dispatch(evts.SUMMARY_CHANGED, {size: 25, page: 1}); 61 | t.equal(dir.isPreviousPageEnabled(), false); 62 | table.dispatch(evts.SUMMARY_CHANGED, {size: 25, page: 2}); 63 | t.equal(dir.isPreviousPageEnabled(), true); 64 | }); 65 | test('pagination directive should tell whether next page is enabled', t => { 66 | const table = fakeTable(); 67 | const dir = pagination({table}); 68 | table.dispatch(evts.SUMMARY_CHANGED, {size: 25, page: 3, filteredCount: 100}); 69 | t.equal(dir.isNextPageEnabled(), true); 70 | table.dispatch(evts.SUMMARY_CHANGED, {size: 25, page: 2, filteredCount: 38}); 71 | t.equal(dir.isNextPageEnabled(), false); 72 | }); 73 | test('pagination directive should return the pagination part of the table state and the summary values', t => { 74 | const table = fakeTable({size: 25, page: 3, filteredCount: 100}); 75 | const dir = pagination({table}); 76 | table.dispatch(evts.SUMMARY_CHANGED, {size: 25, page: 3, filteredCount: 100}); 77 | t.deepEqual(dir.state(), {size: 25, page: 3, filteredCount: 100, pageCount: 4}); 78 | }); 79 | test('pagination directive accepts falsy value for page size', t => { 80 | const table = fakeTable({page: 1}); 81 | const dir = pagination({table}); 82 | t.deepEqual(dir.state().pageCount, 1); 83 | table.dispatch(evts.SUMMARY_CHANGED, {page: 1, size: 0}); 84 | t.deepEqual(dir.state().pageCount, 1); 85 | }); 86 | } -------------------------------------------------------------------------------- /test/directives/sort.js: -------------------------------------------------------------------------------- 1 | import {SmartTableEvents as evts, SortDirection, sortDirective as sort} from '../../dist/bundle/module.js'; 2 | import {emitter} from 'smart-table-events'; 3 | 4 | const fakeTable = (initialState = {}) => { 5 | const table = emitter(); 6 | table.calls = []; 7 | table.sort = input => table.calls.push(input); 8 | table.getTableState = () => ({sort: initialState}); 9 | return table; 10 | }; 11 | const wait = time => new Promise(resolve => { 12 | setTimeout(() => resolve(), time); 13 | }); 14 | 15 | export default ({test}) => { 16 | test('sort directive should be able to register listener', t => { 17 | let counter = 0; 18 | const table = fakeTable(); 19 | const dir = sort({table, pointer: 'foo.bar'}); 20 | dir.onSortToggle(() => counter++); 21 | table.dispatch(evts.TOGGLE_SORT, {}); 22 | t.equal(counter, 1, 'should have updated the counter'); 23 | }); 24 | 25 | test('sort directive dual state mode: sequentially change sort direction', async t => { 26 | const table = fakeTable(); 27 | const dir = sort({table, pointer: 'foo.bar', debounceTime: 10}); 28 | dir.toggle(); 29 | await wait(15); 30 | t.deepEqual(table.calls, [{pointer: 'foo.bar', direction: 'asc'}]); 31 | dir.toggle(); 32 | await wait(15); 33 | t.deepEqual(table.calls, [ 34 | {pointer: 'foo.bar', direction: 'asc'}, 35 | {pointer: 'foo.bar', direction: 'desc'} 36 | ]); 37 | dir.toggle(); 38 | await wait(15); 39 | t.deepEqual(table.calls, [ 40 | {pointer: 'foo.bar', direction: 'asc'}, 41 | {pointer: 'foo.bar', direction: 'desc'}, 42 | {pointer: 'foo.bar', direction: 'asc'} 43 | ]); 44 | }); 45 | 46 | test('sort directive dual state mode: only commit value after debounce time', async t => { 47 | const table = fakeTable(); 48 | const dir = sort({table, pointer: 'foo.bar', debounceTime: 10}); 49 | dir.toggle(); 50 | await wait(5); 51 | dir.toggle(); 52 | await wait(15); 53 | t.deepEqual(table.calls, [{pointer: 'foo.bar', direction: 'desc'}]); 54 | }); 55 | 56 | test('sort directive cycle mode: sequentially change sort direction', async t => { 57 | const table = fakeTable(); 58 | const dir = sort({table, pointer: 'foo.bar', cycle: true, debounceTime: 10}); 59 | dir.toggle(); 60 | await wait(15); 61 | t.deepEqual(table.calls, [{pointer: 'foo.bar', direction: 'asc'}]); 62 | dir.toggle(); 63 | await wait(15); 64 | t.deepEqual(table.calls, [ 65 | {pointer: 'foo.bar', direction: 'asc'}, 66 | {pointer: 'foo.bar', direction: 'desc'} 67 | ]); 68 | dir.toggle(); 69 | await wait(15); 70 | t.deepEqual(table.calls, [ 71 | {pointer: 'foo.bar', direction: 'asc'}, 72 | {pointer: 'foo.bar', direction: 'desc'}, 73 | {pointer: 'foo.bar', direction: 'none'} 74 | ]); 75 | dir.toggle(); 76 | await wait(15); 77 | t.deepEqual(table.calls, [ 78 | {pointer: 'foo.bar', direction: 'asc'}, 79 | {pointer: 'foo.bar', direction: 'desc'}, 80 | {pointer: 'foo.bar', direction: 'none'}, 81 | {pointer: 'foo.bar', direction: 'asc'} 82 | ]); 83 | }); 84 | 85 | test('a sort directive should reset when it is not concerned by the toggle', async t => { 86 | const table = fakeTable(); 87 | const dir = sort({table, pointer: 'foo.bar'}); 88 | dir.toggle(); 89 | await wait(5); 90 | t.deepEqual(table.calls, [{pointer: 'foo.bar', direction: SortDirection.ASC}]); 91 | table.dispatch(evts.TOGGLE_SORT, {pointer: 'woot.woot'}); 92 | dir.toggle(); 93 | await wait(5); 94 | t.deepEqual(table.calls, [ 95 | {pointer: 'foo.bar', direction: SortDirection.ASC}, 96 | {pointer: 'foo.bar', direction: SortDirection.ASC} 97 | ]); 98 | }); 99 | 100 | test('sort should return the sort state of the table state', t => { 101 | const table = fakeTable({pointer: 'foo.bar', direction: SortDirection.DESC}); 102 | const dir = sort({table, pointer: 'foo'}); 103 | t.deepEqual(dir.state(), {pointer: 'foo.bar', direction: SortDirection.DESC}); 104 | }); 105 | 106 | test('sort should init the sequence correctly depending on the initial table state - asc', async t => { 107 | const table = fakeTable({pointer: 'foo.bar', direction: SortDirection.ASC}); 108 | const dir = sort({table, pointer: 'foo.bar'}); 109 | dir.toggle(); 110 | await wait(5); 111 | t.deepEqual(table.calls, [{pointer: 'foo.bar', direction: SortDirection.DESC}]); 112 | }); 113 | 114 | test('sort should init the sequence correctly depending on the initial table state - undefined', async t => { 115 | const table = fakeTable({pointer: 'foo.bar'}); 116 | const dir = sort({table, pointer: 'foo.bar'}); 117 | dir.toggle(); 118 | await wait(5); 119 | t.deepEqual(table.calls, [{pointer: 'foo.bar', direction: SortDirection.DESC}]); 120 | }); 121 | 122 | test('sort should init the sequence correctly depending on the initial table state - desc', async t => { 123 | const table = fakeTable({pointer: 'foo.bar', direction: SortDirection.DESC}); 124 | const dir = sort({table, pointer: 'foo.bar'}); 125 | dir.toggle(); 126 | await wait(5); 127 | t.deepEqual(table.calls, [{pointer: 'foo.bar', direction: SortDirection.ASC}]); 128 | }); 129 | } -------------------------------------------------------------------------------- /src/directives/table.ts: -------------------------------------------------------------------------------- 1 | import {compose, curry, tap} from 'smart-table-operators'; 2 | import {pointer} from 'smart-table-json-pointer'; 3 | import {emitter, Emitter} from 'smart-table-events'; 4 | import {SortConfiguration} from './sort'; 5 | import {SearchConfiguration} from 'smart-table-search'; 6 | import {FilterConfiguration} from 'smart-table-filter'; 7 | import {SliceConfiguration, sliceFactory} from '../slice'; 8 | 9 | export const enum SmartTableEvents { 10 | TOGGLE_SORT = 'TOGGLE_SORT', 11 | DISPLAY_CHANGED = 'DISPLAY_CHANGED', 12 | PAGE_CHANGED = 'CHANGE_PAGE', 13 | EXEC_CHANGED = 'EXEC_CHANGED', 14 | FILTER_CHANGED = 'FILTER_CHANGED', 15 | SUMMARY_CHANGED = 'SUMMARY_CHANGED', 16 | SEARCH_CHANGED = 'SEARCH_CHANGED', 17 | EXEC_ERROR = 'EXEC_ERROR' 18 | } 19 | 20 | export interface TableState { 21 | search: SearchConfiguration; 22 | filter: FilterConfiguration; 23 | sort: SortConfiguration; 24 | slice: SliceConfiguration; 25 | } 26 | 27 | export interface DisplayedItem { 28 | index: number; 29 | value: T; 30 | } 31 | 32 | export interface DisplayChangeCallback { 33 | (items: DisplayedItem[]): void; 34 | } 35 | 36 | export interface ProcessingOptions { 37 | processingDelay: number 38 | } 39 | 40 | export interface SmartTable extends Emitter { 41 | readonly filteredCount: number; 42 | readonly length: number; 43 | 44 | sort(input?: SortConfiguration): void; 45 | 46 | filter(input?: FilterConfiguration): void; 47 | 48 | slice(input: SliceConfiguration): void; 49 | 50 | search(input?: SearchConfiguration): void; 51 | 52 | eval(tableState?: TableState): Promise[]>; 53 | 54 | exec(opts?: ProcessingOptions): void; 55 | 56 | onDisplayChange(callback: DisplayChangeCallback): void; 57 | 58 | getTableState(): TableState; 59 | 60 | getMatchingItems(): T[]; 61 | } 62 | 63 | const curriedPointer = (path: string) => { 64 | const {get, set} = pointer(path); 65 | return {get, set: curry(set)}; 66 | }; 67 | 68 | export interface TableConfiguration { 69 | data: T[]; 70 | tableState: TableState; 71 | sortFactory: (conf: SortConfiguration) => (array: T[]) => T[]; 72 | filterFactory: (conf: FilterConfiguration) => (array: T[]) => T[]; 73 | searchFactory: (conf: SearchConfiguration) => (array: T[]) => T[]; 74 | } 75 | 76 | export const tableDirective = ({sortFactory, tableState, data, filterFactory, searchFactory}: TableConfiguration): SmartTable => { 77 | let filteredCount = data.length; 78 | let matchingItems = data; 79 | const table: SmartTable = >emitter(); 80 | const sortPointer = curriedPointer('sort'); 81 | const slicePointer = curriedPointer('slice'); 82 | const filterPointer = curriedPointer('filter'); 83 | const searchPointer = curriedPointer('search'); 84 | 85 | // We need to register in case the summary comes from outside (like server data) 86 | table.on(SmartTableEvents.SUMMARY_CHANGED, ({filteredCount: count}) => { 87 | filteredCount = count; 88 | }); 89 | 90 | const safeAssign = newState => Object.assign({}, newState); 91 | const dispatch = curry(table.dispatch, 2); 92 | 93 | const dispatchSummary = (filtered: T[]) => { 94 | matchingItems = filtered; 95 | return dispatch(SmartTableEvents.SUMMARY_CHANGED, { 96 | page: tableState.slice.page, 97 | size: tableState.slice.size, 98 | filteredCount: filtered.length 99 | }); 100 | }; 101 | 102 | const exec = ({processingDelay = 20}: ProcessingOptions = {processingDelay: 20}) => { 103 | table.dispatch(SmartTableEvents.EXEC_CHANGED, {working: true}); 104 | setTimeout(() => { 105 | try { 106 | const filterFunc = filterFactory(filterPointer.get(tableState)); 107 | const searchFunc = searchFactory(searchPointer.get(tableState)); 108 | const sortFunc = sortFactory(sortPointer.get(tableState)); 109 | const sliceFunc = sliceFactory(slicePointer.get(tableState)); 110 | const execFunc = compose(filterFunc, searchFunc, tap(dispatchSummary), sortFunc, sliceFunc); 111 | const displayed = execFunc(data); 112 | table.dispatch(SmartTableEvents.DISPLAY_CHANGED, displayed.map(d => ({ 113 | index: data.indexOf(d), 114 | value: d 115 | }))); 116 | } catch (err) { 117 | table.dispatch(SmartTableEvents.EXEC_ERROR, err); 118 | } finally { 119 | table.dispatch(SmartTableEvents.EXEC_CHANGED, {working: false}); 120 | } 121 | }, processingDelay); 122 | }; 123 | 124 | const updateTableState = curry((pter, ev, newPartialState) => compose( 125 | safeAssign, 126 | tap(dispatch(ev)), 127 | pter.set(tableState) 128 | )(newPartialState)); 129 | 130 | const resetToFirstPage = () => updateTableState( 131 | slicePointer, 132 | SmartTableEvents.PAGE_CHANGED, 133 | Object.assign({}, slicePointer.get(tableState), {page: 1}) 134 | ); 135 | 136 | const tableOperation = (pter, ev) => { 137 | const fn = compose( 138 | updateTableState(pter, ev), 139 | resetToFirstPage, 140 | () => table.exec() // We wrap within a function so table.exec can be overwritten (when using with a server for example) 141 | ); 142 | return (arg = {}) => fn(arg); 143 | }; 144 | 145 | const api = { 146 | sort: tableOperation(sortPointer, SmartTableEvents.TOGGLE_SORT), 147 | filter: tableOperation(filterPointer, SmartTableEvents.FILTER_CHANGED), 148 | search: tableOperation(searchPointer, SmartTableEvents.SEARCH_CHANGED), 149 | slice: compose(updateTableState(slicePointer, SmartTableEvents.PAGE_CHANGED), () => table.exec()), 150 | exec, 151 | async eval(state = tableState) { 152 | const sortFunc = sortFactory(sortPointer.get(state)); 153 | const searchFunc = searchFactory(searchPointer.get(state)); 154 | const filterFunc = filterFactory(filterPointer.get(state)); 155 | const sliceFunc = sliceFactory(slicePointer.get(state)); 156 | const execFunc = compose(filterFunc, searchFunc, sortFunc, sliceFunc); 157 | return execFunc(data).map(d => ({index: data.indexOf(d), value: d})); 158 | }, 159 | onDisplayChange(fn) { 160 | table.on(SmartTableEvents.DISPLAY_CHANGED, fn); 161 | }, 162 | getTableState() { 163 | return JSON.parse(JSON.stringify(tableState)); 164 | }, 165 | getMatchingItems() { 166 | return [...matchingItems]; 167 | } 168 | }; 169 | 170 | const instance = Object.assign(table, api); 171 | 172 | Object.defineProperties(instance, { 173 | filteredCount: { 174 | get() { 175 | return filteredCount; 176 | } 177 | }, 178 | length: { 179 | get() { 180 | return data.length; 181 | } 182 | } 183 | }); 184 | 185 | return instance; 186 | }; 187 | -------------------------------------------------------------------------------- /test/directives/table.js: -------------------------------------------------------------------------------- 1 | import { 2 | FilterOperator, 3 | smartTable as tableFactory, 4 | SmartTableEvents as evts, 5 | SortDirection 6 | } from '../../dist/bundle/module.js'; 7 | 8 | const wait = time => new Promise(resolve => { 9 | setTimeout(() => { 10 | resolve('finished'); 11 | }, time); 12 | }); 13 | 14 | export default ({test}) => { 15 | test('table directive: should be able to register listener on display change', t => { 16 | let displayed = null; 17 | const table = tableFactory({}); 18 | table.onDisplayChange((args) => displayed = args); 19 | table.dispatch(evts.DISPLAY_CHANGED, 'foo'); 20 | t.equal(displayed, 'foo'); 21 | }); 22 | 23 | test('table directive: sort should dispatch the mutated sort state', t => { 24 | let sortState = null; 25 | let sliceState = null; 26 | const table = tableFactory({}); 27 | table.on(evts.TOGGLE_SORT, arg => sortState = arg); 28 | table.on(evts.PAGE_CHANGED, arg => sliceState = arg); 29 | const newState = {direction: SortDirection.ASC, pointer: 'foo.bar'}; 30 | table.sort(newState); 31 | t.deepEqual(sortState, newState); 32 | t.deepEqual(sliceState, {page: 1}, 'should have reset to first page'); 33 | }); 34 | 35 | test('table directive: sort should trigger an execution with the new state', async t => { 36 | const table = tableFactory({}); 37 | table.sort({direction: SortDirection.ASC, pointer: 'foo.bar'}); 38 | t.deepEqual(table.getTableState(), { 39 | slice: {page: 1}, 40 | filter: {}, 41 | search: {}, 42 | sort: {direction: SortDirection.ASC, pointer: 'foo.bar'} 43 | }); 44 | }); 45 | 46 | test('table directive: slice should dispatch the mutated slice state', t => { 47 | let sliceState = null; 48 | const table = tableFactory({}); 49 | table.on(evts.PAGE_CHANGED, arg => sliceState = arg); 50 | const newState = {page: 7, size: 25}; 51 | table.slice(newState); 52 | t.deepEqual(sliceState, newState); 53 | }); 54 | 55 | test('table directive: slice should trigger an execution with the new state', t => { 56 | const table = tableFactory({}); 57 | table.slice({page: 4, size: 12}); 58 | t.deepEqual(table.getTableState(), {'sort': {}, 'slice': {'page': 4, 'size': 12}, 'filter': {}, 'search': {}}); 59 | }); 60 | 61 | test('table directive: filter should dispatch the mutated filter state', t => { 62 | let filterState = null; 63 | let sliceState = null; 64 | const table = tableFactory({}); 65 | table.on(evts.FILTER_CHANGED, arg => filterState = arg); 66 | table.on(evts.PAGE_CHANGED, arg => sliceState = arg); 67 | const newState = {foo: [{value: 'bar'}]}; 68 | table.filter(newState); 69 | t.deepEqual(filterState, newState); 70 | t.deepEqual(sliceState, {page: 1}, 'should have reset the page'); 71 | }); 72 | 73 | test('table directive: filter should reset state if no argument is provided', t => { 74 | const table = tableFactory({ 75 | tableState: { 76 | 'sort': {}, 77 | 'slice': {'page': 1}, 78 | 'filter': {'foo': [{'value': 'bar'}]}, 79 | 'search': {} 80 | } 81 | }); 82 | t.deepEqual(table.getTableState(), { 83 | 'sort': {}, 84 | 'slice': {'page': 1}, 85 | 'filter': {'foo': [{'value': 'bar'}]}, 86 | 'search': {} 87 | }); 88 | table.filter(); 89 | t.eq(table.getTableState(), { 90 | sort: {}, 91 | slice: {page: 1}, 92 | filter: {}, 93 | search: {} 94 | }); 95 | }); 96 | 97 | test('table directive: filter should trigger an execution with the new state ', t => { 98 | const table = tableFactory({}); 99 | table.filter({foo: [{value: 'bar'}]}); 100 | t.deepEqual(table.getTableState(), { 101 | 'sort': {}, 102 | 'slice': {'page': 1}, 103 | 'filter': {'foo': [{'value': 'bar'}]}, 104 | 'search': {} 105 | }); 106 | }); 107 | 108 | test('table directive: filter should overwrite the whole filter state', t => { 109 | const table = tableFactory(); 110 | table.filter({foo: [{value: 'bar'}]}); 111 | t.eq(table.getTableState(), { 112 | 'sort': {}, 113 | 'slice': {'page': 1}, 114 | 'filter': {'foo': [{'value': 'bar'}]}, 115 | 'search': {} 116 | }); 117 | table.filter({bar: [{value: 'baz'}]}); 118 | t.eq(table.getTableState(), { 119 | 'sort': {}, 120 | 'slice': {'page': 1}, 121 | 'filter': {'bar': [{value: 'baz'}]}, 122 | 'search': {} 123 | }); 124 | }); 125 | 126 | test('table directive: search should dispatch the mutated search state', t => { 127 | let searchState = null; 128 | let sliceState = null; 129 | const table = tableFactory({}); 130 | table.on(evts.SEARCH_CHANGED, arg => searchState = arg); 131 | table.on(evts.PAGE_CHANGED, arg => sliceState = arg); 132 | const newState = {value: 'foo', scope: ['bar']}; 133 | table.search(newState); 134 | t.deepEqual(searchState, newState); 135 | t.deepEqual(sliceState, {page: 1}, 'should have reset to the first page'); 136 | }); 137 | 138 | test('table directive: search should trigger an execution with the new state', t => { 139 | const table = tableFactory({}); 140 | table.search({value: 'bar', scope: ['bar']}); 141 | t.deepEqual(table.getTableState(), { 142 | 'sort': {}, 143 | 'slice': {'page': 1}, 144 | 'filter': {}, 145 | 'search': {'value': 'bar', scope: ['bar']} 146 | }); 147 | }); 148 | 149 | test('table directive: eval should return the displayed collection based on table state by default', async t => { 150 | const tableState = { 151 | sort: {pointer: 'id', direction: SortDirection.DESC}, 152 | search: {}, 153 | filter: {}, 154 | slice: {page: 1, size: 2} 155 | }; 156 | const table = tableFactory({ 157 | data: [ 158 | {id: 1, name: 'foo'}, 159 | {id: 2, name: 'blah'}, 160 | {id: 3, name: 'bip'} 161 | ], 162 | tableState 163 | }); 164 | const output = await table.eval(); 165 | t.deepEqual(output, [ 166 | {'index': 2, 'value': {'id': 3, 'name': 'bip'}}, 167 | {'index': 1, 'value': {'id': 2, 'name': 'blah'}} 168 | ]); 169 | 170 | //table state has mutated ! 171 | tableState.slice = {page: 2, size: 2}; 172 | const outputBis = await table.eval(); 173 | t.deepEqual(outputBis, [{'index': 0, 'value': {'id': 1, 'name': 'foo'}}]); 174 | }); 175 | 176 | test('table directive: eval should be able to take any state as input', async t => { 177 | const tableState = { 178 | sort: {pointer: 'id', direction: SortDirection.DESC}, 179 | search: {}, 180 | filter: {}, 181 | slice: {page: 1, size: 2} 182 | }; 183 | const table = tableFactory({ 184 | data: [ 185 | {id: 1, name: 'foo'}, 186 | {id: 2, name: 'blah'}, 187 | {id: 3, name: 'bip'} 188 | ], 189 | tableState 190 | }); 191 | const output = await table.eval({sort: {}, slice: {}, filter: {}, search: {}}); 192 | t.deepEqual(output, [ 193 | {'index': 0, 'value': {'id': 1, 'name': 'foo'}}, 194 | {'index': 1, 'value': {'id': 2, 'name': 'blah'}}, 195 | {'index': 2, 'value': {'id': 3, 'name': 'bip'}} 196 | ]); 197 | }); 198 | 199 | test('table directive: eval should not dispatch any event', async t => { 200 | let counter = 0; 201 | const tableState = { 202 | sort: {pointer: 'id', direction: SortDirection.DESC}, 203 | search: {}, 204 | filter: {}, 205 | slice: {page: 1, size: 2} 206 | }; 207 | const incrementCounter = () => counter++; 208 | const table = tableFactory({ 209 | tableState 210 | }); 211 | table.on(evts.DISPLAY_CHANGED, incrementCounter); 212 | table.on(evts.TOGGLE_SORT, incrementCounter); 213 | table.on(evts.PAGE_CHANGED, incrementCounter); 214 | table.on(evts.FILTER_CHANGED, incrementCounter); 215 | table.on(evts.SEARCH_CHANGED, incrementCounter); 216 | table.on(evts.SUMMARY_CHANGED, incrementCounter); 217 | table.on(evts.EXEC_CHANGED, incrementCounter); 218 | await table.eval(); 219 | t.equal(counter, 0, 'counter should not have been updated'); 220 | t.deepEqual(tableState, { 221 | sort: {pointer: 'id', direction: SortDirection.DESC}, 222 | search: {}, 223 | filter: {}, 224 | slice: {page: 1, size: 2} 225 | }, 'table state should not have changed'); 226 | }); 227 | 228 | test('exec should first set the working state to true then false', async t => { 229 | let workingState; 230 | const table = tableFactory({ 231 | data: [ 232 | {id: 1, name: 'foo'}, 233 | {id: 2, name: 'blah'}, 234 | {id: 3, name: 'bip'} 235 | ] 236 | }); 237 | table.on(evts.EXEC_CHANGED, function ({working}) { 238 | workingState = working; 239 | }); 240 | table.exec(); 241 | t.equal(workingState, true); 242 | await wait(25); 243 | t.equal(workingState, false); 244 | }); 245 | 246 | test('exec should dispatch the display changed event with the new displayed value', async t => { 247 | let displayed; 248 | const tableState = { 249 | sort: {pointer: 'id', direction: SortDirection.DESC}, 250 | search: {}, 251 | filter: {}, 252 | slice: {page: 1, size: 2} 253 | }; 254 | const table = tableFactory({ 255 | data: [ 256 | {id: 1, name: 'foo'}, 257 | {id: 2, name: 'blah'}, 258 | {id: 3, name: 'bip'} 259 | ], 260 | tableState 261 | }); 262 | 263 | table.onDisplayChange(val => displayed = val); 264 | table.exec(); 265 | await wait(25); 266 | t.deepEqual(displayed, [ 267 | {'index': 2, 'value': {'id': 3, 'name': 'bip'}}, 268 | {'index': 1, 'value': {'id': 2, 'name': 'blah'}} 269 | ]); 270 | }); 271 | 272 | test('exec should dispatch the summary changed event with the new value', async t => { 273 | let summary; 274 | const tableState = { 275 | sort: {pointer: 'id', direction: SortDirection.DESC}, 276 | search: {}, 277 | filter: {name: [{value: 'b'}]}, 278 | slice: {page: 1, size: 1} 279 | }; 280 | const table = tableFactory({ 281 | data: [ 282 | {id: 1, name: 'foo'}, 283 | {id: 2, name: 'blah'}, 284 | {id: 3, name: 'bip'} 285 | ], 286 | tableState 287 | }); 288 | 289 | table.on(evts.SUMMARY_CHANGED, val => summary = val); 290 | table.exec(); 291 | await wait(25); 292 | t.deepEqual(summary, {'page': 1, 'size': 1, 'filteredCount': 2}); 293 | }); 294 | 295 | test('exec should update the filteredCount property', async t => { 296 | let summary; 297 | const tableState = { 298 | sort: {pointer: 'id', direction: SortDirection.DESC}, 299 | search: {}, 300 | filter: {name: [{value: 'b'}]}, 301 | slice: {page: 1, size: 1} 302 | }; 303 | const table = tableFactory({ 304 | data: [ 305 | {id: 1, name: 'foo'}, 306 | {id: 2, name: 'blah'}, 307 | {id: 3, name: 'bip'} 308 | ], 309 | tableState 310 | }); 311 | t.equal(table.filteredCount, 3, 'initially with the length of data array'); 312 | table.on(evts.SUMMARY_CHANGED, val => summary = val); 313 | table.exec(); 314 | await wait(25); 315 | t.deepEqual(summary, {'page': 1, 'size': 1, 'filteredCount': 2}); 316 | t.equal(table.filteredCount, 2, 'filtered count should have been updated'); 317 | }); 318 | 319 | test('getTableState should return a deep copy of the tableState', t => { 320 | const tableState = { 321 | sort: {pointer: 'foo'}, 322 | slice: {page: 2, size: 25}, 323 | search: {value: 'wat', scope: []}, 324 | filter: {foo: [{value: 'blah'}]} 325 | }; 326 | const table = tableFactory({data: [], tableState}); 327 | const copy = table.getTableState(); 328 | t.deepEqual(copy, tableState); 329 | t.ok(!Object.is(copy.sort, tableState.sort)); 330 | t.ok(!Object.is(copy.search, tableState.search)); 331 | t.ok(!Object.is(copy.filter, tableState.filter)); 332 | t.ok(!Object.is(copy.slice, tableState.slice)); 333 | }); 334 | 335 | test('getMatchingItems should return the whole collection of matching items regardless of pagination', async t => { 336 | const tableState = { 337 | slice: {page: 1, size: 1}, 338 | filter: {}, 339 | search: {}, 340 | sort: {} 341 | }; 342 | 343 | const data = [ 344 | {value: 1}, 345 | {value: 2}, 346 | {value: 3}, 347 | {value: 4} 348 | ]; 349 | 350 | const table = tableFactory({data, tableState}); 351 | table.exec(); 352 | await wait(25); 353 | const evalResult = await table.eval(); 354 | t.deepEqual(evalResult, [{index: 0, value: {value: 1}}]); 355 | t.deepEqual(table.getMatchingItems(), data); 356 | 357 | table.filter({value: [{operator: FilterOperator.GREATER_THAN, value: 2}]}); 358 | await wait(25); 359 | const secondEvalResult = await table.eval(); 360 | t.deepEqual(secondEvalResult, [{index: 2, value: {value: 3}}]); 361 | t.deepEqual(table.getMatchingItems(), [{value: 3}, {value: 4}]); 362 | }); 363 | }; --------------------------------------------------------------------------------