Welcome!

By registering with us, you'll be able to discuss, share and private message with other members of our community.

SignUp Now!
  • Guest, before posting your code please take these rules into consideration:
    • It is required to use our BBCode feature to display your code. While within the editor click < / > or >_ and place your code within the BB Code prompt. This helps others with finding a solution by making it easier to read and easier to copy.
    • You can also use markdown to share your code. When using markdown your code will be automatically converted to BBCode. For help with markdown check out the markdown guide.
    • Don't share a wall of code. All we want is the problem area, the code related to your issue.


    To learn more about how to use our BBCode feature, please click here.

    Thank you, Code Forum.

JavaScript Components aren't being rendered properly using OutPortal and no children in InPortal react-reverse-portal

accol

Coder
Thanks for reading. I can't figure out how to reparent card components to deck components when they overlap each other so the cards don't need to be destroyed and be recreated. Currently I'm just trying to figure out how to show them on the page properly using react-reverse-portal (used for reparenting) and it's not working. I also rarely use React so my apologies if I'm missing something obvious or just not understanding how React's supposed to work. The issue is that it seems like it's hitting all the breakpoints without problems, cardComponents is being populated, CardBody seems to be working, the console has no information.

Am I even approaching what I want to do correctly? Or is there a better way.

JSX:
import React, { useState, useRef, useEffect } from 'react';

import ComponentContainer from '../../utils/component-container';
import CardBody from './card-body';
import Deck from './deck';

import Button from 'react-bootstrap/Button';

import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';

const CardWorkspace = (props) => {
    const [doesCardExist, setDoesCardExist] = useState(false);
    const [cardComponents, setCardComponents] = useState([]);
    const targetComponentRefs = useRef([]);

    const cardPortalNode = createHtmlPortalNode({
      attributes: {
        id: "card",
        style: "background-color: #aaf;" }
    });

    const [decks, setDecks] = useState([]);
   
    const registerComponentRef = (ref, component) => {
      if (ref.current && !targetComponentRefs.current.some(item => item.ref.current === ref.current)) {
        targetComponentRefs.current.push({ ref, component });
      }
  };

  const notifyOverlap = (componentRef, component) => {
    if (!componentRef.current) return;

    let rectCompA = componentRef.current.getBoundingClientRect();

    let callback = (entries, observer) => {
        entries.forEach(entry => {
            // Should probably check via isIntersecting instead?
            if (entry.target !== componentRef.current) {
              let rectCompB = entry.target.getBoundingClientRect();

              let doesOverlap = !(rectCompA.right < rectCompB.left ||
                rectCompA.left > rectCompB.right ||
                rectCompA.bottom < rectCompB.top ||
                rectCompA.top > rectCompB.bottom);
               
                // Temporary, check whether the overlapping card is already part of an existing deck
                // If it is, add to that deck
                // If not then create a new deck with it.
                // This whole think really should be rewritten
                // Not sure if IO are needed, just saw it recommended on StackOverflow
               
                let overlappedComponent = targetComponentRefs.current.find(item => item.ref.current === entry.target);
                if (overlappedComponent) {
                  let cards = [
                    component,
                    overlappedComponent.component
                  ];
                }
              }

              // I think the intersection check has issues, probably gotta fix the root and threshold
              /*
              if (entry.isIntersecting) {
                console.log("Intersecting");
              }*/
            }
        });
    };

    let io = new IntersectionObserver(callback, {
        // root: el,
        threshold: [0, 0.1, 0.95, 1]
    });

    targetComponentRefs.current.forEach(target => {
      if (target.ref.current) {
          io.observe(target.ref.current);
      }
    });

    return () => {
        targetComponentRefs.current.forEach(target => {
          if (target.ref.current) {
              io.unobserve(target.ref.current);
          }
      });
    };
  };

    const addCardComponent = () => {
      setCardComponents([...cardComponents,
        <InPortal node = {cardPortalNode}>
          <ComponentContainer
            key =  {cardComponents.length}
            id={cardComponents.length}
            registerComponentRef={registerComponentRef}
            notifyOverlap={notifyOverlap}
          >
            <CardBody/>
          </ComponentContainer>
        </InPortal>
      ])
    }

    const updateCardExistence = useEffect(() => {
      if (cardComponents.length > 0) {
        setDoesCardExist(true);
      }
      else {
        setDoesCardExist(false);
      }
    }, [cardComponents]);

    return (
    <div>
      <Button variant="primary" onClick={addCardComponent}>
        Add card
      </Button>

      <div>
        {doesCardExist === true && (
          <OutPortal node={cardPortalNode} bgColor="#aaf">
            {cardComponents.map((cardComponent, index) => (
              <React.Fragment key={index}>{cardComponent}</React.Fragment>
            ))}
          </OutPortal>
        )}
      </div>
    </div>
  );
}

export default CardWorkspace;


1722467356946.png
1722467398158.png

1722467473284.png
1722467503908.png

Please ignore the <div></div> right below <div id="card" style="background-color: #aaf;height:1080px"></div>, I had another one there. But I don't get why <div id="card" style="background-color: #aaf;height:1080px"></div> has no children nested inside it, it should have ComponentContainer and the CardBody inside that.
1722467531178.png
1722467560419.png

Description on page about InPortal:
Render the content that you want to move around later.
InPortals render as normal, but send the output to detached DOM.
MyExpensiveComponent will be rendered immediately, but until
portalNode is used in an OutPortal, MyExpensiveComponent, it
will not appear anywhere on the page.

OutPortal:
Show the content of the portal node here:
 

Attachments

  • 1722467330651.png
    1722467330651.png
    281.3 KB · Views: 1
  • 1722467436777.png
    1722467436777.png
    279.3 KB · Views: 2
I went back and rewrote my code, cards definitely render now:
JSX:
// card-workspace
import React, { useState, useRef, useEffect } from 'react';

import ComponentContainer from '../../utils/component-container';
import CardBody from './card-body';
import Deck from './deck';

import Button from 'react-bootstrap/Button';

import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';

const CardWorkspace = (props) => {
    const [cardData, setCardData] = useState([]);
    const [doesCardExist, setDoesCardExist] = useState(false);

    const targetComponentRefs = useRef([]);

    const cardPortalNode = createHtmlPortalNode({
      attributes: {
        id: "card"}
    });

    const [deckData, setDeckData] = useState([]);
    const [doesDeckExist, setDoesDeckExist] = useState(false);
    
    const registerComponentRef = (ref, component) => {
      if (ref.current && !targetComponentRefs.current.some(item => item.ref.current === ref.current)) {
        targetComponentRefs.current.push({ ref, component, id: component.props.id });
      }
  };

  const notifyOverlap = (componentRef, component) => {
    if (!componentRef.current) return;

    let rectCompA = componentRef.current.getBoundingClientRect();

    let callback = (entries, observer) => {
        entries.forEach(entry => {
            // Should probably check via isIntersecting instead?
            if (entry.target !== componentRef.current) {
              let rectCompB = entry.target.getBoundingClientRect();

              console.log(`Rectangle A: ${JSON.stringify(rectCompA)}\nRectangle B: ${JSON.stringify(rectCompB)}`)

              let doesOverlap = !(rectCompA.right < rectCompB.left ||
                rectCompA.left > rectCompB.right ||
                rectCompA.bottom < rectCompB.top ||
                rectCompA.top > rectCompB.bottom);

              if (doesOverlap) {
                // Temporary, check whether the overlapping card is already part of an existing deck
                // If it is, add to that deck
                // If not then create a new deck with it.
                
                let overlappedComponent = targetComponentRefs.current.find(item => item.ref.current === entry.target);

                if (overlappedComponent) {
                  let cards = [
                    component,
                    overlappedComponent.component,
                  ];

                  addDeckData(cards);
                }
              }
            }
        });
    };

    let io = new IntersectionObserver(callback, {
        // root: el,
        threshold: [0, 0.1, 0.95, 1]
    });

    targetComponentRefs.current.forEach(target => {
      if (target.ref.current) {
          io.observe(target.ref.current);
      }
    });
  };

    const addCardData = () => { 
      setCardData([...cardData, {
        id: cardData.length,
      }]);
    }

    // There could be multiple decks simultaneously
    // This should probably be function for creating new deck
    // and another function for adding card to existing deck
    const addDeckData = (cards) => {
      const deckPortalNode = createHtmlPortalNode({
        attributes: {
          id: "deck ${deckData.length}"}
      });

      setDeckData([...deckData,
        {
            key: deckData.length,
            id: deckData.length,
            cards: cards
        }
      ]);

      // How to now shift cards based on their index to this new InPortalNode
      updateCardPortalsToDeckPortal(cards, deckPortalNode);
    }

    const updateCardPortalsToDeckPortal = (cards, deckPortalNode) => {
      // Not sure what to do here, I think I need to somehow change the InPortal the overlapping components belong to
      // And then also store that portal node and then pass it as a prop
  }

    const GenerateCards = (props) => {
      return <div>
        {props.cardData.map((cardDatum, index) => (
          <ComponentContainer
            key={cardDatum.id}
            id={cardDatum.id}
            registerComponentRef={registerComponentRef}
            notifyOverlap = {notifyOverlap}
          >
            <CardBody />
          </ComponentContainer>
        ))}
      </div>
    }

    const RenderInPortal = (props) => {
      return <InPortal node={props.node}>
        {props.children}
      </InPortal>
    }

    const CardsOutPortal = (props) => {
      return <div id = "cards">
        {doesCardExist === true && <OutPortal node={props.node}/>}
      </div>
    }

    const RenderDeck = (props) => {
      return (
        <>
          {doesDeckExist === true && props.deckData.map((deckDatum, index) => (
            <Deck key={index} cards = {deckDatum.cards} node={deckDatum.portalNode} />
          ))}
        </>
      );
    }

    const updatePortals = useEffect(() => {
      if (cardData.length > 0) {
        setDoesCardExist(true);
      }
      else {
        setDoesCardExist(false);
      }

      /*if (deckData.length > 0) {
        setDoesDeckExist(true);
      }
      else {
        setDoesDeckExist(false);
      }*/

    }, [cardData, deckData]);

    return (
    <div>
      <Button variant="primary" onClick={addCardData}>
        Add card
      </Button>

      <RenderInPortal node={cardPortalNode}>
        <GenerateCards cardData={cardData}/>
      </RenderInPortal>

      {/*Render cards here when they aren't in deck*/}
      <CardsOutPortal node ={cardPortalNode}/>

      {/*Render decks on overlap*/}
      <RenderDeck deckData = {deckData}/>
    </div>
  );
}

export default CardWorkspace;

// deck
import React, { useState, useEffect } from "react";
import ComponentContainer from '../../utils/component-container';
import ButtonGroup from 'react-bootstrap/ButtonGroup';
import ToggleButton from 'react-bootstrap/ToggleButton';
import Shuffle from 'shuffle';

import { OutPortal } from 'react-reverse-portal';

const Deck = ({ cards, portalNode }) => {
  const [deck, setDeck] = useState([]);

  // Update the state whenever the `cards` prop changes
  const updateDeck = useEffect(() => {
    setDeck(cards);
  }, [cards]);

  const shuffleDeck = () => {
    if (deck.length < 1) return;

    // Use Shuffle library to shuffle the deck
    // Gotta look into how this works
    const shuffledDeck = Shuffle.shuffle(deck);
    setDeck(shuffledDeck);
  };

  const radios = [
    { name: 'Shuffle', value: '1', onClick: shuffleDeck }
  ];

  // Why is this giving issues?
  // What should be happening is that props.node should be deckPortalNode
  const DeckOutPortal = (props) => {
    return <div id = "deck">
      {deck.length > 0 && <OutPortal node={props.node}/>}
    </div>
  }

  return (
    <ComponentContainer>
      <DeckOutPortal node = {portalNode}/>
      <div>
        <ButtonGroup className="mb-2">
            {radios.map((radio, idx) => (
              <ToggleButton
                key={idx}
                id={`radio-${idx}`}
                type="radio"
                variant="secondary"
                name="radio"
                value={radio.value}
                onClick={radio.onClick}
              >
                {radio.name}
              </ToggleButton>
            ))}
        </ButtonGroup>
      </div>
    </ComponentContainer>
  );
};

export default Deck;


I'm just not sure of how to go about it now where when the cards overlap (I'm definitely screwing up IO, removed the return statement), the cards shift from being children of cardPortalNode to deckPortalNode. My current idea is to take cardData then have a property called portalNode and then assign the nodes that way, then use a function to take in the portal node, then iterate the cardData, see whether the portal node of that card data matches the portal node I want to use, then reparent and rerender it under that node somehow?

From my understanding, InPortal and OutPortal have a 1-1 relationship, but InPortal can have multiple children. Question then is, how would you do that.The goal is to make it so that once the cards are one deck, you drag the deck, you move the entire stack.
 

New Threads

Buy us a coffee!

Back
Top Bottom