Skip to content
This repository was archived by the owner on Apr 15, 2025. It is now read-only.

Commit 7def9cb

Browse files
committed
Added custom placeholder option functionality.
1 parent 6019c28 commit 7def9cb

File tree

5 files changed

+339
-5
lines changed

5 files changed

+339
-5
lines changed

README.md

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ To troubleshoot linking, refer to [the react-native-fetch-blob installation inst
3939

4040
React Native Image Cache HOC creates an advanced image component, \<CacheableImage\>, that is a drop in replacement for the standard \<Image\> component.
4141

42-
The only change in the advanced component API is the component "source" prop only accepts a web accessible url (there's no reason to use this library to render files that already exist on the local filesystem). Additionally there is a new, optional, prop "permanent" that determines if the image file should be stored forever on the local filesystem instead of written to a temperary cache. Typically "permanent" images would be static files that would traditionally ship with with app itself.
42+
**Differences between the advanced image component and standard image component API are as follows:**
43+
44+
1. **Modified "source" Prop:** The advanced component "source" prop only accepts a web accessible url (there's no reason to use this library to render files that already exist on the local filesystem), and it does NOT accept an array of urls.
45+
2. **New "permanent" Prop:** The new, optional (defaults to False), "permanent" prop determines if the image file should be stored forever on the local filesystem instead of written to a temperary cache that is subject to occasional pruning.
46+
3. **New "placeholder" Prop:** The new, optional (defaults to standard Image component), "placeholder" prop determines component to render while remote image file is downloading.
4347

4448
**TL;DR: To cache image files for performance, simply use \<CacheableImage\> as a drop in replacement for \<Image\>. To store files permanently add a permanent={true} prop to \<CacheableImage\>.**
4549

@@ -119,11 +123,97 @@ imageCacheHoc(Image, {
119123
// but sequential writes to the cache will trigger cache pruning
120124
// which will delete cached files until total cache size is below this limit before writing.
121125
// Defaults to 15 MB.
122-
cachePruneTriggerLimit: 1024 * 1024 * 10
126+
cachePruneTriggerLimit: 1024 * 1024 * 10,
127+
128+
// Default placeholder component to render while remote image file is downloading.
129+
// Can be overridden with placeholder prop like <CacheableImage placeholder={placeHolderObject} />.
130+
//
131+
// Placeholder Object is structed like:
132+
// const placeHolderObject = {
133+
// component: ReactComponentToUseHere,
134+
// props: {
135+
// examplePropLikeStyle: componentStylePropValue,
136+
// anotherExamplePropLikeSource: componentSourcePropValue
137+
// }
138+
// };
139+
//
140+
// Defaults to <Image> component with style prop passed through.
141+
defaultPlaceholder: {
142+
component: ActivityIndicator,
143+
props: {
144+
style: activityIndicatorStyle
145+
}
146+
}
123147

124148
});
125149
```
126150

151+
## Using Loading Placeholders
152+
153+
React Native Image Cache HOC allows you to easily supply any component to be used as a placeholder while the remote image file is downloading. While the default placeholder should be great for many use cases, you can easily use your own to match the style of the rest of your app.
154+
155+
```js
156+
157+
const styles = StyleSheet.create({
158+
container: {
159+
flex: 1,
160+
justifyContent: 'center',
161+
alignItems: 'center',
162+
backgroundColor: '#F5FCFF',
163+
},
164+
welcome: {
165+
fontSize: 20,
166+
textAlign: 'center',
167+
margin: 10,
168+
},
169+
image: {
170+
width:150,
171+
height: 204
172+
},
173+
activityIndicatorStyle: {
174+
width: 150,
175+
height: 204,
176+
backgroundColor: '#dc143c'
177+
}
178+
});
179+
180+
// This placeholder object will be used as a placeholder component for all instances of <CacheableImage>
181+
// unless individual <CacheableImage> uses "placeholder" prop to override this default.
182+
const defaultPlaceholderObject = {
183+
component: ActivityIndicator,
184+
props: {
185+
style: styles.activityIndicatorStyle
186+
}
187+
};
188+
189+
// We will use this placeholder object to override the default placeholder.
190+
const propOverridePlaceholderObject = {
191+
component: Image,
192+
props: {
193+
style: styles.image,
194+
source: {require('./localPlaceholderImage.png')}
195+
}
196+
};
197+
198+
const CacheableImage = imageCacheHoc(Image, {
199+
defaultPlaceholder: defaultPlaceholderObject
200+
});
201+
202+
export default class App extends Component<{}> {
203+
render() {
204+
return (
205+
<View style={styles.container}>
206+
<Text style={styles.welcome}>Welcome to React Native!</Text>
207+
<CacheableImage style={styles.image} source={{uri: 'https://i.redd.it/rc29s4bz61uz.png'}} />
208+
<CacheableImage style={styles.image} source={{uri: 'https://i.redd.it/hhhim0kc5swz.jpg'}} placeholder={propOverridePlaceholderObject} />
209+
<CacheableImage style={styles.image} source={{uri: 'https://i.redd.it/17ymhqwgbswz.jpg'}} />
210+
</View>
211+
);
212+
}
213+
}
214+
```
215+
216+
127217
## Jest Test Support
128218

129219
React Native Image Cache HOC must be run in a native environment to work correctly. As a result it will create issues in your jest tests unless you mock it. Since this module is an HOC that adds additional functionality to the standard \<Image\> component, it can be easily mocked with a function that returns the standard \<Image\> component.

lib/imageCacheHoc.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ import uuid from 'react-native-uuid';
2424

2525
export default function imageCacheHoc(Image, options = {}) {
2626

27+
// Validate options
28+
if (options.validProtocols && !Array.isArray(options.validProtocols)) { throw new Error('validProtocols option must be an array of protocol strings.'); }
29+
if (options.fileHostWhitelist && !Array.isArray(options.fileHostWhitelist)) { throw new Error('fileHostWhitelist option must be an array of host strings.'); }
30+
if (options.cachePruneTriggerLimit && !Number.isInteger(options.cachePruneTriggerLimit) ) { throw new Error('cachePruneTriggerLimit option must be an integer.'); }
31+
if (options.fileDirName && typeof options.fileDirName !== 'string') { throw new Error('fileDirName option must be string'); }
32+
if (options.defaultPlaceholder && (!options.defaultPlaceholder.component || !options.defaultPlaceholder.props)) { throw new Error('defaultPlaceholder option object must include "component" and "props" properties (props can be an empty object)'); }
33+
2734
return class extends React.Component {
2835

2936
static propTypes = {
@@ -49,7 +56,8 @@ export default function imageCacheHoc(Image, options = {}) {
4956
validProtocols: options.validProtocols || ['https'],
5057
fileHostWhitelist: options.fileHostWhitelist || [],
5158
cachePruneTriggerLimit: options.cachePruneTriggerLimit || 1024 * 1024 * 15, // Maximum size of image file cache in bytes before pruning occurs. Defaults to 15 MB.
52-
fileDirName: options.fileDirName || null // Namespace local file writing to this directory. Defaults to 'react-native-image-cache-hoc'.
59+
fileDirName: options.fileDirName || null, // Namespace local file writing to this directory. Defaults to 'react-native-image-cache-hoc'.
60+
defaultPlaceholder: options.defaultPlaceholder || null, // Default placeholder component to render while remote image file is downloading. Can be overridden with placeholder prop. Defaults to <Image> component with style prop passed through.
5361
};
5462

5563
// Init file system lib
@@ -117,7 +125,15 @@ export default function imageCacheHoc(Image, options = {}) {
117125
let props = Object.assign({}, filteredProps, { uri: this.state.localFilePath });
118126
return (<Image {...props} />);
119127
} else {
120-
return (<Image style={this.props.style ? this.props.style : undefined} />);
128+
129+
if (this.props.placeholder) {
130+
return (<this.props.placeholder.component {...this.props.placeholder.props} />);
131+
} else if (this.options.defaultPlaceholder) {
132+
return (<this.options.defaultPlaceholder.component {...this.options.defaultPlaceholder.props} />);
133+
} else {
134+
return (<Image style={this.props.style ? this.props.style : undefined} />);
135+
}
136+
121137
}
122138

123139
}

tests/CacheableImage.test.js

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,70 @@ import { Image } from 'react-native';
1010

1111
describe('CacheableImage', function() {
1212

13+
it('HOC options validation should work as expected.', () => {
14+
15+
// Check validation is catching bad option input.
16+
try {
17+
imageCacheHoc(Image, {
18+
validProtocols: 'string'
19+
});
20+
} catch (error) {
21+
error.should.deepEqual(new Error('validProtocols option must be an array of protocol strings.'));
22+
}
23+
24+
try {
25+
imageCacheHoc(Image, {
26+
fileHostWhitelist: 'string'
27+
});
28+
} catch (error) {
29+
error.should.deepEqual(new Error('fileHostWhitelist option must be an array of host strings.'));
30+
}
31+
32+
try {
33+
imageCacheHoc(Image, {
34+
cachePruneTriggerLimit: 'string'
35+
});
36+
} catch (error) {
37+
error.should.deepEqual(new Error('cachePruneTriggerLimit option must be an integer.'));
38+
}
39+
40+
try {
41+
imageCacheHoc(Image, {
42+
fileDirName: 1
43+
});
44+
} catch (error) {
45+
error.should.deepEqual(new Error('fileDirName option must be string'));
46+
}
47+
48+
try {
49+
imageCacheHoc(Image, {
50+
defaultPlaceholder: 5478329
51+
});
52+
} catch (error) {
53+
error.should.deepEqual(new Error('defaultPlaceholder option object must include "component" and "props" properties (props can be an empty object)'));
54+
}
55+
56+
const validOptions = {
57+
validProtocols: ['http', 'https'],
58+
fileHostWhitelist: ['i.redd.it', 'localhost'],
59+
cachePruneTriggerLimit: 1024 * 1024 * 10,
60+
fileDirName: 'test-dir',
61+
defaultPlaceholder: {
62+
component: Image,
63+
props: {}
64+
}
65+
};
66+
67+
// Valid options shouldn't throw an error
68+
const CacheableImage = imageCacheHoc(Image, validOptions);
69+
70+
// Check options are set correctly on component
71+
const cacheableImage = new CacheableImage(mockData.mockCacheableImageProps);
72+
73+
cacheableImage.options.should.have.properties(validOptions);
74+
75+
});
76+
1377
it('Component property type validation should exist.', () => {
1478

1579
const CacheableImage = imageCacheHoc(Image);
@@ -38,7 +102,8 @@ describe('CacheableImage', function() {
38102
validProtocols: [ 'https' ],
39103
fileHostWhitelist: [],
40104
cachePruneTriggerLimit: 15728640,
41-
fileDirName: null
105+
fileDirName: null,
106+
defaultPlaceholder: null
42107
});
43108
cacheableImage.fileSystem.should.have.properties({
44109
os: 'ios',

tests/__snapshots__/imageCacheHoc.test.js.snap

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,68 @@ exports[`CacheableImage renders correctly 1`] = `
3434
/>
3535
</View>
3636
`;
37+
38+
exports[`CacheableImage renders correctly with placeholder option set 1`] = `
39+
<View
40+
style={
41+
Object {
42+
"alignItems": "center",
43+
"flex": 1,
44+
"justifyContent": "center",
45+
}
46+
}
47+
>
48+
<Text
49+
accessible={true}
50+
allowFontScaling={true}
51+
ellipsizeMode="tail"
52+
style={
53+
Object {
54+
"fontSize": 20,
55+
"margin": 10,
56+
"textAlign": "center",
57+
}
58+
}
59+
>
60+
Test CacheableImage Component
61+
</Text>
62+
<Image
63+
style={
64+
Object {
65+
"height": 204,
66+
"width": 150,
67+
}
68+
}
69+
/>
70+
</View>
71+
`;
72+
73+
exports[`CacheableImage renders correctly with placeholder prop set 1`] = `
74+
<View
75+
style={
76+
Object {
77+
"alignItems": "center",
78+
"flex": 1,
79+
"justifyContent": "center",
80+
}
81+
}
82+
>
83+
<Text
84+
accessible={true}
85+
allowFontScaling={true}
86+
ellipsizeMode="tail"
87+
style={
88+
Object {
89+
"fontSize": 20,
90+
"margin": 10,
91+
"textAlign": "center",
92+
}
93+
}
94+
>
95+
Test CacheableImage Component
96+
</Text>
97+
<Image
98+
style={undefined}
99+
/>
100+
</View>
101+
`;

0 commit comments

Comments
 (0)