UNPKG

5.4 kBTypeScriptView Raw
1import React, { Component, ComponentClass } from 'react';
2import { compose } from 'redux';
3import { cloneDeep, isEqual, set } from 'lodash-es';
4
5import {
6 CommerceTypes,
7 FetchDataFunction,
8 ReviewDataSource,
9 ReviewTypes,
10 withCommerceData,
11 WithCommerceProps,
12 WithCommerceProviderProps
13} from '@brandingbrand/fscommerce';
14
15// TODO: This should move into fscommerce
16export type CommerceToReviewMapFunction<
17 T extends CommerceTypes.Product = CommerceTypes.Product
18> = (product: T) => string;
19
20/**
21 * Additional props that are consumed by the high order component.
22 *
23 * @template T The type of product data that will be provided. Defaults to `Product`
24 */
25export interface WithProductDetailProviderProps<
26 T extends CommerceTypes.Product = CommerceTypes.Product
27> extends WithCommerceProviderProps<T> {
28 commerceToReviewMap: keyof T | CommerceToReviewMapFunction<T>;
29 reviewDataSource?: ReviewDataSource;
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> & { reviewsData?: ReviewTypes.ReviewDetails[] };
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'> & { reviewsData?: ReviewTypes.ReviewDetails[] };
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 * @returns {ProductDetailWrapper<P>} A function that wraps a component and returns a new high order
74 * component.
75 */
76export default function withProductDetailData<
77 P,
78 T extends CommerceTypes.Product = CommerceTypes.Product
79>(fetchProduct: FetchDataFunction<P, T>): ProductDetailWrapper<P, T> {
80 type ResultProps = P &
81 WithProductDetailProviderProps<T> &
82 WithCommerceProps<T>;
83
84 /**
85 * A function that wraps a a component and returns a new high order component. The wrapped
86 * component will be given product detail data as props.
87 *
88 * @param {ComponentClass<P & WithProductDetailProps>} WrappedComponent A component to wrap and
89 * provide product detail data to as props.
90 * @returns {ComponentClass<P & WithProductDetailProviderProps>} A high order component.
91 */
92 return (WrappedComponent: ComponentClass<P & WithProductDetailProps<T>>) => {
93 class ProductDetailProvider extends Component<ResultProps, WithProductDetailState<T>> {
94 async componentDidUpdate(prevProps: ResultProps): Promise<void> {
95 const { commerceToReviewMap, reviewDataSource } = this.props;
96
97 // ts isn't detecting the commerceData type correctly, so we have to assert it
98 const commerceData = this.props.commerceData as T | undefined;
99
100 if (commerceData === undefined || reviewDataSource === undefined) {
101 return;
102 }
103
104 if (!isEqual(prevProps.commerceData, commerceData)) {
105 // CommerceData has changed, update review data
106
107 const ids = reviewDataSource.productIdMapper<T>(
108 [commerceData],
109 commerceToReviewMap
110 );
111
112 const reviewsData = await reviewDataSource.fetchReviewDetails({ ids });
113
114 // Merge commerce and reviews data
115 const newCommerceData = cloneDeep(commerceData);
116 set(newCommerceData, 'review', reviewsData[0]);
117 this.setState({
118 commerceData: newCommerceData,
119 reviewsData
120 });
121 }
122 }
123
124 render(): JSX.Element {
125 const {
126 commerceToReviewMap,
127 ...props
128 } = this.props as any; // TypeScript does not support rest parameters for generics :(
129
130 return (
131 <WrappedComponent
132 {...props}
133 commerceData={(this.state && this.state.commerceData) || this.props.commerceData}
134 reviewsData={(this.state && this.state.reviewsData)}
135 />
136 );
137 }
138 }
139
140 return compose<ComponentClass<P & WithProductDetailProviderProps<T>>>(
141 withCommerceData<P, T>(fetchProduct)
142 )(ProductDetailProvider);
143 };
144}