LoadingButton.tsx 3.22 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
/**
 * See the NOTICE file distributed with this work for additional information
 * regarding copyright ownership.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import React, { useState, useRef, ReactNode, useEffect } from 'react';
import { of, from, merge, timer, combineLatest, Subscription } from 'rxjs';
import { tap, mergeMap, delay, map, take, catchError } from 'rxjs/operators';

import ControlledLoadingButton from './ControlledLoadingButton';

import { LoadingState } from 'src/shared/types/loading-state';

type LoadingButtonProps = {
  onClick: () => Promise<unknown>;
  onSuccess?: (x?: unknown) => void;
  onError?: (x?: unknown) => void;
  isDisabled?: boolean;
Andrey Azov's avatar
Andrey Azov committed
30 31 32 33
  classNames?: {
    wrapper?: string;
    button?: string;
  };
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
  children: ReactNode;
};

type RequestHandlerParams = {
  loader: () => Promise<unknown>;
  onSuccess?: (x?: unknown) => void;
  onError?: (x?: unknown) => void;
};

const MINIMUM_SPINNER_TIME = 1000; // 1 second
const COMPLETION_INDICATOR_TIME = 2000; // 2 seconds

const getLoadingState$ = (params: RequestHandlerParams) => {
  const loadingStart$ = of(LoadingState.LOADING);
  const request$ = combineLatest([
    timer(MINIMUM_SPINNER_TIME).pipe(take(1)),
    from(params.loader()).pipe(
      map((result) => ({
        status: LoadingState.SUCCESS as const,
        result
      })),
      catchError((error) =>
        of({
          status: LoadingState.ERROR as const,
          error
        })
      )
    )
  ]).pipe(
    tap(([, response]) => {
      if (response.status === LoadingState.SUCCESS) {
        params?.onSuccess?.(response.result);
      } else {
        params?.onError?.(response.error);
      }
    }),
    mergeMap(([, result]) => {
      return merge(of(result.status), returnToInitial$);
    })
  );
  const returnToInitial$ = of(LoadingState.NOT_REQUESTED).pipe(
    delay(COMPLETION_INDICATOR_TIME)
  );
  return merge(loadingStart$, request$);
};

const LoadingButton = (props: LoadingButtonProps) => {
  const [loadingState, setLoadingState] = useState<LoadingState>(
    LoadingState.NOT_REQUESTED
  );
  const subscriptionRef = useRef<Subscription | null>(null);

  const onClick = () => {
    const loadingState$ = getLoadingState$({
      loader: props.onClick,
      onSuccess: props.onSuccess,
      onError: props.onError
    });
    const subscription = loadingState$.subscribe(setLoadingState);
    subscriptionRef.current = subscription;
  };

  useEffect(() => {
    return () => subscriptionRef.current?.unsubscribe();
  }, []);

  return (
    <ControlledLoadingButton
      status={loadingState}
      onClick={onClick}
Andrey Azov's avatar
Andrey Azov committed
104
      classNames={props.classNames}
105
      isDisabled={props.isDisabled}
106 107 108 109 110 111 112
    >
      {props.children}
    </ControlledLoadingButton>
  );
};

export default LoadingButton;