├── .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 | [](https://github.com/beforesemicolon/flatlist-react/actions)
14 | [](https://github.com/beforesemicolon/flatlist-react/blob/master/LICENSE)
15 | [](https://www.npmjs.com/package/flatlist-react)
16 | [](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 |
70 | List is empty!
}
74 | sortBy={["firstName", {key: "lastName", descending: true}]}
75 | groupBy={person => person.info.age > 18 ? 'Over 18' : 'Under 18'}
76 | />
77 |
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('');
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 |
350 |
351 | `;
352 |
353 | exports[`FlatList Should paginate with custom loading indicator 1`] = `
354 |
355 |
356 | age-1
357 |
358 |
359 | age-2
360 |
361 |
362 | age-3
363 |
364 |
365 | age-4
366 |
367 |
368 | age-5
369 |
370 |
374 |
375 | Loading Items...
376 |
377 |
378 |
379 | `;
380 |
381 | exports[`FlatList Should render default blank with empty list 1`] = `
382 |
383 |
384 | List is empty...
385 |
386 |
387 | `;
388 |
389 | exports[`FlatList Should render items 1`] = `
390 |
391 |
392 | age-1
393 |
394 |
395 | age-3
396 |
397 |
398 | age-45
399 |
400 |
401 | age-8
402 |
403 |
404 | age-0
405 |
406 |
407 | age-20
408 |
409 |
410 | age-10
411 |
412 |
413 | `;
414 |
415 | exports[`FlatList Should render items wrapped in a tag 1`] = `
416 |
417 |
420 |
421 | age-1
422 |
423 |
424 | age-3
425 |
426 |
427 | age-45
428 |
429 |
430 | age-8
431 |
432 |
433 | age-0
434 |
435 |
436 | age-20
437 |
438 |
439 | age-10
440 |
441 |
442 |
443 | `;
444 |
445 | exports[`FlatList Should render on scroll 1`] = `
446 |
447 |
448 | age-1
449 |
450 |
451 | age-3
452 |
453 |
454 | age-45
455 |
456 |
457 | age-8
458 |
459 |
460 | age-0
461 |
462 |
463 | age-20
464 |
465 |
466 | age-10
467 |
468 |
469 | age-1
470 |
471 |
472 | age-3
473 |
474 |
475 | age-45
476 |
477 |
481 |
482 | `;
483 |
484 | exports[`FlatList Should render provided blank with empty list 1`] = `
485 |
486 |
487 | Empty
488 |
489 |
490 | `;
491 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
6 | // "lib": [], /* Specify library files to be included in the compilation. */
7 | // "allowJs": true, /* Allow javascript files to be compiled. */
8 | // "checkJs": true, /* Report errors in .js files. */
9 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
10 | "declaration": true, /* Generates corresponding '.d.ts' file. */
11 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
12 | // "sourceMap": true, /* Generates corresponding '.map' file. */
13 | // "outFile": "./", /* Concatenate and emit output to single file. */
14 | "outDir": "./lib", /* Redirect output structure to the directory. */
15 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
16 | // "composite": true, /* Enable project compilation */
17 | "removeComments": false, /* Do not emit comments to output. */
18 | // "noEmit": true, /* Do not emit outputs. */
19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
22 |
23 | /* Strict Type-Checking Options */
24 | "strict": true, /* Enable all strict type-checking options. */
25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
26 | // "strictNullChecks": true, /* Enable strict null checks. */
27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
28 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
29 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
30 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
31 |
32 | /* Additional Checks */
33 | // "noUnusedLocals": true, /* Report errors on unused locals. */
34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
35 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
37 |
38 | /* Module Resolution Options */
39 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
40 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
41 | "paths": {
42 | "*": [
43 | "node_modules/*"
44 | ]
45 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
47 | // "typeRoots": [], /* List of folders to include type definitions from. */
48 | // "types": [], /* Type declaration files to be included in compilation. */
49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
50 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
52 |
53 | /* Source Map Options */
54 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
55 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
57 | // "inlineSources": true /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
58 |
59 | /* Experimental Options */
60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
62 | },
63 | "include": [
64 | "src/**/*"
65 | ]
66 | }
67 |
--------------------------------------------------------------------------------