Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hooks #85

Merged
merged 18 commits into from
Feb 29, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 14 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,22 @@ npm install --save react-placeholder

### Props

```jsx
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.element
]).isRequired,
ready: PropTypes.bool.isRequired,
delay: PropTypes.number,
firstLaunchOnly: PropTypes.bool,
showLoadingAnimation: PropTypes.bool,
type: PropTypes.oneOf(['text', 'media', 'textRow', 'rect', 'round']),
rows: PropTypes.number,
color: PropTypes.string,
customPlaceholder: PropTypes.oneOfType([
PropTypes.node,
PropTypes.element
])
```tsx
children: ReactElement | null;
ready: boolean;
delay?: number;
firstLaunchOnly?: boolean;
showLoadingAnimation?: boolean;
type?: 'text' | 'media' | 'textRow' | 'rect' | 'round';
rows?: number;
color?: string;
customPlaceholder?: ReactElement;
className?: string;
style?: CSSProperties;
```

The default props will render a `text` placeholder with `3` rows and the color `#CDCDCD`.

### Customization
If the built-in set of placeholders is not enough, you can pass you own through the prop **"customPlaceholder"**

Expand Down
60 changes: 31 additions & 29 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,41 +34,43 @@
],
"homepage": "https://github.com/buildo/react-placeholder",
"typings": "lib",
"dependencies": {
"prop-types": "^15.6.0"
},
"dependencies": {},
"devDependencies": {
"@types/enzyme": "2.8.6",
"@types/jest": "^22",
"@types/node": "9.6.4",
"@types/prop-types": "^15.5.2",
"@types/react": "^16.3.10",
"babel-loader": "^7.1.2",
"@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^9.4.0",
"@types/enzyme": "^3.10.4",
"@types/jest": "^24.0.23",
"@types/lodash": "^4.14.149",
"@types/node": "12.12.17",
"@types/react": "^16.9.16",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"babel-preset-buildo": "^0.1.1",
"css-loader": "^0.28.5",
"enzyme": "^3.2.0",
"enzyme-adapter-react-16": "^1.1.0",
"file-loader": "^1.1.5",
"jest": "^22",
"node-sass": "^4.8.3",
"progress-bar-webpack-plugin": "^1.10.0",
"raf": "^3.4.0",
"css-loader": "^0.28.11",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.15.1",
"file-loader": "^1.1.11",
"jest": "^24.9.0",
"lodash": "^4.17.15",
"node-sass": "^4.13.0",
"progress-bar-webpack-plugin": "^1.12.1",
"raf": "^3.4.1",
"raw-loader": "^0.5.1",
"react": "^16",
"react-docgen-typescript": "^1.1.0",
"react-dom": "^16",
"react": "^16.8.0",
"react-docgen-typescript": "^1.16.1",
"react-dom": "^16.8.0",
"react-styleguidist": "^6.0.33",
"react-test-renderer": "^16.2.0",
"sass-loader": "^6.0.6",
"smooth-release": "^8.0.4",
"ts-jest": "^22",
"ts-loader": "^2.3.3",
"typescript": "^2.8.1",
"react-test-renderer": "^16.8.0",
"sass-loader": "^6.0.7",
"smooth-release": "^8.0.9",
"ts-jest": "^24.2.0",
"ts-loader": "^3.5.0",
"typescript": "~3.6.4",
"webpack": "3.5.5"
},
"peerDependencies": {
"react": "^0.14 || ^15 || ^16",
"react-dom": "^0.14 || ^15 || ^16"
"react": "^16.8.0",
"react-dom": "^16.8.0"
},
"jest": {
"testURL": "http://localhost",
Expand All @@ -77,7 +79,7 @@
"<rootDir>/tests/setup.js"
],
"transform": {
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(.*[.](test))[.](tsx?)$",
"moduleFileExtensions": [
Expand Down
230 changes: 122 additions & 108 deletions src/ReactPlaceholder.tsx
Original file line number Diff line number Diff line change
@@ -1,128 +1,142 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import * as placeholders from './placeholders';
import { joinClassNames } from './utils';

export type CommonProps = {
children: React.ReactNode,
children: React.ReactElement | null;
/** pass `true` when the content is ready and `false` when it's loading */
ready: boolean,
ready: boolean;
/** delay in millis to wait when passing from ready to NOT ready */
delay?: number,
delay?: number;
/** if true, the placeholder will never be rendered again once ready becomes true, even if it becomes false again */
firstLaunchOnly?: boolean,
className?: string,
style?: React.CSSProperties
}

export type Props = (CommonProps & {
/** type of placeholder to use */
type: 'text' | 'media' | 'textRow' | 'rect' | 'round',
/** number of rows displayed in 'media' and 'text' placeholders */
rows?: number,
/** color of the placeholder */
color?: string,
/** pass true to show a nice loading animation on the placeholder */
showLoadingAnimation?: boolean,
customPlaceholder?: undefined
}) | (CommonProps & {
firstLaunchOnly?: boolean;
className?: string;
style?: React.CSSProperties;
};

export type PlaceholderProps = CommonProps & {
// we have a default color, so we can set this as optional
color?: string;
// we have a default color, so we can set this as optional
BrianMitchL marked this conversation as resolved.
Show resolved Hide resolved
rows?: number;
showLoadingAnimation?: boolean;
customPlaceholder?: undefined;
};

export type CustomPlaceholderProps = CommonProps & {
BrianMitchL marked this conversation as resolved.
Show resolved Hide resolved
/** pass any renderable content to be used as placeholder instead of the built-in ones */
customPlaceholder?: React.ReactNode | React.ReactElement<{ [k: string]: any }>,
type?: undefined,
rows?: undefined,
color?: undefined,
showLoadingAnimation?: undefined
})

export default class ReactPlaceholder extends React.Component<Props> {

static propTypes = {
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.element
]).isRequired,
delay: PropTypes.number,
ready: PropTypes.bool.isRequired,
firstLaunchOnly: PropTypes.bool,
type: PropTypes.oneOf(['text', 'media', 'textRow', 'rect', 'round']),
rows: PropTypes.number,
color: PropTypes.string,
showLoadingAnimation: PropTypes.bool,
customPlaceholder: PropTypes.oneOfType([
PropTypes.node,
PropTypes.element
]),
className: PropTypes.string,
style: PropTypes.object
}

static defaultProps = {
delay: 0,
type: 'text',
color: '#CDCDCD'
}

state = {
ready: this.props.ready
}

timeout?: number;

getFiller = () => {
const {
firstLaunchOnly, children, ready, className, // eslint-disable-line no-unused-vars
type, customPlaceholder, showLoadingAnimation, ...rest
} = this.props;

const classes = showLoadingAnimation ?
['show-loading-animation', className].filter(c => c).join(' ') :
className;
customPlaceholder: React.ReactElement<{ [k: string]: any }> | null;
BrianMitchL marked this conversation as resolved.
Show resolved Hide resolved
type?: undefined;
rows?: undefined;
color?: undefined;
showLoadingAnimation?: undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I get the ?: undefined type for all of these props...

Copy link
Contributor Author

@BrianMitchL BrianMitchL Feb 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless we use type guards for these props, this seemed to be the best way to set the values as undefined/not set. Otherwise, we can't even check if they're on the props object, as the compiler will say they don't exist. This pattern is used in the current version of the library. Is there a better way to do this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I actually missed they were already defined this way in the previous version

};

type MediaPlaceholderProps = PlaceholderProps &
Omit<
React.ComponentProps<typeof placeholders.media>,
'color' | 'rows' | 'children'
> & {
type: 'media';
};

if (customPlaceholder && React.isValidElement(customPlaceholder)) {
const mergedCustomClasses = [
customPlaceholder.props.className,
classes
].filter(c => c).join(' ');
return React.cloneElement(customPlaceholder, { className: mergedCustomClasses });
} else if (customPlaceholder) {
return customPlaceholder;
}
type RectPlaceholderProps = PlaceholderProps &
Omit<React.ComponentProps<typeof placeholders.rect>, 'children'> & {
type: 'rect';
};

type RoundPlaceholderProps = PlaceholderProps &
Omit<
React.ComponentProps<typeof placeholders.round>,
'color' | 'children'
> & {
type: 'round';
};

const Placeholder = placeholders[type!];
type TextPlaceholderProps = PlaceholderProps &
Omit<
React.ComponentProps<typeof placeholders.text>,
'color' | 'rows' | 'children'
> & {
type: 'text';
};

return <Placeholder {...rest} className={classes} />;
type TextRowPlaceholderProps = PlaceholderProps &
Omit<
React.ComponentProps<typeof placeholders.textRow>,
'color' | 'children'
> & {
type: 'textRow';
};

setNotReady = () => {
const { delay } = this.props;
export type Props =
| MediaPlaceholderProps
| RectPlaceholderProps
| RoundPlaceholderProps
| TextRowPlaceholderProps
| TextPlaceholderProps
| CustomPlaceholderProps;

const ReactPlaceholder: React.FC<Props> = ({
delay = 0,
type = 'text',
color = '#CDCDCD',
rows = 3,
ready: readyProp,
firstLaunchOnly,
children,
className,
showLoadingAnimation,
customPlaceholder,
...rest
}) => {
const [ready, setReady] = React.useState(readyProp);
const timeout = React.useRef<null | number>(null);

const getFiller = (): React.ReactElement | null => {
const classes = showLoadingAnimation
? joinClassNames('show-loading-animation', className)
: className;

if (delay && delay > 0) {
this.timeout = window.setTimeout(() => {
this.setState({ ready: false });
}, delay);
} else {
this.setState({ ready: false });
if (customPlaceholder && React.isValidElement(customPlaceholder)) {
const mergedCustomClasses = [customPlaceholder.props.className, classes]
BrianMitchL marked this conversation as resolved.
Show resolved Hide resolved
.filter(c => c)
.join(' ');
return React.cloneElement(customPlaceholder, {
className: mergedCustomClasses
});
} else if (customPlaceholder) {
return customPlaceholder;
}
}

setReady = () => {
if (this.timeout) {
window.clearTimeout(this.timeout);
}
const Placeholder = placeholders[type];

return (
<Placeholder {...rest} color={color} rows={rows} className={classes} />
);
};

if (!this.state.ready) {
this.setState({ ready: true });
React.useEffect(() => {
if (!firstLaunchOnly && ready && !readyProp) {
if (delay && delay > 0) {
timeout.current = window.setTimeout(() => {
BrianMitchL marked this conversation as resolved.
Show resolved Hide resolved
setReady(false);
}, delay);
} else {
setReady(false);
}
} else if (readyProp) {
if (timeout.current) {
window.clearTimeout(timeout.current);
}

if (!ready) {
setReady(true);
}
}
}
}, [firstLaunchOnly, ready, readyProp, delay]);

render() {
return this.state.ready ? this.props.children : this.getFiller();
}
return ready ? children : getFiller();
};

componentWillReceiveProps(nextProps: Props) {
if (!this.props.firstLaunchOnly && this.state.ready && !nextProps.ready) {
this.setNotReady();
} else if (nextProps.ready) {
this.setReady();
}
}
}
export default ReactPlaceholder;
Loading