├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── .yml ├── LICENSE ├── README.md ├── documentation └── Doc.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── ___subComponents │ ├── DefaultBlank.tsx │ ├── DefaultLoadIndicator.tsx │ ├── DisplayHandler.tsx │ ├── InfiniteLoader.tsx │ ├── PlainList.tsx │ ├── ScrollRenderer.tsx │ ├── ScrollToTopButton.tsx │ └── uiFunctions.tsx ├── ___utils │ ├── convertListToArray.ts │ ├── convertMapToObject.ts │ ├── filterList.ts │ ├── getObjectDeepKeyValue.ts │ ├── getType.ts │ ├── groupList.ts │ ├── isType.ts │ ├── limitList.ts │ ├── reverseList.ts │ ├── searchList.ts │ └── sortList.ts ├── flatListProps.ts ├── flatlist-react.tsx ├── hooks │ └── use-list.tsx └── index.tsx ├── tests ├── FlatList.test.tsx ├── ___subComponentns │ ├── DefaultBlank.test.tsx │ ├── DefaultLoadingIndicator.test.tsx │ ├── PlainList.test.tsx │ ├── ScrollToTopButton.test.tsx │ ├── __snapshots__ │ │ ├── DefaultBlank.test.tsx.snap │ │ ├── DefaultLoadingIndicator.test.tsx.snap │ │ ├── PlainList.test.tsx.snap │ │ ├── ScrollToTopButton.test.tsx.snap │ │ └── uiFunctions.test.tsx.snap │ └── uiFunctions.test.tsx ├── ___utils │ ├── convertListToArray.test.ts │ ├── filterList.test.ts │ ├── getObjectDeepKeyValue.test.ts │ ├── getType.test.ts │ ├── groupList.test.ts │ ├── isType.test.ts │ ├── limitList.test.ts │ ├── searchList.test.ts │ └── sortList.test.ts └── __snapshots__ │ └── FlatList.test.tsx.snap └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'airbnb-typescript', 4 | 'airbnb/hooks', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:jest/recommended', 7 | 'plugin:prettier/recommended' 8 | ], 9 | plugins: ['react', '@typescript-eslint', 'jest', 'import'], 10 | env: { 11 | browser: true, 12 | es6: true, 13 | jest: true, 14 | }, 15 | globals: { 16 | Atomics: 'readonly', 17 | SharedArrayBuffer: 'readonly', 18 | }, 19 | parser: '@typescript-eslint/parser', 20 | parserOptions: { 21 | ecmaFeatures: { 22 | jsx: true, 23 | }, 24 | ecmaVersion: 2018, 25 | sourceType: 'module', 26 | project: './tsconfig.json', 27 | }, 28 | rules: { 29 | 'linebreak-style': 'off', 30 | '@typescript-eslint/ban-ts-comment': 1, 31 | 'react-hooks/exhaustive-deps': 1, 32 | '@typescript-eslint/no-shadow': 1, 33 | '@typescript-eslint/naming-convention': 1, 34 | 'prettier/prettier': [ 35 | 'error', 36 | { 37 | endOfLine: 'auto', 38 | }, 39 | ], 40 | }, 41 | }; -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [beforesemicolon] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | **FlatList Version** 10 | : 11 | 12 | **Describe the bug** 13 | : 14 | 15 | **Steps to reproduce the behavior** 16 | : 17 | 18 | **Expected behavior** 19 | : 20 | 21 | **Desktop (please complete the following information):** 22 | - OS: 23 | - Browser: 24 | - Version: 25 | 26 | **Smartphone (please complete the following information):** 27 | - Device: 28 | - OS: 29 | - Browser: 30 | - Version: 31 | 32 | **Screenshots** 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x, 16.x, 18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | lib 4 | tools 5 | coverage 6 | *.tgz 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests 2 | src 3 | documentation 4 | tsconfig.json 5 | jest.config.js 6 | *.tgz 7 | .idea 8 | .github 9 | .eslintrc.js 10 | .yml 11 | -------------------------------------------------------------------------------- /.yml: -------------------------------------------------------------------------------- 1 | - name: git Actions 2 | uses: srt32/git-actions@v0.0.3 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Before Semicolon 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

7 | FlatList React 8 |

9 | 10 | A helpful react utility component intended to simplify handling rendering list with ease. 11 | It can handle `grouping`, `sorting`, `filtering`, `searching`, `sorting`, `paginating`, `styling` with very simple props. 12 | 13 | [![Build](https://github.com/beforesemicolon/flatlist-react/workflows/Node.js%20CI/badge.svg)](https://github.com/beforesemicolon/flatlist-react/actions) 14 | [![GitHub](https://img.shields.io/github/license/beforesemicolon/flatlist-react)](https://github.com/beforesemicolon/flatlist-react/blob/master/LICENSE) 15 | [![npm](https://img.shields.io/npm/v/flatlist-react)](https://www.npmjs.com/package/flatlist-react) 16 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/beforesemicolon/flatlist-react)](https://www.npmjs.com/package/flatlist-react) 17 | 18 | --- 19 | 20 | ###### Dear React Native Developer 21 | This is not intended for React-Native usage. Although some features will still work, others will just break your application. Use at your own risk. 22 | 23 | ### Install 24 | 25 | Make sure you are running **react** and **react-dom** version **16.8.0+**. 26 | 27 | ```npm install flatlist-react``` 28 | 29 | ### How to use 30 | 31 | Take in consideration the following list passed to component `PeopleList`: 32 | 33 | ```ts 34 | // App.jsx 35 | people = [ 36 | {firstName: 'Elson', lastName: 'Correia', info: {age: 24}}, 37 | {firstName: 'John', lastName: 'Doe', info: {age: 18}}, 38 | {firstName: 'Jane', lastName: 'Doe', info: {age: 34}}, 39 | {firstName: 'Maria', lastName: 'Carvalho', info: {age: 22}}, 40 | {firstName: 'Kelly', lastName: 'Correia', info:{age: 23}}, 41 | {firstName: 'Don', lastName: 'Quichote', info: {age: 39}}, 42 | {firstName: 'Marcus', lastName: 'Correia', info: {age: 0}}, 43 | {firstName: 'Bruno', lastName: 'Gonzales', info: {age: 25}}, 44 | {firstName: 'Alonzo', lastName: 'Correia', info: {age: 44}} 45 | ] 46 | 47 | 48 | ``` 49 | 50 | Now inside your component file, we create a function `renderPerson` that will be passed to `renderItem`: 51 | 52 | ```tsx 53 | // PeopleList.jsx 54 | import FlatList from 'flatlist-react'; 55 | 56 | ... 57 | 58 | renderPerson = (person, idx) => { 59 | return ( 60 |
  • 61 | {person.firstName} {person.lastName} ({person.info.age}) 62 |
  • 63 | ); 64 | } 65 | 66 | ... 67 | 68 | return ( 69 | 78 | ) 79 | ``` 80 | ### Full Documentation 81 | 82 | | Features | Props / Components | 83 | |:------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 84 | | [Rendering](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#rendering) | [list](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#list-and-renderitem), [renderItem](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#list-and-renderitem), [renderWhenEmpty](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#renderwhenempty), [wrapperHtmlTag](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#wrapperhtmltag), [limit](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#limit), [reversed](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#reversed) | 85 | | [Render Optimization](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#render-optimization) | [renderOnScroll](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#renderonscroll) | 86 | | [Pagination (Infinite Scroll)](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#pagination) | [hasMoreItems](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#hasmoreitems), [loadMoreItems](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#loadmoreitems), [paginationLoadingIndicator](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#paginationloadingindicator), [paginationLoadingIndicatorPosition](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#paginationloadingindicatorposition) | 87 | | [Filtering](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#filtering) | [filterBy](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#filterby) | 88 | | [Searching](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#searching) | [searchTerm](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#searchterm), [searchBy](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#searchby), [searchOnEveryWord](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#searchoneveryword), [searchMinCharactersCount](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#searchmincharacterscount), [searchCaseInsensitive](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#searchcaseinsensitive) | 89 | | [Sorting](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#sorting) | [sortBy](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#sortby), [sortCaseInsensitive](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#sortcaseinsensitive), [sortDescending](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#sortdescending) | 90 | | [Grouping](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#grouping) | [groupOf](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#groupof), [groupBy](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#groupby), [groupSeparator](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#groupseparator), [groupSeparatorAtTheBottom](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#groupseparatoratthebottom), [groupReversed](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#groupreversed), [groupSorted](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#groupsorted), [groupSortedBy](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#groupsortedby), [groupSortedDescending](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#groupsorteddescending), [groupSortedCaseInsensitive](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#groupsortedcaseinsensitive) | 91 | | [Styling](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#styling) | [displayGrid](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#displaygrid), [gridGap](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#gridgap), [minColumnWidth](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#mincolumnwidth), [displayRow](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#displayrow), [rowGap](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#rowgap) | 92 | | [scrollToTop](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#scrolltotop) | [scrollToTopButton](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#scrolltotopbutton), [scrollToTopOffset](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#scrolltotopoffset), [scrollToTopPadding](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#scrolltotoppadding), [scrollToTopPosition](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#scrolltotopposition) | 93 | | [Components](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#components) | [PlainList](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#plainlist) | 94 | | [Utilities](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#utilities) | [sortList](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#sortlist), [searchList](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#searchlist), [filterList](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#filterlist), [groupList](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#grouplist), [limitList](https://github.com/beforesemicolon/flatlist-react/blob/master/documentation/Doc.md#limitlist) | 95 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [ 3 | '/tests' 4 | ], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | }, 8 | testEnvironment: "jsdom" 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flatlist-react", 3 | "version": "1.5.14", 4 | "description": "A helpful utility component to handle lists in react like a champ", 5 | "main": "./lib/index.js", 6 | "types": "./lib/index.d.js", 7 | "scripts": { 8 | "format": "prettier --write src/**/*.ts{,x}", 9 | "lint": "eslint src/**/*.ts{,x}", 10 | "lint:fix": "eslint --fix src/**/*.ts{,x}", 11 | "clean": "rm -rf ./lib", 12 | "build": "npm run clean && npm run format && npm run lint && npm run test && tsc", 13 | "build:local": "npm run format && npm run lint && tsc", 14 | "local": "nodemon --watch ./src -e ts,tsx --exec 'npm run build:local && npm pack'", 15 | "test": "jest --silent", 16 | "test:watch": "jest --watch" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/beforesemicolon/flatlist-react" 21 | }, 22 | "keywords": [ 23 | "react", 24 | "utility", 25 | "flatlist" 26 | ], 27 | "author": "Elson T. Correia", 28 | "license": "MIT", 29 | "peerDependencies": { 30 | "react": ">=16.8.0", 31 | "react-dom": ">=16.8.0" 32 | }, 33 | "devDependencies": { 34 | "@testing-library/react": "^13.4.0", 35 | "@types/core-js": "^2.5.5", 36 | "@types/jest": "^29.4.0", 37 | "@types/node": "^18.14.0", 38 | "@types/react": "^18.0.28", 39 | "@types/react-dom": "^18.0.11", 40 | "@types/warning": "^3.0.0", 41 | "@typescript-eslint/eslint-plugin": "^5.52.0", 42 | "@typescript-eslint/parser": "^5.52.0", 43 | "eslint": "^8.34.0", 44 | "eslint-config-airbnb": "^19.0.4", 45 | "eslint-config-airbnb-typescript": "^17.0.0", 46 | "eslint-config-prettier": "^8.6.0", 47 | "eslint-import-resolver-typescript": "^3.5.3", 48 | "eslint-plugin-import": "^2.27.5", 49 | "eslint-plugin-jest": "^27.2.1", 50 | "eslint-plugin-json": "^3.1.0", 51 | "eslint-plugin-jsx-a11y": "^6.7.1", 52 | "eslint-plugin-prettier": "^4.2.1", 53 | "eslint-plugin-react": "^7.32.2", 54 | "eslint-plugin-react-hooks": "^4.6.0", 55 | "jest": "^29.4.3", 56 | "jest-environment-jsdom": "^29.4.3", 57 | "nodemon": "^2.0.20", 58 | "prettier": "^2.8.4", 59 | "react": "^18.2.0", 60 | "react-dom": "^18.2.0", 61 | "ts-jest": "^29.0.5", 62 | "typescript": "^4.9.5" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/___subComponents/DefaultBlank.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function DefaultBlank() { 4 | return

    List is empty...

    ; 5 | } 6 | 7 | export default DefaultBlank; 8 | -------------------------------------------------------------------------------- /src/___subComponents/DefaultLoadIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function DefaultLoadingIndicator() { 4 | return
    loading...
    ; 5 | } 6 | 7 | export default DefaultLoadingIndicator; 8 | -------------------------------------------------------------------------------- /src/___subComponents/DisplayHandler.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef, useEffect, useState } from "react"; 2 | 3 | export interface DisplayInterface { 4 | row?: boolean; 5 | rowGap?: string; 6 | grid?: boolean; 7 | gridGap?: string; 8 | gridMinColumnWidth?: string; 9 | } 10 | 11 | export interface DisplayHandlerProps { 12 | displayRow?: boolean; 13 | rowGap?: string; 14 | displayGrid?: boolean; 15 | // showGroupSeparatorAtTheBottom?: boolean; 16 | gridGap?: string; 17 | minColumnWidth?: string; 18 | display?: DisplayInterface; 19 | } 20 | 21 | const defaultProps: Required = { 22 | display: { 23 | grid: false, 24 | gridGap: "20px", 25 | gridMinColumnWidth: "200px", 26 | row: false, 27 | rowGap: "20px", 28 | }, 29 | displayGrid: false, 30 | displayRow: false, 31 | gridGap: "20px", 32 | minColumnWidth: "200px", 33 | rowGap: "20px", 34 | }; 35 | 36 | function DisplayHandler(props: DisplayHandlerProps) { 37 | const { displayGrid, displayRow, display, gridGap, minColumnWidth, rowGap } = 38 | { ...defaultProps, ...props }; 39 | const childSpanRef = createRef(); 40 | const [combo, setParentComponent] = useState<[HTMLElement, HTMLElement]>(); 41 | 42 | const styleParentGrid = ( 43 | styleTag: HTMLElement, 44 | container: HTMLElement 45 | ): void => { 46 | if (displayGrid || display.grid) { 47 | const gap = display.gridGap || gridGap || defaultProps.display.gridGap; 48 | const column = 49 | display.gridMinColumnWidth || 50 | minColumnWidth || 51 | defaultProps.display.gridMinColumnWidth; 52 | 53 | styleTag.innerHTML = ` 54 | [data-cont="${container.dataset.cont}"] { 55 | display: grid; 56 | grid-gap: ${gap}; 57 | gap: ${gap}; 58 | grid-template-columns: repeat(auto-fill, minmax(${column}, 1fr)); 59 | grid-template-rows: auto; 60 | align-items: stretch; 61 | } 62 | 63 | [data-cont="${container.dataset.cont}"] .__infinite-loader, 64 | [data-cont="${container.dataset.cont}"] .___scroll-renderer-anchor, 65 | [data-cont="${container.dataset.cont}"] .___list-separator { 66 | grid-column: 1/-1; 67 | } 68 | `; 69 | } else { 70 | styleTag.innerHTML = ""; 71 | } 72 | }; 73 | 74 | const styleParentRow = ( 75 | styleTag: HTMLElement, 76 | container: HTMLElement 77 | ): void => { 78 | if (displayRow || display.row) { 79 | const gap = display.rowGap || rowGap || defaultProps.display.rowGap; 80 | 81 | styleTag.innerHTML = ` 82 | [data-cont="${container.dataset.cont}"] { 83 | display: flex; 84 | flex-direction: column; 85 | } 86 | 87 | [data-cont="${container.dataset.cont}"] > *:not(.__infinite-loader) { 88 | display: block; 89 | flex: 1; 90 | width: 100%; 91 | margin-bottom: ${gap}; 92 | } 93 | `; 94 | } else { 95 | styleTag.innerHTML = ""; 96 | } 97 | }; 98 | 99 | const handleDisplayHandlerProps = ( 100 | container: HTMLElement, 101 | style: HTMLElement 102 | ): void => { 103 | if (container) { 104 | if (display.grid || displayGrid) { 105 | styleParentGrid(style, container); 106 | } else if (display.row || displayRow) { 107 | styleParentRow(style, container); 108 | } 109 | } 110 | }; 111 | 112 | useEffect(() => { 113 | if (combo) { 114 | handleDisplayHandlerProps(combo[0], combo[1]); 115 | } 116 | }); 117 | 118 | useEffect(() => { 119 | const { current } = childSpanRef; 120 | let container: HTMLElement; 121 | let style: HTMLElement; 122 | 123 | if (current) { 124 | const id = `__container-${new Date().getTime()}`; 125 | container = current.parentNode as HTMLElement; 126 | 127 | if (container) { 128 | container.setAttribute("data-cont", id); 129 | style = document.createElement("STYLE"); 130 | style.id = id; 131 | document.head.appendChild(style); 132 | setParentComponent([container, style]); 133 | handleDisplayHandlerProps(container, style); 134 | } 135 | } else { 136 | console.warn( 137 | "FlatList: it was not possible to get container's ref. Styling will not be possible" 138 | ); 139 | } 140 | 141 | return () => { 142 | if (style) { 143 | style.remove(); 144 | } 145 | }; 146 | }, []); 147 | 148 | return ( 149 | <> 150 | {/* following span is only used here to get the parent of these items since they are wrapped */} 151 | {/* in fragment which is not rendered on the dom */} 152 | {!combo && } 153 | 154 | ); 155 | } 156 | 157 | export default DisplayHandler; 158 | -------------------------------------------------------------------------------- /src/___subComponents/InfiniteLoader.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef, CSSProperties, ReactNode } from "react"; 2 | import { isFunction } from "../___utils/isType"; 3 | import DefaultLoadIndicator from "./DefaultLoadIndicator"; 4 | 5 | export interface InfiniteLoaderInterface { 6 | loadingIndicator?: null | (() => ReactNode) | ReactNode; 7 | loadingIndicatorPosition?: string; 8 | hasMore: boolean; 9 | loadMore: null | (() => void); 10 | } 11 | 12 | interface InfiniteLoaderState { 13 | scrollingContainer: HTMLElement | null; 14 | loadIndicatorContainer: HTMLDivElement | null; 15 | loading: boolean; 16 | prevItemsCount: number; 17 | } 18 | 19 | interface InfiniteLoaderProps extends InfiniteLoaderInterface { 20 | itemsCount: number; 21 | } 22 | 23 | class InfiniteLoader extends Component< 24 | InfiniteLoaderProps, 25 | InfiniteLoaderState 26 | > { 27 | state: InfiniteLoaderState = { 28 | prevItemsCount: this.props.itemsCount, 29 | loadIndicatorContainer: null, 30 | loading: false, 31 | scrollingContainer: null, 32 | }; 33 | 34 | loaderContainerRef = createRef(); 35 | 36 | // track the last scroll position so when new dom elements are inserted to avoid scroll jump 37 | lastScrollTop = 0; 38 | 39 | mounted = false; 40 | 41 | // keep track of the dom items in the list 42 | currentItemsCount = 0; 43 | 44 | componentDidMount(): void { 45 | this.mounted = true; 46 | const { current: loadIndicatorContainer } = this.loaderContainerRef; 47 | 48 | if (loadIndicatorContainer) { 49 | this.setState( 50 | { 51 | loadIndicatorContainer, 52 | scrollingContainer: loadIndicatorContainer.parentNode as HTMLElement, 53 | }, 54 | () => { 55 | this.currentItemsCount = this.getScrollingContainerChildrenCount(); 56 | this.setupScrollingContainerEventsListener(); 57 | } 58 | ); 59 | } else { 60 | console.warn( 61 | "FlatList: it was not possible to get container's ref. " + 62 | "Infinite scrolling pagination will not be possible" 63 | ); 64 | } 65 | } 66 | 67 | componentDidUpdate( 68 | prevProps: InfiniteLoaderProps, 69 | prevState: InfiniteLoaderState 70 | ): void { 71 | // reset scroll position to where last was 72 | if (this.state.scrollingContainer) { 73 | this.state.scrollingContainer.scrollTop = this.lastScrollTop; 74 | } 75 | 76 | // reset loading state when the list size changes 77 | if (prevProps.itemsCount !== this.props.itemsCount) { 78 | this.reset(); 79 | } 80 | 81 | this.checkIfLoadingIsNeeded(); 82 | } 83 | 84 | componentWillUnmount(): void { 85 | this.setupScrollingContainerEventsListener(true); 86 | this.mounted = false; 87 | } 88 | 89 | // update the loading flags and items count whether "hasMore" is false or list changed 90 | reset(): void { 91 | this.setState({ loading: false }); 92 | } 93 | 94 | getScrollingContainerChildrenCount = (): number => { 95 | const { scrollingContainer } = this.state; 96 | 97 | if (scrollingContainer) { 98 | return Math.max(0, scrollingContainer.children.length); 99 | } 100 | 101 | return 0; 102 | }; 103 | 104 | setupScrollingContainerEventsListener = (removeEvent = false) => { 105 | const { scrollingContainer } = this.state; 106 | 107 | if (scrollingContainer) { 108 | ["scroll", "mousewheel", "touchmove"].forEach((event: string) => { 109 | scrollingContainer.removeEventListener( 110 | event, 111 | this.checkIfLoadingIsNeeded, 112 | true 113 | ); 114 | 115 | if (!removeEvent) { 116 | scrollingContainer.addEventListener( 117 | event, 118 | this.checkIfLoadingIsNeeded, 119 | true 120 | ); 121 | } 122 | }); 123 | } 124 | }; 125 | 126 | // show or hide loading indicators based on scroll position 127 | // calls the "loadMore" function when is needed 128 | checkIfLoadingIsNeeded = (): void => { 129 | if (!this.mounted || !this.props.hasMore || this.state.loading) { 130 | return; 131 | } 132 | 133 | const { scrollingContainer, loadIndicatorContainer } = this.state; 134 | if (scrollingContainer && loadIndicatorContainer) { 135 | const { scrollTop, offsetTop, offsetHeight } = scrollingContainer; 136 | this.lastScrollTop = scrollTop; 137 | 138 | const loaderPosition = loadIndicatorContainer.offsetTop - scrollTop; 139 | const startingPoint = offsetTop + offsetHeight; 140 | 141 | if (loaderPosition <= startingPoint) { 142 | this.setState( 143 | { prevItemsCount: this.props.itemsCount, loading: true }, 144 | () => { 145 | (this.props.loadMore as () => void)(); 146 | } 147 | ); 148 | } 149 | } 150 | }; 151 | 152 | render(): ReactNode { 153 | const { loading } = this.state; 154 | const { 155 | hasMore, 156 | loadingIndicator = DefaultLoadIndicator, 157 | loadingIndicatorPosition = "left", 158 | } = this.props; 159 | 160 | const spinning = hasMore && loading; 161 | 162 | // do not remove the element from the dom so the ref is not broken but set it invisible enough 163 | const styles: CSSProperties = { 164 | display: "flex", 165 | height: spinning ? "auto" : 0, 166 | justifyContent: 167 | loadingIndicatorPosition === "center" 168 | ? loadingIndicatorPosition 169 | : loadingIndicatorPosition === "right" 170 | ? "flex-end" 171 | : "flex-start", 172 | padding: spinning ? "5px 0" : 0, 173 | visibility: spinning ? "visible" : "hidden", 174 | }; 175 | 176 | const loadingEl = isFunction(loadingIndicator) 177 | ? (loadingIndicator as () => ReactNode)() 178 | : (loadingIndicator as ReactNode); 179 | 180 | return ( 181 |
    186 | {spinning && (loadingIndicator ? loadingEl : )} 187 |
    188 | ); 189 | } 190 | } 191 | 192 | export default InfiniteLoader; 193 | -------------------------------------------------------------------------------- /src/___subComponents/PlainList.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef, forwardRef, ReactNode, Ref, useMemo } from "react"; 2 | import convertListToArray from "../___utils/convertListToArray"; 3 | import { isString } from "../___utils/isType"; 4 | import ScrollRenderer from "./ScrollRenderer"; 5 | import { handleRenderItem, renderBlank, renderFunc } from "./uiFunctions"; 6 | import { List } from "../flatListProps"; 7 | 8 | export interface PlainListProps { 9 | list: List; 10 | renderItem: renderFunc; 11 | renderWhenEmpty?: ReactNode | (() => JSX.Element); 12 | wrapperHtmlTag?: string; 13 | renderOnScroll?: boolean; 14 | [key: string]: any; 15 | } 16 | 17 | function PlainList(props: PlainListProps) { 18 | const { 19 | list, 20 | renderItem, 21 | renderWhenEmpty, 22 | renderOnScroll, 23 | wrapperHtmlTag, 24 | __forwarededRef, 25 | ...tagProps 26 | } = props; 27 | const dataList = convertListToArray(list); 28 | 29 | const renderThisItem = useMemo( 30 | () => handleRenderItem(renderItem, null), 31 | [renderItem] 32 | ); 33 | 34 | if (dataList.length === 0) { 35 | return renderBlank(renderWhenEmpty); 36 | } 37 | 38 | const WrapperElement = `${ 39 | isString(wrapperHtmlTag) && wrapperHtmlTag ? wrapperHtmlTag : "" 40 | }`; 41 | const content = ( 42 | <> 43 | {renderOnScroll ? ( 44 | 45 | ) : ( 46 | dataList.map(renderThisItem) 47 | )} 48 | 49 | ); 50 | 51 | return ( 52 | <> 53 | {WrapperElement ? ( 54 | // @ts-ignore 55 | 56 | {content} 57 | 58 | ) : ( 59 | content 60 | )} 61 | 62 | ); 63 | } 64 | 65 | export default forwardRef( 66 | (props: PlainListProps, ref: Ref) => { 67 | ref = ref || createRef(); 68 | return ; 69 | } 70 | ) as (props: PlainListProps) => JSX.Element; 71 | -------------------------------------------------------------------------------- /src/___subComponents/ScrollRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createRef, 3 | ReactNode, 4 | Ref, 5 | useEffect, 6 | useLayoutEffect, 7 | useMemo, 8 | useState, 9 | } from "react"; 10 | import { 11 | handleRenderGroupSeparator, 12 | handleRenderItem, 13 | renderFunc, 14 | } from "./uiFunctions"; 15 | 16 | interface ScrollRendererProps { 17 | list?: any[]; 18 | renderItem?: renderFunc; 19 | groupSeparator?: 20 | | ReactNode 21 | | ((g: any, idx: number, label: string) => ReactNode | null) 22 | | null; 23 | } 24 | 25 | function ScrollRenderer(props: ScrollRendererProps) { 26 | const { list = [], renderItem = () => null, groupSeparator = null } = props; 27 | const [render, setRender] = useState({ renderList: [], index: 0 }); 28 | const [mounted, setMounted] = useState(false); 29 | const [setupCount, setSetupCount] = useState(-1); 30 | const containerRef: Ref = createRef(); 31 | 32 | const renderThisItem = useMemo( 33 | () => 34 | handleRenderItem(renderItem, handleRenderGroupSeparator(groupSeparator)), 35 | [renderItem, groupSeparator] 36 | ); 37 | 38 | const updateRenderInfo = (count = 10) => { 39 | if (render.index < list.length) { 40 | const index = render.index + count; 41 | setRender({ 42 | renderList: list.slice(0, index) as any, 43 | index, 44 | }); 45 | } 46 | }; 47 | 48 | const onScroll = (span: HTMLSpanElement) => () => { 49 | requestAnimationFrame(() => { 50 | if (span) { 51 | const parent = span.parentNode as HTMLElement; 52 | const startingPoint = parent.offsetTop + parent.offsetHeight; 53 | const anchorPos = span.offsetTop - parent.scrollTop; 54 | 55 | if (anchorPos <= startingPoint + parent.offsetHeight * 2) { 56 | updateRenderInfo(); 57 | } 58 | } 59 | }); 60 | }; 61 | 62 | useEffect(() => { 63 | // when mounted 64 | setMounted(true); 65 | 66 | return () => { 67 | // when unmounted 68 | setMounted(false); 69 | }; 70 | }, []); 71 | 72 | useLayoutEffect(() => { 73 | if (mounted) { 74 | // reset list on list change 75 | const span: any = containerRef.current; 76 | const pos = span.parentNode.scrollTop; 77 | const index = Math.max(render.renderList.length, setupCount); 78 | 79 | setRender({ 80 | renderList: list.slice(0, index) as any, 81 | index, 82 | }); 83 | 84 | requestAnimationFrame(() => { 85 | if (span && span.parentNode) { 86 | span.parentNode.scrollTop = pos; 87 | } 88 | }); 89 | } 90 | }, [list]); 91 | 92 | useLayoutEffect(() => { 93 | const span = containerRef.current as HTMLSpanElement; 94 | const handleScroll = onScroll(span); 95 | let container: any = null; 96 | 97 | if (span) { 98 | container = span.parentNode; 99 | requestAnimationFrame(() => { 100 | // populate double the container height of items 101 | if ( 102 | render.index === 0 || 103 | container.scrollHeight <= container.offsetHeight * 2 104 | ) { 105 | updateRenderInfo(); 106 | } else if (setupCount === -1) { 107 | setSetupCount(render.index); 108 | } 109 | }); 110 | 111 | container.addEventListener("scroll", handleScroll, { passive: true }); 112 | } 113 | 114 | return () => { 115 | // when unmounted 116 | if (span) { 117 | container.removeEventListener("scroll", handleScroll, { 118 | passive: true, 119 | }); 120 | } 121 | }; 122 | }, [render.index, list.length]); 123 | 124 | return ( 125 | <> 126 | {render.renderList.map((item, idx) => renderThisItem(item, `${idx}`))} 127 | 132 | 133 | ); 134 | } 135 | 136 | export default ScrollRenderer; 137 | -------------------------------------------------------------------------------- /src/___subComponents/ScrollToTopButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef, ReactNode, Ref, useEffect, useState } from "react"; 2 | import { isFunction } from "../___utils/isType"; 3 | import { btnPosition } from "./uiFunctions"; 4 | 5 | interface ScrollToTopButtonProps { 6 | button?: ReactNode | (() => ReactNode); 7 | position?: string; 8 | offset?: number; 9 | padding?: number; 10 | scrollingContainer: Ref | undefined; 11 | } 12 | 13 | function ScrollToTopButton(props: ScrollToTopButtonProps) { 14 | const anchor: Ref = createRef(); 15 | const { 16 | button = null, 17 | position = "bottom right", 18 | padding = 20, 19 | offset = 50, 20 | scrollingContainer, 21 | } = props; 22 | const btn = isFunction(button) ? (button as () => ReactNode)() : button; 23 | const [mounted, setMounted] = useState(false); 24 | 25 | useEffect(() => { 26 | const buttonElement = (anchor as any).current.nextElementSibling; 27 | const container = (anchor as any).current.parentNode; 28 | const scrollContainer = (scrollingContainer as any).current; 29 | const containerStyle = getComputedStyle(container); 30 | const ogPos = containerStyle.position; 31 | container.style.position = ["absolute", "fixed", "relative"].includes(ogPos) 32 | ? ogPos 33 | : "relative"; 34 | const positionBtn = btnPosition(scrollContainer, buttonElement); 35 | const pos = position.split(" "); 36 | const updateBtnPosition = () => 37 | positionBtn(pos[0], pos[1], padding, offset); 38 | 39 | window.addEventListener("resize", updateBtnPosition); 40 | 41 | scrollContainer.addEventListener("scroll", updateBtnPosition); 42 | 43 | buttonElement.addEventListener("click", () => { 44 | scrollContainer.scrollTo({ 45 | top: 0, 46 | behavior: "smooth", 47 | }); 48 | }); 49 | 50 | setTimeout(() => updateBtnPosition(), 250); 51 | 52 | setMounted(true); 53 | return () => { 54 | container.style.position = ogPos; 55 | window.removeEventListener("resize", updateBtnPosition); 56 | }; 57 | }, []); 58 | 59 | return ( 60 | <> 61 | {!mounted && } 62 | {button ? btn : } 63 | 64 | ); 65 | } 66 | 67 | export default ScrollToTopButton; 68 | -------------------------------------------------------------------------------- /src/___subComponents/uiFunctions.tsx: -------------------------------------------------------------------------------- 1 | import React, { cloneElement, Component, FC, ReactNode } from "react"; 2 | import { isArray, isFunction } from "../___utils/isType"; 3 | import DefaultBlank from "./DefaultBlank"; 4 | 5 | export type renderFunc = ( 6 | item: ListItem, 7 | key: string 8 | ) => ReactNode | JSX.Element; 9 | 10 | export type renderItem = 11 | | ReactNode 12 | | FC 13 | | Component 14 | | renderFunc 15 | | JSX.Element; 16 | 17 | export const renderBlank = ( 18 | renderWhenEmpty: ReactNode | (() => JSX.Element) = null 19 | ): JSX.Element => 20 | renderWhenEmpty && isFunction(renderWhenEmpty) 21 | ? (renderWhenEmpty as () => JSX.Element)() 22 | : DefaultBlank(); 23 | 24 | export const handleRenderGroupSeparator = 25 | (CustomSeparator: any) => 26 | (sep: any, idx: number | string): JSX.Element => { 27 | const [cls, groupLabel, group] = sep; 28 | const separatorKey = `separator-${idx}`; 29 | 30 | if (CustomSeparator) { 31 | if (isFunction(CustomSeparator)) { 32 | const Sep = CustomSeparator(group, idx, groupLabel); 33 | return ( 34 |
    35 | 36 |
    37 | ); 38 | } 39 | 40 | return ( 41 |
    42 | {cloneElement(CustomSeparator, { groupLabel, group })} 43 |
    44 | ); 45 | } 46 | 47 | return
    ; 48 | }; 49 | 50 | export const handleRenderItem = 51 | ( 52 | renderItem: renderFunc, 53 | renderSeparator: 54 | | null 55 | | ((s: ListItem, i: number | string) => ReactNode) = null 56 | ) => 57 | (item: ListItem, key: number | string) => { 58 | if (!renderItem) { 59 | return null; 60 | } 61 | 62 | const itemId = 63 | (`${item}` === "[object Object]" && 64 | (item as { id: string | number }).id) || 65 | key; 66 | 67 | if (isArray(item) && (item as ListItem[])[0] === "___list-separator") { 68 | return typeof renderSeparator === "function" 69 | ? renderSeparator(item, itemId) 70 | : null; 71 | } 72 | 73 | if (typeof renderItem === "function") { 74 | return renderItem(item, `${itemId}`); 75 | } 76 | 77 | const comp = renderItem as JSX.Element; 78 | 79 | return ; 80 | }; 81 | 82 | export const btnPosition = (scrollContainer: HTMLElement, btn: HTMLElement) => { 83 | const z = window.getComputedStyle(scrollContainer).zIndex; 84 | btn.style.position = "absolute"; 85 | btn.style.zIndex = `${z === "auto" ? 1 : Number(z) + 1}`; 86 | btn.style.visibility = "hidden"; 87 | 88 | return (vertical: string, horizontal: string, padding = 20, offset = 50) => { 89 | let x = "0px"; 90 | let y = "0px"; 91 | 92 | if (vertical === "top") { 93 | y = `${parseFloat(`${padding}`)}px`; 94 | } else if (vertical === "bottom") { 95 | y = `calc(100% - ${parseFloat(`${padding}`) + btn.offsetHeight}px)`; 96 | } 97 | 98 | if (horizontal === "left") { 99 | x = `${parseFloat(`${padding}`)}px`; 100 | } else if (horizontal === "right") { 101 | x = `calc(100% - ${parseFloat(`${padding}`) + btn.offsetWidth}px)`; 102 | } 103 | 104 | window.requestAnimationFrame(() => { 105 | const dist = Number( 106 | (scrollContainer.scrollHeight - scrollContainer.offsetHeight).toFixed(0) 107 | ); 108 | offset = Math.min(offset, dist); 109 | 110 | btn.style.top = y; 111 | btn.style.left = x; 112 | btn.style.visibility = 113 | dist !== 0 && // got scrolled 114 | Number(scrollContainer.scrollTop.toFixed(0)) >= offset // position meets the offset 115 | ? "visible" 116 | : "hidden"; 117 | }); 118 | }; 119 | }; 120 | -------------------------------------------------------------------------------- /src/___utils/convertListToArray.ts: -------------------------------------------------------------------------------- 1 | import getType, { types } from "./getType"; 2 | 3 | export default (list: any): any[] => { 4 | const listType = getType(list); 5 | 6 | switch (listType) { 7 | case types.ARRAY: 8 | return list; 9 | case types.OBJECT: 10 | return Object.values(list); 11 | case types.SET: 12 | return Array.from(list); 13 | case types.MAP: 14 | return Array.from(list.values()); 15 | default: 16 | return []; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/___utils/convertMapToObject.ts: -------------------------------------------------------------------------------- 1 | export default (map: Map) => 2 | Array.from(map).reduce( 3 | (obj: Record, [key, value]) => 4 | Object.assign(obj, { [key]: value }), 5 | {} 6 | ); 7 | -------------------------------------------------------------------------------- /src/___utils/filterList.ts: -------------------------------------------------------------------------------- 1 | import getObjectDeepKeyValue from "./getObjectDeepKeyValue"; 2 | import { isArray, isFunction, isObject, isString } from "./isType"; 3 | 4 | const filterList = ( 5 | list: T[], 6 | by: ((item: T, idx: number) => boolean) | string = "" 7 | ): T[] => 8 | list.filter((item: T, idx: number) => { 9 | if (isString(by) && (isObject(item) || isArray(item))) { 10 | return getObjectDeepKeyValue(item as T, by as string); 11 | } 12 | 13 | if (isFunction(by)) { 14 | return (by as (item: T, idx: number) => boolean)(item as T, idx); 15 | } 16 | 17 | return true; 18 | }); 19 | 20 | export default filterList; 21 | -------------------------------------------------------------------------------- /src/___utils/getObjectDeepKeyValue.ts: -------------------------------------------------------------------------------- 1 | import getType, { types } from "./getType"; 2 | import { isObject, isArray, isString } from "./isType"; 3 | import convertMapToObject from "./convertMapToObject"; 4 | 5 | const convertAnythingToArrayOrObject = (obj: any) => 6 | getType(obj) === types.SET 7 | ? Array.from(obj) 8 | : getType(obj) === types.MAP 9 | ? convertMapToObject(obj) 10 | : isObject(obj) || isArray(obj) 11 | ? obj 12 | : {}; 13 | 14 | const getObjectDeepKeyValue = (value: any, dotSeparatedKeys: string) => { 15 | let convertedValue = convertAnythingToArrayOrObject(value); 16 | let convertedValueType = ""; 17 | 18 | if (isString(dotSeparatedKeys)) { 19 | const keys: any[] = dotSeparatedKeys.split("."); 20 | 21 | for (let i = 0; i < keys.length; i += 1) { 22 | const key = keys[i]; 23 | if (convertedValue[key] === undefined) { 24 | console.error(`Key "${key}" was not found in`, value); 25 | convertedValue = null; 26 | break; 27 | } 28 | 29 | if (getType(convertedValue[key]) === types.SET) { 30 | convertedValue = Array.from(convertedValue[key]); 31 | convertedValueType = types.SET; 32 | } else if (getType(convertedValue[key]) === types.MAP) { 33 | convertedValue = convertMapToObject(convertedValue[key]); 34 | convertedValueType = types.MAP; 35 | } else { 36 | convertedValue = convertedValue[key]; 37 | convertedValueType = ""; 38 | } 39 | } 40 | 41 | // convert convertedValue to its original form 42 | return convertedValueType === types.SET 43 | ? new Set(convertedValue) 44 | : convertedValueType === types.MAP 45 | ? new Map(Object.entries(convertedValue)) 46 | : convertedValue; 47 | } 48 | 49 | throw new Error( 50 | 'getObjectDeepKeyValue: "dotSeparatedKeys" is not a dot separated values string' 51 | ); 52 | }; 53 | 54 | export default getObjectDeepKeyValue; 55 | -------------------------------------------------------------------------------- /src/___utils/getType.ts: -------------------------------------------------------------------------------- 1 | interface StringObjectInterface { 2 | [t: string]: string; 3 | } 4 | 5 | const typesMap: StringObjectInterface = { 6 | array: "ARRAY", 7 | boolean: "BOOLEAN", 8 | function: "FUNCTION", 9 | map: "MAP", 10 | null: "NULL", 11 | number: "NUMBER", 12 | object: "OBJECT", 13 | set: "SET", 14 | string: "STRING", 15 | symbol: "SYMBOL", 16 | undefined: "UNDEFINED", 17 | weakMap: "WEAK_MAP", 18 | weakSet: "WEAK_SET", 19 | }; 20 | 21 | export const types: StringObjectInterface = Object.values(typesMap).reduce( 22 | (obj: StringObjectInterface, type: string): StringObjectInterface => { 23 | obj[type] = type; 24 | return obj; 25 | }, 26 | {} 27 | ); 28 | 29 | const getType = (x: any): string => { 30 | const type: string = typeof x; 31 | 32 | switch (type) { 33 | case "number": 34 | case "string": 35 | case "boolean": 36 | case "undefined": 37 | case "symbol": 38 | case "function": 39 | return typesMap[type]; 40 | default: 41 | return x === null 42 | ? typesMap.null 43 | : x instanceof Set 44 | ? typesMap.set 45 | : x instanceof WeakSet 46 | ? typesMap.weakSet 47 | : x instanceof Map 48 | ? typesMap.map 49 | : x instanceof WeakMap 50 | ? typesMap.weakMap 51 | : Array.isArray(x) 52 | ? typesMap.array 53 | : typesMap.object; // otherwise it is an object 54 | } 55 | }; 56 | 57 | export default getType; 58 | -------------------------------------------------------------------------------- /src/___utils/groupList.ts: -------------------------------------------------------------------------------- 1 | import getObjectDeepKeyValue from "./getObjectDeepKeyValue"; 2 | import { 3 | isBoolean, 4 | isFunction, 5 | isNilOrEmpty, 6 | isNumber, 7 | isString, 8 | } from "./isType"; 9 | import reverseList from "./reverseList"; 10 | 11 | export interface GroupOptionsInterface { 12 | by?: string | ((item: ListItem, idx: number) => string | number); 13 | limit?: number; 14 | reversed?: boolean; 15 | } 16 | 17 | const defaultGroupOptions: GroupOptionsInterface = { 18 | by: "", 19 | limit: 0, 20 | reversed: false, 21 | }; 22 | 23 | interface GroupedItemsObjectInterface { 24 | [s: string]: T[]; 25 | } 26 | 27 | const handleGroupReverse = ( 28 | groupedLists: ListItem[][], 29 | reverse = false 30 | ) => { 31 | if (reverse && isBoolean(reverse)) { 32 | return groupedLists.map((group) => reverseList(group)); 33 | } 34 | 35 | return groupedLists; 36 | }; 37 | 38 | const groupList = ( 39 | list: ListItem[], 40 | options: GroupOptionsInterface = defaultGroupOptions 41 | ) => { 42 | let groupLabels: any[] = []; 43 | 44 | if (isNilOrEmpty(options)) { 45 | options = defaultGroupOptions; 46 | } 47 | 48 | const { by: groupBy, limit } = options; 49 | 50 | if (groupBy && (isFunction(groupBy) || isString(groupBy))) { 51 | const groupedList: GroupedItemsObjectInterface = list.reduce( 52 | ( 53 | prevList: GroupedItemsObjectInterface, 54 | item: ListItem, 55 | idx: number 56 | ) => { 57 | const groupLabel: any = isFunction(groupBy) 58 | ? (groupBy as (item: ListItem, idx: number) => string)(item, idx) 59 | : getObjectDeepKeyValue(item, groupBy as string); 60 | 61 | if (!prevList[groupLabel]) { 62 | prevList[groupLabel] = []; 63 | } 64 | 65 | if (!limit || (limit > 0 && prevList[groupLabel].length < limit)) { 66 | prevList[groupLabel].push(item); 67 | } 68 | 69 | return prevList; 70 | }, 71 | {} 72 | ); 73 | 74 | // using Set here so the order is preserved and prevent duplicates 75 | groupLabels = Array.from(new Set(Object.keys(groupedList))); 76 | 77 | return { 78 | groupLabels, 79 | groupLists: handleGroupReverse( 80 | Object.values(groupedList), 81 | options.reversed 82 | ), 83 | }; 84 | } 85 | if (limit && isNumber(limit) && limit > 0) { 86 | let groupLabel = 1; 87 | const groupLists: GroupedItemsObjectInterface = list.reduce( 88 | (prevList: GroupedItemsObjectInterface, item: ListItem) => { 89 | if (!prevList[groupLabel]) { 90 | prevList[groupLabel] = []; 91 | } 92 | 93 | prevList[groupLabel].push(item); 94 | 95 | if (prevList[groupLabel].length === limit) { 96 | groupLabel += 1; 97 | } 98 | 99 | return prevList; 100 | }, 101 | {} 102 | ); 103 | 104 | groupLabels = Array.from(new Set(Object.keys(groupLists))); 105 | 106 | return { 107 | groupLabels, 108 | groupLists: handleGroupReverse( 109 | Object.values(groupLists), 110 | options.reversed 111 | ), 112 | }; 113 | } 114 | 115 | return { 116 | groupLabels, 117 | groupLists: handleGroupReverse([list], options.reversed), 118 | }; 119 | }; 120 | 121 | export default groupList; 122 | -------------------------------------------------------------------------------- /src/___utils/isType.ts: -------------------------------------------------------------------------------- 1 | import getType, { types } from "./getType"; 2 | 3 | export const isBoolean = (x: any): boolean => getType(x) === types.BOOLEAN; 4 | 5 | export const isNumber = (x: any): boolean => 6 | getType(Number(x)) === types.NUMBER && !isNaN(Number(x)); 7 | 8 | export const isNumeric = (x: any): boolean => isFinite(x) && isNumber(x); 9 | 10 | export const isObject = (x: any): boolean => getType(x) === types.OBJECT; 11 | 12 | export const isObjectLiteral = (x: any): boolean => 13 | isObject(x) && x.constructor === Object; 14 | 15 | export const isString = (x: any): boolean => 16 | getType(x) === types.STRING || x instanceof String; 17 | 18 | export const isArray = (x: any): boolean => getType(x) === types.ARRAY; 19 | 20 | export const isSet = (x: any): boolean => getType(x) === types.SET; 21 | 22 | export const isMap = (x: any): boolean => getType(x) === types.MAP; 23 | 24 | export const isNil = (x: any): boolean => 25 | x === null || getType(x) === types.UNDEFINED; 26 | 27 | export const isEmpty = (x: any): boolean => 28 | ((isString(x) || isArray(x)) && x.length === 0) || 29 | (isObject(x) && Object.keys(x).length === 0) || 30 | (getType(x) === types.MAP && x.size === 0) || 31 | (getType(x) === types.SET && x.size === 0) || 32 | (getType(x) === types.NUMBER && isNaN(x)); 33 | 34 | export const isNilOrEmpty = (x: any): boolean => isNil(x) || isEmpty(x); 35 | 36 | export const isFunction = (x: any): boolean => getType(x) === types.FUNCTION; 37 | 38 | export default { 39 | isArray, 40 | isFunction, 41 | isNil, 42 | isEmpty, 43 | isNilOrEmpty, 44 | isNumber, 45 | isObject, 46 | isString, 47 | isSet, 48 | isMap, 49 | }; 50 | -------------------------------------------------------------------------------- /src/___utils/limitList.ts: -------------------------------------------------------------------------------- 1 | import { isNumber } from "./isType"; 2 | 3 | const limitList = ( 4 | list: T[], 5 | limit: string | number = 0, 6 | to: string | number | undefined = undefined 7 | ): T[] => { 8 | if (!limit || Number(limit) <= 0 || Number(limit) >= list.length) { 9 | return list; 10 | } 11 | 12 | if (to === undefined) { 13 | return list.slice(0, Number(limit)); 14 | } 15 | 16 | return !to || !isNumber(to) || Number(to) === 0 17 | ? list.slice(Number(limit)) 18 | : list.slice(Number(limit), Number(to)); 19 | }; 20 | 21 | export default limitList; 22 | -------------------------------------------------------------------------------- /src/___utils/reverseList.ts: -------------------------------------------------------------------------------- 1 | // avoid "reverse" array method as it changes in place 2 | // we need to create a new reversed list instead 3 | const reverseList = (list: T[]): T[] => 4 | list.map((item: T, i: number, l: T[]) => l[l.length - (i + 1)]); 5 | 6 | export default reverseList; 7 | -------------------------------------------------------------------------------- /src/___utils/searchList.ts: -------------------------------------------------------------------------------- 1 | import filterList from "./filterList"; 2 | import getObjectDeepKeyValue from "./getObjectDeepKeyValue"; 3 | import { isArray, isFunction, isObject, isNilOrEmpty } from "./isType"; 4 | 5 | type cb = (item: ListItem, idx: number) => boolean; 6 | type termCb = (item: ListItem, term: string, idx: number) => boolean; 7 | 8 | export interface SearchOptionsInterface { 9 | term?: string; 10 | everyWord?: boolean; // deprecated 11 | onEveryWord?: boolean; 12 | caseInsensitive?: boolean; 13 | minCharactersCount?: number; 14 | by?: 15 | | string 16 | | Array 17 | | termCb; 18 | } 19 | 20 | const defaultSearchOptions = { 21 | by: "0", 22 | caseInsensitive: false, 23 | everyWord: false, 24 | minCharactersCount: 3, 25 | term: "", 26 | }; 27 | 28 | const defaultFilterByFn = ( 29 | item: any, 30 | term: string | string[], 31 | caseInsensitive = false, 32 | by = "0" 33 | ) => { 34 | const keyValue = 35 | isObject(item) || isArray(item) 36 | ? getObjectDeepKeyValue(item, by as string) 37 | : item; 38 | 39 | const value = caseInsensitive ? `${keyValue}`.toLowerCase() : `${keyValue}`; 40 | 41 | if (isArray(term)) { 42 | return (term as []).some((t: string) => { 43 | t = caseInsensitive ? t.toLowerCase() : t.trim(); 44 | 45 | return value.search(t.trim()) >= 0; 46 | }); 47 | } 48 | 49 | term = caseInsensitive ? (term as string).toLowerCase() : (term as string); 50 | 51 | return value.search(term.trim() as string) >= 0; 52 | }; 53 | 54 | const getFilterByFn = ( 55 | term: string | string[], 56 | by: SearchOptionsInterface["by"], 57 | caseInsensitive = false 58 | ): cb => { 59 | if (isFunction(by)) { 60 | if (isArray(term)) { 61 | return (item: ListItem, idx: number) => 62 | (term as string[]).some((t: string) => { 63 | t = caseInsensitive ? (t as string).toLowerCase() : t; 64 | return (by as termCb)(item, t.trim(), idx); 65 | }); 66 | } 67 | 68 | term = caseInsensitive ? (term as string).toLowerCase() : term; 69 | return (item: ListItem, idx: number) => 70 | (by as termCb)(item, (term as string).trim(), idx); 71 | } 72 | 73 | if (isArray(by)) { 74 | return (item: ListItem) => 75 | (by as []).some((key: any) => { 76 | const keyCaseInsensitive = 77 | isObject(key) && key.caseInsensitive !== undefined 78 | ? key.caseInsensitive 79 | : caseInsensitive; 80 | const keyBy = (isObject(key) ? key.key : key) || "0"; 81 | return defaultFilterByFn(item, term, keyCaseInsensitive, keyBy); 82 | }); 83 | } 84 | 85 | return (item: ListItem) => 86 | defaultFilterByFn(item, term, caseInsensitive, (by as string) || "0"); 87 | }; 88 | 89 | const searchList = ( 90 | list: ListItem[], 91 | options: SearchOptionsInterface 92 | ): ListItem[] => { 93 | if (isNilOrEmpty(options)) { 94 | options = defaultSearchOptions as SearchOptionsInterface; 95 | } 96 | 97 | if (list.length > 0) { 98 | const { term, by = "0", minCharactersCount = 0 } = options; 99 | 100 | if (term && by && term.length >= minCharactersCount) { 101 | const { everyWord, caseInsensitive } = options; 102 | 103 | if (everyWord) { 104 | const termWords = term 105 | .trim() 106 | .split(/\s+/) 107 | .filter((word: string) => word.length >= minCharactersCount); 108 | 109 | if (termWords.length > 0) { 110 | const filterByFn = getFilterByFn( 111 | Array.from(new Set(termWords)), 112 | by, 113 | caseInsensitive 114 | ); 115 | 116 | return filterList(list, filterByFn); 117 | } 118 | } else { 119 | const filterByFn = getFilterByFn(term, by, caseInsensitive); 120 | 121 | return filterList(list, filterByFn); 122 | } 123 | } 124 | } 125 | 126 | return list; 127 | }; 128 | 129 | export default searchList; 130 | -------------------------------------------------------------------------------- /src/___utils/sortList.ts: -------------------------------------------------------------------------------- 1 | import getObjectDeepKeyValue from "./getObjectDeepKeyValue"; 2 | import { isString, isObject, isArray, isNilOrEmpty } from "./isType"; 3 | 4 | export interface SortOptionsInterface { 5 | by?: 6 | | string 7 | | Array< 8 | | string 9 | | { key: string; descending?: boolean; caseInsensitive?: boolean } 10 | >; 11 | descending?: boolean; 12 | caseInsensitive?: boolean; 13 | } 14 | 15 | const defaultSortOptions: SortOptionsInterface = { 16 | caseInsensitive: false, 17 | descending: false, 18 | by: "", 19 | }; 20 | 21 | const compareKeys = ( 22 | first: any, 23 | second: any, 24 | { key = "", caseInsensitive = false, descending = false }: any 25 | ) => { 26 | if (key) { 27 | first = 28 | isObject(first) || isArray(first) 29 | ? getObjectDeepKeyValue(first, key as string) 30 | : first; 31 | second = 32 | isObject(second) || isArray(second) 33 | ? getObjectDeepKeyValue(second, key as string) 34 | : second; 35 | } 36 | 37 | if (caseInsensitive) { 38 | first = isString(first) ? first.toLowerCase() : first; 39 | second = isString(second) ? second.toLowerCase() : second; 40 | } 41 | 42 | return first > second 43 | ? descending 44 | ? -1 45 | : 1 46 | : first < second 47 | ? descending 48 | ? 1 49 | : -1 50 | : 0; 51 | }; 52 | 53 | const sortList = ( 54 | list: T[], 55 | options: SortOptionsInterface = defaultSortOptions 56 | ): T[] => { 57 | const listCopy = [...list]; 58 | 59 | if (isNilOrEmpty(options)) { 60 | options = defaultSortOptions; 61 | } 62 | 63 | options = { ...defaultSortOptions, ...options }; 64 | 65 | listCopy.sort((first: any, second: any) => { 66 | if (isArray(options.by)) { 67 | for (let i = 0; i < (options.by as []).length; i += 1) { 68 | const key = (options.by as [])[i]; 69 | const option = isObject(key) ? key : { ...options, key }; 70 | const res = compareKeys(first, second, option); 71 | 72 | if (res !== 0) { 73 | return res; 74 | } 75 | } 76 | 77 | return 0; 78 | } 79 | 80 | return compareKeys(first, second, { ...options, key: options.by }); 81 | }); 82 | 83 | return listCopy; 84 | }; 85 | 86 | export default sortList; 87 | -------------------------------------------------------------------------------- /src/flatListProps.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { 3 | DisplayHandlerProps, 4 | DisplayInterface, 5 | } from "./___subComponents/DisplayHandler"; 6 | import { InfiniteLoaderInterface } from "./___subComponents/InfiniteLoader"; 7 | import { renderFunc } from "./___subComponents/uiFunctions"; 8 | import { GroupOptionsInterface } from "./___utils/groupList"; 9 | import { SearchOptionsInterface } from "./___utils/searchList"; 10 | import { SortOptionsInterface } from "./___utils/sortList"; 11 | 12 | export type List = Array | Set | Map | { [key: string]: T }; 13 | 14 | export interface GroupInterface 15 | extends GroupOptionsInterface { 16 | limit?: number; 17 | of?: number; 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | separator?: 20 | | ReactNode 21 | | ((g: ListItem[], idx: number, label: string) => ReactNode | null) 22 | | null; 23 | separatorAtTheBottom?: boolean; 24 | sortBy?: SortOptionsInterface["by"]; 25 | sortedBy?: SortOptionsInterface["by"]; 26 | sortDescending?: boolean; 27 | sortedDescending?: boolean; 28 | sortCaseInsensitive?: boolean; 29 | sortedCaseInsensitive?: boolean; 30 | } 31 | 32 | export interface ScrollToTopInterface { 33 | button?: ReactNode | (() => JSX.Element); 34 | offset?: number; 35 | padding?: number; 36 | position?: 37 | | "top" 38 | | "bottom" 39 | | "top right" 40 | | "top left" 41 | | "bottom left" 42 | | "bottom right"; 43 | } 44 | 45 | export interface SortInterface extends SortOptionsInterface { 46 | groupBy?: GroupInterface["sortBy"]; 47 | groupDescending?: GroupInterface["sortDescending"]; 48 | groupCaseInsensitive?: GroupInterface["sortCaseInsensitive"]; 49 | } 50 | 51 | export interface FlatListProps { 52 | // RENDER 53 | list: List; 54 | renderItem: renderFunc; 55 | renderWhenEmpty?: ReactNode | (() => JSX.Element); 56 | renderOnScroll?: boolean; 57 | limit?: number | string; 58 | reversed?: boolean; 59 | wrapperHtmlTag?: string; 60 | // sorting 61 | sort?: boolean | SortInterface; 62 | sortBy?: SortInterface["by"]; 63 | sortCaseInsensitive?: SortInterface["caseInsensitive"]; 64 | sortDesc?: SortInterface["descending"]; 65 | sortDescending?: SortInterface["descending"]; 66 | sortGroupBy?: GroupInterface["sortBy"]; // deprecated 67 | sortGroupDesc?: GroupInterface["sortDescending"]; // deprecated 68 | sortGroupDescending?: GroupInterface["sortDescending"]; 69 | sortGroupCaseInsensitive?: GroupInterface["sortCaseInsensitive"]; // deprecated 70 | // grouping 71 | group?: GroupInterface; 72 | showGroupSeparatorAtTheBottom?: GroupInterface["separatorAtTheBottom"]; // deprecated 73 | groupSeparatorAtTheBottom?: GroupInterface["separatorAtTheBottom"]; 74 | groupReversed?: GroupInterface["reversed"]; 75 | groupSeparator?: GroupInterface["separator"]; 76 | groupBy?: GroupInterface["by"]; 77 | groupOf?: GroupInterface["limit"]; 78 | groupSorted?: boolean; 79 | groupSortedDescending?: GroupInterface["sortDescending"]; 80 | groupSortedCaseInsensitive?: GroupInterface["sortCaseInsensitive"]; 81 | // display 82 | display?: DisplayInterface; 83 | displayRow?: DisplayHandlerProps["displayRow"]; 84 | rowGap?: DisplayHandlerProps["rowGap"]; 85 | displayGrid?: DisplayHandlerProps["displayGrid"]; 86 | gridGap?: DisplayHandlerProps["gridGap"]; 87 | minColumnWidth?: DisplayHandlerProps["minColumnWidth"]; 88 | // filtering 89 | filterBy?: string | ((item: ListItem, idx: number) => boolean); 90 | // searching 91 | search?: SearchOptionsInterface; 92 | searchTerm?: SearchOptionsInterface["term"]; 93 | searchBy?: SearchOptionsInterface["by"]; 94 | searchOnEveryWord?: SearchOptionsInterface["everyWord"]; 95 | searchCaseInsensitive?: SearchOptionsInterface["caseInsensitive"]; 96 | searchableMinCharactersCount?: SearchOptionsInterface["minCharactersCount"]; // deprecated 97 | // pagination 98 | pagination?: InfiniteLoaderInterface; 99 | hasMoreItems?: InfiniteLoaderInterface["hasMore"]; 100 | loadMoreItems?: null | InfiniteLoaderInterface["loadMore"]; 101 | paginationLoadingIndicator?: InfiniteLoaderInterface["loadingIndicator"]; 102 | paginationLoadingIndicatorPosition?: InfiniteLoaderInterface["loadingIndicatorPosition"]; 103 | // scrollToTop 104 | scrollToTop?: boolean | ScrollToTopInterface; 105 | scrollToTopButton?: ReactNode | (() => ReactNode); 106 | scrollToTopOffset?: number; 107 | scrollToTopPadding?: number; 108 | scrollToTopPosition?: string; 109 | // others 110 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 111 | [key: string]: any; 112 | } 113 | 114 | export const defaultProps: FlatListProps = { 115 | __forwarededRef: { current: null }, 116 | // RENDER 117 | list: [], 118 | renderItem: () => null, 119 | limit: 0, 120 | renderWhenEmpty: null, 121 | reversed: false, 122 | renderOnScroll: false, 123 | wrapperHtmlTag: "", 124 | // DISPLAY 125 | display: { 126 | grid: false, 127 | gridGap: "", 128 | gridMinColumnWidth: "", 129 | row: false, 130 | rowGap: "", 131 | }, 132 | displayGrid: false, 133 | displayRow: false, 134 | gridGap: "", 135 | rowGap: "", 136 | minColumnWidth: "", 137 | // FILTER 138 | filterBy: "", 139 | // GROUPS 140 | group: { 141 | by: "", 142 | limit: 0, // deprecated 143 | of: 0, 144 | reversed: false, 145 | separator: null, 146 | separatorAtTheBottom: false, 147 | sortedBy: "", 148 | sortBy: "", // deprecated 149 | sortedCaseInsensitive: false, 150 | sortCaseInsensitive: false, // deprecated 151 | sortedDescending: false, 152 | sortDescending: false, // deprecated 153 | }, 154 | groupBy: "", 155 | groupOf: 0, 156 | groupReversed: false, 157 | groupSeparator: null, 158 | groupSeparatorAtTheBottom: false, 159 | groupSorted: false, 160 | groupSortedBy: "", 161 | groupSortedDescending: false, 162 | groupSortedCaseInsensitive: false, 163 | showGroupSeparatorAtTheBottom: false, // deprecated 164 | // PAGINATION 165 | pagination: { 166 | hasMore: false, 167 | loadMore: null, 168 | loadingIndicator: null, 169 | loadingIndicatorPosition: "", 170 | }, 171 | hasMoreItems: false, 172 | loadMoreItems: null, 173 | paginationLoadingIndicator: null, 174 | paginationLoadingIndicatorPosition: "", 175 | // SCROLL TO TOP 176 | scrollToTop: { 177 | button: null, 178 | offset: undefined, 179 | padding: undefined, 180 | position: undefined, 181 | }, 182 | scrollToTopButton: null, 183 | scrollToTopOffset: undefined, 184 | scrollToTopPadding: undefined, 185 | scrollToTopPosition: undefined, 186 | // SEARCH 187 | search: { 188 | by: "", 189 | caseInsensitive: false, 190 | everyWord: false, // deprecated 191 | onEveryWord: false, 192 | minCharactersCount: 0, 193 | term: "", 194 | }, 195 | searchBy: "", 196 | searchCaseInsensitive: false, 197 | searchOnEveryWord: false, 198 | searchTerm: "", 199 | searchMinCharactersCount: 0, 200 | searchableMinCharactersCount: 0, // deprecated 201 | // SORT 202 | sort: { 203 | by: "", 204 | caseInsensitive: false, 205 | descending: false, 206 | groupBy: "", // deprecated 207 | groupCaseInsensitive: false, // deprecated 208 | groupDescending: false, // deprecated 209 | }, 210 | sortBy: "", 211 | sortCaseInsensitive: false, 212 | sortDesc: false, // deprecated 213 | sortDescending: false, 214 | sortGroupBy: "", // deprecated 215 | sortGroupDesc: false, // deprecated 216 | sortGroupCaseInsensitive: false, // deprecated 217 | }; 218 | -------------------------------------------------------------------------------- /src/flatlist-react.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef, forwardRef, Ref, useMemo } from "react"; 2 | import DisplayHandler, { 3 | DisplayInterface, 4 | } from "./___subComponents/DisplayHandler"; 5 | import InfiniteLoader, { 6 | InfiniteLoaderInterface, 7 | } from "./___subComponents/InfiniteLoader"; 8 | import ScrollRenderer from "./___subComponents/ScrollRenderer"; 9 | import ScrollToTopButton from "./___subComponents/ScrollToTopButton"; 10 | import { 11 | handleRenderGroupSeparator, 12 | handleRenderItem, 13 | renderBlank, 14 | } from "./___subComponents/uiFunctions"; 15 | import { isString } from "./___utils/isType"; 16 | import { 17 | defaultProps, 18 | FlatListProps, 19 | GroupInterface, 20 | ScrollToTopInterface, 21 | } from "./flatListProps"; 22 | import { useList } from "./hooks/use-list"; 23 | import { PlainListProps } from "./___subComponents/PlainList"; 24 | 25 | function FlatList(props: FlatListProps) { 26 | const { 27 | list, 28 | renderWhenEmpty = null, 29 | wrapperHtmlTag, 30 | renderItem, 31 | renderOnScroll, // render/list related props 32 | group = {} as GroupInterface, 33 | groupSeparator, // group props 34 | display = {} as DisplayInterface, 35 | displayRow, 36 | rowGap, 37 | displayGrid, 38 | gridGap, 39 | minColumnWidth, // display props, 40 | hasMoreItems, 41 | loadMoreItems, 42 | paginationLoadingIndicator, 43 | paginationLoadingIndicatorPosition, 44 | scrollToTop, 45 | scrollToTopButton = null, 46 | scrollToTopPadding, 47 | scrollToTopOffset, 48 | scrollToTopPosition, 49 | pagination = {} as InfiniteLoaderInterface, // pagination props 50 | // eslint-disable-next-line @typescript-eslint/naming-convention 51 | // @ts-ignore 52 | __forwarededRef, 53 | ...otherProps 54 | } = { ...defaultProps, ...props }; 55 | const renderList = useList(props); 56 | 57 | const tagProps = useMemo( 58 | () => 59 | Object.keys(otherProps) 60 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 61 | .filter((k: string) => (defaultProps as any)[k] === undefined) 62 | .reduce( 63 | (p, k: string) => ({ 64 | ...p, 65 | [k]: (otherProps as Record)[k], 66 | }), 67 | {} 68 | ), 69 | [otherProps] 70 | ); 71 | 72 | const renderThisItem = useMemo( 73 | () => 74 | handleRenderItem( 75 | renderItem, 76 | handleRenderGroupSeparator(group.separator || groupSeparator) 77 | ), 78 | [renderItem, group.separator, groupSeparator] 79 | ); 80 | 81 | if (renderList.length === 0) { 82 | return renderBlank(renderWhenEmpty); 83 | } 84 | 85 | const content = ( 86 | <> 87 | {renderOnScroll && !(loadMoreItems || pagination.loadMore) ? ( 88 | 93 | ) : ( 94 | renderList.map(renderThisItem) 95 | )} 96 | {(displayRow || displayGrid || display.grid || display.row) && ( 97 | 107 | )} 108 | {(loadMoreItems || pagination.loadMore) && !renderOnScroll && ( 109 | 121 | )} 122 | 123 | ); 124 | 125 | const showScrollToTopButton = 126 | scrollToTop === true || 127 | (scrollToTop as ScrollToTopInterface).button || 128 | scrollToTopButton; 129 | 130 | let WrapperElement = ""; 131 | 132 | if ((isString(wrapperHtmlTag) && wrapperHtmlTag) || showScrollToTopButton) { 133 | WrapperElement = wrapperHtmlTag || "div"; 134 | } 135 | 136 | return ( 137 | <> 138 | {WrapperElement ? ( 139 | // @ts-ignore 140 | 141 | {content} 142 | 143 | ) : ( 144 | content 145 | )} 146 | {showScrollToTopButton && ( 147 | 163 | )} 164 | 165 | ); 166 | } 167 | 168 | // export default FlatList; 169 | export default forwardRef>( 170 | (props: PlainListProps, ref: Ref) => { 171 | ref = ref || createRef(); 172 | return ; 173 | } 174 | ) as (props: FlatListProps) => JSX.Element; 175 | -------------------------------------------------------------------------------- /src/hooks/use-list.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import convertListToArray from "../___utils/convertListToArray"; 3 | import filterList from "../___utils/filterList"; 4 | import groupList from "../___utils/groupList"; 5 | import { isBoolean, isNil } from "../___utils/isType"; 6 | import limitList from "../___utils/limitList"; 7 | import reverseList from "../___utils/reverseList"; 8 | import searchList from "../___utils/searchList"; 9 | import sortList from "../___utils/sortList"; 10 | import { defaultProps, FlatListProps, SortInterface } from "../flatListProps"; 11 | 12 | export const useList = ({ 13 | list, 14 | limit, 15 | reversed, 16 | filterBy, 17 | group, 18 | groupBy, 19 | groupOf, 20 | showGroupSeparatorAtTheBottom, 21 | groupReversed, 22 | groupSeparatorAtTheBottom, 23 | groupSortedCaseInsensitive, 24 | groupSortedDescending, 25 | groupSorted, 26 | groupSortedBy, 27 | sortBy, 28 | sortDesc, 29 | sort, 30 | sortCaseInsensitive, 31 | sortGroupBy, 32 | sortGroupDesc, 33 | sortGroupCaseInsensitive, 34 | sortDescending, 35 | search, 36 | searchBy, 37 | searchOnEveryWord, 38 | searchTerm, 39 | searchCaseInsensitive, 40 | searchableMinCharactersCount, 41 | searchMinCharactersCount, 42 | }: FlatListProps): ListItem[] => { 43 | // convert list to array 44 | let renderList = useMemo(() => convertListToArray(list), [list]); 45 | 46 | // reverse list 47 | renderList = useMemo( 48 | () => 49 | typeof reversed === "boolean" && reversed 50 | ? reverseList(renderList) 51 | : renderList, 52 | [renderList, reversed] 53 | ); 54 | 55 | // limit list 56 | renderList = useMemo(() => { 57 | if (!isNil(limit)) { 58 | const [from, to] = `${limit}`.split(","); 59 | return limitList(renderList, from, to); 60 | } 61 | 62 | return renderList; 63 | }, [renderList, limit]); 64 | 65 | // filter list 66 | renderList = useMemo( 67 | () => (filterBy ? filterList(renderList, filterBy) : renderList), 68 | [renderList, filterBy] 69 | ); 70 | 71 | // search list 72 | renderList = useMemo(() => { 73 | if (searchTerm || (search && search.term)) { 74 | const searchOptions = { 75 | ...defaultProps.search, 76 | ...search, 77 | }; 78 | 79 | return searchList(renderList, { 80 | by: searchOptions.by || searchBy || "0", 81 | caseInsensitive: searchOptions.caseInsensitive || searchCaseInsensitive, 82 | everyWord: 83 | searchOptions.onEveryWord || 84 | searchOptions.everyWord || // deprecated 85 | searchOnEveryWord, 86 | term: searchOptions.term || searchTerm, 87 | minCharactersCount: 88 | // @ts-ignore 89 | searchOptions.searchableMinCharactersCount || // deprecated 90 | searchOptions.minCharactersCount || 91 | searchMinCharactersCount || 92 | searchableMinCharactersCount || // deprecated 93 | 3, 94 | }); 95 | } 96 | 97 | return renderList; 98 | }, [ 99 | renderList, 100 | search, 101 | searchBy, 102 | searchOnEveryWord, 103 | searchTerm, 104 | searchCaseInsensitive, 105 | searchableMinCharactersCount, 106 | searchMinCharactersCount, 107 | ]); 108 | 109 | const sortOptions = useMemo( 110 | () => ({ 111 | ...(defaultProps.sort as SortInterface), 112 | ...(sort as SortInterface), 113 | }), 114 | [renderList, sort] 115 | ); 116 | 117 | // sort list 118 | renderList = useMemo(() => { 119 | if (sortOptions.by || sortBy || (isBoolean(sort) && sort)) { 120 | return sortList(renderList, { 121 | caseInsensitive: 122 | sortOptions.caseInsensitive || sortCaseInsensitive || false, 123 | descending: 124 | sortOptions.descending || sortDescending || sortDesc || false, // deprecated 125 | by: sortOptions.by || sortBy, 126 | }); 127 | } 128 | 129 | return renderList; 130 | }, [ 131 | renderList, 132 | sortOptions, 133 | sortBy, 134 | sortDesc, 135 | sort, 136 | sortCaseInsensitive, 137 | sortDescending, 138 | ]); 139 | 140 | // group list 141 | renderList = useMemo(() => { 142 | const groupOptions = { 143 | ...defaultProps.group, 144 | ...group, 145 | }; 146 | 147 | if ( 148 | groupOptions.by || 149 | groupBy || 150 | groupOf || 151 | groupOptions.of || 152 | groupOptions.limit 153 | ) { 154 | const groupingOptions = { 155 | by: groupOptions.by || groupBy, 156 | limit: groupOptions.of || groupOf || groupOptions.limit, // deprecated 157 | reversed: groupOptions.reversed || groupReversed, 158 | }; 159 | 160 | const gList = groupList(renderList, groupingOptions); 161 | 162 | return gList.groupLists.reduce((newGList: any, aGroup, idx: number) => { 163 | if ( 164 | groupSorted || 165 | // @ts-ignore 166 | groupOptions.sorted || 167 | groupSortedBy || 168 | // @ts-ignore 169 | groupOptions.sortedBy || 170 | groupOptions.sortBy || 171 | sortGroupBy || 172 | sortOptions.groupBy // deprecated 173 | ) { 174 | aGroup = sortList(aGroup, { 175 | caseInsensitive: 176 | groupSortedCaseInsensitive || 177 | // @ts-ignore 178 | groupOptions.sortedCaseInsensitive || 179 | groupOptions.sortCaseInsensitive || // deprecated 180 | sortGroupCaseInsensitive || // deprecated 181 | sortOptions.groupCaseInsensitive, // deprecated 182 | descending: 183 | groupSortedDescending || 184 | // @ts-ignore 185 | groupOptions.sortedDescending || 186 | groupOptions.sortDescending || // deprecated 187 | sortGroupDesc, // deprecated 188 | by: 189 | groupSortedBy || 190 | // @ts-ignore 191 | groupOptions.sortedBy || 192 | groupOptions.sortBy || // deprecated 193 | sortGroupBy, // deprecated 194 | }); 195 | } 196 | 197 | const separator = ["___list-separator", gList.groupLabels[idx], aGroup]; 198 | 199 | if ( 200 | groupOptions.separatorAtTheBottom || 201 | groupSeparatorAtTheBottom || 202 | showGroupSeparatorAtTheBottom 203 | ) { 204 | return [...newGList, ...aGroup, separator]; 205 | } 206 | 207 | return [...newGList, separator, ...aGroup]; 208 | }, []); 209 | } 210 | 211 | return renderList; 212 | }, [ 213 | renderList, 214 | group, 215 | groupReversed, 216 | groupSeparatorAtTheBottom, 217 | groupSortedCaseInsensitive, 218 | groupSortedDescending, 219 | groupSorted, 220 | groupSortedBy, 221 | sortGroupBy, 222 | sortGroupDesc, 223 | sortGroupCaseInsensitive, 224 | ]); 225 | 226 | return renderList; 227 | }; 228 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import FlatList from './flatlist-react'; 2 | 3 | export {default as sortList} from './___utils/sortList'; 4 | export {default as searchList} from './___utils/searchList'; 5 | export {default as filterList} from './___utils/filterList'; 6 | export {default as groupList} from './___utils/groupList'; 7 | export {default as limitList} from './___utils/limitList'; 8 | export {default as PlainList, PlainListProps} from './___subComponents/PlainList'; 9 | export {FlatListProps} from './flatListProps'; 10 | export {GroupInterface, SortInterface, ScrollToTopInterface} from './flatListProps'; 11 | export {DisplayInterface} from './___subComponents/DisplayHandler'; 12 | export {SearchOptionsInterface} from './___utils/searchList'; 13 | export {InfiniteLoaderInterface} from './___subComponents/InfiniteLoader'; 14 | 15 | export default FlatList; 16 | -------------------------------------------------------------------------------- /tests/FlatList.test.tsx: -------------------------------------------------------------------------------- 1 | import {cleanup, fireEvent, render} from '@testing-library/react'; 2 | import React from 'react'; 3 | import FlatList from '../src/flatlist-react'; 4 | 5 | jest.spyOn(React, 'createRef').mockImplementation(() => ({current: null})); 6 | 7 | interface Person { 8 | firstName: string; 9 | lastName: string; 10 | age: number; 11 | } 12 | 13 | describe('FlatList', () => { 14 | let list: Person[] = [ 15 | {firstName: 'John', lastName: 'Doe', age: 1}, 16 | {firstName: 'April', lastName: 'zune', age: 3}, 17 | {firstName: 'June', lastName: 'doe', age: 45}, 18 | {firstName: 'Anibal', lastName: 'Zombie', age: 8}, 19 | {firstName: 'anibal', lastName: 'Doe', age: 0}, 20 | {firstName: 'April', lastName: 'fools', age: 20}, 21 | {firstName: 'april', lastName: 'doe', age: 10} 22 | ]; 23 | const renderItem = (item: any, k: any) => { 24 | // console.log('-- item', item, k); 25 | return
  • age-{item.age === undefined ? item : item.age}
  • ; 26 | }; 27 | const renderNamedItem = (item: any, k: any) =>
  • {item.firstName} {item.lastName}
  • ; 28 | 29 | describe('Should render', () => { 30 | afterEach(cleanup); 31 | 32 | it('items', () => { 33 | const {asFragment, getAllByText} = render( 34 | 38 | ); 39 | 40 | const items = getAllByText(/age-*/); 41 | 42 | expect(asFragment()).toMatchSnapshot(); 43 | expect(items.length).toBe(7); 44 | expect(items.map(item => item.textContent)).toEqual([ 45 | 'age-1', 46 | 'age-3', 47 | 'age-45', 48 | 'age-8', 49 | 'age-0', 50 | 'age-20', 51 | 'age-10', 52 | ]); 53 | }); 54 | 55 | it('limited items', () => { 56 | const {getAllByText} = render( 57 | 62 | ); 63 | 64 | const items = getAllByText(/age-*/); 65 | 66 | expect(items.length).toBe(3); 67 | expect(items.map(item => item.textContent)).toEqual([ 68 | 'age-1', 69 | 'age-3', 70 | 'age-45' 71 | ]); 72 | }); 73 | 74 | it('reversed items', () => { 75 | const {getAllByText} = render( 76 | 81 | ); 82 | 83 | const items = getAllByText(/age-*/); 84 | 85 | expect(items.length).toBe(7); 86 | expect(items.map(item => item.textContent)).toEqual([ 87 | 'age-10', 88 | 'age-20', 89 | 'age-0', 90 | 'age-8', 91 | 'age-45', 92 | 'age-3', 93 | 'age-1', 94 | ]); 95 | }); 96 | 97 | it('different list types', () => { 98 | const l1 = render(); 99 | const l2 = render(); 100 | const l3 = render(); 101 | 102 | expect(l1.container.children[0].outerHTML).toEqual('
  • age-1
  • '); 103 | expect(l2.container.children[0].outerHTML).toEqual('
  • age-1
  • '); 104 | expect(l3.container.children[0].outerHTML).toEqual('
  • age-1
  • '); 105 | }); 106 | 107 | it('items wrapped in a tag', () => { 108 | const {asFragment, container} = render( 109 | 115 | ); 116 | 117 | expect(asFragment()).toMatchSnapshot(); 118 | expect(container.children[0].id).toBe('container'); 119 | }); 120 | 121 | it('default blank with empty list', () => { 122 | const {asFragment, getByText} = render(); 123 | 124 | expect(asFragment()).toMatchSnapshot(); 125 | expect(getByText('List is empty...').outerHTML).toBe('

    List is empty...

    '); 126 | }); 127 | 128 | it('provided blank with empty list', () => { 129 | const {asFragment, getByText} = render( null} 130 | renderWhenEmpty={() =>
    Empty
    }/>); 131 | 132 | expect(asFragment()).toMatchSnapshot(); 133 | expect(getByText('Empty').outerHTML).toBe('
    Empty
    '); 134 | }); 135 | 136 | it('on scroll', () => { 137 | const raf = jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { 138 | cb(1); 139 | return 1; 140 | }); 141 | const container = document.createElement('div'); 142 | container.id = 'container'; 143 | Object.defineProperty(container, 'scrollHeight', { 144 | get() { 145 | return this.children.length * 25; 146 | } 147 | }); 148 | Object.defineProperty(container, 'offsetHeight', { 149 | get() { 150 | return 50; 151 | } 152 | }); 153 | const span = document.createElement('span'); 154 | Object.defineProperty(span, 'offsetTop', { 155 | get() { 156 | console.log('-- offsetTop'); 157 | return container.children.length * 25; 158 | } 159 | }); 160 | 161 | const {asFragment, getAllByText} = render( 162 | , 167 | {container} 168 | ); 169 | 170 | // initial render 171 | let items = getAllByText(/age-.*/); 172 | 173 | expect(asFragment()).toMatchSnapshot(); 174 | expect(items.length).toBe(10); 175 | expect(items.map(i => i.textContent)).toEqual([ 176 | 'age-1', 'age-3', 'age-45', 'age-8', 'age-0', 'age-20', 'age-10', 'age-1', 'age-3', 'age-45' 177 | ]); 178 | 179 | // scroll render 180 | fireEvent.scroll(container, {target: {scrollTop: 150}}); 181 | 182 | items = getAllByText(/age-.*/); 183 | 184 | expect(container.scrollTop).toBe(150); 185 | expect(items.length).toBe(14); 186 | expect(items.map(i => i.textContent)).toEqual([ 187 | 'age-1', 'age-3', 'age-45', 'age-8', 'age-0', 'age-20', 'age-10', 188 | 'age-1', 'age-3', 'age-45', 'age-8', 'age-0', 'age-20', 'age-10' 189 | ]); 190 | raf.mockClear(); 191 | }); 192 | }); 193 | 194 | describe('Should group', () => { 195 | const sep = (g: any, i: any, groupLabel: string) =>
    {groupLabel}
    ; 196 | const groupByFn = (item: any) => item.age >= 18 ? 'Over or 18' : 'Under 18'; 197 | 198 | afterEach(cleanup); 199 | 200 | it('items limited', () => { 201 | const l1 = render( 202 | 207 | ); 208 | const l2 = render( 209 | 216 | ); 217 | 218 | const frag = l1.asFragment(); 219 | 220 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 221 | expect(frag).toMatchSnapshot(); 222 | expect([...l1.container.children as any].map(i => i.nodeName)) 223 | .toEqual(['HR', 'LI', 'LI', 'LI', 'HR', 'LI', 'LI', 'LI', 'HR', 'LI']); 224 | expect([...l1.container.querySelectorAll('li') as any].map(i => i.textContent)) 225 | .toEqual(['age-1', 'age-3', 'age-45', 'age-8', 'age-0', 'age-20', 'age-10']); 226 | }); 227 | 228 | it('by 3 with default separator at the top', () => { 229 | const l1 = render( 230 | 235 | ); 236 | const l2 = render( 237 | 244 | ); 245 | 246 | const frag = l1.asFragment(); 247 | 248 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 249 | expect(frag).toMatchSnapshot(); 250 | expect(frag.querySelectorAll('.___list-separator').length).toBe(3); 251 | expect(frag.querySelectorAll('li').length).toBe(list.length); 252 | expect((l1.container.firstChild as any).nodeName).toBe('HR'); 253 | }); 254 | 255 | it('by 3 with default separator at the bottom', () => { 256 | const l1 = render( 257 | 263 | ); 264 | const l2 = render( 265 | 273 | ); 274 | 275 | const frag = l1.asFragment(); 276 | 277 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 278 | expect(frag).toMatchSnapshot(); 279 | expect(frag.querySelectorAll('.___list-separator').length).toBe(3); 280 | expect(frag.querySelectorAll('li').length).toBe(list.length); 281 | expect((l1.container.lastChild as any).nodeName).toBe('HR'); 282 | }); 283 | 284 | it('with custom separator ', () => { 285 | const l1 = render( 286 | 292 | ); 293 | const l2 = render( 294 | 302 | ); 303 | 304 | const frag = l1.asFragment(); 305 | 306 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 307 | expect(frag).toMatchSnapshot(); 308 | expect(frag.querySelectorAll('.___list-separator').length).toBe(3); 309 | expect(frag.querySelectorAll('.separator').length).toBe(3); 310 | expect([...frag.querySelectorAll('.separator') as any].map(s => s.textContent)).toEqual(['1','2', '3']); 311 | expect(frag.querySelectorAll('li').length).toBe(list.length); 312 | }); 313 | 314 | it('items reversed', () => { 315 | const l1 = render( 316 | 322 | ); 323 | const l2 = render( 324 | 332 | ); 333 | 334 | const frag = l1.asFragment(); 335 | 336 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 337 | expect(frag).toMatchSnapshot(); 338 | expect([...l1.container.querySelectorAll('li') as any].map(i => i.textContent)) 339 | .toEqual(['age-45', 'age-3', 'age-1', 'age-20', 'age-0', 'age-8', 'age-10']); 340 | }); 341 | 342 | it('items by over 18', () => { 343 | const l1 = render( 344 | 350 | ); 351 | const l2 = render( 352 | 360 | ); 361 | 362 | const frag = l1.asFragment(); 363 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 364 | expect(frag).toMatchSnapshot(); 365 | expect(frag.querySelectorAll('.___list-separator').length).toBe(2); 366 | expect([...l1.container.children as any].map(i => i.textContent)) 367 | .toEqual(['Under 18', 'age-1', 'age-3', 'age-8', 'age-0', 'age-10', 'Over or 18', 'age-45', 'age-20']); 368 | }); 369 | 370 | it('sorted', () => { 371 | const l1 = render( 372 | 379 | ); 380 | const l2 = render( 381 | 390 | ); 391 | 392 | const frag = l1.asFragment(); 393 | 394 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 395 | expect(frag).toMatchSnapshot(); 396 | expect(frag.querySelectorAll('.___list-separator').length).toBe(2); 397 | expect([...l1.container.children as any].map(i => i.textContent)) 398 | .toEqual(['Under 18', 'age-0', 'age-1', 'age-3', 'age-8', 'age-10', 'Over or 18', 'age-20', 'age-45']); 399 | 400 | l1.rerender( 401 | 409 | ); 410 | l2.rerender( 411 | 421 | ); 422 | 423 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 424 | expect([...l1.container.children as any].map(i => i.textContent)) 425 | .toEqual(['Under 18', 'age-10', 'age-8', 'age-3', 'age-1', 'age-0', 'Over or 18', 'age-45', 'age-20']); 426 | }); 427 | }); 428 | 429 | describe('Should sort', () => { 430 | afterEach(cleanup); 431 | 432 | it('number list', () => { 433 | const nList = list.map(i => i.age); 434 | const l1 = render( 435 | 440 | ); 441 | 442 | let items = [...l1.container.children as any]; 443 | 444 | expect(items.map(i => i.textContent)).toEqual([ 445 | 'age-0', 446 | 'age-1', 447 | 'age-3', 448 | 'age-8', 449 | 'age-10', 450 | 'age-20', 451 | 'age-45', 452 | ]); 453 | 454 | l1.rerender( 455 | 461 | ); 462 | 463 | items = [...l1.container.children as any]; 464 | 465 | expect(items.map(i => i.textContent)).toEqual([ 466 | 'age-45', 467 | 'age-20', 468 | 'age-10', 469 | 'age-8', 470 | 'age-3', 471 | 'age-1', 472 | 'age-0', 473 | ]); 474 | }); 475 | 476 | it('by age', () => { 477 | const l1 = render( 478 | 483 | ); 484 | const l2 = render( 485 | 492 | ); 493 | 494 | let items = [...l1.container.children as any]; 495 | 496 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 497 | expect(items.map(i => i.textContent)).toEqual([ 498 | 'age-0', 499 | 'age-1', 500 | 'age-3', 501 | 'age-8', 502 | 'age-10', 503 | 'age-20', 504 | 'age-45', 505 | ]); 506 | }); 507 | 508 | it('by age descending', () => { 509 | const l1 = render( 510 | 516 | ); 517 | const l2 = render( 518 | 526 | ); 527 | 528 | let items = [...l1.container.children as any]; 529 | 530 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 531 | expect(items.map(i => i.textContent)).toEqual([ 532 | 'age-45', 533 | 'age-20', 534 | 'age-10', 535 | 'age-8', 536 | 'age-3', 537 | 'age-1', 538 | 'age-0', 539 | ]); 540 | }); 541 | 542 | it('by firstName and lastName descending and case sensitive', () => { 543 | const l1 = render( 544 | 550 | ); 551 | const l2 = render( 552 | 560 | ); 561 | 562 | let items = [...l1.container.children as any]; 563 | 564 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 565 | expect(items.map(i => i.textContent)).toEqual([ 566 | 'april doe', 567 | 'anibal Doe', 568 | 'June doe', 569 | 'John Doe', 570 | 'April zune', 571 | 'April fools', 572 | 'Anibal Zombie', 573 | ]); 574 | 575 | }); 576 | 577 | it('by firstName and lastName ascending and case sensitive', () => { 578 | const l1 = render( 579 | 584 | ); 585 | const l2 = render( 586 | 593 | ); 594 | 595 | let items = [...l1.container.children as any]; 596 | 597 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 598 | expect(items.map(i => i.textContent)).toEqual([ 599 | 'Anibal Zombie', 600 | 'April fools', 601 | 'April zune', 602 | 'John Doe', 603 | 'June doe', 604 | 'anibal Doe', 605 | 'april doe', 606 | ]); 607 | }); 608 | 609 | it('by firstName and lastName descending and case insensitive', () => { 610 | const l1 = render( 611 | 618 | ); 619 | const l2 = render( 620 | 629 | ); 630 | 631 | let items = [...l1.container.children as any]; 632 | 633 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 634 | expect(items.map(i => i.textContent)).toEqual([ 635 | 'June doe', 636 | 'John Doe', 637 | 'April zune', 638 | 'April fools', 639 | 'april doe', 640 | 'Anibal Zombie', 641 | 'anibal Doe', 642 | ]); 643 | }); 644 | 645 | it('by firstName (descending case insensitive) and lastName (ascending case sensitive)', () => { 646 | const l1 =render( 647 | 654 | ); 655 | const l2 = render( 656 | 665 | ); 666 | 667 | let items = [...l1.container.children as any]; 668 | 669 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 670 | expect(items.map(i => i.textContent)).toEqual([ 671 | 'June doe', 672 | 'John Doe', 673 | 'april doe', 674 | 'April fools', 675 | 'April zune', 676 | 'anibal Doe', 677 | 'Anibal Zombie' 678 | ]); 679 | }); 680 | 681 | it('by lastName (ascending case sensitive) and firstName (ascending case sensitive)', () => { 682 | const l1 = render( 683 | 688 | ); 689 | const l2 = render( 690 | 697 | ); 698 | 699 | let items = [...l1.container.children as any]; 700 | 701 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 702 | expect(items.map(i => i.textContent)).toEqual([ 703 | 'April zune', 704 | 'April fools', 705 | 'June doe', 706 | 'april doe', 707 | 'Anibal Zombie', 708 | 'John Doe', 709 | 'anibal Doe' 710 | ]); 711 | }); 712 | }); 713 | 714 | describe('Should filter', () => { 715 | afterEach(cleanup); 716 | 717 | it('by age (string)', () => { 718 | const l1 = render( 719 | 724 | ); 725 | 726 | let items = [...l1.container.children as any]; 727 | 728 | expect(items.map(i => i.textContent)).toEqual([ 729 | 'age-1', 730 | 'age-3', 731 | 'age-45', 732 | 'age-8', 733 | 'age-20', 734 | 'age-10', 735 | ]); 736 | }); 737 | 738 | it('by age (function)', () => { 739 | const l1 = render( 740 | i.age >= 10} 744 | /> 745 | ); 746 | 747 | let items = [...l1.container.children as any]; 748 | 749 | expect(items.map(i => i.textContent)).toEqual([ 750 | 'age-45', 751 | 'age-20', 752 | 'age-10' 753 | ]); 754 | }); 755 | }); 756 | 757 | describe('Should search', () => { 758 | afterEach(cleanup); 759 | 760 | it('with default', () => { 761 | const searchTerm = 'Apr'; 762 | const l1 = render( 763 | 768 | ); 769 | const l2 = render( 770 | 777 | ); 778 | 779 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 780 | expect(l1.container.outerHTML).toEqual('

    List is empty...

    '); 781 | 782 | l1.rerender( 783 | 789 | ); 790 | l2.rerender( 791 | 799 | ); 800 | 801 | let items = [...l1.container.children as any]; 802 | 803 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 804 | expect(items.map(i => i.textContent)).toEqual([ 805 | 'April zune', 806 | 'April fools' 807 | ]); 808 | }); 809 | 810 | it('on every word', () => { 811 | const searchTerm = 'Doe apr'; 812 | const l1 = render( 813 | 820 | ); 821 | const l2 = render( 822 | 831 | ); 832 | 833 | let items = [...l1.container.children as any]; 834 | 835 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 836 | expect(items.map(i => i.textContent)).toEqual([ 837 | 'John Doe', 838 | 'anibal Doe', 839 | 'april doe', 840 | ]); 841 | }); 842 | 843 | it('on every word case insensitive', () => { 844 | const searchTerm = 'doe ani'; 845 | const l1 = render( 846 | 854 | ); 855 | const l2 = render( 856 | 866 | ); 867 | 868 | let items = [...l1.container.children as any]; 869 | 870 | expect(l1.container.outerHTML).toEqual(l2.container.outerHTML); 871 | expect(items.map(i => i.textContent)).toEqual([ 872 | 'John Doe', 873 | 'June doe', 874 | 'Anibal Zombie', 875 | 'anibal Doe', 876 | 'april doe' 877 | ]); 878 | }); 879 | }); 880 | 881 | describe('Should paginate', () => { 882 | let randomList: any[] = []; 883 | const hasMore = () => { 884 | return randomList.length < 10; 885 | }; 886 | const loadMore = () => { 887 | randomList = randomList.concat(Array(5).fill(0).map((x, i) => randomList.length + (i+1))); 888 | }; 889 | 890 | beforeEach(() => { 891 | randomList = Array(5).fill(0).map((x, i) => (i+1)); 892 | }); 893 | 894 | afterEach(cleanup); 895 | 896 | it('on scroll', () => { 897 | const container = document.createElement('div'); 898 | container.id = 'container'; 899 | container.style.height = '50px'; 900 | Object.defineProperty(container, 'scrollHeight', { 901 | get() { 902 | return this.children.length * 15; 903 | } 904 | }); 905 | 906 | const l1 = render( 907 | , 913 | {container} 914 | ); 915 | 916 | let loadingIndicator: any = container.querySelector('.__infinite-loader'); 917 | 918 | expect(l1.asFragment()).toMatchSnapshot(); 919 | expect(l1.getAllByText(/age-.*/).length).toBe(5); 920 | expect(loadingIndicator.style.justifyContent).toBe('flex-start'); 921 | expect(loadingIndicator.style.visibility).toBe('visible'); 922 | expect(loadingIndicator.style.padding).toBe('5px 0px'); 923 | expect(loadingIndicator.style.height).toBe('auto'); 924 | expect(loadingIndicator.children[0].textContent).toBe('loading...'); 925 | 926 | fireEvent.scroll(container, {target: {scrollTop: 120}}); 927 | 928 | l1.rerender( 929 | 935 | ); 936 | 937 | expect(l1.asFragment()).toMatchSnapshot(); 938 | expect(l1.getAllByText(/age-.*/).length).toBe(10); 939 | expect(loadingIndicator.style.visibility).toBe('hidden'); 940 | expect(loadingIndicator.style.padding).toBe('0px'); 941 | expect(loadingIndicator.style.height).toBe('0px'); 942 | expect(loadingIndicator.children[0]).toBeUndefined(); 943 | }); 944 | 945 | it('with custom loading indicator ', () => { 946 | const l1 = render( 947 | null} 952 | paginationLoadingIndicator={() =>
    Loading Items...
    } 953 | paginationLoadingIndicatorPosition="center" 954 | /> 955 | ); 956 | 957 | let loadingIndicator: any = l1.container.querySelector('.__infinite-loader'); 958 | 959 | expect(l1.asFragment()).toMatchSnapshot(); 960 | expect(l1.getByText('Loading Items...')).toBeDefined(); 961 | expect(loadingIndicator.style.justifyContent).toBe('center'); 962 | 963 | l1.rerender( 964 | null} 969 | paginationLoadingIndicator={() =>
    Loading Items...
    } 970 | paginationLoadingIndicatorPosition="right" 971 | /> 972 | ); 973 | 974 | expect(loadingIndicator.style.justifyContent).toBe('flex-end'); 975 | }); 976 | }); 977 | 978 | describe('Should style', () => { 979 | afterEach(cleanup); 980 | 981 | it('grid', () => { 982 | const l1 = render( 983 | 990 | ); 991 | 992 | const id = l1.container.dataset.cont; 993 | const style = getComputedStyle(l1.container); 994 | const styleElement = document.querySelector(`style#${id}`) as HTMLStyleElement; 995 | 996 | expect(styleElement).toBeDefined(); 997 | expect(style.display).toBe('grid'); 998 | expect(style.gap).toBe('50px'); 999 | expect(style.gridTemplateColumns).toBe('repeat(auto-fill, minmax(100px, 1fr))'); 1000 | 1001 | l1.unmount(); 1002 | 1003 | expect(document.querySelector(`style#${id}`)).toBe(null); 1004 | }); 1005 | 1006 | it('row', () => { 1007 | const l1 = render( 1008 | 1014 | ); 1015 | 1016 | const id = l1.container.dataset.cont; 1017 | const containerStyle = getComputedStyle(l1.container); 1018 | const itemStyle = getComputedStyle(l1.container.children[0]); 1019 | 1020 | expect(document.querySelector(`style#${id}`)).toBeDefined(); 1021 | expect(containerStyle.display).toBe('flex'); 1022 | expect(containerStyle.flexDirection).toBe('column'); 1023 | expect(itemStyle.display).toBe('block'); 1024 | expect(itemStyle.flex).toBe('1'); 1025 | expect(itemStyle.width).toBe('100%'); 1026 | expect(itemStyle.marginBottom).toBe('50px'); 1027 | 1028 | l1.unmount(); 1029 | 1030 | expect(document.querySelector(`style#${id}`)).toBe(null); 1031 | }); 1032 | }); 1033 | }); 1034 | -------------------------------------------------------------------------------- /tests/___subComponentns/DefaultBlank.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DefaultBlank from './../../src/___subComponents/DefaultBlank'; 3 | import {render} from '@testing-library/react'; 4 | 5 | describe('DefaultBlank', () => { 6 | it('Should match snapshot', () => { 7 | const {asFragment} = render(); 8 | 9 | expect(asFragment()).toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/___subComponentns/DefaultLoadingIndicator.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DefaultLoadIndicator from './../../src/___subComponents/DefaultLoadIndicator'; 3 | import {render} from '@testing-library/react'; 4 | 5 | describe('DefaultLoadIndicator', () => { 6 | it('Should match snapshot', () => { 7 | const {asFragment} = render(); 8 | 9 | expect(asFragment()).toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/___subComponentns/PlainList.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PlainList from '../../src/___subComponents/PlainList'; 3 | import {render} from '@testing-library/react'; 4 | 5 | describe('PlainList', () => { 6 | const list = [ 7 | {name: 'item-1'}, 8 | {name: 'item-2'}, 9 | {name: 'item-3'} 10 | ]; 11 | 12 | it('Should render items', () => { 13 | const {asFragment, getAllByText} = render( 14 |
  • {item.name}
  • } 17 | /> 18 | ); 19 | 20 | const items = getAllByText(/item-*/); 21 | 22 | expect(asFragment()).toMatchSnapshot(); 23 | expect(items.length).toBe(3); 24 | expect(items.map(item => item.textContent)).toEqual(['item-1', 'item-2', 'item-3']); 25 | }); 26 | 27 | it('Should render items wrapped in a tag', () => { 28 | const {asFragment, container} = render( 29 |
  • {item.name}
  • } 32 | wrapperHtmlTag="div" 33 | id="container" 34 | /> 35 | ); 36 | 37 | expect(asFragment()).toMatchSnapshot(); 38 | expect(container.children[0].id).toBe('container'); 39 | }); 40 | 41 | it('Should render blank with empty list', () => { 42 | const {asFragment, getByText} = render( null}/>); 43 | 44 | expect(asFragment()).toMatchSnapshot(); 45 | expect(getByText('List is empty...').outerHTML).toBe('

    List is empty...

    '); 46 | }); 47 | 48 | it('Should render provided blank with empty list', () => { 49 | const {asFragment, getByText} = render( null} 50 | renderWhenEmpty={() =>
    Empty
    }/>); 51 | 52 | expect(asFragment()).toMatchSnapshot(); 53 | expect(getByText('Empty').outerHTML).toBe('
    Empty
    '); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/___subComponentns/ScrollToTopButton.test.tsx: -------------------------------------------------------------------------------- 1 | import {render} from '@testing-library/react'; 2 | import React, {createRef} from 'react'; 3 | import ScrollToTopButton from '../../src/___subComponents/ScrollToTopButton'; 4 | 5 | describe('ScrollToTopButton', () => { 6 | it('Should match snapshot', () => { 7 | const ref: any = createRef(); 8 | const {asFragment} = render( 9 |
    10 | 11 |
    ); 12 | 13 | expect(asFragment()).toMatchSnapshot(); 14 | }); 15 | 16 | it('Should have button with initial style', () => { 17 | const ref: any = createRef(); 18 | const {getAllByText} = render( 19 |
    20 | 21 |
    22 | ); 23 | const btn = getAllByText('To Top')[0]; 24 | 25 | expect(btn.style.cssText).toBe('position: absolute; z-index: 1; visibility: hidden;'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/___subComponentns/__snapshots__/DefaultBlank.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DefaultBlank Should match snapshot 1`] = ` 4 | 5 |

    6 | List is empty... 7 |

    8 |
    9 | `; 10 | -------------------------------------------------------------------------------- /tests/___subComponentns/__snapshots__/DefaultLoadingIndicator.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DefaultLoadIndicator Should match snapshot 1`] = ` 4 | 5 |
    8 | loading... 9 |
    10 |
    11 | `; 12 | -------------------------------------------------------------------------------- /tests/___subComponentns/__snapshots__/PlainList.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PlainList Should render blank with empty list 1`] = ` 4 | 5 |

    6 | List is empty... 7 |

    8 |
    9 | `; 10 | 11 | exports[`PlainList Should render items 1`] = ` 12 | 13 |
  • 14 | item-1 15 |
  • 16 |
  • 17 | item-2 18 |
  • 19 |
  • 20 | item-3 21 |
  • 22 |
    23 | `; 24 | 25 | exports[`PlainList Should render items wrapped in a tag 1`] = ` 26 | 27 |
    30 |
  • 31 | item-1 32 |
  • 33 |
  • 34 | item-2 35 |
  • 36 |
  • 37 | item-3 38 |
  • 39 |
    40 |
    41 | `; 42 | 43 | exports[`PlainList Should render provided blank with empty list 1`] = ` 44 | 45 |
    46 | Empty 47 |
    48 |
    49 | `; 50 | -------------------------------------------------------------------------------- /tests/___subComponentns/__snapshots__/ScrollToTopButton.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ScrollToTopButton Should match snapshot 1`] = ` 4 | 5 |
    8 | 14 |
    15 |
    16 | `; 17 | -------------------------------------------------------------------------------- /tests/___subComponentns/__snapshots__/uiFunctions.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`uiFunctions handleRenderGroupSeparator Should match snapshot 1`] = ` 4 | 5 |
    8 |
    9 | `; 10 | 11 | exports[`uiFunctions handleRenderGroupSeparator Should match snapshot 2`] = ` 12 | 13 |
    16 |

    17 |

    18 |
    19 | `; 20 | 21 | exports[`uiFunctions handleRenderGroupSeparator Should match snapshot 3`] = ` 22 | 23 |
    26 |

    27 | label 28 |

    29 |
    30 |
    31 | `; 32 | 33 | exports[`uiFunctions handleRenderGroupSeparator Should match snapshot 4`] = ` 34 | 35 |
    38 |

    39 |

    40 |
    41 | `; 42 | 43 | exports[`uiFunctions handleRenderItem Should match snapshot 1`] = ``; 44 | 45 | exports[`uiFunctions handleRenderItem Should match snapshot 2`] = ` 46 | 47 |
    48 | item name 12 49 |
    50 |
    51 | `; 52 | 53 | exports[`uiFunctions renderBlank Should match snapshot 1`] = ` 54 | 55 |

    56 | List is empty... 57 |

    58 |
    59 | `; 60 | 61 | exports[`uiFunctions renderBlank Should match snapshot 2`] = ` 62 | 63 |

    64 | nothing 65 |

    66 |
    67 | `; 68 | -------------------------------------------------------------------------------- /tests/___subComponentns/uiFunctions.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | renderBlank, 4 | handleRenderGroupSeparator, 5 | handleRenderItem, 6 | btnPosition 7 | } from '../../src/___subComponents/uiFunctions'; 8 | import {render} from '@testing-library/react'; 9 | 10 | describe('uiFunctions', () => { 11 | describe('renderBlank', () => { 12 | it('Should match snapshot', () => { 13 | const blankNoRenderWhenEmpty = renderBlank(null); 14 | const blankWithRenderWhenEmpty = renderBlank(() =>

    nothing

    ); 15 | let {asFragment: b1} = render(blankNoRenderWhenEmpty); 16 | let {asFragment: b2} = render(blankWithRenderWhenEmpty); 17 | 18 | expect(b1()).toMatchSnapshot(); 19 | expect(b2()).toMatchSnapshot(); 20 | }); 21 | }); 22 | 23 | describe('handleRenderGroupSeparator', () => { 24 | it('Should match snapshot', () => { 25 | const separator = ['___separator', 'label', []]; 26 | const CustomSeparator = ({groupLabel}: any) => { 27 | return

    {groupLabel}

    28 | }; 29 | const groupSeparatorNoCustom = handleRenderGroupSeparator(null); 30 | const groupSeparatorWithElCustom = handleRenderGroupSeparator(CustomSeparator); 31 | const groupSeparatorWithElCustom2 = handleRenderGroupSeparator(); 32 | const groupSeparatorWithFnCustom = handleRenderGroupSeparator( 33 | ({groupLabel}: any) => 34 | ); 35 | const {asFragment: s1} = render(groupSeparatorNoCustom(separator, 0)); 36 | const {asFragment: s2} = render(groupSeparatorWithElCustom(separator, 0)); 37 | const {asFragment: s3} = render(groupSeparatorWithElCustom2(separator, 0)); 38 | const {asFragment: s4} = render(groupSeparatorWithFnCustom(separator, 0)); 39 | 40 | expect(s1()).toMatchSnapshot(); 41 | expect(s2()).toMatchSnapshot(); 42 | expect(s3()).toMatchSnapshot(); 43 | expect(s4()).toMatchSnapshot(); 44 | }); 45 | }); 46 | 47 | describe('handleRenderItem', () => { 48 | it('Should match snapshot', () => { 49 | const item = {name: 'item name', id: 12}; 50 | const itemNull = handleRenderItem(() => null); 51 | const itemFn = handleRenderItem((item: any, key: string) =>
    {item.name} {key}
    ); 52 | 53 | const {asFragment: i1} = render(itemNull(item, 0) as any); 54 | const {asFragment: i2} = render(itemFn(item, 0) as any); 55 | 56 | expect(i1()).toMatchSnapshot(); 57 | expect(i2()).toMatchSnapshot(); 58 | }); 59 | }); 60 | 61 | describe('btnPosition', () => { 62 | const btn = document.createElement('button'); 63 | const container = document.createElement('div'); 64 | container.id = 'unique-id'; 65 | container.style.width = '200px'; 66 | container.style.height = '200px'; 67 | btn.innerText = 'to top'; 68 | const pos = btnPosition(container, btn); 69 | container.appendChild(btn); 70 | document.body.appendChild(container); 71 | 72 | beforeAll(() => { 73 | jest.spyOn(container, 'getBoundingClientRect').mockImplementation(() => { 74 | return { 75 | left: 0, 76 | top: 0, 77 | width: 200, 78 | height: 200, 79 | } as DOMRect; 80 | }) 81 | }); 82 | 83 | afterAll(() => { 84 | jest.resetAllMocks(); 85 | }); 86 | 87 | it('Should set initial style', () => { 88 | expect(container.id).toBe('unique-id'); 89 | expect(container.style.cssText).toBe('width: 200px; height: 200px;'); 90 | expect(btn.style.cssText).toBe('position: absolute; z-index: 1; visibility: hidden;'); 91 | }); 92 | 93 | it('Should position btn top left', (done) => { 94 | const raf = (cb: any) => { 95 | cb(); 96 | expect(btn.style.top).toBe('20px'); 97 | expect(btn.style.left).toBe('20px'); 98 | done(); 99 | return 0; 100 | }; 101 | 102 | const spy = jest.spyOn(window, 'requestAnimationFrame').mockImplementation(raf); 103 | 104 | pos('top', 'left'); 105 | 106 | expect(window.requestAnimationFrame).toHaveBeenCalled(); 107 | spy.mockRestore(); 108 | }); 109 | 110 | it('Should position btn top right', (done) => { 111 | const raf = (cb: any) => { 112 | cb(); 113 | expect(btn.style.top).toBe('20px'); 114 | expect(btn.style.left).toBe('calc(100% - 20px)'); 115 | done(); 116 | return 0; 117 | }; 118 | 119 | const spy = jest.spyOn(window, 'requestAnimationFrame').mockImplementation(raf); 120 | 121 | pos('top', 'right'); 122 | 123 | expect(window.requestAnimationFrame).toHaveBeenCalled(); 124 | spy.mockRestore(); 125 | }); 126 | 127 | it('Should position btn bottom left', (done) => { 128 | const raf = (cb: any) => { 129 | cb(); 130 | expect(btn.style.top).toBe('calc(100% - 20px)'); 131 | expect(btn.style.left).toBe('20px'); 132 | done(); 133 | return 0; 134 | }; 135 | 136 | const spy = jest.spyOn(window, 'requestAnimationFrame').mockImplementation(raf); 137 | 138 | pos('bottom', 'left'); 139 | 140 | expect(window.requestAnimationFrame).toHaveBeenCalled(); 141 | spy.mockRestore(); 142 | }); 143 | 144 | it('Should position btn bottom right', (done) => { 145 | const raf = (cb: any) => { 146 | cb(); 147 | expect(btn.style.top).toBe('calc(100% - 20px)'); 148 | expect(btn.style.left).toBe('calc(100% - 20px)'); 149 | done(); 150 | return 0; 151 | }; 152 | 153 | const spy = jest.spyOn(window, 'requestAnimationFrame').mockImplementation(raf); 154 | 155 | pos('bottom', 'right'); 156 | 157 | expect(window.requestAnimationFrame).toHaveBeenCalled(); 158 | spy.mockRestore(); 159 | }); 160 | }) 161 | }); 162 | -------------------------------------------------------------------------------- /tests/___utils/convertListToArray.test.ts: -------------------------------------------------------------------------------- 1 | import convertListToArray from '../../src/___utils/convertListToArray'; 2 | 3 | describe('Util: convertListToArray()', () => { 4 | let arr: any[]; 5 | 6 | beforeAll(() => { 7 | arr = [ 8 | {name: 'test a'}, 9 | {name: 'test b'}, 10 | {name: 'test c'} 11 | ]; 12 | }); 13 | 14 | it('should convert Object to an array', () => { 15 | expect.assertions(1); 16 | const obj = {...arr}; 17 | 18 | expect(convertListToArray(obj)).toEqual(arr); 19 | }); 20 | 21 | it('should convert Set to a array', () => { 22 | expect.assertions(1); 23 | const set = new Set(arr); 24 | 25 | expect(convertListToArray(set)).toEqual(arr); 26 | }); 27 | 28 | it('should convert Map to a array', () => { 29 | expect.assertions(1); 30 | const map = new Map(arr.map((o, i) => [i, o])); 31 | 32 | expect(convertListToArray(map)).toEqual(arr); 33 | }); 34 | 35 | it('should convert WeakSet to an EMPTY array', () => { 36 | expect.assertions(1); 37 | const wset = new WeakSet(arr); 38 | 39 | expect(convertListToArray(wset)).toEqual([]); 40 | }); 41 | 42 | it('should convert WeakMap to an EMPTY array', () => { 43 | expect.assertions(1); 44 | const wmap = new WeakMap(arr.map((o, i) => [{i}, o])); 45 | 46 | expect(convertListToArray(wmap)).toEqual([]); 47 | }); 48 | 49 | it('should return array intact', () => { 50 | expect.assertions(1); 51 | 52 | expect(convertListToArray(arr)).toEqual(arr); 53 | }); 54 | 55 | it('should return EMPTY array FOR anything that is not Set, Map, Object or Array', () => { 56 | expect.assertions(5); 57 | 58 | expect(convertListToArray(() => null)).toEqual([]); 59 | expect(convertListToArray('string')).toEqual([]); 60 | expect(convertListToArray(12)).toEqual([]); 61 | expect(convertListToArray(new WeakSet())).toEqual([]); 62 | expect(convertListToArray(new WeakMap())).toEqual([]); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/___utils/filterList.test.ts: -------------------------------------------------------------------------------- 1 | import filterList from '../../src/___utils/filterList'; 2 | 3 | describe('Util: filterList()', () => { 4 | 5 | it('Should filter list given a string "by"', () => { 6 | expect.assertions(4); 7 | const objectList = [{age: 2}, {age: 45}, {age: 10}, {age: null}, {age: 0}]; 8 | const arrayList = [[{age: 2}], [{age: 45}], [{age: 10}], [{age: null}], [{age: 0}]]; 9 | 10 | const filteredObjectList = filterList(objectList, 'age'); 11 | const filteredArrayList = filterList(arrayList, '0.age'); 12 | 13 | expect(filteredObjectList).toEqual([{age: 2}, {age: 45}, {age: 10}]); 14 | expect(filteredObjectList).toHaveLength(3); 15 | 16 | expect(filteredArrayList).toEqual([[{age: 2}], [{age: 45}], [{age: 10}]]); 17 | expect(filteredArrayList).toHaveLength(3); 18 | }); 19 | 20 | it('Should filter list given a function "by"', () => { 21 | expect.assertions(4); 22 | const objectList = [{age: 2}, {age: 45}, {age: 10}, {age: null}, {age: 0}]; 23 | const arrayList = [[{age: 2}], [{age: 45}], [{age: 10}], [{age: null}], [{age: 0}]]; 24 | 25 | const filteredObjectList = filterList(objectList, (item: any) => item.age && item.age > 10); 26 | const filteredArrayList = filterList(arrayList, (item: any) => item[0] && item[0].age && item[0].age >= 10); 27 | 28 | expect(filteredObjectList).toEqual([{age: 45}]); 29 | expect(filteredObjectList).toHaveLength(1); 30 | 31 | expect(filteredArrayList).toEqual([[{age: 45}], [{age: 10}]]); 32 | expect(filteredArrayList).toHaveLength(2); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/___utils/getObjectDeepKeyValue.test.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable only-arrow-functions no-empty quotemark */ 2 | import getObjectDeepKeyValue from '../../src/___utils/getObjectDeepKeyValue'; 3 | 4 | interface TestingObjectInterface { 5 | [s: string]: any; 6 | } 7 | 8 | describe('Util: getObjectDeepKeyValue()', () => { 9 | 10 | const testingObject: TestingObjectInterface = { 11 | children: [ 12 | { 13 | age: 3, 14 | name: 'Jane Doe', 15 | }, 16 | { 17 | age: 3, 18 | name: 'Another Doe', 19 | } 20 | ], 21 | name: 'John Doe', 22 | map: new Map([['one', 1], ['two', 2]]), 23 | personalInfo: { 24 | age: 28, 25 | height: '5\'11', 26 | weight: '200lb', 27 | years: new Set(['1999', '2000', '2001', '2018']) 28 | } 29 | }; 30 | const testingArray: TestingObjectInterface[] = []; 31 | 32 | beforeAll(() => { 33 | testingArray.push(testingObject); 34 | }); 35 | 36 | it('Should get one level deep key values in an object', () => { 37 | const objectKey1: string = 'children'; 38 | const objectKey2: string = 'name'; 39 | const objectKey3: string = 'personalInfo'; 40 | 41 | expect.assertions(3); 42 | expect(getObjectDeepKeyValue(testingObject, objectKey1)).toEqual(testingObject[objectKey1]); 43 | expect(getObjectDeepKeyValue(testingObject, objectKey2)).toEqual(testingObject[objectKey2]); 44 | expect(getObjectDeepKeyValue(testingObject, objectKey3)).toEqual(testingObject[objectKey3]); 45 | }); 46 | 47 | it('Should get one level deep key values in an array', () => { 48 | const arrKey: any = '0'; 49 | const arrKey2: any = '*'; 50 | 51 | expect.assertions(1); 52 | 53 | expect(getObjectDeepKeyValue(testingArray, arrKey)).toEqual(testingArray[arrKey]); 54 | }); 55 | 56 | it('Should get several levels deep key values in an object', () => { 57 | const objectKey1: string = 'children.0.name'; 58 | const objectKey2: string = 'children.1.age'; 59 | const objectKey3: string = 'personalInfo.weight'; 60 | const objectKey4: string = 'personalInfo.years.1'; 61 | const objectKey5: string = 'map.one'; 62 | 63 | expect.assertions(5); 64 | expect(getObjectDeepKeyValue(testingObject, objectKey1)).toEqual(testingObject.children[0].name); 65 | expect(getObjectDeepKeyValue(testingObject, objectKey2)).toEqual(testingObject.children[1].age); 66 | expect(getObjectDeepKeyValue(testingObject, objectKey3)).toEqual(testingObject.personalInfo.weight); 67 | expect(getObjectDeepKeyValue(testingObject, objectKey4)).toEqual('2000'); 68 | expect(getObjectDeepKeyValue(testingObject, objectKey5)).toEqual(1); 69 | }); 70 | 71 | it('Should get several levels deep key values in an array', () => { 72 | const arrKey1: string = '0.children.0.name'; 73 | const arrKey2: string = '0.children.1.age'; 74 | const arrKey3: string = '0.personalInfo.weight'; 75 | const arrKey4: string = '0.personalInfo.years.1'; 76 | const arrKey5: string = '0.map.one'; 77 | 78 | expect.assertions(5); 79 | expect(getObjectDeepKeyValue(testingArray, arrKey1)).toEqual(testingArray[0].children[0].name); 80 | expect(getObjectDeepKeyValue(testingArray, arrKey2)).toEqual(testingArray[0].children[1].age); 81 | expect(getObjectDeepKeyValue(testingArray, arrKey3)).toEqual(testingArray[0].personalInfo.weight); 82 | expect(getObjectDeepKeyValue(testingArray, arrKey4)).toEqual('2000'); 83 | expect(getObjectDeepKeyValue(testingArray, arrKey5)).toEqual(1); 84 | }); 85 | 86 | it('Should return null when key does not exists', () => { 87 | const objectKey1: string = 'children.0.nothing'; // nothing does not exists 88 | const objectKey2: string = 'children.2.age'; // 2 does not exists 89 | const objectKey3: string = 'personalInfo.dominance'; // dominance does not exists 90 | const arrKey1: string = '0.children.0.error'; // error does not exists 91 | const arrKey2: string = '3.children.1.age'; // 3 does not exists 92 | const arrKey3: string = '0.test.weight'; // test does not exists 93 | 94 | expect.assertions(6); 95 | expect(getObjectDeepKeyValue(testingObject, objectKey1)).toBe(null); 96 | expect(getObjectDeepKeyValue(testingObject, objectKey2)).toBe(null); 97 | expect(getObjectDeepKeyValue(testingObject, objectKey3)).toBe(null); 98 | expect(getObjectDeepKeyValue(testingArray, arrKey1)).toBe(null); 99 | expect(getObjectDeepKeyValue(testingArray, arrKey2)).toBe(null); 100 | expect(getObjectDeepKeyValue(testingArray, arrKey3)).toBe(null); 101 | }); 102 | 103 | it('Should be undefined when not OBJECT or ARRAY is passed as hey-stack', () => { 104 | expect.assertions(3); 105 | 106 | expect(getObjectDeepKeyValue('', 'children')).toBeNull(); 107 | expect(getObjectDeepKeyValue(() => {}, 'children')).toBeNull(); 108 | expect(getObjectDeepKeyValue(new Map(), 'children')).toBeNull(); 109 | }); 110 | 111 | it('Should throw an error when not STRING is passed as key', () => { 112 | expect.assertions(3); 113 | 114 | // @ts-ignore 115 | expect(() => getObjectDeepKeyValue([testingObject, ])) 116 | .toThrowError('getObjectDeepKeyValue: "dotSeparatedKeys" is not a dot separated values string'); 117 | // @ts-ignore 118 | expect(() => getObjectDeepKeyValue(testingArray, 1)) 119 | .toThrowError('getObjectDeepKeyValue: "dotSeparatedKeys" is not a dot separated values string'); 120 | // @ts-ignore 121 | expect(() => getObjectDeepKeyValue({testingObject, })) 122 | .toThrowError('getObjectDeepKeyValue: "dotSeparatedKeys" is not a dot separated values string'); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /tests/___utils/getType.test.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable only-arrow-functions no-empty quotemark */ 2 | import getType, {types} from '../../src/___utils/getType'; 3 | 4 | describe('Util: getType()', () => { 5 | const data: { [t: string]: any[] } = {}; 6 | let totalDataToTest = 0; 7 | 8 | beforeAll(() => { 9 | data.arrays = [[], new Array(), [1, 2, 3], Array.from('test'), {a: []}.a, Array(3)]; 10 | data.booleans = [true, false, Boolean(1), 1 === 1]; 11 | data.functions = [() => { 12 | }, function() { 13 | }]; 14 | data.maps = [new Map()]; 15 | data.nulls = [null, {a: null}.a]; 16 | data.numbers = [1, -1, 9.3, NaN, (0 / 3), Infinity]; 17 | data.objects = [{}, new Object({}), Object.create(null)]; 18 | data.sets = [new Set()]; 19 | data.strings = ['123', '', `${123} - n`, 'empty', '']; 20 | data.symbols = [Symbol()]; 21 | // @ts-ignore 22 | data.undefineds = [{random: undefined}.random, {}.x, undefined, (() => { 23 | })(), (() => undefined)(), 24 | Object.create(null).prototype]; 25 | data.weakMaps = [new WeakMap()]; 26 | data.weakSets = [new WeakSet()]; 27 | 28 | totalDataToTest = Object.keys(data).reduce((acc, dataKey) => acc + data[dataKey].length, 0); 29 | }); 30 | 31 | it('Should be of type ARRAY only those inside data.arrays', () => { 32 | expect.assertions(totalDataToTest); 33 | Object.keys(data).forEach((dataTypeGroup: string) => { 34 | data[dataTypeGroup].forEach((x: any) => { 35 | if (dataTypeGroup === 'arrays') { 36 | expect(getType(x)).toEqual(types.ARRAY); 37 | } else { 38 | expect(getType(x)).not.toEqual(types.ARRAY); 39 | } 40 | }); 41 | }); 42 | }); 43 | 44 | it('Should be of type BOOLEAN only those inside data.booleans', () => { 45 | expect.assertions(totalDataToTest); 46 | Object.keys(data).forEach((dataTypeGroup: string) => { 47 | data[dataTypeGroup].forEach((x: any) => { 48 | if (dataTypeGroup === 'booleans') { 49 | expect(getType(x)).toEqual(types.BOOLEAN); 50 | } else { 51 | expect(getType(x)).not.toEqual(types.BOOLEAN); 52 | } 53 | }); 54 | }); 55 | }); 56 | 57 | it('Should be of type FUNCTION only those inside data.functions', () => { 58 | expect.assertions(totalDataToTest); 59 | Object.keys(data).forEach((dataTypeGroup: string) => { 60 | data[dataTypeGroup].forEach((x: any) => { 61 | if (dataTypeGroup === 'functions') { 62 | expect(getType(x)).toEqual(types.FUNCTION); 63 | } else { 64 | expect(getType(x)).not.toEqual(types.FUNCTION); 65 | } 66 | }); 67 | }); 68 | }); 69 | 70 | it('Should be of type MAP only those inside data.maps', () => { 71 | expect.assertions(totalDataToTest); 72 | Object.keys(data).forEach((dataTypeGroup: string) => { 73 | data[dataTypeGroup].forEach((x: any) => { 74 | if (dataTypeGroup === 'maps') { 75 | expect(getType(x)).toEqual(types.MAP); 76 | } else { 77 | expect(getType(x)).not.toEqual(types.MAP); 78 | } 79 | }); 80 | }); 81 | }); 82 | 83 | it('Should be of type NULL only those inside data.nulls', () => { 84 | expect.assertions(totalDataToTest); 85 | Object.keys(data).forEach((dataTypeGroup: string) => { 86 | data[dataTypeGroup].forEach((x: any) => { 87 | if (dataTypeGroup === 'nulls') { 88 | expect(getType(x)).toEqual(types.NULL); 89 | } else { 90 | expect(getType(x)).not.toEqual(types.NULL); 91 | } 92 | }); 93 | }); 94 | }); 95 | 96 | it('Should be of type NUMBER only those inside data.numbers', () => { 97 | expect.assertions(totalDataToTest); 98 | Object.keys(data).forEach((dataTypeGroup: string) => { 99 | data[dataTypeGroup].forEach((x: any) => { 100 | if (dataTypeGroup === 'numbers') { 101 | expect(getType(x)).toEqual(types.NUMBER); 102 | } else { 103 | expect(getType(x)).not.toEqual(types.NUMBER); 104 | } 105 | }); 106 | }); 107 | }); 108 | 109 | it('Should be of type OBJECT only those inside data.objects', () => { 110 | expect.assertions(totalDataToTest); 111 | Object.keys(data).forEach((dataTypeGroup: string) => { 112 | data[dataTypeGroup].forEach((x: any) => { 113 | if (dataTypeGroup === 'objects') { 114 | expect(getType(x)).toEqual(types.OBJECT); 115 | } else { 116 | expect(getType(x)).not.toEqual(types.OBJECT); 117 | } 118 | }); 119 | }); 120 | }); 121 | 122 | it('Should be of type SET only those inside data.sets', () => { 123 | expect.assertions(totalDataToTest); 124 | Object.keys(data).forEach((dataTypeGroup: string) => { 125 | data[dataTypeGroup].forEach((x: any) => { 126 | if (dataTypeGroup === 'sets') { 127 | expect(getType(x)).toEqual(types.SET); 128 | } else { 129 | expect(getType(x)).not.toEqual(types.SET); 130 | } 131 | }); 132 | }); 133 | }); 134 | 135 | it('Should be of type STRING only those inside data.strings', () => { 136 | expect.assertions(totalDataToTest); 137 | Object.keys(data).forEach((dataTypeGroup: string) => { 138 | data[dataTypeGroup].forEach((x: any) => { 139 | if (dataTypeGroup === 'strings') { 140 | expect(getType(x)).toEqual(types.STRING); 141 | } else { 142 | expect(getType(x)).not.toEqual(types.STRING); 143 | } 144 | }); 145 | }); 146 | }); 147 | 148 | it('Should be of type SYMBOL only those inside data.symbols', () => { 149 | expect.assertions(totalDataToTest); 150 | Object.keys(data).forEach((dataTypeGroup: string) => { 151 | data[dataTypeGroup].forEach((x: any) => { 152 | if (dataTypeGroup === 'symbols') { 153 | expect(getType(x)).toEqual(types.SYMBOL); 154 | } else { 155 | expect(getType(x)).not.toEqual(types.SYMBOL); 156 | } 157 | }); 158 | }); 159 | }); 160 | 161 | it('Should be of type UNDEFINED only those inside data.undefineds', () => { 162 | expect.assertions(totalDataToTest); 163 | Object.keys(data).forEach((dataTypeGroup: string) => { 164 | data[dataTypeGroup].forEach((x: any) => { 165 | if (dataTypeGroup === 'undefineds') { 166 | expect(getType(x)).toEqual(types.UNDEFINED); 167 | } else { 168 | expect(getType(x)).not.toEqual(types.UNDEFINED); 169 | } 170 | }); 171 | }); 172 | }); 173 | 174 | it('Should be of type WEAK_MAP only those inside data.weakMaps', () => { 175 | expect.assertions(totalDataToTest); 176 | Object.keys(data).forEach((dataTypeGroup: string) => { 177 | data[dataTypeGroup].forEach((x: any) => { 178 | if (dataTypeGroup === 'weakMaps') { 179 | expect(getType(x)).toEqual(types.WEAK_MAP); 180 | } else { 181 | expect(getType(x)).not.toEqual(types.WEAK_MAP); 182 | } 183 | }); 184 | }); 185 | }); 186 | 187 | it('Should be of type WEAK_SET only those inside data.weakSets', () => { 188 | expect.assertions(totalDataToTest); 189 | Object.keys(data).forEach((dataTypeGroup: string) => { 190 | data[dataTypeGroup].forEach((x: any) => { 191 | if (dataTypeGroup === 'weakSets') { 192 | expect(getType(x)).toEqual(types.WEAK_SET); 193 | } else { 194 | expect(getType(x)).not.toEqual(types.WEAK_SET); 195 | } 196 | }); 197 | }); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /tests/___utils/groupList.test.ts: -------------------------------------------------------------------------------- 1 | import groupList from '../../src/___utils/groupList'; 2 | 3 | describe('Util: groupList()', () => { 4 | 5 | it('Should return array of ONE list if no or invalid options provided', () => { 6 | expect.assertions(3); 7 | const list = [1, 3, 45, 8, 0]; 8 | 9 | const groupedList = groupList(list); 10 | 11 | expect(groupedList.groupLists).toHaveLength(1); 12 | expect(groupedList.groupLists).toEqual([list]); 13 | expect(groupedList.groupLabels).toHaveLength(0); 14 | }); 15 | 16 | it('Should return array groups length matching "limit" option', () => { 17 | expect.assertions(4); 18 | const list = [1, 3, 45, 8, 0, 20, 10]; 19 | 20 | const groupedList = groupList(list, {limit: 2}); 21 | 22 | expect(groupedList.groupLists).toHaveLength(4); 23 | expect(groupedList.groupLists).toEqual([[1, 3], [45, 8], [0, 20], [10]]); 24 | expect(groupedList.groupLabels).toHaveLength(4); 25 | expect(groupedList.groupLabels).toEqual(['1', '2', '3', '4']); 26 | }); 27 | 28 | it('Should group list by age', () => { 29 | expect.assertions(4); 30 | const list = [{age: 1}, {age: 3}, {age: 45}, {age: 8}, {age: 0}, {age: 20}, {age: 10}]; 31 | 32 | const groupBy: (item: any, idx: number) => string | number = (item: any) => { 33 | return item.age % 2 === 0 ? 'divided by 2' : 'not divided by 2'; 34 | }; 35 | 36 | const groupedList = groupList(list, {by: groupBy}); 37 | 38 | expect(groupedList.groupLists).toHaveLength(2); 39 | expect(groupedList.groupLists) 40 | .toEqual([[{age: 1}, {age: 3}, {age: 45}], [{age: 8}, {age: 0}, {age: 20}, {age: 10}]]); 41 | expect(groupedList.groupLabels).toHaveLength(2); 42 | expect(groupedList.groupLabels).toEqual(['not divided by 2', 'divided by 2']); 43 | }); 44 | 45 | it('Should group list by age with group length be max 2', () => { 46 | expect.assertions(4); 47 | const list = [{age: 1}, {age: 3}, {age: 45}, {age: 8}, {age: 0}, {age: 20}, {age: 10}]; 48 | 49 | const groupBy: (item: any, idx: number) => string | number = (item: any) => { 50 | return item.age % 2 === 0 ? 'divided by 2' : 'not divided by 2'; 51 | }; 52 | 53 | const groupedList = groupList(list, {by: groupBy, limit: 2}); 54 | 55 | expect(groupedList.groupLists).toHaveLength(2); 56 | expect(groupedList.groupLists) 57 | .toEqual([[{age: 1}, {age: 3}], [{age: 8}, {age: 0}]]); 58 | expect(groupedList.groupLabels).toHaveLength(2); 59 | expect(groupedList.groupLabels).toEqual(['not divided by 2', 'divided by 2']); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/___utils/isType.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isFunction, 3 | isNilOrEmpty, 4 | isString, 5 | isArray, 6 | isNumber, 7 | isObject, 8 | isNumeric, 9 | isBoolean, 10 | isObjectLiteral, 11 | isNil, 12 | isEmpty 13 | } from '../../src/___utils/isType'; 14 | 15 | describe('Util: isType()', () => { 16 | const data: { [t: string]: any[] } = {}; 17 | 18 | beforeAll(() => { 19 | const ObjConstructor = function() { 20 | // @ts-ignore 21 | this.object = {}; 22 | }; 23 | 24 | data.arrays = [[], new Array(), [1, 2, 3], Array.from('test'), {a: []}.a, Array(3)]; 25 | data.functions = [() => { 26 | // tslint:disable-next-line:only-arrow-functions 27 | }, function() { 28 | }]; 29 | data.nil = [null, undefined, (() => {})()]; 30 | data.empty = ['', ``, [], {}, new Map(), new Set(), NaN]; 31 | data.nilOrEmpty = [null, '', ``, [], {}, undefined, new Map(), new Set()]; 32 | data.booleans = [true, false, 1 === 1, 10 > 5]; 33 | data.numbers = [1, -1, 9.3, (0 / 3), Infinity, 0]; 34 | data.numerics = [1, -1, 9.3, (0 / 3), 0]; 35 | // @ts-ignore 36 | data.objects = [{}, new Object({}), Object.create(null), new ObjConstructor()]; 37 | // @ts-ignore 38 | data.objectLiterals = [{}, new Object({})]; 39 | data.strings = ['123', '', `${123} - n`, 'empty', '']; 40 | }); 41 | 42 | it('Should be boolean only those in data.booleans', () => { 43 | expect.assertions(data.booleans.length); 44 | data.booleans.forEach((x: any) => { 45 | expect(isBoolean(x)).toBe(true); 46 | }); 47 | }); 48 | 49 | it('Should be functions only those in data.functions', () => { 50 | expect.assertions(data.functions.length); 51 | data.functions.forEach((x: any) => { 52 | expect(isFunction(x)).toBe(true); 53 | }); 54 | }); 55 | 56 | it('Should be string only those in data.strings', () => { 57 | expect.assertions(data.strings.length); 58 | data.strings.forEach((x: any) => { 59 | expect(isString(x)).toBe(true); 60 | }); 61 | }); 62 | 63 | it('Should be nil only those in data.nil', () => { 64 | expect.assertions(data.nil.length); 65 | data.nil.forEach((x: any) => { 66 | expect(isNil(x)).toBe(true); 67 | }); 68 | }); 69 | 70 | it('Should be empty only those in data.empty', () => { 71 | expect.assertions(data.empty.length); 72 | data.empty.forEach((x: any) => { 73 | expect(isEmpty(x)).toBe(true); 74 | }); 75 | }); 76 | 77 | it('Should be nil or empty only those in data.nilOrEmpty', () => { 78 | expect.assertions(data.nilOrEmpty.length); 79 | data.nilOrEmpty.forEach((x: any) => { 80 | expect(isNilOrEmpty(x)).toBe(true); 81 | }); 82 | }); 83 | 84 | it('Should be array only those in data.arrays', () => { 85 | expect.assertions(data.arrays.length); 86 | data.arrays.forEach((x: any) => { 87 | expect(isArray(x)).toBe(true); 88 | }); 89 | }); 90 | 91 | it('Should be object only those in data.objects', () => { 92 | expect.assertions(data.objects.length); 93 | data.objects.forEach((x: any) => { 94 | expect(isObject(x)).toBe(true); 95 | }); 96 | }); 97 | 98 | it('Should be object literal only those in data.objectLiterals', () => { 99 | expect.assertions(data.objectLiterals.length); 100 | data.objectLiterals.forEach((x: any) => { 101 | expect(isObjectLiteral(x)).toBe(true); 102 | }); 103 | }); 104 | 105 | it('Should be number only those in data.numbers', () => { 106 | expect.assertions(data.numbers.length); 107 | data.numbers.forEach((x: any) => { 108 | expect(isNumber(x)).toBe(true); 109 | }); 110 | }); 111 | 112 | it('Should be numeric only those in data.numerics', () => { 113 | expect.assertions(data.numerics.length); 114 | data.numerics.forEach((x: any) => { 115 | expect(isNumeric(x)).toBe(true); 116 | }); 117 | }); 118 | 119 | }); 120 | -------------------------------------------------------------------------------- /tests/___utils/limitList.test.ts: -------------------------------------------------------------------------------- 1 | import limitList from '../../src/___utils/limitList'; 2 | 3 | describe('Util: limitList()', () => { 4 | 5 | it('Should return same list size when limit is LESS OR EQUAL THAN ZERO', () => { 6 | expect.assertions(2); 7 | const list = [1, 2, 3, 4, 5]; 8 | const newList1 = limitList(list, 0); 9 | const newList2 = limitList(list, 0); 10 | 11 | expect(newList1.length).toEqual(list.length); 12 | expect(newList2.length).toEqual(list.length); 13 | }); 14 | 15 | it('Should return same list size when limit is GREATER OR EQUAL THAN THE ARRAY LENGTH', () => { 16 | expect.assertions(2); 17 | const list = [1, 2, 3, 4, 5]; 18 | const newList1 = limitList(list, list.length); 19 | const newList2 = limitList(list, list.length + 2); 20 | 21 | expect(newList1.length).toEqual(list.length); 22 | expect(newList2.length).toEqual(list.length); 23 | }); 24 | 25 | it('Should return list at the size of the specified limit', () => { 26 | expect.assertions(2); 27 | const list = [1, 2, 3, 4, 5]; 28 | const newList1 = limitList(list, list.length - 2); 29 | const newList2 = limitList(list, 1); 30 | 31 | expect(newList1.length).toBe(3); 32 | expect(newList2.length).toBe(1); 33 | }); 34 | 35 | it('Should return sliced list', () => { 36 | expect.assertions(6); 37 | const list = [1, 2, 3, 4, 5]; 38 | const newList1 = limitList(list, '2', 4); 39 | const newList2 = limitList(list, 3, ''); 40 | const newList3 = limitList(list, 2, '-2'); 41 | 42 | expect(newList1.length).toBe(2); 43 | expect(newList1).toEqual([3, 4]); 44 | expect(newList2.length).toBe(2); 45 | expect(newList2).toEqual([4, 5]); 46 | expect(newList3.length).toBe(1); 47 | expect(newList3).toEqual([3]); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /tests/___utils/searchList.test.ts: -------------------------------------------------------------------------------- 1 | import searchList from '../../src/___utils/searchList'; 2 | 3 | describe('Util: searchList()', () => { 4 | describe('Non Primitive Array', () => { 5 | const objectArrays = [{name: 'Last'}, {name: 'First'}, {name: 'First middle'}, {name: 'Last back'}]; 6 | 7 | it('Should search on specified provided string "by" with/without options', () => { 8 | expect.assertions(16); 9 | 10 | const search = (term: string, caseInsensitive: boolean, everyWord: boolean) => 11 | searchList(objectArrays, { 12 | by: 'name', 13 | caseInsensitive, 14 | everyWord, 15 | term 16 | }); 17 | 18 | expect(search('first', false, false)).toHaveLength(0); 19 | expect(search('first', false, false)).toEqual([]); 20 | 21 | expect(search('first', true, false)).toHaveLength(2); 22 | expect(search('first', true, false)) 23 | .toEqual([{name: 'First'}, {name: 'First middle'}]); 24 | 25 | expect(search('first', true, true)).toHaveLength(2); 26 | expect(search('first', true, true)) 27 | .toEqual([{name: 'First'}, {name: 'First middle'}]); 28 | 29 | expect(search('first', false, true)).toHaveLength(0); 30 | expect(search('first', false, true)).toEqual([]); 31 | 32 | expect(search('first Mid Back', false, false)).toHaveLength(0); 33 | expect(search('first Mid Back', false, false)).toEqual([]); 34 | 35 | expect(search('first Mid Back', true, false)).toHaveLength(0); 36 | expect(search('first Mid Back', true, false)).toEqual([]); 37 | 38 | expect(search('first Mid Back', true, true)).toHaveLength(3); 39 | expect(search('first Mid Back', true, true)) 40 | .toEqual([{name: 'First'}, {name: 'First middle'}, {name: 'Last back'}]); 41 | 42 | expect(search('first Mid Back', false, true)).toHaveLength(0); 43 | expect(search('first Mid Back', false, true)).toEqual([]); 44 | }); 45 | 46 | it('Should search using function "by" with/without options', () => { 47 | // expect.assertions(8); 48 | 49 | const search = (term: string, caseInsensitive: boolean, everyWord: boolean) => 50 | searchList(objectArrays, { 51 | by: (item, searchTerm: string) => 52 | (caseInsensitive ? item.name.toLowerCase() : item.name).search(searchTerm) >= 0, 53 | caseInsensitive, 54 | everyWord, 55 | term 56 | }); 57 | 58 | expect(search('first', false, false)).toHaveLength(0); 59 | expect(search('first', false, false)).toEqual([]); 60 | 61 | expect(search('first', true, false)).toHaveLength(2); 62 | expect(search('first', true, false)) 63 | .toEqual([{name: 'First'}, {name: 'First middle'}]); 64 | 65 | expect(search('first', true, true)).toHaveLength(2); 66 | expect(search('first', true, true)) 67 | .toEqual([{name: 'First'}, {name: 'First middle'}]); 68 | 69 | expect(search('first', false, true)).toHaveLength(0); 70 | expect(search('first', false, true)).toEqual([]); 71 | 72 | expect(search('first Mid Back', false, false)).toHaveLength(0); 73 | expect(search('first Mid Back', false, false)).toEqual([]); 74 | 75 | expect(search('first Mid Back', true, false)).toHaveLength(0); 76 | expect(search('first Mid Back', true, false)).toEqual([]); 77 | 78 | expect(search('first Mid Back', true, true)).toHaveLength(3); 79 | expect(search('first Mid Back', true, true)) 80 | .toEqual([{name: 'First'}, {name: 'First middle'}, {name: 'Last back'}]); 81 | 82 | expect(search('first Mid Back', false, true)).toHaveLength(0); 83 | expect(search('first Mid Back', false, true)).toEqual([]); 84 | }); 85 | 86 | describe('multiple key search', () => { 87 | const objectArray = [ 88 | {name: 'Last', other: 'Zer'}, 89 | {name: 'First', other: 'Last'}, 90 | {name: 'Middle', other: 'Zer'}, 91 | {name: 'First', other: 'Middle'}, 92 | {name: 'Last', other: 'Abo'} 93 | ]; 94 | 95 | it('Should search on "name" and "other" keys', () => { 96 | expect(searchList(objectArray, { 97 | by: ['name', 'other'], 98 | term: 'Last', 99 | everyWord: false, 100 | caseInsensitive: false 101 | })).toEqual([ 102 | {name: 'Last', other: 'Zer'}, 103 | {name: 'First', other: 'Last'}, 104 | {name: 'Last', other: 'Abo'} 105 | ]); 106 | 107 | expect(searchList(objectArray, { 108 | by: ['name', 'other'], 109 | term: 'last', 110 | everyWord: false, 111 | caseInsensitive: true 112 | })).toEqual([ 113 | {name: 'Last', other: 'Zer'}, 114 | {name: 'First', other: 'Last'}, 115 | {name: 'Last', other: 'Abo'} 116 | ]); 117 | 118 | expect(searchList(objectArray, { 119 | by: ['name', 'other'], 120 | term: 'last', 121 | everyWord: false, 122 | caseInsensitive: false 123 | })).toEqual([]); 124 | 125 | expect(searchList(objectArray, { 126 | by: ['name', {key: 'other', caseInsensitive: true}], 127 | term: 'last', 128 | everyWord: false, 129 | caseInsensitive: false 130 | })).toEqual([ 131 | {name: 'First', other: 'Last'}, 132 | ]); 133 | 134 | expect(searchList(objectArray, { 135 | by: ['name', {key: 'other', caseInsensitive: true}], 136 | term: 'zer last', 137 | everyWord: true, 138 | caseInsensitive: false 139 | })).toEqual([ 140 | {name: 'Last', other: 'Zer'}, 141 | {name: 'First', other: 'Last'}, 142 | {name: 'Middle', other: 'Zer'}, 143 | ]); 144 | }); 145 | }) 146 | }); 147 | 148 | describe('Primitive array', () => { 149 | const stringArrays = ['Last', 'First', 'First middle', 'Last back']; 150 | 151 | it('Should search without "by" with/without options', () => { 152 | expect.assertions(16); 153 | 154 | const search = (term: string, caseInsensitive: boolean, everyWord: boolean) => 155 | searchList(stringArrays, { 156 | caseInsensitive, 157 | everyWord, 158 | term 159 | }); 160 | 161 | expect(search('first', false, false)).toHaveLength(0); 162 | expect(search('first', false, false)).toEqual([]); 163 | 164 | expect(search('first', true, false)).toHaveLength(2); 165 | expect(search('first', true, false)) 166 | .toEqual(['First', 'First middle']); 167 | 168 | expect(search('first', true, true)).toHaveLength(2); 169 | expect(search('first', true, true)) 170 | .toEqual(['First', 'First middle']); 171 | 172 | expect(search('first', false, true)).toHaveLength(0); 173 | expect(search('first', false, true)).toEqual([]); 174 | 175 | expect(search('first Mid Back', false, false)).toHaveLength(0); 176 | expect(search('first Mid Back', false, false)).toEqual([]); 177 | 178 | expect(search('first Mid Back', true, false)).toHaveLength(0); 179 | expect(search('first Mid Back', true, false)).toEqual([]); 180 | 181 | expect(search('first Mid Back', true, true)).toHaveLength(3); 182 | expect(search('first Mid Back', true, true)) 183 | .toEqual(['First', 'First middle', 'Last back']); 184 | 185 | expect(search('first Mid Back', false, true)).toHaveLength(0); 186 | expect(search('first Mid Back', false, true)).toEqual([]); 187 | }); 188 | }) 189 | }); 190 | -------------------------------------------------------------------------------- /tests/___utils/sortList.test.ts: -------------------------------------------------------------------------------- 1 | import sortList from '../../src/___utils/sortList'; 2 | 3 | describe('Util sortList()', () => { 4 | 5 | it('Should return already sorted array intact', () => { 6 | expect.assertions(7); 7 | const numberArr1 = [1, 2, 3, 4, 5]; 8 | const numberArr2 = [19, 13, 10, 8]; 9 | const stringArr1 = ['a', 'b', 'b', 'c', 'f', 'z']; 10 | const stringArr2 = ['x', 't', 'p', 'm', 'a', 'a']; 11 | const stringArr3 = ['AA', 'Aa', 'a', 'aa', 'ba', 'tc']; 12 | const stringArr4 = ['z', 'xy', 'pa', 'p', 'lm', 'ad']; 13 | 14 | expect(sortList(numberArr1)).toEqual(numberArr1); 15 | expect(sortList(numberArr2, {descending: true})).toEqual(numberArr2); 16 | expect(sortList(stringArr1)).toEqual(stringArr1); 17 | expect(sortList(stringArr2, {descending: true})).toEqual(stringArr2); 18 | expect(sortList(stringArr3)).toEqual(stringArr3); 19 | expect(sortList(stringArr3, {caseInsensitive: true})).toEqual(['a', 'AA', 'Aa', 'aa', 'ba', 'tc']); 20 | expect(sortList(stringArr4, {descending: true})).toEqual(stringArr4); 21 | }); 22 | 23 | it('Should sort number array desc', () => { 24 | expect.assertions(4); 25 | const numberArr1 = [4, 3, 8, 1, 9, 0]; 26 | const numberArr2 = [0.1, -1, 18, 13, 90, 0]; 27 | const numberArr3 = [NaN, -1, 18, 13, Infinity, 0]; 28 | const numberArr4 = [1000.92, 5, -40, Infinity, 13, -0]; 29 | 30 | expect(sortList(numberArr1, {descending: true})).toEqual([9, 8, 4, 3, 1, 0]); 31 | expect(sortList(numberArr2, {descending: true})).toEqual([90, 18, 13, 0.1, 0, -1]); 32 | expect(sortList(numberArr3, {descending: true})).toEqual([NaN, Infinity, 18, 13, 0, -1]); 33 | expect(sortList(numberArr4, {descending: true})).toEqual([Infinity, 1000.92, 13, 5, -0, -40]); 34 | }); 35 | 36 | it('Should sort number array asc', () => { 37 | expect.assertions(4); 38 | const numberArr1 = [4, 3, 8, 1, 9, 0]; 39 | const numberArr2 = [0.1, -1, 18, 13, 90, 0]; 40 | const numberArr3 = [NaN, -1, 18, 13, Infinity, 0]; 41 | const numberArr4 = [1000.92, 5, -40, Infinity, 13, -0]; 42 | 43 | expect(sortList(numberArr1)).toEqual([0, 1, 3, 4, 8, 9]); 44 | expect(sortList(numberArr2)).toEqual([-1, 0, 0.1, 13, 18, 90]); 45 | expect(sortList(numberArr3)).toEqual([NaN, -1, 0, 13, 18, Infinity]); 46 | expect(sortList(numberArr4)).toEqual([-40, -0, 5, 13, 1000.92, Infinity]); 47 | }); 48 | 49 | it('Should sort string array desc', () => { 50 | expect.assertions(6); 51 | const stringArr1 = ['a', 'aa', 'aA', 'Aa', 'b', 'BB']; 52 | const stringArr2 = ['c', 'a B', 'aA', 'AA', 'B', 'B 1']; 53 | const stringArr3 = ['9', '0 B', '1A', '-0A', 'B 32', 'z 1']; 54 | 55 | expect(sortList(stringArr1, {descending: true})) 56 | .toEqual(['b', 'aa', 'aA', 'a', 'BB', 'Aa']); 57 | expect(sortList(stringArr1, {descending: true, caseInsensitive: true})) 58 | .toEqual(['BB', 'b', 'aa', 'aA', 'Aa', 'a']); 59 | expect(sortList(stringArr2, {descending: true})) 60 | .toEqual(['c', 'aA', 'a B', 'B 1', 'B', 'AA']); 61 | expect(sortList(stringArr2, {descending: true, caseInsensitive: true})) 62 | .toEqual(['c', 'B 1', 'B', 'aA', 'AA', 'a B']); 63 | expect(sortList(stringArr3, {descending: true})) 64 | .toEqual(['z 1', 'B 32', '9', '1A', '0 B', '-0A']); 65 | expect(sortList(stringArr3, {descending: true, caseInsensitive: true})) 66 | .toEqual(['z 1', 'B 32', '9', '1A', '0 B', '-0A']); 67 | }); 68 | 69 | it('Should sort string array asc', () => { 70 | expect.assertions(6); 71 | const stringArr1 = ['a', 'aa', 'aA', 'Aa', 'b', 'BB']; 72 | const stringArr2 = ['c', 'a B', 'aA', 'AA', 'B', 'B 1']; 73 | const stringArr3 = ['9', '0 B', '1A', '-0A', 'B 32', 'z 1']; 74 | 75 | expect(sortList(stringArr1)).toEqual(['Aa', 'BB', 'a', 'aA', 'aa', 'b', ]); 76 | expect(sortList(stringArr1, {caseInsensitive: true})) 77 | .toEqual(['a', 'aa', 'aA', 'Aa', 'b', 'BB']); 78 | expect(sortList(stringArr2)).toEqual(['AA', 'B', 'B 1', 'a B', 'aA', 'c']); 79 | expect(sortList(stringArr2, {caseInsensitive: true})) 80 | .toEqual(['a B', 'aA', 'AA', 'B', 'B 1', 'c']); 81 | expect(sortList(stringArr3)).toEqual(['-0A', '0 B', '1A', '9', 'B 32', 'z 1']); 82 | expect(sortList(stringArr3, {caseInsensitive: true})) 83 | .toEqual(['-0A', '0 B', '1A', '9', 'B 32', 'z 1']); 84 | }); 85 | 86 | it('Should sort object array by name desc', () => { 87 | expect.assertions(6); 88 | const objectArr1 = [{name: 'a'}, {name: 'aa'}, {name: 'aA'}, {name: 'Aa'}, {name: 'b'}, {name: 'BB'}]; 89 | const objectArr2 = [{name: 'c'}, {name: 'a B'}, {name: 'aA'}, {name: 'AA'}, {name: 'B'}, {name: 'B 1'}]; 90 | const objectArr3 = [{name: '9'}, {name: '0 B'}, {name: '1A'}, {name: '-0A'}, {name: 'B 32'}, {name: 'z 1'}]; 91 | 92 | expect(sortList(objectArr1, {descending: true, by: 'name'})) 93 | .toEqual([{name: 'b'}, {name: 'aa'}, {name: 'aA'}, {name: 'a'}, {name: 'BB'}, {name: 'Aa'}]); 94 | expect(sortList(objectArr1, {descending: true, caseInsensitive: true, by: 'name'})) 95 | .toEqual([{name: 'BB'}, {name: 'b'}, {name: 'aa'}, {name: 'aA'}, {name: 'Aa'}, {name: 'a'}]); 96 | expect(sortList(objectArr2, {descending: true, by: 'name'})) 97 | .toEqual([{name: 'c'}, {name: 'aA'}, {name: 'a B'}, {name: 'B 1'}, {name: 'B'}, {name: 'AA'}]); 98 | expect(sortList(objectArr2, {descending: true, caseInsensitive: true, by: 'name'})) 99 | .toEqual([{name: 'c'}, {name: 'B 1'}, {name: 'B'}, {name: 'aA'}, {name: 'AA'}, {name: 'a B'}]); 100 | expect(sortList(objectArr3, {descending: true, by: 'name'})) 101 | .toEqual([{name: 'z 1'}, {name: 'B 32'}, {name: '9'}, {name: '1A'}, {name: '0 B'}, {name: '-0A'}]); 102 | expect(sortList(objectArr3, {descending: true, caseInsensitive: true, by: 'name'})) 103 | .toEqual([{name: 'z 1'}, {name: 'B 32'}, {name: '9'}, {name: '1A'}, {name: '0 B'}, {name: '-0A'}]); 104 | }); 105 | 106 | it('Should sort object array by name asc', () => { 107 | expect.assertions(6); 108 | const stringArr1 = [{name: 'a'}, {name: 'aa'}, {name: 'aA'}, {name: 'Aa'}, {name: 'b'}, {name: 'BB'}]; 109 | const stringArr2 = [{name: 'c'}, {name: 'a B'}, {name: 'aA'}, {name: 'AA'}, {name: 'B'}, {name: 'B 1'}]; 110 | const stringArr3 = [{name: '9'}, {name: '0 B'}, {name: '1A'}, {name: '-0A'}, {name: 'B 32'}, {name: 'z 1'}]; 111 | 112 | expect(sortList(stringArr1, {by: 'name'})) 113 | .toEqual([{name: 'Aa'}, {name: 'BB'}, {name: 'a'}, {name: 'aA'}, {name: 'aa'}, {name: 'b'}]); 114 | expect(sortList(stringArr1, {caseInsensitive: true, by: 'name'})) 115 | .toEqual([{name: 'a'}, {name: 'aa'}, {name: 'aA'}, {name: 'Aa'}, {name: 'b'}, {name: 'BB'}]); 116 | expect(sortList(stringArr2, {by: 'name'})) 117 | .toEqual([{name: 'AA'}, {name: 'B'}, {name: 'B 1'}, {name: 'a B'}, {name: 'aA'}, {name: 'c'}]); 118 | expect(sortList(stringArr2, {caseInsensitive: true, by: 'name'})) 119 | .toEqual([{name: 'a B'}, {name: 'aA'}, {name: 'AA'}, {name: 'B'}, {name: 'B 1'}, {name: 'c'}]); 120 | expect(sortList(stringArr3, {by: 'name'})) 121 | .toEqual([{name: '-0A'}, {name: '0 B'}, {name: '1A'}, {name: '9'}, {name: 'B 32'}, {name: 'z 1'}]); 122 | expect(sortList(stringArr3, {caseInsensitive: true, by: 'name'})) 123 | .toEqual([{name: '-0A'}, {name: '0 B'}, {name: '1A'}, {name: '9'}, {name: 'B 32'}, {name: 'z 1'}]); 124 | }); 125 | 126 | it('Should sort object array by count desc', () => { 127 | expect.assertions(4); 128 | const numberArr1 = [{count: 4}, {count: 3}, {count: 8}, {count: 1}, {count: 9}, {count: 0}]; 129 | const numberArr2 = [{count: 0.1}, {count: -1}, {count: 18}, {count: 13}, {count: 90}, {count: 0}]; 130 | const numberArr3 = [{count: NaN}, {count: -1}, {count: 18}, {count: 13}, {count: Infinity}, {count: 0}]; 131 | const numberArr4 = [{count: 1000.92}, {count: 5}, {count: -40}, {count: Infinity}, {count: 13}, {count: -0}]; 132 | 133 | expect(sortList(numberArr1, {descending: true, by: 'count'})) 134 | .toEqual([{count: 9}, {count: 8}, {count: 4}, {count: 3}, {count: 1}, {count: 0}]); 135 | expect(sortList(numberArr2, {descending: true, by: 'count'})) 136 | .toEqual([{count: 90}, {count: 18}, {count: 13}, {count: 0.1}, {count: 0}, {count: -1}]); 137 | expect(sortList(numberArr3, {descending: true, by: 'count'})) 138 | .toEqual([{count: NaN}, {count: Infinity}, {count: 18}, {count: 13}, {count: 0}, {count: -1}]); 139 | expect(sortList(numberArr4, {descending: true, by: 'count'})) 140 | .toEqual([{count: Infinity}, {count: 1000.92}, {count: 13}, {count: 5}, {count: -0}, {count: -40}]); 141 | }); 142 | 143 | it('Should sort object array by count asc', () => { 144 | expect.assertions(4); 145 | const numberArr1 = [{count: 4}, {count: 3}, {count: 8}, {count: 1}, {count: 9}, {count: 0}]; 146 | const numberArr2 = [{count: 0.1}, {count: -1}, {count: 18}, {count: 13}, {count: 90}, {count: 0}]; 147 | const numberArr3 = [{count: NaN}, {count: -1}, {count: 18}, {count: 13}, {count: Infinity}, {count: 0}]; 148 | const numberArr4 = [{count: 1000.92}, {count: 5}, {count: -40}, {count: Infinity}, {count: 13}, {count: -0}]; 149 | 150 | expect(sortList(numberArr1, {by: 'count'})) 151 | .toEqual([{count: 0}, {count: 1}, {count: 3}, {count: 4}, {count: 8}, {count: 9}]); 152 | expect(sortList(numberArr2, {by: 'count'})) 153 | .toEqual([{count: -1}, {count: 0}, {count: 0.1}, {count: 13}, {count: 18}, {count: 90}]); 154 | expect(sortList(numberArr3, {by: 'count'})) 155 | .toEqual([{count: NaN}, {count: -1}, {count: 0}, {count: 13}, {count: 18}, {count: Infinity}]); 156 | expect(sortList(numberArr4, {by: 'count'})) 157 | .toEqual([{count: -40}, {count: -0}, {count: 5}, {count: 13}, {count: 1000.92}, {count: Infinity}]); 158 | }); 159 | 160 | it('Should sort on many keys', () => { 161 | const objectArray = [ 162 | {name: 'Last', other: 'Zer', age: 2}, 163 | {name: 'First', other: 'Last', age: 8}, 164 | {name: 'Middle', other: 'Zer', age: 1}, 165 | {name: 'First', other: 'Middle', age: 8}, 166 | {name: 'Last', other: 'Abo', age: 2} 167 | ]; 168 | 169 | expect(sortList(objectArray, { 170 | by: ['name', 'other'], 171 | descending: false, 172 | caseInsensitive: false, 173 | })).toEqual([ 174 | {name: 'First', other: 'Last', age: 8}, 175 | {name: 'First', other: 'Middle', age: 8}, 176 | {name: 'Last', other: 'Abo', age: 2}, 177 | {name: 'Last', other: 'Zer', age: 2}, 178 | {name: 'Middle', other: 'Zer', age: 1} 179 | ]) 180 | 181 | expect(sortList(objectArray, { 182 | by: ['name', {key: 'other', descending: true}], 183 | descending: false, 184 | caseInsensitive: false, 185 | })).toEqual([ 186 | {name: 'First', other: 'Middle', age: 8}, 187 | {name: 'First', other: 'Last', age: 8}, 188 | {name: 'Last', other: 'Zer', age: 2}, 189 | {name: 'Last', other: 'Abo', age: 2}, 190 | {name: 'Middle', other: 'Zer', age: 1} 191 | ]) 192 | 193 | expect(sortList(objectArray, { 194 | by: ['name', {key: 'age', descending: true}], 195 | descending: false, 196 | caseInsensitive: false, 197 | })).toEqual([ 198 | {name: 'First', other: 'Last', age: 8}, 199 | {name: 'First', other: 'Middle', age: 8}, 200 | {name: 'Last', other: 'Zer', age: 2}, 201 | {name: 'Last', other: 'Abo', age: 2}, 202 | {name: 'Middle', other: 'Zer', age: 1}, 203 | ]) 204 | 205 | expect(sortList(objectArray, { 206 | by: ['name', 'other'], 207 | descending: true, 208 | caseInsensitive: false, 209 | })).toEqual([ 210 | {name: 'Middle', other: 'Zer', age: 1}, 211 | {name: 'Last', other: 'Zer', age: 2}, 212 | {name: 'Last', other: 'Abo', age: 2}, 213 | {name: 'First', other: 'Middle', age: 8}, 214 | {name: 'First', other: 'Last', age: 8}, 215 | ]) 216 | 217 | expect(sortList(objectArray, { 218 | by: ['age', 'other'], 219 | descending: true, 220 | caseInsensitive: false, 221 | })).toEqual([ 222 | {name: 'First', other: 'Middle', age: 8}, 223 | {name: 'First', other: 'Last', age: 8}, 224 | {name: 'Last', other: 'Zer', age: 2}, 225 | {name: 'Last', other: 'Abo', age: 2}, 226 | {name: 'Middle', other: 'Zer', age: 1}, 227 | ]) 228 | 229 | expect(sortList(objectArray, { 230 | by: [{key: 'age', descending: false}, 'other'], 231 | descending: true, 232 | caseInsensitive: false, 233 | })).toEqual([ 234 | { name: 'Middle', other: 'Zer', age: 1 }, 235 | { name: 'Last', other: 'Zer', age: 2 }, 236 | { name: 'Last', other: 'Abo', age: 2 }, 237 | { name: 'First', other: 'Middle', age: 8 }, 238 | { name: 'First', other: 'Last', age: 8 } 239 | ]) 240 | }); 241 | 242 | it('Should keep the same for object or array arrays if key is no found', () => { 243 | expect.assertions(2); 244 | const objectArray = [{name: 'Ta'}, {count: 1}]; 245 | const arrayArray = [[{name: 'Ta'}], [{count: 1}]]; 246 | 247 | expect(sortList(objectArray, {by: 'name'})).toEqual([{name: 'Ta'}, {count: 1}]); 248 | expect(sortList(arrayArray, {by: '0.count'})).toEqual([[{name: 'Ta'}], [{count: 1}]]); 249 | }); 250 | }); 251 | -------------------------------------------------------------------------------- /tests/__snapshots__/FlatList.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FlatList Should group by 3 with default separator at the bottom 1`] = ` 4 | 5 |
  • 6 | age-1 7 |
  • 8 |
  • 9 | age-3 10 |
  • 11 |
  • 12 | age-45 13 |
  • 14 |
    17 |
  • 18 | age-8 19 |
  • 20 |
  • 21 | age-0 22 |
  • 23 |
  • 24 | age-20 25 |
  • 26 |
    29 |
  • 30 | age-10 31 |
  • 32 |
    35 |
    36 | `; 37 | 38 | exports[`FlatList Should group by 3 with default separator at the top 1`] = ` 39 | 40 |
    43 |
  • 44 | age-1 45 |
  • 46 |
  • 47 | age-3 48 |
  • 49 |
  • 50 | age-45 51 |
  • 52 |
    55 |
  • 56 | age-8 57 |
  • 58 |
  • 59 | age-0 60 |
  • 61 |
  • 62 | age-20 63 |
  • 64 |
    67 |
  • 68 | age-10 69 |
  • 70 |
    71 | `; 72 | 73 | exports[`FlatList Should group items by over 18 1`] = ` 74 | 75 |
    78 |
    81 | Under 18 82 |
    83 |
    84 |
  • 85 | age-1 86 |
  • 87 |
  • 88 | age-3 89 |
  • 90 |
  • 91 | age-8 92 |
  • 93 |
  • 94 | age-0 95 |
  • 96 |
  • 97 | age-10 98 |
  • 99 |
    102 |
    105 | Over or 18 106 |
    107 |
    108 |
  • 109 | age-45 110 |
  • 111 |
  • 112 | age-20 113 |
  • 114 |
    115 | `; 116 | 117 | exports[`FlatList Should group items limited 1`] = ` 118 | 119 |
    122 |
  • 123 | age-1 124 |
  • 125 |
  • 126 | age-3 127 |
  • 128 |
  • 129 | age-45 130 |
  • 131 |
    134 |
  • 135 | age-8 136 |
  • 137 |
  • 138 | age-0 139 |
  • 140 |
  • 141 | age-20 142 |
  • 143 |
    146 |
  • 147 | age-10 148 |
  • 149 |
    150 | `; 151 | 152 | exports[`FlatList Should group items reversed 1`] = ` 153 | 154 |
    157 |
  • 158 | age-45 159 |
  • 160 |
  • 161 | age-3 162 |
  • 163 |
  • 164 | age-1 165 |
  • 166 |
    169 |
  • 170 | age-20 171 |
  • 172 |
  • 173 | age-0 174 |
  • 175 |
  • 176 | age-8 177 |
  • 178 |
    181 |
  • 182 | age-10 183 |
  • 184 |
    185 | `; 186 | 187 | exports[`FlatList Should group sorted 1`] = ` 188 | 189 |
    192 |
    195 | Under 18 196 |
    197 |
    198 |
  • 199 | age-0 200 |
  • 201 |
  • 202 | age-1 203 |
  • 204 |
  • 205 | age-3 206 |
  • 207 |
  • 208 | age-8 209 |
  • 210 |
  • 211 | age-10 212 |
  • 213 |
    216 |
    219 | Over or 18 220 |
    221 |
    222 |
  • 223 | age-20 224 |
  • 225 |
  • 226 | age-45 227 |
  • 228 |
    229 | `; 230 | 231 | exports[`FlatList Should group with custom separator 1`] = ` 232 | 233 |
    236 |
    239 | 1 240 |
    241 |
    242 |
  • 243 | age-1 244 |
  • 245 |
  • 246 | age-3 247 |
  • 248 |
  • 249 | age-45 250 |
  • 251 |
    254 |
    257 | 2 258 |
    259 |
    260 |
  • 261 | age-8 262 |
  • 263 |
  • 264 | age-0 265 |
  • 266 |
  • 267 | age-20 268 |
  • 269 |
    272 |
    275 | 3 276 |
    277 |
    278 |
  • 279 | age-10 280 |
  • 281 |
    282 | `; 283 | 284 | exports[`FlatList Should paginate on scroll 1`] = ` 285 | 286 |
  • 287 | age-1 288 |
  • 289 |
  • 290 | age-2 291 |
  • 292 |
  • 293 | age-3 294 |
  • 295 |
  • 296 | age-4 297 |
  • 298 |
  • 299 | age-5 300 |
  • 301 |
    305 |
    308 | loading... 309 |
    310 |
    311 |
    312 | `; 313 | 314 | exports[`FlatList Should paginate on scroll 2`] = ` 315 | 316 |
  • 317 | age-1 318 |
  • 319 |
  • 320 | age-2 321 |
  • 322 |
  • 323 | age-3 324 |
  • 325 |
  • 326 | age-4 327 |
  • 328 |
  • 329 | age-5 330 |
  • 331 |
  • 332 | age-6 333 |
  • 334 |
  • 335 | age-7 336 |
  • 337 |
  • 338 | age-8 339 |
  • 340 |
  • 341 | age-9 342 |
  • 343 |
  • 344 | age-10 345 |
  • 346 |