├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .watchmanconfig ├── LICENSE ├── README.md ├── example ├── dist.js ├── example.js ├── index.html └── volunteer.png ├── index.d.ts ├── lib └── index.js ├── package-lock.json ├── package.json ├── src └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-0" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | 8 | "parser": "babel-eslint", 9 | "plugins": [ 10 | "react" 11 | ], 12 | 13 | "ecmaFeatures": { 14 | "modules": true, 15 | "jsx": true 16 | }, 17 | 18 | "rules": { 19 | "strict": [2, "global"], 20 | 21 | "comma-dangle": [2, "always-multiline"], 22 | "no-floating-decimal": 2, 23 | "no-use-before-define": [2, "nofunc"], 24 | 25 | "indent": [2, 2, {"indentSwitchCase": true}], 26 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 27 | "comma-style": [2, "last"], 28 | "consistent-this": [2, "self"], 29 | "consistent-return": 0, 30 | "curly": [2, "multi-line"], 31 | "new-cap": [2, {"newIsCap": true, "capIsNew": false}], 32 | "object-curly-spacing": [2, "never"], 33 | "quote-props": [2, "as-needed"], 34 | "quotes": [2, "single", "avoid-escape"], 35 | "space-after-function-name": [2, "never"], 36 | "space-after-keywords": [2, "always"], 37 | "space-before-blocks": [2, "always"], 38 | "space-in-brackets": [2, "never"], 39 | "space-in-parens": [2, "never"], 40 | "spaced-line-comment": [2, "always"], 41 | "yoda": [2, "never"], 42 | 43 | "no-var": 2, 44 | "generator-star-spacing": [2, "before"], 45 | 46 | "valid-jsdoc": [2, { 47 | "prefer": { 48 | "return": "returns" 49 | } 50 | }], 51 | 52 | "react/jsx-boolean-value": [2, "never"], 53 | "react/jsx-no-undef": 2, 54 | "react/jsx-quotes": [2, "double", "avoid-escape"], 55 | "react/jsx-uses-react": 1, 56 | "react/jsx-uses-vars": 1, 57 | "react/no-did-mount-set-state": 1, 58 | "react/no-did-update-set-state": 1, 59 | "react/no-unknown-property": 1, 60 | "react/prop-types": 2, 61 | "react/self-closing-comp": 2, 62 | "react/sort-comp": 2, 63 | "react/wrap-multilines": 2 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .eslintrc 3 | .babelrc 4 | src/ 5 | example/ 6 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": [ 3 | ".git", 4 | "node_modules" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 DeedMob -- MIT License 2 | 3 | Copyright 2014 HZDG 4 | http://hzdg.com 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-load-image 2 | ================= 3 | ![](https://img.shields.io/npm/dw/react-load-image.svg) 4 | ![](https://img.shields.io/npm/l/react-load-image.svg) 5 | ![](https://img.shields.io/npm/v/react-load-image.svg) 6 | 7 | This is a fork of https://github.com/hzdg/react-imageloader, however many design changes were made and deprecations fixed so it warranted its own repo/package. 8 | 9 | This React component allows you to display 10 | content while waiting for the image to load, as well as by showing alternate 11 | content if the image fails to load. 12 | 13 | Installing 14 | ----- 15 | `npm i react-load-image` 16 | 17 | 18 | Usage 19 | ----- 20 | 21 | ```js 22 | import React from 'react'; 23 | import ImageLoader from 'react-load-image'; 24 | 25 | function Preloader(props) { 26 | return ; 27 | } 28 | 29 | React.render(( 30 | 33 | 34 |
Error!
35 | 36 |
37 | ), document.body); 38 | 39 | ``` 40 | 41 | 42 | Props 43 | ----- 44 | 45 | Name | Type | Description 46 | ------------|----------|------------ 47 | `onError` | function | An optional handler for the [error] event. 48 | `onLoad` | function | An optional handler for the [load] event. 49 | `src` | string | The URL of the image to be loaded, will be passed as the src prop to your first child provided. If you want to use it as a background image, make your first child a react component like Name = (props) =>
and do 50 | `srcSet` | string | An optional value for the srcset attribute of the img 51 | 52 | 53 | Children 54 | -------- 55 | The first child of `ImageLoader` will be rendered when the image is successfully loaded. The `src` prop will be passed to it. 56 | 57 | The second child of `ImageLoader` will be rendered when the image load fails. 58 | 59 | The third child of `ImageLoader` will be rendered when the image is in the process of loading 60 | 61 | 62 | Avoiding duplication Example 63 | ------- 64 | ```js 65 | import React from 'react'; 66 | import ImageLoader from 'react-load-image'; 67 | import ImageError from './ImageError'; 68 | import ImageLoading from './ImageLoading'; 69 | 70 | const Image = (props) => 71 | 72 | {this.props.children[0]} 73 | 74 | 75 | 76 | 77 | export default Image; 78 | ``` 79 | ----- 80 | ```js 81 | import Image from './Image'; 82 | 83 | ... 84 | 85 | 86 | 87 | ... 88 | 89 | ``` 90 | 91 | 92 | Using it as a backgroundImage 93 | ----- 94 | ```js 95 | import React from 'react'; 96 | 97 | const BackgroundImage = ({src, style = {}, ...props} = {}) => 98 |
99 | 100 | export default BackgroundImage; 101 | ``` 102 | 103 | ```js 104 | 105 | 106 | 107 | ``` 108 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import ImageLoader from '../lib'; 4 | 5 | const Preloader = () =>
SPINNNNER
6 | 7 | render( 8 | 9 | 10 |
Error!
11 | 12 |
, 13 | document.getElementById('app') 14 | ); 15 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | react-load-image demo 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/volunteer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeedMob/react-load-image/0e70a6f6489369a4126210671a2f0541f93411db/example/volunteer.png -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export = ImageLoader; 4 | 5 | interface ImageLoaderProps { 6 | /** 7 | * The URL of the image to be loaded, will be passed as the src prop to your 8 | * first child provided. If you want to use it as a background image, make 9 | * your first child a react component like 10 | * `(props) =>
` 11 | */ 12 | src: string; 13 | /** 14 | * The first child of `ImageLoader` will be rendered when the image is successfully loaded. The `src` prop will be passed to it. 15 | * The second child of `ImageLoader` will be rendered when the image load fails. 16 | * The third child of `ImageLoader` will be rendered when the image is in the process of loading 17 | */ 18 | children: React.ReactNode; 19 | /** An optional value for the srcset attribute of the img */ 20 | srcSet?: string; 21 | /** An optional handler for the [load] event. */ 22 | onLoad?: (this: GlobalEventHandlers, ev: Event) => any; 23 | /** An optional handler for the [error] event. */ 24 | onError?: OnErrorEventHandler; 25 | /** Wrapper container class name */ 26 | className?: string; 27 | /** Optional attributes of wrapper container */ 28 | wrapperProps?: Record; 29 | } 30 | 31 | declare var ImageLoader: React.ComponentType; 32 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _propTypes = require('prop-types'); 16 | 17 | var _propTypes2 = _interopRequireDefault(_propTypes); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 20 | 21 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 22 | 23 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 24 | 25 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 26 | 27 | var Status = { 28 | PENDING: 'pending', 29 | LOADING: 'loading', 30 | LOADED: 'loaded', 31 | FAILED: 'failed' 32 | }; 33 | 34 | var ImageLoader = function (_React$Component) { 35 | _inherits(ImageLoader, _React$Component); 36 | 37 | function ImageLoader(props) { 38 | _classCallCheck(this, ImageLoader); 39 | 40 | var _this = _possibleConstructorReturn(this, (ImageLoader.__proto__ || Object.getPrototypeOf(ImageLoader)).call(this, props)); 41 | 42 | _this.state = { status: props.src ? Status.LOADING : Status.PENDING }; 43 | if (_react2.default.Children.count(props.children) !== 3) console.error('wrong # of children provided to ImageLoader'); 44 | return _this; 45 | } 46 | 47 | _createClass(ImageLoader, [{ 48 | key: 'componentDidMount', 49 | value: function componentDidMount() { 50 | if (this.state.status === Status.LOADING) { 51 | this.createLoader(); 52 | } 53 | } 54 | }, { 55 | key: 'componentWillReceiveProps', 56 | value: function componentWillReceiveProps(nextProps) { 57 | if (this.props.src !== nextProps.src) { 58 | this.setState({ 59 | status: nextProps.src ? Status.LOADING : Status.PENDING 60 | }); 61 | } 62 | } 63 | }, { 64 | key: 'componentDidUpdate', 65 | value: function componentDidUpdate() { 66 | if (this.state.status === Status.LOADING && !this.img) { 67 | this.createLoader(); 68 | } 69 | } 70 | }, { 71 | key: 'componentWillUnmount', 72 | value: function componentWillUnmount() { 73 | this.destroyLoader(); 74 | } 75 | }, { 76 | key: 'createLoader', 77 | value: function createLoader() { 78 | this.destroyLoader(); // We can only have one loader at a time. 79 | 80 | var img = new Image(); 81 | img.onload = this.handleLoad.bind(this); 82 | img.onerror = this.handleError.bind(this); 83 | img.src = this.props.src; 84 | 85 | // if srcSet is not passed in then use src for srcset 86 | // Setting srcset to a non-string is a bad idea. E.g. img.srcset = undefined actually sets srcset to the string "undefined", causing a load failure) 87 | img.srcset = this.props.srcSet || this.props.src; 88 | this.img = img; 89 | } 90 | }, { 91 | key: 'destroyLoader', 92 | value: function destroyLoader() { 93 | if (this.img) { 94 | this.img.onload = null; 95 | this.img.onerror = null; 96 | this.img = null; 97 | } 98 | } 99 | }, { 100 | key: 'handleLoad', 101 | value: function handleLoad(event) { 102 | this.destroyLoader(); 103 | this.setState({ status: Status.LOADED }); 104 | 105 | if (this.props.onLoad) this.props.onLoad(event); 106 | } 107 | }, { 108 | key: 'handleError', 109 | value: function handleError(error) { 110 | this.destroyLoader(); 111 | this.setState({ status: Status.FAILED }); 112 | 113 | if (this.props.onError) this.props.onError(error); 114 | } 115 | }, { 116 | key: 'getClassName', 117 | value: function getClassName() { 118 | var className = 'imageloader imageloader-' + this.state.status; 119 | if (this.props.className) className = className + ' ' + this.props.className; 120 | return className; 121 | } 122 | }, { 123 | key: 'render', 124 | value: function render() { 125 | var _props = this.props, 126 | src = _props.src, 127 | srcSet = _props.srcSet, 128 | onLoad = _props.onLoad, 129 | onError = _props.onError, 130 | wrapperProps = _props.wrapperProps, 131 | children = _props.children; 132 | 133 | var childrenArray = _react2.default.Children.toArray(children); 134 | 135 | return _react2.default.createElement( 136 | 'div', 137 | _extends({}, wrapperProps, { className: this.getClassName() }), 138 | this.state.status === Status.LOADED && _react2.default.cloneElement(childrenArray[0], { src: src, srcSet: srcSet }), 139 | this.state.status === Status.FAILED && childrenArray[1], 140 | (this.state.status === Status.LOADING || this.state.status === Status.PENDING) && childrenArray[2] 141 | ); 142 | } 143 | }]); 144 | 145 | return ImageLoader; 146 | }(_react2.default.Component); 147 | 148 | ImageLoader.propTypes = { 149 | src: _propTypes2.default.string.isRequired, 150 | srcSet: _propTypes2.default.string, 151 | onLoad: _propTypes2.default.func, 152 | onError: _propTypes2.default.func, 153 | children: _propTypes2.default.arrayOf(_propTypes2.default.node) 154 | // Allow any extras 155 | }; 156 | exports.default = ImageLoader; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-load-image", 3 | "version": "0.1.7", 4 | "description": "A React component for showing placeholder content during image loading", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "build:example": "webpack example/example.js example/dist.js", 8 | "build:commonjs": "babel src --out-dir lib", 9 | "build": "npm run build:commonjs", 10 | "prepublish": "npm run build", 11 | "watch": "onchange 'src/**/*.js' -- npm run build" 12 | }, 13 | "pre-commit": [ 14 | "build" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/DeedMob/react-load-image.git" 19 | }, 20 | "keywords": [ 21 | "react-component", 22 | "react", 23 | "loader", 24 | "component" 25 | ], 26 | "author": "David Furlong ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/DeedMob/react-load-image/issues" 30 | }, 31 | "homepage": "https://github.com/DeedMob/react-load-image", 32 | "devDependencies": { 33 | "babel-cli": "^6.24.0", 34 | "babel-core": "^6.25.0", 35 | "babel-eslint": "^3.1.16", 36 | "babel-loader": "^7", 37 | "babel-preset-es2015": "^6.24.1", 38 | "babel-preset-react": "^6.24.1", 39 | "babel-preset-stage-0": "^6.24.1", 40 | "babel-runtime": "^5.5.8", 41 | "chai": "^3.0.0", 42 | "eslint": "^0.23.0", 43 | "eslint-plugin-react": "^2.5.2", 44 | "express": "^4.12.4", 45 | "git-release-notes": "0.0.2", 46 | "mocha": "^2.2.5", 47 | "mversion": "^1.10.0", 48 | "node-libs-browser": "^0.5.2", 49 | "onchange": "^3.2.1", 50 | "opn": "^2.0.1", 51 | "prop-types": "^15.6.0", 52 | "react": "^16.0.0", 53 | "react-dom": "^16.0.0", 54 | "webpack": "^3.3.0", 55 | "webpack-dev-middleware": "^1.0.11", 56 | "yargs": "^3.11.0" 57 | }, 58 | "peerDependencies": { 59 | "prop-types": "^15.6.0", 60 | "react": "^16.0.0" 61 | }, 62 | "dependencies": {} 63 | } 64 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Status = { 5 | PENDING: 'pending', 6 | LOADING: 'loading', 7 | LOADED: 'loaded', 8 | FAILED: 'failed', 9 | }; 10 | 11 | export default class ImageLoader extends React.Component { 12 | static propTypes = { 13 | src: PropTypes.string.isRequired, 14 | srcSet: PropTypes.string, 15 | onLoad: PropTypes.func, 16 | onError: PropTypes.func, 17 | children: PropTypes.arrayOf(PropTypes.node), 18 | // Allow any extras 19 | }; 20 | 21 | constructor(props) { 22 | super(props); 23 | this.state = {status: props.src ? Status.LOADING : Status.PENDING}; 24 | if(React.Children.count(props.children) !== 3) 25 | console.error('wrong # of children provided to ImageLoader') 26 | } 27 | 28 | componentDidMount() { 29 | if (this.state.status === Status.LOADING) { 30 | this.createLoader(); 31 | } 32 | } 33 | 34 | componentWillReceiveProps(nextProps) { 35 | if (this.props.src !== nextProps.src) { 36 | this.setState({ 37 | status: nextProps.src ? Status.LOADING : Status.PENDING, 38 | }); 39 | } 40 | } 41 | 42 | componentDidUpdate() { 43 | if (this.state.status === Status.LOADING && !this.img) { 44 | this.createLoader(); 45 | } 46 | } 47 | 48 | componentWillUnmount() { 49 | this.destroyLoader(); 50 | } 51 | 52 | createLoader() { 53 | this.destroyLoader(); // We can only have one loader at a time. 54 | 55 | const img = new Image(); 56 | img.onload = ::this.handleLoad; 57 | img.onerror = ::this.handleError; 58 | img.src = this.props.src; 59 | 60 | // if srcSet is not passed in then use src for srcset 61 | // Setting srcset to a non-string is a bad idea. E.g. img.srcset = undefined actually sets srcset to the string "undefined", causing a load failure) 62 | img.srcset = this.props.srcSet || this.props.src; 63 | this.img = img; 64 | } 65 | 66 | destroyLoader() { 67 | if (this.img) { 68 | this.img.onload = null; 69 | this.img.onerror = null; 70 | this.img = null; 71 | } 72 | } 73 | 74 | handleLoad(event) { 75 | this.destroyLoader(); 76 | this.setState({status: Status.LOADED}); 77 | 78 | if (this.props.onLoad) this.props.onLoad(event); 79 | } 80 | 81 | handleError(error) { 82 | this.destroyLoader(); 83 | this.setState({status: Status.FAILED}); 84 | 85 | if (this.props.onError) this.props.onError(error); 86 | } 87 | 88 | getClassName() { 89 | let className = `imageloader imageloader-${this.state.status}`; 90 | if (this.props.className) className = `${className} ${this.props.className}`; 91 | return className; 92 | } 93 | 94 | 95 | render() { 96 | const { src, srcSet, onLoad, onError, wrapperProps, children } = this.props; 97 | const childrenArray = React.Children.toArray(children); 98 | 99 | return ( 100 |
101 | {this.state.status === Status.LOADED && 102 | React.cloneElement(childrenArray[0], { src, srcSet }) 103 | } 104 | {this.state.status === Status.FAILED && 105 | childrenArray[1] 106 | } 107 | {(this.state.status === Status.LOADING || this.state.status === Status.PENDING) && 108 | childrenArray[2] 109 | } 110 |
111 | ) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.js$/, 9 | exclude: /node_modules/, 10 | use: 'babel-loader' 11 | }, 12 | ], 13 | }, 14 | }; 15 | --------------------------------------------------------------------------------