Skip to content

Latest commit

 

History

History
executable file
·
1501 lines (1162 loc) · 57.3 KB

README.md

File metadata and controls

executable file
·
1501 lines (1162 loc) · 57.3 KB

Dynamic Fields

General

A simple usage, no default value.

Component <DynamicFields /> is not re-rendering.

import React from "react";
import DynamicFields from 'funda-ui/DynamicFields';
import Input from 'funda-ui/Input';
import Select from 'funda-ui/Select';

// component styles
import 'funda-ui/Select/index.css';


type DynamicFieldsValueProps = {
    init: React.ReactNode[];
    tmpl: React.ReactNode;
};


export default () => {

    const LABEL_WIDTH = '200px';

    // For some operations to initialize controls, you can use querySelectorAll to query nodes.
    /*
    setTimeout(() => {
        [].slice.call(document.querySelectorAll(`[data-xxx-control]`)).forEach((field) => {
            field.style.background = 'red';
            //...
        });
    }, 500);
    */


    //initialize default value
    const tmpl = (val: any, init: boolean = true) => {
        let data: any = null;
        if (init) {
            const {...rest} = val;
            data = rest;
        } else {
            data = {index: Math.random()};
        }

        const currentRowNum = val !== null ? val.index : undefined;

        return <React.Fragment key={'tmpl-' + data.index}>
                {/* ///////////// */}
                <div className="row align-items-center">
                    <div className="text-end" style={{ width: LABEL_WIDTH }}>
                        User Name
                    </div>
                    <div className="col">
                        {/* CONTROL */}
                        <Input
                            wrapperClassName="position-relative"
                            value={data.user_name}
                            tabIndex={-1}
                            name="user_name[]"
                        />
                        {/* /CONTROL */}
                    </div>
             
                    <div className="text-end" style={{ width: LABEL_WIDTH }}>
                        Role(ID)
                    </div>
                    <div className="col">
                        {/* CONTROL */}
                        <Select
                            wrapperClassName="position-relative"
                            value={data.role_id}
                            name="role_id[]"
                            placeholder="Select"
                            options={`
                            [
                                {"label": "Option 1","value": "value-1","queryString": "option1"},
                                {"label": "Option 2","value": "value-2","queryString": "option2"},
                                {"label": "Option 3","value": "value-3","queryString": "option3"}
                            ]  
                            `}
                            onChange={(e: any, e2: any, val: any) => {
                                const targetId = e2.dataset.id;
                                [].slice.call(document.querySelectorAll(`[name="role_name[]"]`)).forEach((node: any) => {
                                    if (node.id === targetId) {
                                        node.value = val.label;
                                    }
                                });
                            }}
                        />
                        {/* /CONTROL */}
                    </div>
                

                    <div style={{ width: '40px' }}></div>
                </div>  

                <hr />

            {/* ///////////// */}
        </React.Fragment>
    };


    return (
        <>
            <DynamicFields
                data={{
                    init: [],
                    tmpl: tmpl(null, false)
                } as DynamicFieldsValueProps}
                maxFields="10"
                confirmText="Are you sure?"
                iconAdd={<><div className="mt-1"><svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM16 12.75H12.75V16C12.75 16.41 12.41 16.75 12 16.75C11.59 16.75 11.25 16.41 11.25 16V12.75H8C7.59 12.75 7.25 12.41 7.25 12C7.25 11.59 7.59 11.25 8 11.25H11.25V8C11.25 7.59 11.59 7.25 12 7.25C12.41 7.25 12.75 7.59 12.75 8V11.25H16C16.41 11.25 16.75 11.59 16.75 12C16.75 12.41 16.41 12.75 16 12.75Z" fill="#000" /></svg></div></>}
                iconRemove={<><div className="position-absolute top-0 end-0 mt-2 mx-1"><svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path fillRule="evenodd" clipRule="evenodd" d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10ZM8 11a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2H8Z" fill="#f00" /></svg></div></>}
                onAdd={(items: HTMLDivElement[], rootNode: HTMLDivElement, btnNode: HTMLAnchorElement, perRowDomClassName: string) => {
                    console.log('add', items);
                    // do something

                }}
                onRemove={(items: HTMLDivElement[], key: number | string, index: number | string, rootNode: HTMLDivElement, btnNode: HTMLAnchorElement, perRowDomClassName: string) => {
                    console.log('remove', items, key, index);
                }}
            />


        </>
    );
}

No spacing

import React from "react";
import DynamicFields from 'funda-ui/DynamicFields';

export default () => {


    return (
        <>

            <DynamicFields
                ...
                wrapperClassName="position-relative"
                ...
            />

             <DynamicFields
                ...
                wrapperClassName=""
                ...
            />

        </>
    );
}

Specify a custom HTML attribute for each row

import React from "react";
import DynamicFields from 'funda-ui/DynamicFields';

export default () => {


    return (
        <>

            <DynamicFields
                ...
                data-my-attr1="1"
                data-my-attr2="2"
                style={...}
                ...
            />

        </>
    );
}

Asynchronous Usage (Use Vanilla JS to manipulate Dom elements)

Component <DynamicFields /> is not re-rendering.

import React, { useState, useEffect } from "react";
import DynamicFields from 'funda-ui/DynamicFields';

import Input from 'funda-ui/Input';
import Select from 'funda-ui/Select';
import Switch from 'funda-ui/Switch';
import CascadingSelectE2E from 'funda-ui/CascadingSelectE2E';

// component styles
import 'funda-ui/Select/index.css';
import 'funda-ui/CascadingSelectE2E/index.css';




type DynamicFieldsValueProps = {
    init: React.ReactNode[];
    tmpl: React.ReactNode;
};


const myData: any[] = [
    {
        user_name: 'Test 1',
        role_id: 'value-2',
        role_name: 'Name 1',
        role_cat: '',
        role_disabled: false
    },
    {
        user_name: 'Test 2',
        role_id: 'value-3',
        role_name: 'Name 2',
        role_cat: 'Title 2[2],Title 5[5]',
        role_disabled: true
    }
];



class DataService {
    
    // `getListFirst()` must be a Promise Object
    async getListFirst(searchStr = '', limit = 0, otherParam = '') {


        const demoData = [
            {
                "parent_id": 0,
                "item_code": 1,
                "item_name": "Title 1",
                "item_type": "web"
            },
            {
                "parent_id": 0,
                "item_code": 2,
                "item_name": "Title 2",
                "item_type": "dev"
            }
        ];   

        return {
            code: 0,
            message: 'OK',
            data: demoData
        };
    }


    // `getListSecond()` must be a Promise Object
    async getListSecond(searchStr = '', limit = 0, parentId = 0) {

  
        const demoData = [
            {
                "parent_id": 1,
                "item_code": 3,
                "item_name": "Title 3",
                "item_type": "web/ui"
            },
            {
                "parent_id": 1,
                "item_code": 4,
                "item_name": "Title 4",
                "item_type": "web/ui"
            },
            {
                "parent_id": 2,
                "item_code": 5,
                "item_name": "Title 5",
                "item_type": "dev"
            }
        ];   

        const res = demoData.filter( item => {
            return item.parent_id == parentId;
        } );

        return {
            code: 0,
            message: 'OK',
            data: res
        };
    }


}



export default () => {

    const LABEL_WIDTH = '200px';
    const [dynamicFieldsValue, setDynamicFieldsValue] = useState<DynamicFieldsValueProps | null>(null);
    const [dynamicFieldsJsonValue, setDynamicFieldsJsonValue] = useState<any[]>([]);


    // For some operations to initialize controls, you can use querySelectorAll to query nodes.
    /*
    setTimeout(() => {
        [].slice.call(document.querySelectorAll(`[data-xxx-control]`)).forEach((field) => {
            field.style.background = 'red';
            //...
        });
    }, 500);
    */

    //initialize default value
    const tmpl = (val: any, init: boolean = true) => {
        let data: any = null;
        if (init) {
            const {...rest} = val;
            data = rest;
        } else {
            data = {index: Math.random()};
        }

        const currentRowNum = val !== null ? val.index : undefined;
        
        return <React.Fragment key={'tmpl-' + data.index}>
                {/* ///////////// */}
                <div className="row align-items-center">
                    <div className="text-end" style={{ width: LABEL_WIDTH }}>
                        User Name
                    </div>
                    <div className="col-auto">
                        {/* CONTROL */}
                        <Input
                            value={data.user_name}
                            tabIndex={-1}
                            name="user_name[]"
                        />
                        {/* /CONTROL */}
                    </div>
             
                    <div className="text-end" style={{ width: LABEL_WIDTH }}>
                        Role(ID)
                    </div>
                    <div className="col-auto">
                        {/* CONTROL */}
                        <div style={{width: '80%'}}>
                            <Select
                                value={data.role_id}
                                name="role_id[]"
                                data-id={init ? `role_name-dy-${data.index}` : `role_name-dy-%i%`}
                                placeholder="Select"
                                options={`
                                [
                                    {"label": "Option 1","value": "value-1","queryString": "option1"},
                                    {"label": "Option 2","value": "value-2","queryString": "option2"},
                                    {"label": "Option 3","value": "value-3","queryString": "option3"}
                                ]  
                                `}
                                onChange={(e: any, e2: any, val: any) => {
                                    const targetId = e2.dataset.id;
                                    [].slice.call(document.querySelectorAll(`[name="role_name[]"]`)).forEach((node: any) => {
                                        if (node.id === targetId) {
                                            node.value = val.label;
                                        }
                                    });
                                }}
                            />
                        </div>
                        {/* /CONTROL */}
                    </div>
                
                
                    <div className="text-end" style={{ width: LABEL_WIDTH }}>
                        Role Name
                    </div>
                    <div className="col-auto">
                        {/* CONTROL */}
                        <Input
                            value={data.role_name}
                            tabIndex={-1}
                            id={init ? `role_name-dy-${data.index}` : `role_name-dy-%i%`}
                            name="role_name[]"
                        />
                        {/* /CONTROL */}
                    </div>


                    <div className="text-end" style={{ width: LABEL_WIDTH }}>
                        Role Category
                    </div>
                    <div className="col">
                        {/* CONTROL */}
                        <CascadingSelectE2E
                            value={data.role_cat}
                            name="role_cat[]"
                            depth={103}
                            displayResult={true}
                            valueType="label"
                            placeholder="Select Category"
                            columnTitle={['Heading 1', 'Heading 2']}
                            loader={<><span>Loading...</span></>}
                            fetchArray={[
                                {
                                    "fetchFuncAsync": new DataService,
                                    "fetchFuncMethod": "getListFirst",
                                    "fetchFuncMethodParams": ['', 0, 1],
                                    "fetchCallback": (res) => {

                                        // prevent orginal data
                                        let placesMap: Record<string, unknown[]> = {};
                                        for (const val of res) {
                                            placesMap[val.item_code] = [val.item_name, val.item_type, val.item_code];
                                        }

                                        //
                                        const data: any[] = [];
                                        for (const key in placesMap) {
                                            data.push({
                                                id: key,
                                                queryId: placesMap[key][2],
                                                name: placesMap[key][0],
                                                type: placesMap[key][1]
                                            } as never);
                                        }

                                        return data;

                                    }
                                },
                                {
                                    "fetchFuncAsync": new DataService,
                                    "fetchFuncMethod": "getListSecond",
                                    "fetchFuncMethodParams": ['', 0, '$QUERY_ID'],
                                    "fetchCallback": (res) => {

                                        // prevent orginal data
                                        let placesMap: Record<string, unknown[]> = {};
                                        for (const val of res) {
                                            placesMap[val.item_code] = [val.item_name, val.item_type, val.item_code];
                                        }

                                        //
                                        const data: any[] = [];
                                        for (const key in placesMap) {
                                            data.push({
                                                id: key,
                                                queryId: placesMap[key][2],
                                                name: placesMap[key][0],
                                                type: placesMap[key][1]
                                            } as never);
                                        }

                                        return data;

                                    }
                                }
                            ]}
                            />
                            {/* /CONTROL */}
                    </div>

                    <div style={{ width: '80px' }}>
                        <Switch
                            checked={data.role_disabled}
                            name="role_disabled[]"
                            value="ok"
                        />

                    </div> 

                    <div style={{ width: '40px' }}></div>
                </div>  

                <hr />

            {/* ///////////// */}
        </React.Fragment>
    };

    useEffect(() => {


        //initialize JSON value
        setDynamicFieldsJsonValue(myData.map((item: any, index: number) => (
            {
                user_name: item.user_name,
                role_id: item.role_id,
                role_name: item.role_name,
                role_cat: item.role_cat,
                role_disabled: item.role_disabled
            }
        )));



        //initialize default value
        const initData = myData.map((item: any, index: number) => {
            const {...rest} = item;
            return tmpl({...rest, index});
        });

        const tmplData = tmpl(null, false); 

        setDynamicFieldsValue({
            init: initData,
            tmpl: tmplData
        });

    }, []);



    return (
        <>
            <DynamicFields
                key={JSON.stringify(dynamicFieldsJsonValue)}  // Trigger child component update when prop of parent changes
                data={dynamicFieldsValue}
                maxFields="10"
                confirmText="Are you sure?"
                iconAdd={<><div className="mt-1"><svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM16 12.75H12.75V16C12.75 16.41 12.41 16.75 12 16.75C11.59 16.75 11.25 16.41 11.25 16V12.75H8C7.59 12.75 7.25 12.41 7.25 12C7.25 11.59 7.59 11.25 8 11.25H11.25V8C11.25 7.59 11.59 7.25 12 7.25C12.41 7.25 12.75 7.59 12.75 8V11.25H16C16.41 11.25 16.75 11.59 16.75 12C16.75 12.41 16.41 12.75 16 12.75Z" fill="#000" /></svg></div></>}
                iconRemove={<><div className="position-absolute top-0 end-0 mt-2 mx-1"><svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path fillRule="evenodd" clipRule="evenodd" d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10ZM8 11a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2H8Z" fill="#f00" /></svg></div></>}
                onAdd={(items: HTMLDivElement[], rootNode: HTMLDivElement, btnNode: HTMLAnchorElement, perRowDomClassName: string) => {
                    console.log('add', items);
                
                    //update `data-id` and `id` attributes of control
                    items.forEach((node: any) => {
                        const keyIndex = node.dataset.key;
                        [].slice.call(node.querySelectorAll(`[name]`)).forEach((field: any) => {
                            if (typeof field.id !== 'undefined' ) field.id = field.id.replace('%i%', keyIndex);
                            if (typeof field.dataset.id !== 'undefined' ) field.dataset.id = field.dataset.id.replace('%i%', keyIndex);
                        });


                        // if using `<File />` component 
                        // ==> <label for="xxxx-%i%-yyyyy">
                        [].slice.call(node.querySelectorAll(`[data-label]`)).forEach((field: any) => {
                            if (field.getAttribute('for') !== null) field.setAttribute('for', field.getAttribute('for').replace('%i%', keyIndex));
                        });

                    });


                }}
                onRemove={(items: HTMLDivElement[], key: number | string, index: number | string, rootNode: HTMLDivElement, btnNode: HTMLAnchorElement, perRowDomClassName: string) => {
                    console.log('remove', items, key, index);
                }}
            />


        </>
    );
}

Asynchronous Usage (Re-render using data changes)

Modify the original data of <DynamicFields />, that is the data attribute. it will make the whole component re-render. It is often used for states of multiple nested components.

Once the delete button is clicked, the DOMElement is removed first. You need to set the doNotRemoveDom attribute to prevent removing actively.

import React, { useState, useEffect } from "react";
import DynamicFields from 'funda-ui/DynamicFields';

import Input from 'funda-ui/Input';
import Select from 'funda-ui/Select';

// component styles
import 'funda-ui/Select/index.css';



type DynamicFieldsValueProps = {
    init: React.ReactNode[];
    tmpl: React.ReactNode;
};


const myData: any[] = [
    {
        user_name: 'Test 1',
        role_id: 'value-2',
        role_name: ''
    },
    {
        user_name: 'Test 2',
        role_id: 'value-3',
        role_name: ''
    }
];



export default () => {

    const LABEL_WIDTH = '200px';
    const [rawData, setRawData] = useState<any[]>([]);
    const [dynamicFieldsValue, setDynamicFieldsValue] = useState<DynamicFieldsValueProps | null>(null);


    const getRowIndex = (node: any) => {
        const curItem = node.closest('.dynamic-fields__data__wrapper') as HTMLDivElement;
        return Number(curItem.dataset.index);
    };

    const updateComponentData = (inputData: any[]) => {
        const initData = inputData.map((item: any, index: number) => {
            const {...rest} = item;
            return tmpl({...rest, index});
        });

        const tmplData = tmpl(null, false); 

        setDynamicFieldsValue({
            init: initData,
            tmpl: tmplData
        });

        console.log('rawData: ', inputData);
    };


    const updateJsonNode = (inputData: any[], curIndex: number, nodes: any) => {
        
        return inputData.map((v: any, i: number) => {
            if (i === curIndex) {

                const params: any[] = Object.keys(nodes);
                params.forEach((key: string) => {
                    delete v[key];
                });

                const {...rest} = v;
                return {
                    ...nodes,
                    ...rest
                }
            } else {
                return v;
            }

        });
    };

    const addNewRow = () => {
        // updata row data
        setRawData((prevState: any[]) => {
            const newData = [...prevState, {
                user_name: '',
                role_id: '',
                role_name: ''
            }];

            // data of Dynamic Fields
            updateComponentData(newData);

            return newData;
        });

    };

    const deleteRow = (index: number | string) => {
        // updata row data
        setRawData((prevState: any[]) => {

            // Once the delete button is clicked, the DOMElement is removed first.
            // You need to set the `doNotRemoveDom` attribute to prevent removing actively.
            prevState.splice(index as number, 1);

            // data of Dynamic Fields
            updateComponentData(prevState);

            return prevState;
        });

    };


    // For some operations to initialize controls, you can use querySelectorAll to query nodes.
    /*
    setTimeout(() => {
        [].slice.call(document.querySelectorAll(`[data-xxx-control]`)).forEach((field) => {
            field.style.background = 'red';
            //...
        });
    }, 500);
    */


    //initialize default value
    const tmpl = (val: any, init: boolean = true) => {
        let data: any = null;
        if (init) {
            const {...rest} = val;
            data = rest;
        } else {
            data = {index: Math.random()};
        }

        const currentRowNum = val !== null ? val.index : undefined;
        
        return <React.Fragment key={'tmpl-' + data.index}>
                {/* ///////////// */}
                <div className="row align-items-center">
                    <div className="text-end" style={{ width: LABEL_WIDTH }}>
                        User Name
                    </div>
                    <div className="col-auto">
                        {/* CONTROL */}
                        <Input
                            value={data.user_name}
                            tabIndex={-1}
                            name="user_name[]"
                            onChange={(e: any) => {
                                const curIndex = getRowIndex(e.currentTarget);

                                // updata row data
                                setRawData((prevState: any[]) => {

                                    const newData = updateJsonNode(prevState, curIndex, {
                                        user_name: e.currentTarget.value
                                    });
                                
                                    // data of Dynamic Fields
                                    updateComponentData(newData);

                                    return newData;
                                });

                            }}
                        />
                        {/* /CONTROL */}
                    </div>
             
                    <div className="text-end" style={{ width: LABEL_WIDTH }}>
                        Role(ID)
                    </div>
                    <div className="col-auto">
                        {/* CONTROL */}
                        <div style={{width: '80%'}}>
                            <Select
                                value={data.role_id}
                                name="role_id[]"
                                placeholder="Select"
                                options={`
                                [
                                    {"label": "Option 1","value": "value-1","queryString": "option1"},
                                    {"label": "Option 2","value": "value-2","queryString": "option2"},
                                    {"label": "Option 3","value": "value-3","queryString": "option3"}
                                ]  
                                `}
                                onChange={(e: any, e2: any, val: any) => {
                                    const curIndex = getRowIndex(e2);

                                    // updata row data
                                    setRawData((prevState: any[]) => {

                                        const newData = updateJsonNode(prevState, curIndex, {
                                            role_id: val.value,
                                            role_name: val.label,
                                        });

                                        // data of Dynamic Fields
                                        updateComponentData(newData);

                                        return newData;
                                    });


                                }}
                            />
                        </div>
                        
                        {/* /CONTROL */}
                    </div>
                

                    <div className="text-end" style={{ width: LABEL_WIDTH }}>
                        Role Name
                    </div>
                    <div className="col-auto">
                        {/* CONTROL */}
                        <Input
                            value={data.role_name}
                            tabIndex={-1}
                            name="role_name[]"
                        />
                        {/* /CONTROL */}
                    </div>



                    <div style={{ width: '40px' }}></div>
                </div>  

                <hr />

            {/* ///////////// */}
        </React.Fragment>
    };

    const initData = myData.map((item: any, index: number) => {
        const {...rest} = item;
        return tmpl({...rest, index});
    });

    const tmplData = tmpl(null, false); 


    useEffect(() => {
        setRawData(myData);
    }, []);




    return (
        <>
            <DynamicFields
                data={dynamicFieldsValue ? dynamicFieldsValue : {
                    init: initData,
                    tmpl: tmplData
                }}
                maxFields="10"
                confirmText="Are you sure?"
                iconAdd={<><div className="mt-1"><svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM16 12.75H12.75V16C12.75 16.41 12.41 16.75 12 16.75C11.59 16.75 11.25 16.41 11.25 16V12.75H8C7.59 12.75 7.25 12.41 7.25 12C7.25 11.59 7.59 11.25 8 11.25H11.25V8C11.25 7.59 11.59 7.25 12 7.25C12.41 7.25 12.75 7.59 12.75 8V11.25H16C16.41 11.25 16.75 11.59 16.75 12C16.75 12.41 16.41 12.75 16 12.75Z" fill="#000" /></svg></div></>}
                iconRemove={<><div className="position-absolute top-0 end-0 mt-2 mx-1"><svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path fillRule="evenodd" clipRule="evenodd" d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10ZM8 11a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2H8Z" fill="#f00" /></svg></div></>}
                onAdd={(items: HTMLDivElement[], rootNode: HTMLDivElement, btnNode: HTMLAnchorElement, perRowDomClassName: string) => {
                    addNewRow();
                }}
                onRemove={(items: HTMLDivElement[], key: number | string, index: number | string, rootNode: HTMLDivElement, btnNode: HTMLAnchorElement, perRowDomClassName: string) => {
                    deleteRow(index);
                }}
                doNotRemoveDom
            />


        </>
    );
}

Table layout

Use the following properties innerAppend* to change the layout to an HTML table.

styles.scss:

/* ---------- Table Div  ----------- */
.app-div-table__wrapper {

    --app-div-table-scrollbar-color: rgba(0, 0, 0, 0.2);
    --app-div-table-scrollbar-track: rgba(0, 0, 0, 0);
    --app-div-table-scrollbar-h: 3px;

    overflow-x: auto;

    &::-webkit-scrollbar {
        height: var(--app-div-table-scrollbar-h);
    }

    &::-webkit-scrollbar-thumb {
        background: var(--app-div-table-scrollbar-color);
    }

    &::-webkit-scrollbar-track {
        background: var(--app-div-table-scrollbar-track);
    }
}

.app-div-table {
    .app-div-table__body {
        display: table-row-group;

        &.last .border {
            border-bottom-width: 1px !important;
        }
    }

    .border {
        border-bottom-width: 0 !important;
        border-right-width: 0 !important;
    
        &.last {
            border-right-width: 1px !important;
        }
    }

}

index.tsx:

import React, { useState } from "react";
import DynamicFields from 'funda-ui/DynamicFields';
import Input from 'funda-ui/Input';
import Select from 'funda-ui/Select';

// component styles
import 'funda-ui/Select/index.css';


type DynamicFieldsValueProps = {
    init: React.ReactNode[];
    tmpl: React.ReactNode;
};


export default () => {

    // For some operations to initialize controls, you can use querySelectorAll to query nodes.
    /*
    setTimeout(() => {
        [].slice.call(document.querySelectorAll(`[data-xxx-control]`)).forEach((field) => {
            field.style.background = 'red';
            //...
        });
    }, 500);
    */

    //initialize default value
    const tmpl = (val: any, init: boolean = true) => {
        let data: any = null;
        if (init) {
            const {...rest} = val;
            data = rest;
        } else {
            data = {index: Math.random()};
        }
        
        const currentRowNum = val !== null ? val.index : undefined;

        return <React.Fragment key={'tmpl-' + data.index}>
                {/* ///////////// */}
                <div className="d-table-cell border py-2 px-2">
                    {/* CONTROL */}
                    <Input
                        wrapperClassName="position-relative"
                        value={data.user_name}
                        tabIndex={-1}
                        name="user_name[]"
                    />
                    {/* /CONTROL */}
                </div>
            
                <div className="d-table-cell border py-2 px-2"  style={{width: '150px'}}>
                    {/* CONTROL */}
                    <Select
                        wrapperClassName="position-relative"
                        value={data.role_id}
                        name="role_id[]"
                        placeholder="Select"
                        options={`
                        [
                            {"label": "Option 1","value": "value-1","queryString": "option1"},
                            {"label": "Option 2","value": "value-2","queryString": "option2"},
                            {"label": "Option 3","value": "value-3","queryString": "option3"}
                        ]  
                        `}
                        onChange={(e: any, e2: any, val: any) => {
                            const targetId = e2.dataset.id;
                            [].slice.call(document.querySelectorAll(`[name="role_name[]"]`)).forEach((node: any) => {
                                if (node.id === targetId) {
                                    node.value = val.label;
                                }
                            });
                        }}
                    />
                    {/* /CONTROL */}
                </div>
            
                <div className="d-table-cell border py-2 px-2 last" style={{ width: '40px' }}></div>
            {/* ///////////// */}
        </React.Fragment>
    };


    return (
        <>
            <DynamicFields
                wrapperClassName="mb-3 position-relative app-div-table__wrapper"
                btnRemoveWrapperClassName="position-relative d-inline-block align-middle"   // Compatible with safari
                data={{
                    init: [],
                    tmpl: tmpl(null, false)
                } as DynamicFieldsValueProps}
                maxFields="10"
                confirmText="Are you sure?"
                iconAdd={<><div className="mt-1"><svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM16 12.75H12.75V16C12.75 16.41 12.41 16.75 12 16.75C11.59 16.75 11.25 16.41 11.25 16V12.75H8C7.59 12.75 7.25 12.41 7.25 12C7.25 11.59 7.59 11.25 8 11.25H11.25V8C11.25 7.59 11.59 7.25 12 7.25C12.41 7.25 12.75 7.59 12.75 8V11.25H16C16.41 11.25 16.75 11.59 16.75 12C16.75 12.41 16.41 12.75 16 12.75Z" fill="#000" /></svg></div></>}
                iconRemove={<><div className="position-absolute top-0 end-0 mx-2" style={{marginTop: '-10px'}}><svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path fillRule="evenodd" clipRule="evenodd" d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10ZM8 11a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2H8Z" fill="#f00" /></svg></div></>}
                onAdd={(items: HTMLDivElement[], rootNode: HTMLDivElement, btnNode: HTMLAnchorElement, perRowDomClassName: string) => {
                    console.log('add', items);
                    // do something

                }}
                onRemove={(items: HTMLDivElement[], key: number | string, index: number | string, rootNode: HTMLDivElement, btnNode: HTMLAnchorElement, perRowDomClassName: string) => {
                    console.log('remove', items, key, index);
                }}

                innerAppendClassName="app-div-table d-table w-100"
                innerAppendCellClassName="d-table-row"
                innerAppendLastCellClassName="last"
                innerAppendHideClassName="d-none"
                innerAppendBodyClassName="app-div-table__body"
                innerAppendHeadData={[
                    <>User Name</>,
                    <>Role(ID)</>,
                    <>&nbsp;</>
                ]}
                innerAppendHeadRowClassName="d-table-row fw-bold bg-body-tertiary"
                innerAppendHeadCellClassName="d-table-cell border py-2 px-2"  
                innerAppendEmptyContent={<><div className={`app-div-table__body--empty px-2 py-2 border`}>No data.</div></>}
            />


        </>
    );
}

Use custom ADD behavior

Use the onLoad() method to get the ID of the add button and then trigger it.

The current example achieves the following goals:

  • Place the add button in the first row of the table
  • The head of the table is displayed by default
  • Set the style of each column of the table head.

styles.scss:

/* ---------- Table Div  ----------- */
.app-div-table__wrapper {

    --app-div-table-scrollbar-color: rgba(0, 0, 0, 0.2);
    --app-div-table-scrollbar-track: rgba(0, 0, 0, 0);
    --app-div-table-scrollbar-h: 3px;

    overflow-x: auto;

    &::-webkit-scrollbar {
        height: var(--app-div-table-scrollbar-h);
    }

    &::-webkit-scrollbar-thumb {
        background: var(--app-div-table-scrollbar-color);
    }

    &::-webkit-scrollbar-track {
        background: var(--app-div-table-scrollbar-track);
    }
}

.app-div-table {
    .app-div-table__body {
        display: table-row-group;

        &.last .border {
            border-bottom-width: 1px !important;
        }
    }

    .border {
        border-bottom-width: 0 !important;
        border-right-width: 0 !important;
    
        &.last {
            border-right-width: 1px !important;
        }
    }

}

index.tsx:

import React, { useState } from "react";
import DynamicFields from 'funda-ui/DynamicFields';
import Input from 'funda-ui/Input';

// component styles
import 'funda-ui/Select/index.css';


type DynamicFieldsValueProps = {
    init: React.ReactNode[];
    tmpl: React.ReactNode;
};


export default () => {


    const [dynamicFieldsInnerAppendHeadInit, setDynamicFieldsInnerAppendHeadInit] = useState<boolean>(false);
    const [dynamicFieldsInnerAppendHeadData, setDynamicFieldsInnerAppendHeadData] = useState<any[]>([]);

    const initInnerAppendHeadData = (addBtnId: string) => {
        if (dynamicFieldsInnerAppendHeadInit) return;

        setDynamicFieldsInnerAppendHeadData([
            <>User Name</>,
            <>User Pass</>,
            <>
                <span className="d-inline-block text-success btn-sm" style={{ transform: 'translateX(4px)', cursor: 'pointer' }} onClick={(e: React.MouseEvent) => {
                    e.preventDefault();
                    document.getElementById(addBtnId)?.click();
                }}><svg width="25px" height="25px" viewBox="0 0 24 28" fill="none"><path d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM16 12.75H12.75V16C12.75 16.41 12.41 16.75 12 16.75C11.59 16.75 11.25 16.41 11.25 16V12.75H8C7.59 12.75 7.25 12.41 7.25 12C7.25 11.59 7.59 11.25 8 11.25H11.25V8C11.25 7.59 11.59 7.25 12 7.25C12.41 7.25 12.75 7.59 12.75 8V11.25H16C16.41 11.25 16.75 11.59 16.75 12C16.75 12.41 16.41 12.75 16 12.75Z" fill="#146c43" /></svg></span>
            </>
        ]);
    };


    //initialize default value
    const tmpl = (val: any, init: boolean = true) => {
        let data: any = null;
        if (init) {
            const {...rest} = val;
            data = rest;
        } else {
            data = {index: Math.random()};
        }
        
        const currentRowNum = val !== null ? val.index : undefined;

        return <React.Fragment key={'tmpl-' + data.index}>
                {/* ///////////// */}
                <div className="d-table-cell border py-2 px-2">
                    {/* CONTROL */}
                    <Input
                        wrapperClassName="position-relative"
                        value={data.user_name}
                        tabIndex={-1}
                        name="user_name[]"
                    />
                    {/* /CONTROL */}
                </div>

                {/* ///////////// */}
                <div className="d-table-cell border py-2 px-2">
                    {/* CONTROL */}
                    <Input
                        wrapperClassName="position-relative"
                        value={data.user_pass}
                        tabIndex={-1}
                        name="user_pass[]"
                    />
                    {/* /CONTROL */}
                </div>


                <div className="d-table-cell border py-2 px-2 last" style={{ width: '40px' }}></div>
            {/* ///////////// */}

        </React.Fragment>
    };



    return (
        <>
            <DynamicFields
                wrapperClassName="mb-3 position-relative app-div-table__wrapper"
                btnRemoveWrapperClassName="position-relative d-inline-block align-middle"  // Compatible with safari
                data={{
                    init: [],
                    tmpl: tmpl(null, false)
                } as DynamicFieldsValueProps}
                maxFields="10"
                confirmText="Are you sure?"
                iconAdd={<><div className="d-none"><svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM16 12.75H12.75V16C12.75 16.41 12.41 16.75 12 16.75C11.59 16.75 11.25 16.41 11.25 16V12.75H8C7.59 12.75 7.25 12.41 7.25 12C7.25 11.59 7.59 11.25 8 11.25H11.25V8C11.25 7.59 11.59 7.25 12 7.25C12.41 7.25 12.75 7.59 12.75 8V11.25H16C16.41 11.25 16.75 11.59 16.75 12C16.75 12.41 16.41 12.75 16 12.75Z" fill="#000" /></svg></div></>}
                iconRemove={<><div className="position-absolute top-0 end-0 mx-2" style={{marginTop: '-10px'}}><svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path fillRule="evenodd" clipRule="evenodd" d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10ZM8 11a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2H8Z" fill="#f00" /></svg></div></>}
                onAdd={(items: HTMLDivElement[], rootNode: HTMLDivElement, btnNode: HTMLAnchorElement, perRowDomClassName: string) => {
                    console.log('add', items);
                    // do something

                }}
                onRemove={(items: HTMLDivElement[], key: number | string, index: number | string, rootNode: HTMLDivElement, btnNode: HTMLAnchorElement, perRowDomClassName: string) => {
                    console.log('remove', items, key, index);
                }}
                onLoad={(addBtn: string, rootNode: HTMLDivElement, perRowDomClassName: string) => {
             
                    // initialize list head
                    initInnerAppendHeadData(addBtn);
                    setDynamicFieldsInnerAppendHeadInit(true);

                
                }}


                innerAppendClassName="app-div-table d-table w-100"
                innerAppendCellClassName="d-table-row"
                innerAppendLastCellClassName="last"
                innerAppendHideClassName="d-none"
                innerAppendBodyClassName="app-div-table__body"
                innerAppendHeadData={dynamicFieldsInnerAppendHeadData}
                innerAppendHeadRowClassName="d-table-row fw-bold bg-body-tertiary"
                innerAppendHeadCellClassName="d-table-cell border py-2 px-2"  
                innerAppendHeadCellStyles={dynamicFieldsInnerAppendHeadData.map((v: any, i: number) => {
                    if (i === dynamicFieldsInnerAppendHeadData.length - 1) {
                        return { width: "40px" };
                    } else {
                        return { background: "#f60", color: '#fff' };
                    }
                })}
                innerAppendEmptyContent={<><div className={`app-div-table__body--empty px-2 py-2 border`}>No data.</div></>}
                innerAppendHeadRowShowFirst
            />


        </>
    );
}

Example of switching between edit and preview modes

import React, { useEffect, useState } from "react";
import Textarea from 'funda-ui/Textarea';
import DynamicFields from 'funda-ui/DynamicFields';
import {
    Table,
    TableBody,
    TableCell,
    TableHead,
    TableRow,
} from 'funda-ui/Table';

// component styles
import 'funda-ui/Table/index.css';

type DynamicFieldsValueProps = {
    init: React.ReactNode[];
    tmpl: React.ReactNode;
};

const myData: any[] = [
    {
        myname: `string here\nstring here\nstring here\nstring here\nstring here\nstring here\nstring here\n`
    },
    {
        myname: `long string, long string long string long string long string long string long string long string long stringlong string, long string long string long string long string long string long string long string long stringlong string, long string long string long string long string long string long string long string long stringlong string, long string long string long string long string long string long string long string long stringlong string, long string long string long string long string long string long string long string long stringlong string, long string long string long string long string long string long string long string long string`
    }
];

export default () => {

    const dfRef = useRef<any>(null);
    const [dynamicFieldsValue, setDynamicFieldsValue] = useState<DynamicFieldsValueProps | null>(null);
    const [dynamicFieldsJsonValue, setDynamicFieldsJsonValue] = useState<any[]>([]);
    const [edit, setEdit] = useState<boolean>(false);


    //initialize default value
    const tmpl = (val: any, init: boolean = true) => {
        let data: any = null;
        if (init) {
            const { ...rest } = val;
            data = rest;
        } else {
            data = { index: Math.random() };
        }

        const currentRowNum = val !== null ? val.index : undefined;

        return <React.Fragment key={'tmpl-' + data.index}>
            {/* ///////////// */}
            <div className="row align-items-center">
               <div className="text-end" style={{ width: '55px' }}>
                    ID
                </div>
                <div className="col-auto text-primary" data-id-show>
                    {data.index+1}
                </div>

                <div className="text-end" style={{ width: '150px' }}>
                    Content
                </div>
                <div className="col">
                    {/* CONTROL */}
                    <Textarea
                        placeholder="String"
                        rows={3}
                        value={data.myname}
                        name="myname[]"
                        autoSize
                    />
                    {/* /CONTROL */}
                </div>

                <div style={{ width: '40px' }}></div>
            </div>

            <hr />

            {/* ///////////// */}
        </React.Fragment>
    };

    useEffect(() => {


        if (dfRef.current && myData.length === 0) {
            setTimeout(() => {
                if (dfRef.current.appendedItemsCounter() === 0) dfRef.current.showAddBtn();
            }, 500);
        }

        //initialize JSON value
        setDynamicFieldsJsonValue(myData.map((item: any, index: number) => (
            {
                itemId: index+1,
                myname: item.myname
            }
        )));

        //initialize default value
        const initData = myData.map((item: any, index: number) => {
            const { ...rest } = item;
            return tmpl({ ...rest, index });
        });

        const tmplData = tmpl(null, false);

        setDynamicFieldsValue({
            init: initData,
            tmpl: tmplData
        });

    }, []);



    return (
        <>



            {/* LIST */}
            {dynamicFieldsJsonValue.length > 0 ? <>
                {!edit ? <button tabIndex={-1} type="button" onClick={(e: any) => {
                    setEdit(true);
                    if (dfRef.current) {
                        if (dfRef.current.appendedItemsCounter() === 0) dfRef.current.showAddBtn();
                    }
                }} className="btn btn-outline-primary btn-sm mb-2"><i className="fa-solid fa-pen-to-square" aria-hidden="true"></i> Edit</button> : <button tabIndex={-1} type="button" onClick={(e: any) => {
                    setEdit(false);
                }} className="btn btn-outline-primary btn-sm mb-2"><i className="fa-solid fa-arrow-left" aria-hidden="true"></i> Cancel</button>}
            </> : null}


            {dynamicFieldsJsonValue.length > 0 && !edit ? <><Table
                tableClassName="table table-hover table-bordered"
            >
                
                <TableHead className="table-light">
                    <TableRow>
                        <TableCell scope="col">Id</TableCell>
                        <TableCell scope="col">Content</TableCell>
                    </TableRow>
                </TableHead>
                <TableBody>
                    {dynamicFieldsJsonValue.map((item: any, index: number) => {
                        return <TableRow
                            key={`row-${index}`}
                            data-key={`row-${index}`}
                            itemData={item}
                        >
                            <TableCell scope="row" style={{fontWeight: 'normal'}}>{item.itemId}</TableCell>
                            <TableCell>{item.myname}</TableCell>
                            
                        </TableRow>

                    })}
                </TableBody>
            </Table></> : null}

            <div style={edit || dynamicFieldsJsonValue.length === 0 ? {} : {height: '0', overflow: 'hidden'}}>
                <DynamicFields
                    contentRef={dfRef}
                    key={JSON.stringify(dynamicFieldsJsonValue)}  // Trigger child component update when prop of parent changes
                    data={dynamicFieldsValue}
                    maxFields="10"
                    confirmText="Are you sure?"
                    iconAdd={<><div className="mt-1"><svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM16 12.75H12.75V16C12.75 16.41 12.41 16.75 12 16.75C11.59 16.75 11.25 16.41 11.25 16V12.75H8C7.59 12.75 7.25 12.41 7.25 12C7.25 11.59 7.59 11.25 8 11.25H11.25V8C11.25 7.59 11.59 7.25 12 7.25C12.41 7.25 12.75 7.59 12.75 8V11.25H16C16.41 11.25 16.75 11.59 16.75 12C16.75 12.41 16.41 12.75 16 12.75Z" fill="#000" /></svg></div></>}
                    iconRemove={<><div className="position-absolute top-0 end-0 mx-2" style={{ marginTop: '-10px' }}><svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path fillRule="evenodd" clipRule="evenodd" d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10ZM8 11a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2H8Z" fill="#f00" /></svg></div></>}
                    onAdd={(items: HTMLDivElement[], rootNode: HTMLDivElement, btnNode: HTMLAnchorElement, perRowDomClassName: string) => {
                        console.log('add', items);
                    
                        //update `data-id` and `id` attributes of control
                        items.forEach((node) => {
                            const keyIndex = node.dataset.key;
                            [].slice.call(node.querySelectorAll(`[data-id-show]`)).forEach((field) => {
                                field.textContent = parseFloat(Number(keyIndex)+1);
                            });
                        });


                    }}
                />
            </div>
        </>
    );
}

API

Dynamic Fields

import DynamicFields from 'funda-ui/DynamicFields';
Property Type Default Description Required
contentRef React.RefObject - It exposes the following methods:
  1. contentRef.current.appendedItemsCounter()
  2. contentRef.current.showAddBtn()
  3. contentRef.current.hideAddBtn()
-
key React.key - Trigger child component update when prop of parent changes.
Ensure that complex dynamic form components update in real time on the page.
-
wrapperClassName string mb-3 position-relative The class name of the control wrapper. -
btnAddWrapperClassName string align-middle The class name of the add button wrapper. -
btnRemoveWrapperClassName string align-middle The class name of the remove button wrapper. -
label string | ReactNode - It is used to specify a label for an element of a form.
Support html tags
-
data JSON Object - Control group are dynamically added after the button is triggered
confirmText string - The text to display in the confirm box. -
doNotRemoveDom boolean false Click the delete button without removing the Dom element. You can customize the status to delete each group. -
iconAddBefore ReactNode - The button before add. -
iconAddAfter ReactNode - The button after add. -
iconAdd string | ReactNode <svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM16 12.75H12.75V16C12.75 16.41 12.41 16.75 12 16.75C11.59 16.75 11.25 16.41 11.25 16V12.75H8C7.59 12.75 7.25 12.41 7.25 12C7.25 11.59 7.59 11.25 8 11.25H11.25V8C11.25 7.59 11.59 7.25 12 7.25C12.41 7.25 12.75 7.59 12.75 8V11.25H16C16.41 11.25 16.75 11.59 16.75 12C16.75 12.41 16.41 12.75 16 12.75Z" fill="#000" /></svg> The label of the button to add a new item -
iconAddPosition start | end end Position of Add button. -
iconRemove string | ReactNode <svg width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path fillRule="evenodd" clipRule="evenodd" d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10ZM8 11a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2H8Z" fill="#f00" /></svg> The label of the button to delete current item, if it is not set, only the SVG icon will be included -
innerAppendClassName string - Class names of table.
Customize the class names of the append area, usually used in table styles
-
innerAppendCellClassName string - Class names of table cell.
Customize the class names of the append area, usually used in table styles
-
innerAppendLastCellClassName string - Class names of table cell.
Customize the class names of the append area, usually used in table styles
-
innerAppendHideClassName string - Specify a hidden class name.
Customize the class names of the append area, usually used in table styles
-
innerAppendBodyClassName string - Class names of last table cell.
Customize the class names of the append area, usually used in table styles
-
innerAppendHeadData React.ReactNode[] | string[] - The data of group header content in an HTML table. such as [<>User Name</>,<>Role(ID)</>,<>&nbsp;</>]
Customize the class names of the append area, usually used in table styles
-
innerAppendHeadRowShowFirst boolean false The first row of the table head is displayed by default. -
innerAppendHeadRowClassName string - Class names of group header in an HTML table.
Customize the class names of the append area, usually used in table styles
-
innerAppendHeadCellClassName string | string[] - Class names of a cell as the header of a group of table cells. If Array it should be equal to the number of innerAppendHeadData.
Customize the class names of the append area, usually used in table styles
-
innerAppendHeadCellStyles React.CSSProperties[] false Use inline styles per cell of table head. It should be equal to the number of innerAppendHeadData. such as [{ background: "#f60" },{ background: "#f60" },{ width: "40px" }] -
innerAppendEmptyContent React.ReactNode - Content displayed when there are no content. If this property is not set, all content will be automatically hidden.
Customize the class names of the append area, usually used in table styles
-
maxFields number 10 Maximum number of control group allowed to be added -
onAdd function - Call a function when add a control. It returns four callback values.
  1. The first is each group of fields (HTMLDivElement[])
  2. The second is the root div (HTMLDivElement)
  3. The third is the current trigger (HTMLAnchorElement)
  4. The last is classname of the container for each row that is dynamically added (String)
-
onRemove function - Call a function when remove a control. It returns six callback values.
  1. The first is each group of fields (HTMLDivElement[])
  2. The second is the current key of removed item (number | string)
  3. The third is the current index of removed item (number | string)
  4. The fourth is the root div (HTMLDivElement)
  5. The fifth is the current trigger (HTMLAnchorElement)
  6. The last is classname of the container for each row that is dynamically added (String)
-
onLoad function - Call a function when the component has been rendered completely. It returns three callback values.
  1. The first is the button ID of add (String)
  2. The second is the root div (HTMLDivElement)
  3. The last is classname of the container for each row that is dynamically added (String)
-

Element of per row accepts all props which this control support. Such as style, data-*, tabIndex, id, and so on.


Array configuration properties of the data:

Property Type Default Description Required
init React.ReactNode[] [] Initial fields group with data. -
tmpl React.ReactNode null An empty fields group. -

Note

You can use the placeholder %i% instead of the key for your component attributes.
It allows you to use Vanilla JS to control the component Dom of each row.