From f5e715066702a9aa0856702f32386d73475e4777 Mon Sep 17 00:00:00 2001 From: Asad Date: Sat, 8 Mar 2025 20:47:44 -0500 Subject: [PATCH] first build --- LICENSE | 215 ++++++++++- README.md | 579 +++++++++++++++++++++++++++- example/src/App.tsx | 78 +++- example/src/BasicExample.tsx | 42 ++ example/src/ExampleFive.tsx | 136 +++++++ example/src/ExampleFour.tsx | 57 +++ example/src/ExampleThree.tsx | 76 ++++ example/src/ScrollableExample.tsx | 82 ++++ example/src/StickyColumnExample.tsx | 41 ++ package.json | 8 +- react-native-tabeller-0.1.0.tgz | Bin 0 -> 25006 bytes src/components/cell.tsx | 81 ++++ src/components/cols.tsx | 83 ++++ src/components/rows.tsx | 116 ++++++ src/components/sticky-table.tsx | 129 +++++++ src/components/table.tsx | 82 ++++ src/index.tsx | 29 +- src/types.ts | 130 +++++++ src/util/index.ts | 9 + 19 files changed, 1923 insertions(+), 50 deletions(-) create mode 100644 example/src/BasicExample.tsx create mode 100644 example/src/ExampleFive.tsx create mode 100644 example/src/ExampleFour.tsx create mode 100644 example/src/ExampleThree.tsx create mode 100644 example/src/ScrollableExample.tsx create mode 100644 example/src/StickyColumnExample.tsx create mode 100644 react-native-tabeller-0.1.0.tgz create mode 100644 src/components/cell.tsx create mode 100644 src/components/cols.tsx create mode 100644 src/components/rows.tsx create mode 100644 src/components/sticky-table.tsx create mode 100644 src/components/table.tsx create mode 100644 src/types.ts create mode 100644 src/util/index.ts diff --git a/LICENSE b/LICENSE index 1dd7498..4abbe55 100644 --- a/LICENSE +++ b/LICENSE @@ -1,20 +1,201 @@ -MIT License + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Copyright (c) 2025 Asadbek Karimov -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + 1. Definitions. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (C) 2012-present SheetJS LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index 7df4e68..8117953 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,573 @@ -# react-native-tabeller +

React Native Tabeller

+ +

+ +

+ +This is a table component for react native. + +- [Installation](#installation) +- [Examples](#examples) +- [Properties](#properties) +- [Notice](#notice) +- [License](#license) -React Native table with reanimated and gestures. ## Installation - ```sh npm install react-native-tabeller ``` -## Usage - - -```js -import { multiply } from 'react-native-tabeller'; - -// ... - -const result = await multiply(3, 7); +```tsx +import { Table, TableWrapper, Row, Rows, Col, Cols, Cell } from 'react-native-tabeller'; ``` +## Examples + +### Basic Table + + +```tsx +import { Table, Row, Rows } from 'react-native-tabeller'; +import { View, StyleSheet } from 'react-native'; + +export const BasicExample = () => { + const tableHead: string[] = ['Name', 'Index']; + const tableData: string[][] = [ + ['Bill Clinton', '42'], + ['GeorgeW Bush', '43'], + ['Barack Obama', '44'], + ['Donald Trump', '45'], + ['Joseph Biden', '46'] + ]; + return ( + + + + +
+
+ ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + paddingTop: 30, + backgroundColor: '#fff' + }, + head: { + height: 44, + backgroundColor: '#C6F3E0' + }, + text: { + textAlign: 'center', + padding: 5 + }, + headText: { + textAlign: 'center', + fontWeight: 'bold' + } +}); +``` + +### Scrollable Example + + +```tsx +import { Table, Row, Rows } from 'react-native-tabeller'; +import { View, StyleSheet, ScrollView } from 'react-native'; + +export const ScrollableExample = () => { + const tableHead = ['Head', 'Head2', 'Head3', 'Head4', 'Head5', 'Head6', 'Head7', 'Head8']; + const widthArr = [40, 69, 80, 100, 120, 140, 160, 180]; + + // generate a large table data + const tableData = []; + for (let i = 0; i < 30; i += 1) { + const rowData = []; + for (let j = 0; j < 8; j += 1) { + rowData.push(`${i}${j}`); + } + tableData.push(rowData); + } + + return ( + + + + + +
+ + + +
+
+
+
+
+ ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + paddingTop: 30, + height: 400, + backgroundColor: '#fff', + }, + header: { + height: 50, + backgroundColor: '#C6F3E0' + }, + headerText: { + textAlign: 'center', + fontWeight: 'bold' + }, + text: { + textAlign: 'center', + padding: 5 + }, + dataWrapper: { + marginTop: -1 + }, + row: { + height: 40, + backgroundColor: '#fff' + } +}); +``` + +### Example three + + +```tsx +import { Table, TableWrapper, Row, Rows, Col } from 'react-native-tabeller'; +import { View, StyleSheet } from 'react-native'; + +export const ExampleThree = () => { + const tableHead = ['', 'Head1', 'Head2', 'Head3']; + const tableTitle = ['Title1', 'Title2', 'Title3']; + const tableData = [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'] + ]; + + const onCellPress = (data: any) => { + console.log(`Cell pressed: ${data}`); + }; + + return ( + + + + + + + +
+
+ ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + paddingTop: 30, + backgroundColor: '#fff' + }, + head: { + height: 44, + backgroundColor: '#C6F3E0' + }, + text: { + textAlign: 'center', + padding: 5 + }, + headText: { + textAlign: 'center', + fontWeight: 'bold' + }, + row: { + height: 40, + backgroundColor: '#fff' + }, + wrapper: { + flexDirection: 'row' + }, + title: { + flex: 1, + backgroundColor: '#C6F3E0' + }, + titleText: { + textAlign: 'left', + marginLeft: 6, + fontWeight: '600' + } +}); +``` + +### Example Four + + +```tsx +import React, { useState } from 'react'; +import { StyleSheet, View, Text, TouchableOpacity, Alert } from 'react-native'; +import { Table, TableWrapper, Row, Cell } from 'react-native-tabeller'; + +type TableDataType = string[][]; + +export const ExampleFour: React.FC = () => { + const [tableHead] = useState(['Head', 'Head2', 'Head3', 'Head4']); + const [tableData] = useState([ + ['1', '2', '3', '4'], + ['a', 'b', 'c', 'd'], + ['1', '2', '3', '4'], + ['a', 'b', 'c', 'd'] + ]); + + const alertIndex = (index: number): void => { + Alert.alert(`This is row ${index + 1}`); + }; + + const element = (data: any, index: number): React.ReactElement => ( + alertIndex(index)}> + + button + + + ); + + return ( + + + + { + tableData.map((rowData, index) => ( + + { + rowData.map((cellData, cellIndex) => ( + + )) + } + + )) + } +
+
+ ); +}; + +const styles = StyleSheet.create({ + container: { flex: 1, padding: 16, paddingTop: 30, backgroundColor: '#fff' }, + head: { height: 40, backgroundColor: '#C6F3E0' }, + text: { margin: 8 }, + row: { flexDirection: 'row', backgroundColor: '#FFF1C1' }, + btn: { width: 58, height: 18, backgroundColor: '#78B7BB', borderRadius: 2 }, + btnText: { textAlign: 'center', color: '#fff' } +}); + +export default ExampleFour; +``` + +### Example Five + + +```tsx +import React, { useState } from 'react'; +import { StyleSheet, View, Text, TouchableOpacity, Alert } from 'react-native'; +import { Table, TableWrapper, Row, Col, Rows } from 'react-native-tabeller'; + +export const ExampleFive: React.FC = () => { + const alertIndex = (value: string): void => { + Alert.alert(`This is column ${value}`); + }; + + const elementButton = (value: string): React.ReactElement => ( + alertIndex(value)}> + + button + + + ); + + const [tableHead] = useState(['', elementButton('1'), elementButton('2'), elementButton('3')]); + const [sideHead] = useState(['H1', 'H2']); + const [rowTitles] = useState(['Title', 'Title2', 'Title3', 'Title4']); + + // Table data - matching the structure in the screenshot + const [tableData] = useState([ + ['a', '1', 'a'], + ['b', '2', 'b'], + ['c', '3', 'c'], + ['d', '4', 'd'] + ]); + + return ( + + + {/* table header row with buttons */} + + + + {/* left column with H1, H2 */} + + + + {/* row titles column */} + + + {/* content cells */} + + + +
+
+ ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + paddingTop: 30, + backgroundColor: '#fff' + }, + header: { + height: 40, + backgroundColor: '#fff' + }, + headerText: { + textAlign: 'center', + fontWeight: '500' + }, + wrapper: { + flexDirection: 'row' + }, + sideHeader: { + width: 60, + backgroundColor: '#C6F3E0' + }, + sideHeaderText: { + textAlign: 'center', + fontWeight: '500' + }, + tableContentWrapper: { + flex: 1, + flexDirection: 'row' + }, + title: { + width: 80, + backgroundColor: '#f6f8fa' + }, + titleText: { + textAlign: 'left', + paddingLeft: 5 + }, + row: { + height: 30 + }, + text: { + textAlign: 'center' + }, + btn: { + width: 58, + height: 18, + backgroundColor: '#c8e1ff', + borderRadius: 2, + alignSelf: 'center' + }, + btnText: { + textAlign: 'center', + fontSize: 12 + } +}); + +export default ExampleFive; +``` + +--- + +

+ + +## Properties + +### `Table` Component Properties + +| Prop | Type | Description | Default | +|---|---|---|---| +| **style** | Style | Container style for the table | `null` | +| **borderStyle** | Object | Table border line width and color | `{ borderWidth: 0, borderColor: '#000' }` | +| **children** | ReactNode | Table content | Required | + +### `TableWrapper` Component Properties + +| Prop | Type | Description | Default | +|---|---|---|---| +| **style** | Style | Container style | `null` | +| **borderStyle** | Object | Table border line width and color | `{ borderWidth: 0, borderColor: '#000' }` | +| **children** | ReactNode | TableWrapper content | Required | + +### `Cell` Component Properties + +| Prop | Type | Description | Default | +|---|---|---|---| +| **data** | string \| number \| null | Cell content | `null` | +| **width** | number | Cell width in pixels | `null` | +| **height** | number | Cell height in pixels | `null` | +| **flex** | number | Flex value for the cell | `1` (if no width, height, or style) | +| **style** | Style | Container style | `null` | +| **textStyle** | Style | Text style for cell content | `null` | +| **borderStyle** | Object | Cell border line width and color | `{ borderWidth: 0, borderColor: '#000' }` | +| **cellContainerProps** | ViewProps | Props passed to the cell container | `{}` | +| **onPress** | Function | Callback when cell is pressed | `null` | +| **children** | ReactNode | Children to render inside the cell | `null` | + +### `Row` Component Properties + +| Prop | Type | Description | Default | +|---|---|---|---| +| **data** | Array | Array of data items for each cell in the row | Required | +| **style** | Style | Container style | `null` | +| **widthArr** | number[] | Array of widths for each cell | `[]` | +| **height** | number | Height for the entire row | `null` | +| **flexArr** | number[] | Array of flex values for each cell in the row | `[]` | +| **textStyle** | Style | Text style applied to all cells in the row | `null` | +| **borderStyle** | Object | Border line width and color | `{ borderWidth: 0, borderColor: '#000' }` | +| **cellTextStyle** | Function | Function to generate custom text styles for individual cells | `null` | +| **onPress** | Function | Callback when a cell is pressed | `null` | + +### `Rows` Component Properties + +| Prop | Type | Description | Default | +|---|---|---|---| +| **data** | Array> | 2D array of data for rows and cells | Required | +| **style** | Style | Container style | `null` | +| **widthArr** | number[] | Array of widths for each column | `[]` | +| **heightArr** | number[] | Array of heights for each row | `[]` | +| **flexArr** | number[] | Array of flex values for each column | `[]` | +| **textStyle** | Style | Text style applied to all cells | `null` | +| **borderStyle** | Object | Border line width and color | `{ borderWidth: 0, borderColor: '#000' }` | +| **onPress** | Function | Callback when a cell is pressed | `null` | + +### `Col` Component Properties + +| Prop | Type | Description | Default | +|---|---|---|---| +| **data** | Array | Array of data items for each cell in the column | Required | +| **style** | Style | Container style | `null` | +| **width** | number | Width for the entire column | `null` | +| **heightArr** | number[] | Array of heights for each cell | `[]` | +| **flex** | number | Flex value for the column | `null` | +| **textStyle** | Style | Text style applied to all cells in the column | `null` | +| **borderStyle** | Object | Border line width and color | `{ borderWidth: 0, borderColor: '#000' }` | + +### `Cols` Component Properties + +| Prop | Type | Description | Default | +|---|---|---|---| +| **data** | Array> | 2D array of data for columns and cells | Required | +| **style** | Style | Container style | `null` | +| **widthArr** | number[] | Array of widths for each column | `[]` | +| **heightArr** | number[] | Array of heights for each cell in a column | `[]` | +| **flexArr** | number[] | Array of flex values for each column | `[]` | +| **textStyle** | Style | Text style applied to all cells | `null` | +| **borderStyle** | Object | Border line width and color | `{ borderWidth: 0, borderColor: '#000' }` | + +### `StickyTable` Component Properties + +| Prop | Type | Description | Default | +|---|---|---|---| +| **data** | Array> | Full table data including first column | Required | +| **stickyColumnWidth** | number | Width of the sticky column | Required | +| **columnWidths** | number[] | Widths for non-sticky columns | `[]` | +| **style** | Style | Style for the container | `null` | +| **cellStyle** | Style | Style for cells | `null` | +| **textStyle** | Style | Text style for cell content | `null` | +| **headerStyle** | Style | Style for header row | `null` | +| **headerTextStyle** | Style | Text style for header cells | `null` | +| **borderStyle** | Object | Border style | `{ borderWidth: 1, borderColor: '#000' }` | +--- + + + +

+ +## Notice ++ `Col` and `Cols` components do not support automatic height adjustment ++ Use the `textStyle` property to set margins - avoid using padding ++ If the parent element is Not `Table` component, specify the `borderStyle` + +```tsx + + {/* add borderStyle if the parent is not a Table component */} + + + + +``` ## Contributing @@ -26,8 +575,4 @@ See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the ## License -MIT - ---- - -Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) +Apache License, Version 2.0 [(ALv2)](LICENSE) diff --git a/example/src/App.tsx b/example/src/App.tsx index 9d05b24..0548ef5 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,20 +1,78 @@ -import { multiply } from 'react-native-tabeller'; -import { Text, View, StyleSheet } from 'react-native'; - -const result = multiply(3, 7); +import { Text, View, StyleSheet, ScrollView } from 'react-native'; +import { BasicExample } from './BasicExample'; +import { ScrollableExample } from './ScrollableExample'; +import { ExampleThree } from './ExampleThree'; +import { StickyTableExample } from './StickyColumnExample'; +import { ExampleFour } from './ExampleFour'; +import { ExampleFive } from './ExampleFive'; export default function App() { return ( - - Result: {result} - + + React Native Tabeller + + + Basic Table + + + + + Scrollable Table + + + + + + Scrollable Sticky 1st Column Table + + + + + + Example Three + + + + + Example Four + + + + + Example Five + + + ); } const styles = StyleSheet.create({ container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', + flexGrow: 1, + paddingHorizontal: 20, + paddingTop: 60, + backgroundColor: '#f5f5f5', + }, + heading: { + fontSize: 24, + fontWeight: 'bold', + textAlign: 'center', + marginBottom: 20, + }, + section: { + marginBottom: 30, + padding: 15, + backgroundColor: '#fff', + borderRadius: 8, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + subheading: { + fontSize: 18, + fontWeight: '600', + marginBottom: 10, }, }); diff --git a/example/src/BasicExample.tsx b/example/src/BasicExample.tsx new file mode 100644 index 0000000..663ef4f --- /dev/null +++ b/example/src/BasicExample.tsx @@ -0,0 +1,42 @@ +import { Table, Row, Rows } from 'react-native-tabeller'; +import { View, StyleSheet } from 'react-native'; + +export const BasicExample = () => { + const tableHead: string[] = ['Name', 'Index']; + const tableData: string[][] = [ + ['Bill Clinton', '42'], + ['GeorgeW Bush', '43'], + ['Barack Obama', '44'], + ['Donald Trump', '45'], + ['Joseph Biden', '46'], + ]; + return ( + + + + +
+
+ ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + paddingTop: 30, + backgroundColor: '#fff', + }, + head: { + height: 44, + backgroundColor: '#C6F3E0', + }, + text: { + textAlign: 'center', + padding: 5, + }, + headText: { + textAlign: 'center', + fontWeight: 'bold', + }, +}); diff --git a/example/src/ExampleFive.tsx b/example/src/ExampleFive.tsx new file mode 100644 index 0000000..b33eebc --- /dev/null +++ b/example/src/ExampleFive.tsx @@ -0,0 +1,136 @@ +import React, { useState } from 'react'; +import { StyleSheet, View, Text, TouchableOpacity, Alert } from 'react-native'; +import { Table, TableWrapper, Row, Col, Rows } from 'react-native-tabeller'; + +export const ExampleFive: React.FC = () => { + const alertIndex = (value: string): void => { + Alert.alert(`This is column ${value}`); + }; + const elementButton = (value: string): React.ReactElement => ( + alertIndex(value)}> + + button + + + ); + + const [tableHead] = useState([ + '', + elementButton('1'), + elementButton('2'), + elementButton('3'), + ]); + const [sideHead] = useState(['H1', 'H2']); + const [rowTitles] = useState([ + 'Title', + 'Title2', + 'Title3', + 'Title4', + ]); + + // Table data - matching the structure in the screenshot + const [tableData] = useState([ + ['a', '1', 'a'], + ['b', '2', 'b'], + ['c', '3', 'c'], + ['d', '4', 'd'], + ]); + + return ( + + + {/* table header row with buttons */} + + + {/* left column with H1, H2 */} + + + {/* row titles column */} + + {/* content cells */} + + + +
+
+ ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + paddingTop: 30, + backgroundColor: '#fff', + }, + header: { + height: 40, + backgroundColor: '#fff', + }, + headerText: { + textAlign: 'center', + fontWeight: '500', + }, + wrapper: { + flexDirection: 'row', + }, + sideHeader: { + width: 60, + backgroundColor: '#C6F3E0', + }, + sideHeaderText: { + textAlign: 'center', + fontWeight: '500', + }, + tableContentWrapper: { + flex: 1, + flexDirection: 'row', + }, + title: { + width: 80, + backgroundColor: '#f6f8fa', + }, + titleText: { + textAlign: 'left', + paddingLeft: 5, + }, + row: { + height: 30, + }, + text: { + textAlign: 'center', + }, + btn: { + width: 58, + height: 18, + backgroundColor: '#c8e1ff', + borderRadius: 2, + alignSelf: 'center', + }, + btnText: { + textAlign: 'center', + fontSize: 12, + }, +}); + +export default ExampleFive; diff --git a/example/src/ExampleFour.tsx b/example/src/ExampleFour.tsx new file mode 100644 index 0000000..f2e296b --- /dev/null +++ b/example/src/ExampleFour.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import { StyleSheet, View, Text, TouchableOpacity, Alert } from 'react-native'; +import { Table, TableWrapper, Row, Cell } from 'react-native-tabeller'; + +type TableDataType = string[][]; + +export const ExampleFour: React.FC = () => { + const [tableHead] = useState(['Head', 'Head2', 'Head3', 'Head4']); + const [tableData] = useState([ + ['1', '2', '3', '4'], + ['a', 'b', 'c', 'd'], + ['1', '2', '3', '4'], + ['a', 'b', 'c', 'd'], + ]); + + const alertIndex = (index: number): void => { + Alert.alert(`This is row ${index + 1}`); + }; + + const element = (data: any, index: number): React.ReactElement => ( + alertIndex(index)}> + + button + + + ); + + return ( + + + + {tableData.map((rowData, index) => ( + + {rowData.map((cellData, cellIndex) => ( + + ))} + + ))} +
+
+ ); +}; + +const styles = StyleSheet.create({ + container: { flex: 1, padding: 16, paddingTop: 30, backgroundColor: '#fff' }, + head: { height: 40, backgroundColor: '#C6F3E0' }, + text: { margin: 8 }, + row: { flexDirection: 'row', backgroundColor: '#FFF1C1' }, + btn: { width: 58, height: 18, backgroundColor: '#78B7BB', borderRadius: 2 }, + btnText: { textAlign: 'center', color: '#fff' }, +}); + +export default ExampleFour; diff --git a/example/src/ExampleThree.tsx b/example/src/ExampleThree.tsx new file mode 100644 index 0000000..42f2e30 --- /dev/null +++ b/example/src/ExampleThree.tsx @@ -0,0 +1,76 @@ +import { Table, TableWrapper, Row, Rows, Col } from 'react-native-tabeller'; +import { View, StyleSheet } from 'react-native'; + +export const ExampleThree = () => { + const tableHead = ['', 'Head1', 'Head2', 'Head3']; + const tableTitle = ['Title1', 'Title2', 'Title3']; + const tableData = [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ]; + + const onCellPress = (data: any) => { + console.log(`Cell pressed: ${data}`); + }; + + return ( + + + + + + + +
+
+ ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + paddingTop: 30, + backgroundColor: '#fff', + }, + head: { + height: 44, + backgroundColor: '#C6F3E0', + }, + text: { + textAlign: 'center', + padding: 5, + }, + headText: { + textAlign: 'center', + fontWeight: 'bold', + }, + row: { + height: 40, + backgroundColor: '#fff', + }, + wrapper: { + flexDirection: 'row', + }, + title: { + flex: 1, + backgroundColor: '#C6F3E0', + }, + titleText: { + textAlign: 'left', + marginLeft: 6, + fontWeight: '600', + }, +}); diff --git a/example/src/ScrollableExample.tsx b/example/src/ScrollableExample.tsx new file mode 100644 index 0000000..93e158a --- /dev/null +++ b/example/src/ScrollableExample.tsx @@ -0,0 +1,82 @@ +import { Table, Row, Rows } from 'react-native-tabeller'; +import { View, StyleSheet, ScrollView } from 'react-native'; + +export const ScrollableExample = () => { + const tableHead = [ + 'Head', + 'Head2', + 'Head3', + 'Head4', + 'Head5', + 'Head6', + 'Head7', + 'Head8', + ]; + const widthArr = [40, 69, 80, 100, 120, 140, 160, 180]; + + // generates large table data + const tableData = []; + for (let i = 0; i < 30; i += 1) { + const rowData = []; + for (let j = 0; j < 8; j += 1) { + rowData.push(`${i}${j}`); + } + tableData.push(rowData); + } + + return ( + + + + + +
+ + + +
+
+
+
+
+ ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + paddingTop: 30, + height: 400, + backgroundColor: '#fff', + }, + header: { + height: 50, + backgroundColor: '#C6F3E0', + }, + headerText: { + textAlign: 'center', + fontWeight: 'bold', + }, + text: { + textAlign: 'center', + padding: 5, + }, + dataWrapper: { + marginTop: -1, + }, + row: { + height: 40, + backgroundColor: '#fff', + }, +}); diff --git a/example/src/StickyColumnExample.tsx b/example/src/StickyColumnExample.tsx new file mode 100644 index 0000000..8393918 --- /dev/null +++ b/example/src/StickyColumnExample.tsx @@ -0,0 +1,41 @@ +import { StyleSheet } from 'react-native'; +import { StickyTable } from 'react-native-tabeller'; + +export const StickyTableExample = () => { + // first column will be sticky + const tableData = [ + ['Header', 'Col 1', 'Col 2', 'Col 3', 'Col 4'], + ['Row 1', 'Data 1-1', 'Data 1-2', 'Data 1-3', 'Data 1-4'], + ['Row 2', 'Data 2-1', 'Data 2-2', 'Data 2-3', 'Data 2-4'], + ['Row 3', 'Data 3-1', 'Data 3-2', 'Data 3-3', 'Data 3-4'], + ]; + + const stickyColumnWidth: number = 120; + const columnWidths: number[] = [120, 120, 120, 120]; + + return ( + + ); +}; + +const styles = StyleSheet.create({ + header: { + backgroundColor: '#C6F3E0', + }, + headerText: { + fontWeight: 'bold', + textAlign: 'center', + }, + text: { + textAlign: 'center', + padding: 5, + }, +}); diff --git a/package.json b/package.json index b5b5f46..7169917 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-native-tabeller", "version": "0.1.0", - "description": "React Native table with reanimated and gestures.�", + "description": "React Native table with reanimated and gestures.", "source": "./src/index.tsx", "main": "./lib/commonjs/index.js", "module": "./lib/module/index.js", @@ -48,14 +48,16 @@ "keywords": [ "react-native", "ios", - "android" + "android", + "react-native-table", + "tabeller" ], "repository": { "type": "git", "url": "git+https://git.sheetjs.com/asadbek064/react-native-tabeller.git.git" }, "author": "Asadbek Karimov (https://asadk.dev)", - "license": "MIT", + "license": "ALv2", "bugs": { "url": "https://git.sheetjs.com/asadbek064/react-native-tabeller.git/issues" }, diff --git a/react-native-tabeller-0.1.0.tgz b/react-native-tabeller-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..fab8eaabbf1fd78635950dd07e7fa67d833be1eb GIT binary patch literal 25006 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 = ({ + data, + width, + height, + flex, + style, + textStyle, + borderStyle, + children, + onPress, + cellContainerProps = {}, + ...props +}) => { + const textDom = children ?? ( + + {data} + + ); + + const borderTopWidth = borderStyle?.borderWidth ?? 0; + const borderRightWidth = borderTopWidth; + const borderColor = borderStyle?.borderColor ?? '#000'; + + const composedStyles = useMemo(() => { + const styles: ViewStyle = {}; + if (width) styles.width = width; + if (height) styles.height = height; + if (flex) styles.flex = flex; + if (!width && !flex && !height && !style) styles.flex = 1; + + return styles; + }, [width, height, flex, style]); + + return ( + + onPress(data) : undefined} + disabled={!onPress} + > + {textDom} + + + ); +}; + +const styles = StyleSheet.create({ + cell: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + touchableContainer: { + flex: 1, + width: '100%', + justifyContent: 'center', + alignItems: 'center', + }, +}); diff --git a/src/components/cols.tsx b/src/components/cols.tsx new file mode 100644 index 0000000..4a6e40d --- /dev/null +++ b/src/components/cols.tsx @@ -0,0 +1,83 @@ +import type { FC } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Cell } from './cell'; +import type { ColProps, ColsProps } from '../types'; +import { sum } from '../util'; + +export const Col: FC = ({ + data, + style, + width, + heightArr, + flex, + textStyle, + borderStyle, + ...props +}) => { + if (!data) return null; + + return ( + + {data.map((item, i) => { + const height = heightArr?.[i]; + return ( + + ); + })} + + ); +}; + +export const Cols: FC = ({ + data, + style, + widthArr, + heightArr, + flexArr, + textStyle, + borderStyle, + ...props +}) => { + if (!data) return null; + + const width = widthArr ? sum(widthArr) : 0; + + return ( + + {data.map((item, i) => { + const flex = flexArr?.[i]; + const wth = widthArr?.[i]; + return ( + + ); + })} + + ); +}; + +const styles = StyleSheet.create({ + cols: { flexDirection: 'row' }, +}); diff --git a/src/components/rows.tsx b/src/components/rows.tsx new file mode 100644 index 0000000..700298f --- /dev/null +++ b/src/components/rows.tsx @@ -0,0 +1,116 @@ +import { useMemo } from 'react'; +import type { FC } from 'react'; +import { View, StyleSheet } from 'react-native'; +import type { ViewStyle } from 'react-native'; + +import { Cell } from './cell'; +import type { RowProps, RowsProps } from '../types'; +import { sum } from '../util'; + +/** Row component - Renders a single row of cells*/ +export const Row: FC = ({ + data, + style, + widthArr, + height, + flexArr, + textStyle, + borderStyle, + onPress, + cellTextStyle, + ...props +}) => { + // calc total width based on width array + const totalWidth = widthArr ? sum(widthArr) : 0; + + // calc row styles based on props + const rowStyle = useMemo((): ViewStyle => { + const styles: ViewStyle = {}; + if (totalWidth) styles.width = totalWidth; + if (height) styles.height = height; + return styles; + }, [totalWidth, height]); + + if (!data || !data.length) return null; + + return ( + + {data.map((item, index) => { + const cellFlex = flexArr?.[index]; + const cellWidth = widthArr?.[index]; + const customTextStyle = cellTextStyle + ? cellTextStyle(item, index) + : undefined; + + return ( + + ); + })} + + ); +}; + +/** Rows component - Renders multiple rows*/ +export const Rows: FC = ({ + data, + style, + widthArr, + heightArr, + flexArr, + textStyle, + borderStyle, + ...props +}) => { + // calc total flex and width + const totalFlex = flexArr ? sum(flexArr) : 0; + const totalWidth = widthArr ? sum(widthArr) : 0; + + // calc container styles + const containerStyle = useMemo((): ViewStyle => { + const styles: ViewStyle = {}; + if (totalFlex) styles.flex = totalFlex; + if (totalWidth) styles.width = totalWidth; + return styles; + }, [totalFlex, totalWidth]); + + if (!data || !data.length) return null; + + return ( + + {data.map((rowData, index) => { + const rowHeight = heightArr?.[index]; + + return ( + + ); + })} + + ); +}; + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + overflow: 'hidden', + }, +}); diff --git a/src/components/sticky-table.tsx b/src/components/sticky-table.tsx new file mode 100644 index 0000000..d185c09 --- /dev/null +++ b/src/components/sticky-table.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { View, ScrollView, StyleSheet } from 'react-native'; +import { Cell } from './cell'; +import type { StickyTableProps } from '../types'; + +/** StickyTable component - Table with sticky first column */ +export const StickyTable: React.FC = ({ + data, + stickyColumnWidth, + columnWidths = [], + style, + cellStyle, + textStyle, + headerStyle, + headerTextStyle, + borderStyle = { borderWidth: 1, borderColor: '#000' }, +}) => { + if (!data || data.length === 0) return null; + + // calc content width + const contentWidth = columnWidths.reduce((sum, width) => sum + width, 0); + + return ( + + {/* sticky Column */} + + {data.map((row, rowIndex) => { + const isHeader = rowIndex === 0; + return ( + + + + ); + })} + + + {/* scrollable Content */} + + + {data.map((row, rowIndex) => { + const isHeader = rowIndex === 0; + // skip first column as it's already in the sticky part + const rowData = row.slice(1); + + return ( + + {rowData.map((cellData, cellIndex) => { + const colWidth = columnWidths[cellIndex]; + + return ( + + + + ); + })} + + ); + })} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + }, + stickyColumn: { + zIndex: 1, + backgroundColor: 'white', + elevation: 3, + }, + scrollView: { + flex: 1, + }, + row: { + flexDirection: 'row', + }, + cell: { + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'white', + overflow: 'hidden', + }, +}); diff --git a/src/components/table.tsx b/src/components/table.tsx new file mode 100644 index 0000000..cf02f7a --- /dev/null +++ b/src/components/table.tsx @@ -0,0 +1,82 @@ +import type { FC } from 'react'; +import React from 'react'; +import { View, StyleSheet } from 'react-native'; + +import type { TableProps, TableWrapperProps } from '../types'; + +export const Table: FC = ({ style, borderStyle, children }) => { + const borderLeftWidth = borderStyle?.borderWidth ?? 0; + const borderBottomWidth = borderLeftWidth; + const borderColor = borderStyle?.borderColor ?? '#000'; + + const renderChildren = () => + React.Children.map(children, (child) => { + if (!React.isValidElement(child)) return child; + + // check if we should add the border style | skip known type names + const elementType = child.type as any; + const isScrollView = + elementType?.displayName === 'ScrollView' || + elementType?.name === 'ScrollView'; + + if (borderStyle && !isScrollView) { + return React.cloneElement(child, { + borderStyle, + ...child.props, + }); + } + + return child; + }); + + return ( + + {renderChildren()} + + ); +}; + +export const TableWrapper: FC = ({ + style, + borderStyle, + children, +}) => { + const renderChildren = () => + React.Children.map(children, (child) => { + if (!React.isValidElement(child)) return child; + + if (borderStyle) { + return React.cloneElement(child, { + borderStyle, + ...child.props, + }); + } + + return child; + }); + + return ( + + {renderChildren()} + + ); +}; + +const styles = StyleSheet.create({ + table: { + overflow: 'hidden', + }, + wrapper: { + flex: 1, + }, +}); diff --git a/src/index.tsx b/src/index.tsx index 14289fe..13843e6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,3 +1,26 @@ -export function multiply(a: number, b: number): number { - return a * b; -} +/** + * Tabeller - Table Components for React Native + * A set of lightweight and customizable components to build tables + * in React Native apps. Includes rows, columns, cells, and table wrappers. + */ + +// components +export { Cell } from './components/cell'; +export { Row, Rows } from './components/rows'; +export { Col, Cols } from './components/cols'; +export { Table, TableWrapper } from './components/table'; +export { StickyTable } from './components/sticky-table'; + +// types +export type { + BorderStyle, + BaseTableProps, + CellProps, + RowProps, + RowsProps, + ColProps, + ColsProps, + TableProps, + TableWrapperProps, + StickyTableProps, +} from './types'; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..a54c33f --- /dev/null +++ b/src/types.ts @@ -0,0 +1,130 @@ +import type { ReactNode } from 'react'; +import type { ViewStyle, TextStyle, StyleProp, ViewProps } from 'react-native'; + +/** border style configuration for table elements */ +export interface BorderStyle { + borderColor?: string; // defaults to #000 + borderWidth?: number; // defaults to 0 +} + +/** base props shared across table components */ +export interface BaseTableProps { + /** custom style for the component */ + style?: StyleProp; + /** border style configuration */ + borderStyle?: BorderStyle; +} + +/** props for the Cell component */ +export interface CellProps extends BaseTableProps { + /** cell content */ + data?: string | number | null; + /** cell width (pixels) */ + width?: number; + /** cell height (pixels) */ + height?: number; + /** flex value for the cell */ + flex?: number; + /** text style for cell content */ + textStyle?: StyleProp; + /** props passed to the cell container */ + cellContainerProps?: ViewProps; + /** callback when cell is pressed */ + onPress?: (data: any) => void; + /** children to render inside the cell */ + children?: ReactNode; +} + +/** props for the Row component */ +export interface RowProps extends BaseTableProps { + /** array of data items for each cell in the row */ + data: Array; + /** array of widths for each cell */ + widthArr?: number[]; + /** height for the entire row */ + height?: number; + /** array of flex values for each cell in the row */ + flexArr?: number[]; + /** text style applied to all cell in the row */ + textStyle?: StyleProp; + /** function to generate custom text styles for individual cell */ + cellTextStyle?: (item: any, index: number) => StyleProp; + /** callback when a cell is pressed */ + onPress?: (item: any) => void; +} + +/** props for the Rows componen */ +export interface RowsProps extends BaseTableProps { + /** 2D array of data for rows and cells */ + data: Array>; + /** array of widths for each column */ + widthArr?: number[]; + /** array of heights for each cell in a column */ + heightArr?: number[]; + /** array of flex values for each column */ + flexArr?: number[]; + /** text style applied to all cells */ + textStyle?: StyleProp; + /** callback when a cell is pressed */ + onPress?: (item: any) => void; +} + +/** props for the Col component */ +export interface ColProps extends BaseTableProps { + /** array of data items for each cell in the column */ + data: Array; + /** width for the entire column */ + width?: number; + /** array of heights for each cell */ + heightArr?: number[]; + /** flex value for the column */ + flex?: number; + /** text style applied to all cells in the column */ + textStyle?: StyleProp; +} + +/** props for the Cols component */ +export interface ColsProps extends BaseTableProps { + /** 2D array of data for columns and cells */ + data: Array>; + /** array of widths for each column */ + widthArr?: number[]; + /** array of heights for each cell in a column */ + heightArr?: number[]; + /** array of flex values for each column */ + flexArr?: number[]; + /** text style applied to all cells */ + textStyle?: StyleProp; +} + +/** props for the Table component */ +export interface TableProps extends BaseTableProps { + children: ReactNode; // Table content +} + +/** props for the TableWrapper component */ +export interface TableWrapperProps extends BaseTableProps { + children: ReactNode; // TableWrapper content +} + +/** props for the StickyTable component */ +export interface StickyTableProps { + /** full table data including first column */ + data: (string | number | null)[][]; + /** width of the sticky column */ + stickyColumnWidth: number; + /** widths for non-sticky columns */ + columnWidths?: number[]; + /** style for the container */ + style?: StyleProp; + /** style for cells */ + cellStyle?: StyleProp; + /** text style for cell content */ + textStyle?: StyleProp; + /** style for header row */ + headerStyle?: StyleProp; + /** text style for header cells */ + headerTextStyle?: StyleProp; + /** border style */ + borderStyle?: BorderStyle; +} diff --git a/src/util/index.ts b/src/util/index.ts new file mode 100644 index 0000000..b87d83d --- /dev/null +++ b/src/util/index.ts @@ -0,0 +1,9 @@ +/** + * sum all numbers in an array + * @param arr - arr of numbers + * @returns sum of all numbers + */ +export const sum = (arr: number[]): number => { + if (!arr || !arr.length) return 0; + return arr.reduce((acc, n) => acc + n, 0); +};