10

I'm trying to implement the custom toggle drop-down example from the react-bootstrap page, in Typescript, using react functional components.

Here's my component code:


    import React from 'react';
    import Dropdown from 'react-bootstrap/Dropdown';
    import FormControl from 'react-bootstrap/FormControl';


    export const DropdownSelector =() => (
      <Dropdown>
        <Dropdown.Toggle as={CustomToggle} id="dropdown-custom-components">
          Custom toggle
        </Dropdown.Toggle>

        <Dropdown.Menu as={CustomMenu}>
          <Dropdown.Item eventKey="1">Red</Dropdown.Item>
          <Dropdown.Item eventKey="2">Blue</Dropdown.Item>
          <Dropdown.Item eventKey="3" active>
            Orange
          </Dropdown.Item>
          <Dropdown.Item eventKey="1">Red-Orange</Dropdown.Item>
        </Dropdown.Menu>
      </Dropdown>
    )

    // The forwardRef is important!!
    // Dropdown needs access to the DOM node in order to position the Menu
    const CustomToggle = React.forwardRef(({ children, onClick }, ref) => (
      <a
        href=""
        ref={ref}
        onClick={(e) => {
          e.preventDefault();
          onClick(e);
        }}
      >
        {children}
        &#x25bc;
      </a>
    ));

    // forwardRef again here!
    // Dropdown needs access to the DOM of the Menu to measure it
    const CustomMenu = React.forwardRef(
      ({ children, style, className, 'aria-labelledby': labeledBy }, ref) => {
        const [value, setValue] = useState('');

        return (
          <div
            ref={ref}
            style={style}
            className={className}
            aria-labelledby={labeledBy}
          >
            <FormControl
              autoFocus
              className="mx-3 my-2 w-auto"
              placeholder="Type to filter..."
              onChange={(e) => setValue(e.target.value)}
              value={value}
            />
            <ul className="list-unstyled">
              {React.Children.toArray(children).filter(
                (child) =>
                  !value || child.props.children.toLowerCase().startsWith(value),
              )}
            </ul>
          </div>
        );
      },
    );

This fails to compile:


./src/components/helpers/dropdown-selector.tsx
TypeScript error in ./src/components/helpers/dropdown-selector.tsx(25,52):
Property 'onClick' does not exist on type '{ children?: ReactNode; }'. TS2339


What am I doing wrong?

Stackblitz sandbox version here. Using that editor, I see a bunch of type errors (although it does run); but the IDE I'm using to develop the app won't let me run it with those errors...

1
  • Please share code in any sandbox Commented Sep 15, 2020 at 16:11

3 Answers 3

21

I Think I did resolve your problem


import React, { useState } from "react";
import Dropdown from "react-bootstrap/Dropdown";
import FormControl from "react-bootstrap/FormControl";

interface IFruity {
  id: number;
  fruit: string;
  prefix: string;
  suffix?: string;
}

const fruits: IFruity[] = [
  { id: 1, fruit: "Apples", prefix: "How's about them " },
  { id: 2, fruit: "Pear", prefix: "A cracking ", suffix: "!" },
  { id: 3, fruit: "Oranges", prefix: "What rhymes with ", suffix: "?" },
  { id: 4, fruit: "Banana", prefix: "Fruit flies like a " },
  { id: 5, fruit: "Coconuts", prefix: "Oh what a lovely bunch of " },
  { id: 6, fruit: "Avocado", prefix: "Is an ", suffix: " even a fruit?" }
];

type CustomToggleProps = {
  children?: React.ReactNode;
  onClick?: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {};
};

// The forwardRef is important!!
// Dropdown needs access to the DOM node in order to position the Menu
const CustomToggle = React.forwardRef(
  (props: CustomToggleProps, ref: React.Ref<HTMLAnchorElement>) => (
    <a
      href=""
      ref={ref}
      onClick={e => {
        e.preventDefault();
        props.onClick(e);
      }}
    >
      {props.children}
      <span style={{ paddingLeft: "5px" }}>&#x25bc;</span>
    </a>
  )
);

type CustomMenuProps = {
  children?: React.ReactNode;
  style?: React.CSSProperties;
  className?: string;
  labeledBy?: string;
};

// forwardRef again here!
// Dropdown needs access to the DOM of the Menu to measure it
const CustomMenu = React.forwardRef(
  (props: CustomMenuProps, ref: React.Ref<HTMLDivElement>) => {
    const [value, setValue] = useState("");

    return (
      <div
        ref={ref}
        style={props.style}
        className={props.className}
        aria-labelledby={props.labeledBy}
      >
        <FormControl
          autoFocus
          className="mx-3 my-2 w-auto"
          placeholder="Type to filter..."
          onChange={e => setValue(e.target.value)}
          value={value}
        />
        <ul className="list-unstyled">
          {React.Children.toArray(props.children).filter(
            (child: any) =>
              !value || child.props.children.toLowerCase().startsWith(value)
          )}
        </ul>
      </div>
    );
  }
);

export const DropdownSelector = () => {
  const [selectedFruit, setSelectedFruit] = useState(0);

  const theChosenFruit = () => {
    const chosenFruit: IFruity = fruits.find(f => f.id === selectedFruit);
    return chosenFruit
      ? chosenFruit.prefix + chosenFruit.fruit + (chosenFruit.suffix || "")
      : "Select a fruit";
  };

  return (
    <Dropdown onSelect={(e: string) => setSelectedFruit(Number(e))}>
      <Dropdown.Toggle as={CustomToggle} id="dropdown-custom-components">
        {theChosenFruit()}
      </Dropdown.Toggle>

      <Dropdown.Menu as={CustomMenu}>
        {fruits.map(fruit => {
          return (
            <Dropdown.Item key={fruit.id} eventKey={fruit.id.toString()}>
              {fruit.fruit}
            </Dropdown.Item>
          );
        })}
      </Dropdown.Menu>
    </Dropdown>
  );
};


Sign up to request clarification or add additional context in comments.

1 Comment

I've marked this as the accepted answer (apologies for the delay!) - as it almost works for me as-is. I had to make a couple of additional amendments to get it to compile without any errors in my environment - I've created a new stackblitz here, with the version that now compiles :)
1

I'm facing the same issue when defining a custom dropdown with react-bootstrap,

What I did to workaround this problem was

const CustomToggle = React.forwardRef((props: any, ref) => (
const { children, onClick } = props 

1 Comment

The key word here is "workaround". This isn't a solution but setting types to any will get TypeScript to STFU
0

This is the best way of typing it I was able to do so far:

type DropdownMenu = typeof Dropdown['Menu']['prototype'];

type Props = {
  className?: string;
};

const CustomDropdownMenu = forwardRef<DropdownMenu, Props>((props, ref) => (
  <Dropdown.Menu ref={ref}>
    ...
  </Dropdown.Menu>
));

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.