So basically I am trying to make a table with arbitrarily typed inputs (i.e. select, number, text) that allow the user to add/edit/remove rows of the tables with said inputs:
When you click the green check a table row is supposed to be added with the data inputted by the user, the added row has an edit button (which will remove the row from the table and set the inputs to the rows values so that they can be edited and readded) and a remove button.
My issue is because I am trying to give arbitrary inputs and allow the user to specify them (they all have a handle change function from an IHandleChange interface) in the tsx index file I still need reference to the Table components functions to pass down to the child components the user chooses to put in the Table. Right now I use a default function:
const default_function = () => {}
and then expect my render method in the Table to override the props when it calls React.cloneElement with the new prop-set from the Table. However as can be seen in the component window my add, edit, handleChanges, and remove functions were never passed correctly to my TableBody and therefore its child components.
Is there something major I am doing wrong or am I just not passing the props correctly when I cloneElement in the Table::render function?
index.tsx:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import "bootstrap/dist/css/bootstrap.min.css"
//import * as FormInputs from './FormInputs';
import {Table, HeaderRow, TableBody, InputRow} from './SmartTable';
import * as FormInputs from './FormInputs';
const defaultFunc = () => {}
ReactDOM.render(
<Table rowList={[]}>
<HeaderRow tableHeaders={[{title:"select"},
{title:"text"},
{title:"date"},
{title:"number"}]}/>
<TableBody
rowList={[]}
edit={defaultFunc}
remove={defaultFunc}
add={defaultFunc}
handleChange={defaultFunc}
>
<InputRow add={defaultFunc} handleChange={defaultFunc}>
<FormInputs.SelectInput selectOptions={{name: "select", value: ""}} options={[{name:"", value:""}, {name: "one", value: "1"}, {name: "two", value: "2"}]} className="form-control"/>
<FormInputs.TextInput options={{name: "text_input", value: ""}} placeholder="enter text here" className="form-control"/>
<FormInputs.DateInput options={{name: "date_input", value: ""}} className="form-control" min="2019-09-02" max={new Date().toISOString().split('T')[0]}/>
<FormInputs.NumberInput options={{name:"number", value: ""}} className="form-control" min="-10" max="10" step="2"/>
</InputRow>
</TableBody>
</Table>, document.getElementById('root'));
Table.tsx
/*********************************************************************************************/
interface IFilterable {
title: string;
filterable?: boolean;
//this will need a filter type field in the future
}
interface IHeaderRowProps {
tableHeaders: IFilterable[];
}
export const HeaderRow: React.FC<IHeaderRowProps> = (props: IHeaderRowProps) => {
return (
<thead>
<tr>
{props.tableHeaders.map(header => header.filterable
? <th key={header.title}>{header.title}<FilterButton/></th>
: <th key={header.title}>{header.title}</th>)}
</tr>
</thead>
);
}
/*********************************************************************************************/
/*********************************************************************************************/
interface IInputRowProps extends FormInputs.ChangeHandled {
add(): void;
}
export class InputRow extends React.Component<IInputRowProps> {
private childRefs: any[] = [];
constructor(props: any) {
super(props);
if (this.props.children) {
[this.props.children].forEach((child) => {
this.childRefs.push(React.createRef());
})
}
}
handleAddRow = (event: any) => {
this.props.add();
}
handleChange = (event: any) => {
if (this.props.handleChange) {
this.props.handleChange(event);
}
}
render () {
if (this.props.children !== null && typeof this.props.children !== 'undefined') {
return (
<tr>
{React.Children.map(this.props.children, (child: any, index) => <td>{React.cloneElement(child, {handleChange: this.handleChange, ref: this.childRefs[index]})}</td>)}
<td>
<button type="button" className="btn btn-success" onClick={this.handleAddRow}>
<FontAwesomeIcon icon={faCheck}/>
</button>
</td>
</tr>
);
} else return null;
}
}
/*********************************************************************************************/
/*********************************************************************************************/
interface ITableDataProps extends FormInputs.IFormOptions {
text: string;
}
export const TableData: React.FC<ITableDataProps> = (props: ITableDataProps) => {
return (
<td>
{props.text}
{<FormInputs.HiddenInput name={props.name} value={props.value}/>}
</td>
);
}
/*********************************************************************************************/
/*********************************************************************************************/
interface ITableRowProps {
values: string[];
edit(values: string[]): void;
remove(values: string[]): void;
}
export class TableRow extends React.Component<ITableRowProps> {
handleEdit = () => {
this.props.edit(this.props.values);
}
handleRemove = () => {
this.props.remove(this.props.values);
}
render () {
return (
<tr>
{this.props.values.map((val, index) => <TableData key={uid(index) + "-d"} text={val} name={`td${index}`} value={val}/>)}
<td>
<div className="btn-group">
<button type="button" className="btn btn-warning" onClick={this.handleEdit}><FontAwesomeIcon icon={faEdit}/></button>
<button type="button" className="btn btn-danger" onClick={this.handleRemove}><FontAwesomeIcon icon={faTimes}/></button>
</div>
</td>
</tr>
);
}
}
/*********************************************************************************************/
/*********************************************************************************************/
interface ITableBodyProps {
rowList: string[][];
edit(values: string[]): void;
remove(values: string[]): void;
add(): void;
handleChange(event: any): void;
}
export class TableBody extends React.Component<ITableBodyProps> {
render () {
return (
<tbody>
{React.cloneElement(this.props.children as React.ReactElement<any>, {add: this.props.add, handleChange: this.props.handleChange})}
{this.props.rowList.map((row, index) => <TableRow key={uid(index) + "-r"} values={row} edit={this.props.edit} remove={this.props.remove}/>)}
</tbody>
);
}
}
/*********************************************************************************************/
/*********************************************************************************************/
interface ITableProps {
rowList?: string[][];
}
interface ITableState {
rowList: string[][];
[key: string]: any;
}
export class Table extends React.Component<ITableProps, ITableState> {
constructor (props: any) {
super(props);
this.state = {
rowList: typeof this.props.rowList === 'undefined' ? [] : this.props.rowList
};
}
handleInputRowChange = (event: any) => {
this.setState({
[event.target.name]: event.target.value
});
}
handleRemoveRow = (values: string[]) => {
this.setState({
rowList: this.state.rowList.slice().filter(r => r !== values)
});
}
handleEditRow = (values: string[]) => {
let i = 0;
let obj: any = {};
Object.keys(this.state).forEach((key) => {
if (key !== 'rowList') {
obj[key] = values[i++];
}
});
this.setState(obj);
this.handleRemoveRow(values);
}
handleAddRow = () => {
let values: string[] = [];
let obj: any = {};
Object.keys(this.state).forEach((key) => {
if (key !== 'rowList') {
values.push(this.state[key]);
obj[key] = "";
}
});
var newList = this.state.rowList.slice().concat([values]);
this.setState({
rowList: newList,
...obj
});
}
render () {
let children = React.Children.toArray(this.props.children);
return (
<table className="table">
{React.cloneElement(children[0] as React.ReactElement<any>)}
{React.cloneElement(children[1] as React.ReactElement<any>, {rowList: this.props.rowList, edit: this.handleEditRow, remove: this.handleRemoveRow, add: this.handleAddRow, handleChange: this.handleInputRowChange})}
</table>
);
}
}
/*********************************************************************************************/
FormInputs.tsx (just in case you need to run it):
import * as React from 'react';
import "bootstrap/dist/css/bootstrap.min.css"
interface IStyleOptions {
className?: string;
}
export interface IFormOptions {
name?: string;
value?: string;
}
export interface ChangeHandled {
handleChange?(event: any): void;
}
/*********************************************************************************************/
interface ISelectInputProps extends IStyleOptions, ChangeHandled {
options: IFormOptions[];
selectOptions: IFormOptions;
}
export class SelectInput extends React.Component<ISelectInputProps> {
render () {
return (
<select className={this.props.className}
name={this.props.selectOptions.name}
value={typeof this.props.selectOptions.value === 'undefined' ? '' : this.props.selectOptions.value}
onChange={this.props.handleChange}>
{this.props.options.map(opt => <option key={opt.name} value={opt.value}>{opt.name}</option>)}
</select>
);
}
}
/*********************************************************************************************/
/*********************************************************************************************/
interface ITextInputProps extends IStyleOptions, ChangeHandled {
options: IFormOptions;
placeholder?: string;
}
export const TextInput: React.FC<ITextInputProps> = (props) => {
return (
<input
type="text"
className={props.className}
name={props.options.name}
value={typeof props.options.value === 'undefined' ? '' : props.options.value}
placeholder={props.placeholder}
onChange={props.handleChange}
/>
);
}
/*********************************************************************************************/
/*********************************************************************************************/
interface IDateInputProps extends IStyleOptions, ChangeHandled {
options: IFormOptions;
min?: string;
max?: string;
}
export const DateInput: React.FC<IDateInputProps> = (props) => {
return (
<input
type="date"
className={props.className}
name={props.options.name}
value={typeof props.options.value === 'undefined' ? '' : props.options.value}
defaultValue={props.options.value}
min={props.min}
max={props.max}
onChange={props.handleChange}
/>
);
}
/*********************************************************************************************/
/*********************************************************************************************/
export const DateTimeInput: React.FC<IDateInputProps> = (props) => {
return (
<input
type="datetime-local"
className={props.className}
name={props.options.name}
value={typeof props.options.value === 'undefined' ? '' : props.options.value}
defaultValue={props.options.value}
min={props.min}
max={props.max}
onChange={props.handleChange}
/>
);
}
/*********************************************************************************************/
/*********************************************************************************************/
interface ICheckboxInputProps extends IStyleOptions, ChangeHandled {
options: IFormOptions;
disabled?: boolean;
}
export const CheckboxInput: React.FC<ICheckboxInputProps> = (props) => {
return (
<input
type="checkbox"
className={props.className}
name={props.options.name}
value={typeof props.options.value === 'undefined' ? '' : props.options.value}
disabled={props.disabled}
onChange={props.handleChange}
/>
);
}
/*********************************************************************************************/
/*********************************************************************************************/
interface INumberInputProps extends IStyleOptions, ChangeHandled {
options: IFormOptions;
min?: string;
max?: string;
step?: string;
}
export const NumberInput: React.FC<INumberInputProps> = (props) => {
return (
<input
type="number"
className={props.className}
name={props.options.name}
value={typeof props.options.value === 'undefined' ? '' : props.options.value}
defaultValue={props.options.value}
min={props.min}
max={props.max}
step={props.step}
onChange={props.handleChange}
/>
);
}
/*********************************************************************************************/
/*********************************************************************************************/
export const HiddenInput: React.FC<IFormOptions> = (props) => {
return (
<input
type="hidden"
name={props.name}
value={props.value}
/>
);
}
/*********************************************************************************************/