UNPKG

5.62 kBTypeScriptView Raw
1import React, { Component, ComponentClass } from 'react';
2import { compose } from 'redux';
3import { cloneDeep, get, isEqual, isFunction, set } from 'lodash-es';
4
5import {
6 CommerceTypes,
7 FetchDataFunction,
8 withCommerceData,
9 WithCommerceProps,
10 WithCommerceProviderProps,
11 withReviewData,
12 WithReviewProps,
13 WithReviewState
14} from '@brandingbrand/fscommerce';
15
16// TODO: This should move into fscommerce
17export type CommerceToReviewMapFunction<
18 T extends CommerceTypes.Product = CommerceTypes.Product
19> = (product: T) => string;
20
21/**
22 * Additional props that are consumed by the high order component.
23 *
24 * @template T The type of product data that will be provided. Defaults to `Product`
25 */
26export interface WithProductDetailProviderProps<
27 T extends CommerceTypes.Product = CommerceTypes.Product
28> extends WithCommerceProviderProps<T>, WithReviewProps {
29 commerceToReviewMap: string | CommerceToReviewMapFunction<T>;
30}
31
32/**
33 * Additional props that will be provided to the wrapped component.
34 *
35 * @template T The type of product data that will be provided. Defaults to `Product`
36 */
37export type WithProductDetailProps<
38 T extends CommerceTypes.Product = CommerceTypes.Product
39> = WithCommerceProps<T> & WithReviewState;
40
41/**
42 * The state of the ProductDetailProvider component which is passed to the wrapped component as a
43 * prop.
44 *
45 * @template T The type of product data that will be provided. Defaults to `Product`
46 */
47export type WithProductDetailState<
48 T extends CommerceTypes.Product = CommerceTypes.Product
49> = Pick<WithCommerceProps<T>, 'commerceData'>;
50
51/**
52 * A function that wraps a a component and returns a new high order component. The wrapped
53 * component will be given product detail data as props.
54 *
55 * @template T The type of product data that will be provided. Defaults to `Product`
56 *
57 * @param {ComponentClass<P & WithProductDetailProps>} WrappedComponent A component to wrap and
58 * provide product detail data to as props.
59 * @returns {ComponentClass<P & WithProductDetailProviderProps>} A high order component.
60 */
61export type ProductDetailWrapper<P, T extends CommerceTypes.Product = CommerceTypes.Product> = (
62 WrappedComponent: ComponentClass<P & WithProductDetailProps<T>>
63) => ComponentClass<P & WithProductDetailProviderProps<T>>;
64
65/**
66 * Returns a function that wraps a component and returns a new high order component. The wrapped
67 * component will be given product detail data as props.
68 *
69 * @template P The original props of the wrapped component. They'll be passed through unmodified.
70 * @template T The type of product data that will be provided. Defaults to `Product`
71 *
72 * @param {FetchDataFunction<P, T>} fetchProduct A function that will return product data.
73 * @param {Function} fetchReview A function that will return review data.
74 * @returns {ProductDetailWrapper<P>} A function that wraps a component and returns a new high order
75 * component.
76 */
77export default function withProductDetailData<
78 P,
79 T extends CommerceTypes.Product = CommerceTypes.Product
80>(fetchProduct: FetchDataFunction<P, T>, fetchReview: Function): ProductDetailWrapper<P, T> {
81 type ResultProps = P &
82 WithProductDetailProviderProps<T> &
83 WithCommerceProps<T> &
84 WithReviewState;
85
86 /**
87 * A function that wraps a a component and returns a new high order component. The wrapped
88 * component will be given product detail data as props.
89 *
90 * @param {ComponentClass<P & WithProductDetailProps>} WrappedComponent A component to wrap and
91 * provide product detail data to as props.
92 * @returns {ComponentClass<P & WithProductDetailProviderProps>} A high order component.
93 */
94 return (WrappedComponent: ComponentClass<P & WithProductDetailProps<T>>) => {
95 class ProductDetailProvider extends Component<ResultProps, WithProductDetailState<T>> {
96 // TODO: This should be replaced with getDerivedStateFromProps
97 componentWillReceiveProps(nextProps: ResultProps): void {
98 const { commerceData, commerceToReviewMap, reviewProviderDoUpdate } = this.props;
99
100 const getReviewId = (product: T) => {
101 if (isFunction(commerceToReviewMap)) {
102 return commerceToReviewMap(product);
103 } else if ('string' === typeof commerceToReviewMap) {
104 return get(product, commerceToReviewMap);
105 }
106
107 // Default to the product id
108 return product.id;
109 };
110
111 if (nextProps.commerceData) {
112 if (!isEqual(commerceData, nextProps.commerceData) && reviewProviderDoUpdate) {
113 // CommerceData has changed, update review data
114 const ids = getReviewId(nextProps.commerceData);
115 reviewProviderDoUpdate({ ids });
116 } else if (nextProps.reviewsData && nextProps.reviewsData.length) {
117 // Merge commerce and reviews data
118 const newCommerceData = cloneDeep(nextProps.commerceData);
119 set(newCommerceData, 'review', nextProps.reviewsData[0]);
120 this.setState({ commerceData: newCommerceData });
121 }
122 }
123 }
124
125 render(): JSX.Element {
126 const {
127 commerceToReviewMap,
128 ...props
129 } = this.props as any; // TypeScript does not support rest parameters for generics :(
130
131 return (
132 <WrappedComponent
133 {...props}
134 commerceData={(this.state && this.state.commerceData) || this.props.commerceData}
135 />
136 );
137 }
138 }
139
140 return compose<ComponentClass<P & WithProductDetailProviderProps<T>>>(
141 withCommerceData<P, T>(fetchProduct),
142 withReviewData(fetchReview)
143 )(ProductDetailProvider);
144 };
145}