TypeScript: How to make a function behave reactively

Viewed 63

I have a function written in TypeScript to show a color picker control, which will return a button property. That button property will then be passed down to an external component where they handle the actual rendering logic.

My function is something like below in the ts file:

const getColorPickerControl = (): ButtonProps => {
    ....
    return createButton(
        id: 'colorPicker',
        label: 'color picker',
        onColorCellSelected: (colorItem: colorParam) => {
             .....
             // here I want to update the icon color below based on the selected color, 
             // so I want this function return a new ButtonProps with the updated icon color
        },
        iconColor: 'red'   // I can only hard-code this color for now
    );
}

Basically, onColorCellSelected is the click event handler when a color cell is selected in the color picker, so I want to change the icon color dynamically, based on the current selected color.

I know that I can make this component reactive/observable by converting it to a tsx file where we can handle the rendering logic on my own and use state/store, but given that the rendering logic is done by a third party, I am wondering if there is a way to just have the function re-do its job when a new color is selected?

1 Answers

This should get you headed in the right direction -- using mapped generics with React TSX definitions for various HTMLElements. Here is the Button being "unwrapped" -- all props become conditional and all are accessible

export type UnwrapButtonProps<
  T extends keyof ButtonHTMLAttributes<HTMLButtonElement>
> = {
  [P in T]?: ButtonHTMLAttributes<HTMLButtonElement>[P];
};


export const getColorPickerControl = ({
  color,
  style,
  onChange,
  ...props
}: UnwrapButtonProps<
  'color' | 'onSelect' | 'onSelectCapture' | 'onChange' | 'style'
>) => {

};

This logic is applicable to any Type that has children props (SVG, Anchor, Input, etc etc etc)

If it's icon color you're after, assuming they're svgs, then this will also be a useful helper function:

export type SVGAttribs<T extends keyof SVGAttributes<SVGSVGElement>> = {
  [P in T]?: SVGAttributes<SVGSVGElement>[P];
};

Here's an example of the SVGAttribs type in action for an "X" icon for users to close given views

import { SVGAttribs } from '@/types/index';

const X = ({
  ...props
}: SVGAttribs<
  'className' | 'width' | 'height' | 'onMouseOver' | 'aria-hidden'
>) => {
  return (
    <svg
      width='24'
      height='24'
      viewBox='0 0 24 24'
      fill='none'
      xmlns='http://www.w3.org/2000/svg'
      {...props}>
      <path
        d='M6 6L18 18M6 18L18 6L6 18Z'
        stroke='rgb(29, 78, 216)'
        strokeWidth='2'
        className='ring-2 ring-blue-700'
        strokeLinecap='round'
        strokeLinejoin='round'
      />
    </svg>
  );
};

export default X;

Note how by only using ...props as the returned value all properties defined within the <"" | ""> of SVGAttribs are now conditionally available for consumption with this Component, anywhere in the project

Conditional Unwrapped Props

Here is an example of the X component having those props called inline

                <div className='relative z-10 flex items-center lg:hidden'>
                  {/* Mobile menu button */}
                  <Disclosure.Button
                    key={4 ** 5 / 2 ** 3 - 1}
                    className='rounded-md p-2 inline-flex items-center justify-center text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-600'>
                    <span className='sr-only'>Open menu</span>
                    {open ? (
                      <X className='block h-6 w-6' height='24' width='24' onMouseOver={(e) => {
                        // additional logic for onMouseOverEvent, etc.
                      }}  aria-hidden='true' />
                    ) : (
                      <MenuStaggered
                        className='block h-6 w-6'
                        aria-hidden='true'
                      />
                    )}
                  </Disclosure.Button>
                </div>

as for conditionally changing color, you'll need to do that by mapping elements that the user will toggle to change to a given color. Something like this

          {page?.afcSocial?.afcTemplate?.map((heroImg, i) => (
            <div
              key={i++}
              className={cn(
                'bg-white max-w-none bg-current',
                i === 0
                  ? 'bg-[#D2D2D2]'
                  : i === 1
                  ? 'bg-[#0AA8C4]'
                  : i === 2
                  ? 'bg-[#BFDAED]'
                  : ''
              )}>
              <div className='max-w-2xl mx-auto sm:px-6 lg:max-w-8xl'>
                <div className='relative overflow-hidden h-[160px] lg:h-[210px] '>
                  <div className='absolute inset-0 max-w-[490px] mx-auto group'>
                    <Image
                      sizes={`${heroImg?.cta?.image?.sizes}`}
                      quality={85}
                      priority={true}
                      loader={ImageLoader}
                      placeholder='blur'
                      objectFit='cover'
                      layout='responsive'
                      blurDataURL={blurDataURLShimmer({
                        w: 203.38,
                        h: 479.97
                      })}
                      className=' h-full object-center max-w-[490px] group-hover:bg-opacity-75 duration-150 animated fadeIn'
                      src={`${heroImg?.cta?.image?.sourceUrl}`}
                      alt={`${heroImg?.cta?.image?.altText}`}
                      width={`${479.97}`}
                      height={`${203.38}`}
                    />
                  </div>

                  <div
                    className={cn(
                      'relative max-w-[130px] lg:max-w-[190px] mx-auto flex flex-col left-[7rem] lg:left-[8rem]  lg:px-0',
                      i === 0 ? 'text-[#00588F]' : 'text-white'
                    )}>
                    <div
                      className={cn(
                        ' tracking-tight text-white lg:text-2xl absolute mt-5 md:mt-0 origin-right right-[0%]',
                        i === 0 ? 'text-[#00588F]' : 'text-white'
                      )}>
                      <div className='inline-flex flex-shrink font-medium align-middle text-[18px] w-[160px] sm:w-[190px] md:mt-2 '>
                        <p>{flow[i].icon}</p>&nbsp;
                        <p className='mt-2 my-auto align-middle'>
                          {' '}
                          <SocialChevron
                            className={cn(
                              i === 0 ? `fill-[#00588F]` : 'white',
                              ''
                            )}
                          />
                        </p>
                        &nbsp;&nbsp;&nbsp;
                        <p
                          className={cn(
                            'sm:text-[20px] text-[18.6px] mt-1 mr-0.25 leading-[24px] ',
                            i === 1 ? 'max-w-[83px] sm:max-w-none' : ''
                          )}>
                          {heroImg?.cta?.caption}
                        </p>
                      </div>
                      <p
                        className={cn(
                          'lg:mt-4 mt-2.5 md:text-[16px] md:max-w-[275px] text-[12.25px] leading-[16px] md:leading-[20.26px] text-center font-normal',
                          i === 0
                            ? 'w-[160px] md:max-w-[275px] text-[#00588F]'
                            : i === 1
                            ? 'w-[200px] md:max-w-[380px] text-[#e5e5e5]'
                            : i === 2
                            ? 'w-[200px] md:max-w-[340px] text-[#e5e5e5]'
                            : ''
                        )}>
                        {heroImg?.cta?.description}
                      </p>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          ))}

Note: The cn value is classnames, imported from the classnames package

Related