├── .eslintrc ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── bower.json ├── package.json ├── src └── index.js ├── standalone └── react-imageloader.js ├── test ├── index.html ├── server.js ├── tests.js └── tiger.svg └── webpack.config.js /.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 | /node_modules/ 2 | /lib/ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /test/ 3 | /vendor/ 4 | /bower.json 5 | /.eslintrc 6 | /.babelrc 7 | /src/ 8 | /standalone/ 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.0 2 | 3 | * Assume React >= 15 4 | * React compatibility fixes (`PropTypes`, `React.createFactory`) 5 | * Pass image dimensions as props to `wrapper` 6 | * Cancel pending image request when destroyed 7 | 8 | ## 2.1.0 9 | 10 | * Add `imgProps` prop 11 | * Add a `style` prop for the `wrapper` 12 | 13 | ## 2.0.0 14 | 15 | * :construction: Rewrite in ES6(+) 16 | * Assume React >= 0.13 17 | * Remove dependency on react-loadermixin 18 | * Do loading off DOM, using a JS `Image()` instead of a React `` 19 | 20 | ## 1.2.0 21 | 22 | * Show preloader only while loading 23 | 24 | ## 1.1.3 25 | 26 | * Allow children as alternative content 27 | 28 | ## 1.1.2 29 | 30 | * Relax React requirement 31 | 32 | ## 1.1.1 33 | 34 | * Maintain compatibility with React 0.11 35 | 36 | ## 1.1.0 37 | 38 | * Compatibility with React 0.12 39 | 40 | ## 1.0.6 41 | 42 | * Make xtend a dependency 43 | 44 | ## 1.0.5 45 | 46 | * Map react to external React 47 | 48 | ## 1.0.4 49 | 50 | * Use spread to avoid React key warning 51 | 52 | ## 1.0.3 53 | 54 | * Don't render the image on the server 55 | 56 | ## 1.0.2 57 | 58 | * Don't forward ImageLoader props to underlying Img 59 | * Don't use transferPropsTo 60 | 61 | ## 1.0.1 62 | 63 | * Transfer ImageLoader props to underlying img 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 HZDG 2 | http://hzdg.com 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | node: 2 | babel --stage=0 src --out-dir lib 3 | 4 | browser: 5 | babel-node ./node_modules/.bin/webpack --config ./webpack.config.js 6 | 7 | build: node browser 8 | 9 | test: 10 | babel-node ./test/server.js 11 | 12 | dev: 13 | babel-node ./test/server.js --open 14 | 15 | major: 16 | mversion major 17 | 18 | minor: 19 | mversion minor 20 | 21 | patch: 22 | mversion patch 23 | 24 | changelog.template.ejs: 25 | @echo "## x.x.x\n\n<% commits.forEach(function(commit) { -%>\n* <%= commit.title %>\n<% }) -%>" > changelog.template.ejs 26 | 27 | changelog: changelog.template.ejs 28 | @git-release-notes $$(git describe --abbrev=0)..HEAD $< | cat - CHANGELOG.md >> CHANGELOG.md.new 29 | @mv CHANGELOG.md{.new,} 30 | @rm changelog.template.ejs 31 | @echo "Added changes since $$(git describe --abbrev=0) to CHANGELOG.md" 32 | 33 | .PHONY: dev test changelog major minor patch 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-imageloader 2 | ================= 3 | 4 | 🚨 This project is not maintained! 🚨 5 | 6 | We are no longer using this component in our day-to-day work, so unfortunately, 7 | we have neglected maintenance of it for quite some time. Among the reasons 8 | why we haven't been using this component are: 9 | 10 | - It has [some design flaws](#design-decisions-mistakes) 11 | - We've been more focus on react-native 12 | 13 | However, it may still work for you. If you are looking for something like 14 | this, but don't want to take on an unmaintained dependency, check out 15 | [this fork](https://github.com/DeedMob/react-load-image). 16 | 17 | See the [support matrix](#supported-versions-of-react) below 18 | if you are determined to use this. 19 | 20 | --- 21 | 22 | One of the hardest things to wrangle in the browser is loading. When images and 23 | other linked elements appear in the DOM, the browser makes decisions on when to 24 | load them that sometimes result in problems for a site and its users, such as 25 | [FOUC], unexpected load ordering, and degraded performance when many loads are 26 | occurring. 27 | 28 | This [React] component can improve the situation by allowing you to display 29 | content while waiting for the image to load, as well as by showing alternate 30 | content if the image fails to load. 31 | 32 | 33 | Usage 34 | ----- 35 | 36 | ```javascript 37 | import React from 'react'; 38 | import ImageLoader from 'react-imageloader'; 39 | 40 | function preloader() { 41 | return ; 42 | } 43 | 44 | React.render(( 45 | 49 | Image load failed! 50 | 51 | ), document.body); 52 | 53 | ``` 54 | 55 | 56 | Props 57 | ----- 58 | 59 | Name | Type | Description 60 | ------------|----------|------------ 61 | `className` | string | An optional class name for the `wrapper` component. 62 | `imgProps` | object | An optional object containing props for the underlying `img` component. 63 | `onError` | function | An optional handler for the [error] event. 64 | `onLoad` | function | An optional handler for the [load] event. 65 | `preloader` | function | An optional function that returns a React element to be shown while the image loads. 66 | `src` | string | The URL of the image to be loaded. 67 | `style` | object | An optional object containing styles for the `wrapper` component. 68 | `wrapper` | function | A function that takes a props argument and returns a React element to be used as the wrapper component. Defaults to `React.createFactory('span')`. 69 | 70 | 71 | Children 72 | -------- 73 | 74 | Children passed to `ImageLoader` will be rendered *only if* the image fails to load. Children are essentially alternate content to show when the image is missing or unavailable. 75 | 76 | For example: 77 | 78 | ```javascript 79 | 80 | React.createClass({ 81 | // This will only show if "notgonnaload.jpg" doesn't load. 82 | errorMessage() { 83 | return ( 84 |
85 |

Something went wrong!

86 |

Not gonna load "notgonnaload.jpg". bummer.

87 |
88 | ); 89 | }, 90 | render() { 91 | return ( 92 | 93 | {this.errorMessage()} 94 | 95 | ); 96 | } 97 | }) 98 | 99 | ``` 100 | 101 | Design Decisions (mistakes?) 102 | ---------------------------- 103 | Since v2.0, loading is done 'off DOM' in a JavaScript `Image()` (instead of 104 | hidden in the DOM via a React ``), so values passed to the `onLoad` 105 | and `onError` callbacks will be the browser native values, not React's 106 | synthesized values. While this shouldn't be a problem for the vast majority 107 | of use cases, it can cause weirdness when browser caching is disabled 108 | (i.e., images loading twice, preloaders disappearing before the image is ready). 109 | 110 | Supported versions of React 111 | --------------------------- 112 | 113 | React | ImageLoader 114 | -------------|------------ 115 | <0.13 | 1.x 116 | >=0.13, <15 | 2.x 117 | >=15 | 3.x 118 | 119 | [FOUC]: http://en.wikipedia.org/wiki/FOUC 120 | [React]: http://facebook.github.io/react/ 121 | [load]: https://developer.mozilla.org/en-US/docs/Web/Events/load 122 | [error]: https://developer.mozilla.org/en-US/docs/Web/Events/error 123 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-imageloader", 3 | "version": "3.0.0", 4 | "authors": [ 5 | "Eric Eldredge " 6 | ], 7 | "description": "A React component for wrangling image loading", 8 | "main": "./standalone/react-imageloader.js", 9 | "keywords": [ 10 | "react-component", 11 | "react", 12 | "loader", 13 | "component" 14 | ], 15 | "license": "MIT", 16 | "homepage": "https://github.com/hzdg/react-imageloader", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "vendor", 21 | "test" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-imageloader", 3 | "version": "3.0.0", 4 | "description": "A React component for wrangling image loading", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "prepare": "make node", 8 | "test": "make test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/hzdg/react-imageloader.git" 13 | }, 14 | "keywords": [ 15 | "react-component", 16 | "react", 17 | "loader", 18 | "component" 19 | ], 20 | "author": "Eric Eldredge ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/hzdg/react-imageloader/issues" 24 | }, 25 | "homepage": "https://github.com/hzdg/react-imageloader", 26 | "devDependencies": { 27 | "babel": "^5.5.8", 28 | "babel-core": "^5.5.8", 29 | "babel-eslint": "^3.1.16", 30 | "babel-loader": "^5.1.4", 31 | "babel-runtime": "^5.5.8", 32 | "chai": "^3.0.0", 33 | "eslint": "^0.23.0", 34 | "eslint-plugin-react": "^2.5.2", 35 | "express": "^4.12.4", 36 | "git-release-notes": "0.0.2", 37 | "mocha": "^2.2.5", 38 | "mversion": "^1.10.0", 39 | "node-libs-browser": "^0.5.2", 40 | "opn": "^2.0.1", 41 | "prop-types": "^15.6.0", 42 | "react": "^15.6.2", 43 | "react-dom": "^15.6.2", 44 | "webpack": "^1.9.11", 45 | "webpack-dev-middleware": "^1.0.11", 46 | "yargs": "^3.11.0" 47 | }, 48 | "peerDependencies": { 49 | "react": ">=15 || >=16", 50 | "prop-types": ">=15" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /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 | 12 | export default class ImageLoader extends React.Component { 13 | static propTypes = { 14 | wrapper: PropTypes.func, 15 | className: PropTypes.string, 16 | style: PropTypes.object, 17 | preloader: PropTypes.func, 18 | src: PropTypes.string, 19 | onLoad: PropTypes.func, 20 | onError: PropTypes.func, 21 | imgProps: PropTypes.object, 22 | }; 23 | 24 | static defaultProps = { 25 | wrapper: React.createFactory('span'), 26 | }; 27 | 28 | constructor(props) { 29 | super(props); 30 | this.state = {status: props.src ? Status.LOADING : Status.PENDING}; 31 | } 32 | 33 | componentDidMount() { 34 | if (this.state.status === Status.LOADING) { 35 | this.createLoader(); 36 | } 37 | } 38 | 39 | componentWillReceiveProps(nextProps) { 40 | if (this.props.src !== nextProps.src) { 41 | this.setState({ 42 | status: nextProps.src ? Status.LOADING : Status.PENDING, 43 | }); 44 | } 45 | } 46 | 47 | componentDidUpdate() { 48 | if (this.state.status === Status.LOADING && !this.img) { 49 | this.createLoader(); 50 | } 51 | } 52 | 53 | componentWillUnmount() { 54 | this.destroyLoader(); 55 | } 56 | 57 | getClassName() { 58 | let className = `imageloader ${this.state.status}`; 59 | if (this.props.className) className = `${className} ${this.props.className}`; 60 | return className; 61 | } 62 | 63 | createLoader() { 64 | this.destroyLoader(); // We can only have one loader at a time. 65 | 66 | this.img = new Image(); 67 | this.img.onload = ::this.handleLoad; 68 | this.img.onerror = ::this.handleError; 69 | this.img.src = this.props.src; 70 | } 71 | 72 | destroyLoader() { 73 | if (this.img) { 74 | this.img.src = ''; 75 | this.img.onload = null; 76 | this.img.onerror = null; 77 | this.img = null; 78 | } 79 | } 80 | 81 | handleLoad(event) { 82 | const { naturalWidth, naturalHeight } = event.target; 83 | this.destroyLoader(); 84 | this.setState({ status: Status.LOADED, width: naturalWidth, height: naturalHeight }); 85 | 86 | if (this.props.onLoad) this.props.onLoad(event); 87 | } 88 | 89 | handleError(error) { 90 | this.destroyLoader(); 91 | this.setState({status: Status.FAILED}); 92 | 93 | if (this.props.onError) this.props.onError(error); 94 | } 95 | 96 | renderImg() { 97 | const {src, imgProps} = this.props; 98 | let props = {src}; 99 | 100 | for (let k in imgProps) { 101 | if (imgProps.hasOwnProperty(k)) { 102 | props[k] = imgProps[k]; 103 | } 104 | } 105 | 106 | return ; 107 | } 108 | 109 | render() { 110 | const { width, height } = this.state; 111 | let wrapperProps = { 112 | width, 113 | height, 114 | className: this.getClassName() 115 | }; 116 | 117 | if (this.props.style) { 118 | wrapperProps.style = this.props.style; 119 | } 120 | 121 | let wrapperArgs = [wrapperProps]; 122 | 123 | switch (this.state.status) { 124 | case Status.LOADED: 125 | wrapperArgs.push(this.renderImg()); 126 | break; 127 | 128 | case Status.FAILED: 129 | if (this.props.children) wrapperArgs.push(this.props.children); 130 | break; 131 | 132 | default: 133 | if (this.props.preloader) wrapperArgs.push(this.props.preloader()); 134 | break; 135 | } 136 | 137 | return this.props.wrapper(...wrapperArgs); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /standalone/react-imageloader.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("React")):"function"==typeof define&&define.amd?define(["React"],t):"object"==typeof exports?exports.ReactImageLoader=t(require("React")):e.ReactImageLoader=t(e.React)}(this,function(e){return function(e){function t(n){if(r[n])return r[n].exports;var o=r[n]={exports:{},id:n,loaded:!1};return e[n].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var r={};return t.m=e,t.c=r,t.p="",t(0)}([function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var a=function(){function e(e,t){for(var r=0;r1)for(var r=1;rs&&(i(!1,"You are manually calling a React.PropTypes validation function for the `%s` prop on `%s`. This is deprecated and will throw in the standalone `prop-types` package. You may be seeing this warning due to a third-party PropTypes library. See https://fb.me/react-warning-dont-call-proptypes for details.",y,p),a[v]=!0,s++)}return null==c[f]?n?new l(null===c[f]?"The "+d+" `"+y+"` is marked as required "+("in `"+p+"`, but its value is `null`."):"The "+d+" `"+y+"` is marked as required in "+("`"+p+"`, but its value is `undefined`.")):null:e(c,f,p,d,y)}if("production"!==t.env.NODE_ENV)var a={},s=0;var c=n.bind(null,!1);return c.isRequired=n.bind(null,!0),c}function d(e){function t(t,r,n,o,i,a){var u=t[r],s=k(u);if(s!==e){var c=T(u);return new l("Invalid "+o+" `"+i+"` of type "+("`"+c+"` supplied to `"+n+"`, expected ")+("`"+e+"`."))}return null}return p(t)}function y(){return p(n.thatReturnsNull)}function h(e){function t(t,r,n,o,i){if("function"!=typeof e)return new l("Property `"+i+"` of component `"+n+"` has invalid PropType notation inside arrayOf.");var a=t[r];if(!Array.isArray(a)){var s=k(a);return new l("Invalid "+o+" `"+i+"` of type "+("`"+s+"` supplied to `"+n+"`, expected an array."))}for(var c=0;cr;r++)t["_"+String.fromCharCode(r)]=r;var n=Object.getOwnPropertyNames(t).map(function(e){return t[e]});if("0123456789"!==n.join(""))return!1;var o={};return"abcdefghijklmnopqrst".split("").forEach(function(e){o[e]=e}),"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},o)).join("")?!1:!0}catch(i){return!1}}var o=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable;e.exports=n()?Object.assign:function(e,t){for(var n,u,s=r(e),c=1;c1?t-1:0),n=1;t>n;n++)r[n-1]=arguments[n];var o=0,i="Warning: "+e.replace(/%s/g,function(){return r[o++]});"undefined"!=typeof console&&console.error(i);try{throw new Error(i)}catch(a){}};o=function(e,t){if(void 0===t)throw new Error("`warning(condition, format, ...args)` requires a warning message argument");if(0!==t.indexOf("Failed Composite propType: ")&&!e){for(var r=arguments.length,n=Array(r>2?r-2:0),o=2;r>o;o++)n[o-2]=arguments[o];i.apply(void 0,[t].concat(n))}}}e.exports=o}).call(t,r(3))},function(e,t){"use strict";var r="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";e.exports=r},function(e,t,r){(function(t){"use strict";function n(e,r,n,s,c){if("production"!==t.env.NODE_ENV)for(var f in e)if(e.hasOwnProperty(f)){var l;try{o("function"==typeof e[f],"%s: %s type `%s` is invalid; it must be a function, usually from the `prop-types` package, but received `%s`.",s||"React class",n,f,typeof e[f]),l=e[f](r,f,s,n,null,a)}catch(p){l=p}if(i(!l||l instanceof Error,"%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).",s||"React class",n,f,typeof l),l instanceof Error&&!(l.message in u)){u[l.message]=!0;var d=c?c():"";i(!1,"Failed %s type: %s%s",n,l.message,null!=d?d:"")}}}if("production"!==t.env.NODE_ENV)var o=r(7),i=r(8),a=r(9),u={};e.exports=n}).call(t,r(3))},function(e,t,r){"use strict";var n=r(6),o=r(7),i=r(9);e.exports=function(){function e(e,t,r,n,a,u){u!==i&&o(!1,"Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types")}function t(){return e}e.isRequired=e;var r={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t};return r.checkPropTypes=n,r.PropTypes=r,r}}])}); -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import express from 'express'; 3 | import webpack from 'webpack'; 4 | import webpackMiddleware from 'webpack-dev-middleware'; 5 | import open from 'opn'; 6 | import yargs from 'yargs'; 7 | 8 | const argv = yargs 9 | .boolean('open', 'o', false) 10 | .argv; 11 | 12 | const webpackConfig = { 13 | devtool: 'eval', 14 | entry: {tests: path.join(__dirname, 'tests.js')}, 15 | output: {path: path.join(__dirname, 'built'), filename: '[name].js'}, 16 | module: {loaders: [{test: /\.js$/, exclude: /node_modules/, loader: 'babel?stage=0&optional[]=runtime'}]}, 17 | }; 18 | 19 | const app = express() 20 | .get('/', (req, res) => { res.sendFile(path.join(__dirname, './index.html')); }) 21 | .get('/chai.js', (req, res) => { res.sendFile(path.join(__dirname, '../node_modules/chai/chai.js')); }) 22 | .get('/mocha.js', (req, res) => { res.sendFile(path.join(__dirname, '../node_modules/mocha/mocha.js')); }) 23 | .get('/mocha.css', (req, res) => { res.sendFile(path.join(__dirname, '../node_modules/mocha/mocha.css')); }) 24 | .use('/built', webpackMiddleware(webpack(webpackConfig), {stats: {colors: true}})) 25 | .use(express.static(path.join(__dirname))); 26 | 27 | app.listen(8080, err => { 28 | if (err) throw err; 29 | console.log('server listening at 0.0.0.0:8080'); 30 | if (argv.open) open('http://0.0.0.0:8080'); 31 | }); 32 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global chai */ 3 | import ImageLoader from '../src'; 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import TestUtils from 'react-dom/test-utils'; 7 | 8 | const {assert} = chai; 9 | const nocache = (url) => `${url}?r=${Math.floor(Math.random() * Date.now() / 16)}`; 10 | 11 | 12 | describe('ReactImageLoader', () => { 13 | 14 | it('gives the wrapper a className', () => { 15 | const loader = TestUtils.renderIntoDocument(); 16 | assert(TestUtils.findRenderedDOMComponentWithClass(loader, 'imageloader')); 17 | assert(TestUtils.findRenderedDOMComponentWithClass(loader, 'test')); 18 | assert(TestUtils.findRenderedDOMComponentWithClass(loader, 'value')); 19 | }); 20 | 21 | it('uses a custom wrapper', () => { 22 | let loader = TestUtils.renderIntoDocument(); 23 | assert(TestUtils.findRenderedDOMComponentWithTag(loader, 'span')); 24 | loader = TestUtils.renderIntoDocument(); 25 | assert(TestUtils.findRenderedDOMComponentWithTag(loader, 'div')); 26 | }); 27 | 28 | it('gives the wrapper a custom style object', () => { 29 | const loader = TestUtils.renderIntoDocument(); 30 | const wrapper = TestUtils.findRenderedComponentWithType(loader, ImageLoader); 31 | assert.equal(wrapper.props.style.display, 'block', 'Expected span to be set to `display: block`'); 32 | assert.equal(wrapper.props.style.position, 'absolute', 'Expected position to be `absolute`'); 33 | assert.equal(wrapper.props.style.top, '50%', 'Expected to be positioned at 50% from top boundary'); 34 | assert.equal(wrapper.props.style.left, '50%', 'Expected to be positioned at 50% from left boundary'); 35 | assert.equal(wrapper.props.style.marginTop, '-50px', 'Expected top margin to be -50px'); 36 | assert.equal(wrapper.props.style.marginLeft, '-50px', 'Expected left margin to be -50px'); 37 | assert.equal(wrapper.props.style.width, '123px', 'Expected width to be 123px'); 38 | assert.equal(wrapper.props.style.height, '246px', 'Expected height to be 246px'); 39 | }); 40 | 41 | it('does not render the image without a src', () => { 42 | const loader = TestUtils.renderIntoDocument(); 43 | assert(TestUtils.findRenderedDOMComponentWithClass(loader, 'pending')); 44 | assert.throws(() => { TestUtils.findRenderedDOMComponentWithClass(loader, 'loading'); }); 45 | assert.throws(() => { TestUtils.findRenderedDOMComponentWithTag(loader, 'img'); }); 46 | }); 47 | 48 | it('does not render the image initially', () => { 49 | const loader = TestUtils.renderIntoDocument(); 50 | assert(TestUtils.findRenderedDOMComponentWithClass(loader, 'loading')); 51 | assert.throws(() => { TestUtils.findRenderedDOMComponentWithTag(loader, 'img'); }); 52 | }); 53 | 54 | it('renders the image when load completes', async function() { 55 | const loader = await loadImage({src: nocache('tiger.svg')}); 56 | assert(TestUtils.findRenderedDOMComponentWithTag(loader, 'img')); 57 | }); 58 | 59 | it('shows alternative string on error', async function() { 60 | try { 61 | await loadImage({src: nocache('fake.jpg')}, 'error'); 62 | } catch ({loader}) { 63 | const span = TestUtils.findRenderedComponentWithType(loader, ImageLoader); 64 | assert.equal(span.props.children, 'error', 'Expected error message to be rendered'); 65 | assert.throws(() => { TestUtils.findRenderedDOMComponentWithTag(loader, 'img'); }); 66 | return; 67 | } 68 | throw new Error('Load should have failed!'); 69 | }); 70 | 71 | it('shows alternative img node on error', async function() { 72 | try { 73 | await loadImage({src: nocache('fake.jpg')}, ); 74 | } catch ({loader}) { 75 | const span = TestUtils.findRenderedComponentWithType(loader, ImageLoader); 76 | const imgNodes = TestUtils.scryRenderedDOMComponentsWithTag(span, 'img'); 77 | assert.lengthOf(imgNodes, 1, 'Expected one img node'); 78 | assert.include(imgNodes[0].src, 'tiger.svg'); 79 | return; 80 | } 81 | throw new Error('Load should have failed!'); 82 | }); 83 | 84 | it('shows a preloader if provided', () => { 85 | const loader = TestUtils.renderIntoDocument(); 86 | assert(TestUtils.findRenderedDOMComponentWithTag(loader, 'div')); 87 | }); 88 | 89 | it('removes a preloader when load completes', async function() { 90 | const loader = await loadImage({src: nocache('tiger.svg'), preloader: React.createFactory('div')}); 91 | assert.throws(() => { TestUtils.findRenderedDOMComponentWithTag(loader, 'div'); }); 92 | }); 93 | 94 | it('removes a preloader when load fails', async function() { 95 | try { 96 | await loadImage({src: nocache('fake.jpg'), preloader: React.createFactory('div')}); 97 | } catch ({loader}) { 98 | assert.throws(() => { TestUtils.findRenderedDOMComponentWithTag(loader, 'div'); }); 99 | return; 100 | } 101 | throw new Error('Load should have failed!'); 102 | }); 103 | 104 | it('transfers `imgProps` to the underlying img element', async function() { 105 | const src = nocache('tiger.svg'); 106 | const loader = await loadImage({src, imgProps: {alt: 'alt text', className: 'test'}}); 107 | const img = TestUtils.findRenderedDOMComponentWithTag(loader, 'img'); 108 | assert.include(img.src, src, `Expected img to have src ${src}`); 109 | assert.equal(img.alt, 'alt text', 'Expected img to have alt text'); 110 | assert.equal(img.className, 'test', 'Expected img to have className'); 111 | }); 112 | 113 | it('removes a previous image when src changes', async function() { 114 | const domEl = document.createElement('div'); 115 | const loader = await loadImage({src: nocache('tiger.svg')}, null, domEl); 116 | assert.match(TestUtils.findRenderedComponentWithType(loader, ImageLoader).props.src, /tiger\.svg/); 117 | 118 | // Rerender with a different source. 119 | loadImage(null, null, domEl).catch(() => {}); 120 | assert.throws(() => TestUtils.findRenderedDOMComponentWithTag(loader, 'img')); 121 | }); 122 | 123 | it('recovers from an error to load a new image', async function() { 124 | const domEl = document.createElement('div'); 125 | 126 | // Fail to load an image. 127 | try { 128 | await loadImage({src: nocache('fake.jpg')}, null, domEl); 129 | } catch ({loader}) { 130 | assert.throws(() => TestUtils.findRenderedDOMComponentWithTag(loader, 'img')); 131 | } 132 | 133 | // Rerender with a different source. 134 | const loader = await loadImage({src: nocache('tiger.svg')}, null, domEl); 135 | assert(() => TestUtils.findRenderedDOMComponentWithTag(loader, 'img')); 136 | }); 137 | 138 | it('abandons a load when unmounted', done => { 139 | const domEl = document.createElement('div'); 140 | const loader = ReactDOM.render( { done(new Error('This load should have been abandoned!')); }} 143 | />, domEl); 144 | 145 | // Remove ImageLoader from the DOM. 146 | ReactDOM.render(
, domEl, () => { 147 | assert.throws(() => TestUtils.findRenderedDOMComponentWithTag(loader, 'img')); 148 | done(); 149 | }); 150 | }); 151 | 152 | }); 153 | 154 | 155 | function loadImage(props, children, el) { 156 | let render; 157 | if (el) render = reactElement => ReactDOM.render(reactElement, el); 158 | else render = TestUtils.renderIntoDocument; 159 | 160 | return new Promise((resolve, reject) => { 161 | const loader = render( 162 | { resolve(loader); }} 165 | onError={error => { reject(Object.assign(error, {loader})); }}> 166 | {children} 167 | 168 | ); 169 | }); 170 | } 171 | -------------------------------------------------------------------------------- /test/tiger.svg: -------------------------------------------------------------------------------- 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 | 30 | 31 | 32 | 33 | 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 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | 4 | export default { 5 | entry: path.join(__dirname, 'src', 'index.js'), 6 | output: { 7 | path: path.join(__dirname, 'standalone'), 8 | filename: 'react-imageloader.js', 9 | library: 'ReactImageLoader', 10 | libraryTarget: 'umd', 11 | target: 'web', 12 | }, 13 | externals: ['React', {react: 'React'}], 14 | module: { 15 | loaders: [ 16 | {test: /\.js$/, exclude: /node_modules/, loader: 'babel?stage=0'}, 17 | ], 18 | }, 19 | plugins: [ 20 | new webpack.optimize.UglifyJsPlugin(), 21 | ], 22 | }; 23 | --------------------------------------------------------------------------------