pnkflydgr
pnkflydgr

Reputation: 715

How to get Position of Components using React Function Components

I'm trying to do something like this:

enter image description here

When I click on a card on the left and then ctrl+click on a card on the right.. a line will be drawn. Ctrl+click on the card on the right again and the line will be removed. I'm going to draw the line using an svg path but I have to get the positions of the cards in the Container Component to do so.

I'm trying to do this with React Function Components using ref and I'm not quite getting it. I understand that function components don't have an "instance" and therefore ref doesn't work quite like it does in classes.

Please answer the following:

Any help is appreciated and thanks in advance!

EDIT

This is what I have so far:

ContainerComponent

import React, { useState, useRef } from 'react';

import { Container, Row, Col, Card, Button } from 'reactstrap';
import SectionContainer from './SectionContainer';

const ContainerComponent = (props) => 
{
    const [keyCards, setKeyCards] = useState([
        { id: 1, name: 'key one', description: "this is a key card for one" },
        { id: 2, name: 'key two', description: "this is a key card for two" }
    ])

    const [valueCards, setValueCards] = useState([
        { id: 1, name: 'value one', description: "this is a value card" },
        { id: 2, name: 'value two', description: "this is a value card" },
        { id: 3, name: 'value three', description: "this is a value card" }
    ])

    const [connections, setConnections] = useState([
        // I need something like this for all connections that need a line
        {
            keyID: 1,
            keyPos: { x: 100, y: 200 },
            attachedValues: [
                { valueID: 1, valPos: { x: 200, y: 200 } },
                { valueID: 2, valPos: { x: 200, y: 300 } }
            ]
        }
    ])

    const cardRef = useRef()

    const getPositions = () =>
    {
        console.log(cardRef.current)
        // if I can get positions and if they are selected
        // then I can set the state and generate svg paths
    }

    return (
        <>
            <Row >
                <Col className="display-section" md='6'>
                    <SectionContainer
                        title="Rules for Life"
                        items={keyCards}
                        cardRef={cardRef}
                        onClickHandler={getPositions}
                    />
                </Col>

                <Col className="display-section" md='6'>
                    <SectionContainer
                        title="Entitlements for Days"
                        items={valueCards}
                        cardRef={cardRef}
                        onClickHandler={getPositions}
                    />
                </Col>
            </Row>
            {/* <DrawLines connections={connections} /> */}
        </>
    )
}

export default ContainerComponent 

SectionContainer

import React from 'react';

import { Row, Input, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';

import CardContainer from './CardContainer'

const SectionContainer = ({ title, items, cardRef, onClickHandler }) => 
{
    return (
        <>
            <h6>{title}</h6>
            <Row >
                <InputGroup>
                    <InputGroupAddon addonType="prepend" >
                        <InputGroupText>filter:</InputGroupText>
                    </InputGroupAddon>
                    <Input />
                </InputGroup>
            </Row>
            <CardContainer
                items={items}
                cardRef={cardRef}
                onClickHandler={onClickHandler}
            />
        </>
    )
}

export default SectionContainer

Card Container

import React from 'react'
import { Container, Row } from 'reactstrap'
import CardComponent from './CardComponent'

const CardContainer = ({ items, cardRef, onClickHandler }) => 
{
    const getItems = (items) =>
    {
        return items.map(item =>
        {
            return <Row key={item.id}>
                <CardComponent
                    item={item}
                    cardRef={cardRef}
                    onClickHandler={onClickHandler}
                />
            </Row>
        })
    }
    return (
        <Container fluid className="node-container">
            {getItems(items)}
        </Container>
    )
}

export default CardContainer

Card Component

import React, { useState } from 'react';

import { Card, CardHeader, CardTitle, CardBody, CardText } from 'reactstrap';

const CardComponent = React.forwardRef(({ item, cardRef, onClickHandler }, ref) => 
{
    return (
        <Card ref={cardRef} >
            <CardHeader onClick={onClickHandler} >
                <CardTitle>{item.name}</CardTitle>
            </CardHeader>
            <CardBody>
                <CardText>{item.description}</CardText>
            </CardBody>
        </Card>)
})

export default CardComponent

This displays the cards similar to the drawing above no problem. I just can't get the ref thing to work.

I'm getting the following error for CardComponent even tho I have wrapped it with React.ForwardRef():

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Check the render method of ForwardRef. in Card (at CardComponent.js:8) ...

Edit

I fixed the forwardRef() error by wrapping my Card in a div. Even tho the bootstrap Card renders as a div, you can't ref it.

Only one position is passing up to the ContainerComponent tho. Still working on that.

New CardComponent

import React, { useState } from 'react';

import { Card, CardHeader, CardTitle, CardBody, CardText } from 'reactstrap';

const CardComponent = ({ item, cardRef, onClickHandler }) =>
    (
        <div ref={cardRef}>
            <Card >
                <CardHeader onClick={onClickHandler} >
                    <CardTitle>{item.name}</CardTitle>
                </CardHeader>
                <CardBody>
                    <CardText>{item.description}</CardText>
                </CardBody>
            </Card>
        </div>
    )


export default CardComponent

Upvotes: 2

Views: 8692

Answers (2)

pnkflydgr
pnkflydgr

Reputation: 715

I'm going to answer my own question here. I owe it to the community since my question was pretty vague. Big thanks to @dlya for his answer which led me down the path to righteousness :)

  • Is it possible to do this with just function components?

Yes. I was able to complete the task end to end without classy components.

  • Which components require to be wrapped in React.forwardRef()?

None. I didn't end up having to use React.forwardRef() at all. In fact... I didn't use useRef() or useEffect() either. I just passed a function to the child and added the function to the ref like this:

<Card innerRef={updateCardLocations} id={`${type}-${item.id}`}>

Notice that I had to use innerRef instead of ref because I really needed to get the reactstrap ref instead of having to wrap Card in a div.

Then I just collect all of my Card positions in the parent like this:

    let connections = {}

    const updateCardLocations = (card) =>
    {
        if (card)
        {
            const id = card.id.substr(card.id.indexOf("-") + 1)
            const pos = card.getBoundingClientRect()

            connections[id] = {
                ...connections[id],
                connectionPoint: [pos.right, pos.top + (pos.height / 2)]
            }

            console.log(connections)
        }
    }

  • I read that I can just pass a function from the Container to the children in the ref attribute and the children can call that function when clicked. Is that true and what would that look like?

I just showed what that looks like in the previous bullet. The parent has a function called updateCardLocations which is passed as a prop to the child. Pass the updateCardLocations function prop to a ref in the Child. In my case innerRef.

  • bonus: I've read through the React Refs and the DOM documentation several times and it just won't click for me. Can anyone explain it in my context?

What I was missing from the documentation was that you're just creating a placeholder in the parent that you pass to the child component as a prop. Then when the child component is rendered it updates that placeholder with it's dom information. I don't fully grasp things until I can get a successful hands on experience... but in my defense, the whole forwardRef thing really confused me because I couldn't tell which direction I was forwarding the ref and at that time I still thought that I had created a ref in the parent as well with useRef(). Hopefully this helps someone that is as easily confused as me :)

Here's a working sample on CodeSandbox. I also included an onClickHandler that adds cards and you can see the positions updating in the console.

Upvotes: 4

dlya
dlya

Reputation: 93

Using react hooks you can basically do anything that you did in a class component in a functional component.

I guess you can get the position using getBoundingClientRect().

import React, { useRef } from 'react';
//import Card from '.../...';

const App = () =>
{
    const cardRef = useRef();

    const onClickHandler = () =>
    {
        const position = cardRef.current.getBoundingClientRect();
        //This returns an object with left, top, right, bottom, x, y, width, and height.
        console.log(position)
    }

    return (
        <div>
            <Card cardRef={cardRef} onClickHandler={onClickHandler} />
        </div>
    );

}

export default App

const Card = ({ cardRef, onClickHandler }) => (
    <div ref={cardRef} onClick={onClickHandler}>
        <button > Click Me </button>
    </div>
)

Upvotes: 2

Related Questions