├── .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;c>",A={array:d("array"),bool:d("boolean"),func:d("function"),number:d("number"),object:d("object"),string:d("string"),symbol:d("symbol"),any:y(),arrayOf:h,element:v(),instanceOf:m,node:w(),objectOf:b,oneOf:g,oneOfType:O,shape:E,exact:j};return l.prototype=Error.prototype,A.checkPropTypes=s,A.PropTypes=A,A}}).call(t,r(3))},function(e,t){/*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 | "use strict";function r(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}function n(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},r=0;10>r;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 |
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 |
--------------------------------------------------------------------------------