Skip to content

Commit 4f613e2

Browse files
committed
2015-02-05 updates
- [ReactServer] Fix newly introduced bug | Amjad Masad - [ReactServer] Last sync from github | Amjad Masad - [RFC-ReactNative] Subscribable overhaul, clean up AppState/Reachability | Eric Vicenti - [ReactKit] Enable tappable <Text /> subnodes | Alex Akers
1 parent fd8b7de commit 4f613e2

File tree

26 files changed

+787
-259
lines changed

26 files changed

+787
-259
lines changed

Examples/UIExplorer/TextExample.ios.js

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,20 @@ var AttributeToggler = React.createClass({
3434
render: function() {
3535
var curStyle = {fontSize: this.state.fontSize};
3636
return (
37-
<View>
37+
<Text>
3838
<Text style={curStyle}>
3939
Tap the controls below to change attributes.
4040
</Text>
4141
<Text>
42-
<Text>
43-
See how it will even work on{' '}
44-
<Text style={curStyle}>
45-
this nested text
46-
</Text>
42+
See how it will even work on{' '}
43+
<Text style={curStyle}>
44+
this nested text
45+
</Text>
46+
<Text onPress={this.increaseSize}>
47+
{'>> Increase Size <<'}
4748
</Text>
4849
</Text>
49-
<Text onPress={this.increaseSize}>
50-
{'>> Increase Size <<'}
51-
</Text>
52-
</View>
50+
</Text>
5351
);
5452
}
5553
});

Libraries/Components/Subscribable.js

Lines changed: 263 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,270 @@
55
*/
66
'use strict';
77

8-
var Subscribable = {
9-
Mixin: {
10-
componentWillMount: function() {
11-
this._subscriptions = [];
12-
},
13-
componentWillUnmount: function() {
14-
this._subscriptions.forEach((subscription) => subscription.remove());
15-
this._subscriptions = null;
16-
},
17-
18-
/**
19-
* Special form of calling `addListener` that *guarantees* that a
20-
* subscription *must* be tied to a component instance, and therefore will
21-
* be cleaned up when the component is unmounted. It is impossible to create
22-
* the subscription and pass it in - this method must be the one to create
23-
* the subscription and therefore can guarantee it is retained in a way that
24-
* will be cleaned up.
25-
*
26-
* @param {EventEmitter} eventEmitter emitter to subscribe to.
27-
* @param {string} eventType Type of event to listen to.
28-
* @param {function} listener Function to invoke when event occurs.
29-
* @param {object} context Object to use as listener context.
30-
*/
31-
addListenerOn: function(eventEmitter, eventType, listener, context) {
32-
this._subscriptions.push(
33-
eventEmitter.addListener(eventType, listener, context)
34-
);
8+
/**
9+
* Subscribable wraps EventEmitter in a clean interface, and provides a mixin
10+
* so components can easily subscribe to events and not worry about cleanup on
11+
* unmount.
12+
*
13+
* Also acts as a basic store because it records the last data that it emitted,
14+
* and provides a way to populate the initial data. The most recent data can be
15+
* fetched from the Subscribable by calling `get()`
16+
*
17+
* Advantages over EventEmitter + Subscibable.Mixin.addListenerOn:
18+
* - Cleaner usage: no strings to identify the event
19+
* - Lifespan pattern enforces cleanup
20+
* - More logical: Subscribable.Mixin now uses a Subscribable class
21+
* - Subscribable saves the last data and makes it available with `.get()`
22+
*
23+
* Legacy Subscribable.Mixin.addListenerOn allowed automatic subscription to
24+
* EventEmitters. Now we should avoid EventEmitters and wrap with Subscribable
25+
* instead:
26+
*
27+
* ```
28+
* AppState.networkReachability = new Subscribable(
29+
* RCTDeviceEventEmitter,
30+
* 'reachabilityDidChange',
31+
* (resp) => resp.network_reachability,
32+
* RKReachability.getCurrentReachability
33+
* );
34+
*
35+
* var myComponent = React.createClass({
36+
* mixins: [Subscribable.Mixin],
37+
* getInitialState: function() {
38+
* return {
39+
* isConnected: AppState.networkReachability.get() !== 'none'
40+
* };
41+
* },
42+
* componentDidMount: function() {
43+
* this._reachSubscription = this.subscribeTo(
44+
* AppState.networkReachability,
45+
* (reachability) => {
46+
* this.setState({ isConnected: reachability !== 'none' })
47+
* }
48+
* );
49+
* },
50+
* render: function() {
51+
* return (
52+
* <Text>
53+
* {this.state.isConnected ? 'Network Connected' : 'No network'}
54+
* </Text>
55+
* <Text onPress={() => this._reachSubscription.remove()}>
56+
* End reachability subscription
57+
* </Text>
58+
* );
59+
* }
60+
* });
61+
* ```
62+
*/
63+
64+
var EventEmitter = require('EventEmitter');
65+
66+
var invariant = require('invariant');
67+
var logError = require('logError');
68+
69+
var SUBSCRIBABLE_INTERNAL_EVENT = 'subscriptionEvent';
70+
71+
72+
class Subscribable {
73+
/**
74+
* Creates a new Subscribable object
75+
*
76+
* @param {EventEmitter} eventEmitter Emitter to trigger subscription events.
77+
* @param {string} eventName Name of emitted event that triggers subscription
78+
* events.
79+
* @param {function} eventMapping (optional) Function to convert the output
80+
* of the eventEmitter to the subscription output.
81+
* @param {function} getInitData (optional) Async function to grab the initial
82+
* data to publish. Signature `function(successCallback, errorCallback)`.
83+
* The resolved data will be transformed with the eventMapping before it
84+
* gets emitted.
85+
*/
86+
constructor(eventEmitter, eventName, eventMapping, getInitData) {
87+
88+
this._internalEmitter = new EventEmitter();
89+
this._eventMapping = eventMapping || (data => data);
90+
91+
eventEmitter.addListener(
92+
eventName,
93+
this._handleEmit,
94+
this
95+
);
96+
97+
// Asyncronously get the initial data, if provided
98+
getInitData && getInitData(this._handleInitData.bind(this), logError);
99+
}
100+
101+
/**
102+
* Returns the last data emitted from the Subscribable, or undefined
103+
*/
104+
get() {
105+
return this._lastData;
106+
}
107+
108+
/**
109+
* Add a new listener to the subscribable. This should almost never be used
110+
* directly, and instead through Subscribable.Mixin.subscribeTo
111+
*
112+
* @param {object} lifespan Object with `addUnmountCallback` that accepts
113+
* a handler to be called when the component unmounts. This is required and
114+
* desirable because it enforces cleanup. There is no easy way to leave the
115+
* subsciption hanging
116+
* {
117+
* addUnmountCallback: function(newUnmountHanlder) {...},
118+
* }
119+
* @param {function} callback Handler to call when Subscribable has data
120+
* updates
121+
* @param {object} context Object to bind the handler on, as "this"
122+
*
123+
* @return {object} the subscription object:
124+
* {
125+
* remove: function() {...},
126+
* }
127+
* Call `remove` to terminate the subscription before unmounting
128+
*/
129+
subscribe(lifespan, callback, context) {
130+
invariant(
131+
typeof lifespan.addUnmountCallback === 'function',
132+
'Must provide a valid lifespan, which provides a way to add a ' +
133+
'callback for when subscription can be cleaned up. This is used ' +
134+
'automatically by Subscribable.Mixin'
135+
);
136+
invariant(
137+
typeof callback === 'function',
138+
'Must provide a valid subscription handler.'
139+
);
140+
141+
// Add a listener to the internal EventEmitter
142+
var subscription = this._internalEmitter.addListener(
143+
SUBSCRIBABLE_INTERNAL_EVENT,
144+
callback,
145+
context
146+
);
147+
148+
// Clean up subscription upon the lifespan unmount callback
149+
lifespan.addUnmountCallback(() => {
150+
subscription.remove();
151+
});
152+
153+
return subscription;
154+
}
155+
156+
/**
157+
* Callback for the initial data resolution. Currently behaves the same as
158+
* `_handleEmit`, but we may eventually want to keep track of the difference
159+
*/
160+
_handleInitData(dataInput) {
161+
var emitData = this._eventMapping(dataInput);
162+
this._lastData = emitData;
163+
this._internalEmitter.emit(SUBSCRIBABLE_INTERNAL_EVENT, emitData);
164+
}
165+
166+
/**
167+
* Handle new data emissions. Pass the data through our eventMapping
168+
* transformation, store it for later `get()`ing, and emit it for subscribers
169+
*/
170+
_handleEmit(dataInput) {
171+
var emitData = this._eventMapping(dataInput);
172+
this._lastData = emitData;
173+
this._internalEmitter.emit(SUBSCRIBABLE_INTERNAL_EVENT, emitData);
174+
}
175+
}
176+
177+
178+
Subscribable.Mixin = {
179+
180+
/**
181+
* @return {object} lifespan Object with `addUnmountCallback` that accepts
182+
* a handler to be called when the component unmounts
183+
* {
184+
* addUnmountCallback: function(newUnmountHanlder) {...},
185+
* }
186+
*/
187+
_getSubscribableLifespan: function() {
188+
if (!this._subscribableLifespan) {
189+
this._subscribableLifespan = {
190+
addUnmountCallback: (cb) => {
191+
this._endSubscribableLifespanCallbacks.push(cb);
192+
},
193+
};
35194
}
195+
return this._subscribableLifespan;
196+
},
197+
198+
_endSubscribableLifespan: function() {
199+
this._endSubscribableLifespanCallbacks.forEach(cb => cb());
200+
},
201+
202+
/**
203+
* Components use `subscribeTo` for listening to Subscribable stores. Cleanup
204+
* is automatic on component unmount.
205+
*
206+
* To stop listening to the subscribable and end the subscription early,
207+
* components should store the returned subscription object and invoke the
208+
* `remove()` function on it
209+
*
210+
* @param {Subscribable} subscription to subscribe to.
211+
* @param {function} listener Function to invoke when event occurs.
212+
* @param {object} context Object to bind the handler on, as "this"
213+
*
214+
* @return {object} the subscription object:
215+
* {
216+
* remove: function() {...},
217+
* }
218+
* Call `remove` to terminate the subscription before unmounting
219+
*/
220+
subscribeTo: function(subscribable, handler, context) {
221+
invariant(
222+
subscribable instanceof Subscribable,
223+
'Must provide a Subscribable'
224+
);
225+
return subscribable.subscribe(
226+
this._getSubscribableLifespan(),
227+
handler,
228+
context
229+
);
230+
},
231+
232+
componentWillMount: function() {
233+
this._endSubscribableLifespanCallbacks = [];
234+
235+
// DEPRECATED addListenerOn* usage:
236+
this._subscribableSubscriptions = [];
237+
},
238+
239+
componentWillUnmount: function() {
240+
// Resolve the lifespan, which will cause Subscribable to clean any
241+
// remaining subscriptions
242+
this._endSubscribableLifespan && this._endSubscribableLifespan();
243+
244+
// DEPRECATED addListenerOn* usage uses _subscribableSubscriptions array
245+
// instead of lifespan
246+
this._subscribableSubscriptions.forEach(
247+
(subscription) => subscription.remove()
248+
);
249+
this._subscribableSubscriptions = null;
250+
},
251+
252+
/**
253+
* DEPRECATED - Use `Subscribable` and `Mixin.subscribeTo` instead.
254+
* `addListenerOn` subscribes the component to an `EventEmitter`.
255+
*
256+
* Special form of calling `addListener` that *guarantees* that a
257+
* subscription *must* be tied to a component instance, and therefore will
258+
* be cleaned up when the component is unmounted. It is impossible to create
259+
* the subscription and pass it in - this method must be the one to create
260+
* the subscription and therefore can guarantee it is retained in a way that
261+
* will be cleaned up.
262+
*
263+
* @param {EventEmitter} eventEmitter emitter to subscribe to.
264+
* @param {string} eventType Type of event to listen to.
265+
* @param {function} listener Function to invoke when event occurs.
266+
* @param {object} context Object to use as listener context.
267+
*/
268+
addListenerOn: function(eventEmitter, eventType, listener, context) {
269+
this._subscribableSubscriptions.push(
270+
eventEmitter.addListener(eventType, listener, context)
271+
);
36272
}
37273
};
38274

ReactKit/Base/RCTTouchHandler.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ - (void)_recordNewTouches:(NSSet *)touches
8080

8181
RCTAssert(targetView.reactTag && targetView.userInteractionEnabled,
8282
@"No react view found for touch - something went wrong.");
83-
83+
8484
// Get new, unique touch id
8585
const NSUInteger RCTMaxTouches = 11; // This is the maximum supported by iDevices
8686
NSInteger touchID = ([_reactTouches.lastObject[@"target"] integerValue] + 1) % RCTMaxTouches;
@@ -97,7 +97,7 @@ - (void)_recordNewTouches:(NSSet *)touches
9797

9898
// Create touch
9999
NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:9];
100-
reactTouch[@"target"] = targetView.reactTag;
100+
reactTouch[@"target"] = [targetView reactTagAtPoint:[touch locationInView:targetView]];
101101
reactTouch[@"identifier"] = @(touchID);
102102
reactTouch[@"touches"] = [NSNull null]; // We hijack this touchObj to serve both as an event
103103
reactTouch[@"changedTouches"] = [NSNull null]; // and as a Touch object, so making this JIT friendly.

ReactKit/Base/RCTViewNodeProtocol.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- (void)insertReactSubview:(id<RCTViewNodeProtocol>)subview atIndex:(NSInteger)atIndex;
1414
- (void)removeReactSubview:(id<RCTViewNodeProtocol>)subview;
1515
- (NSMutableArray *)reactSubviews;
16+
- (NSNumber *)reactTagAtPoint:(CGPoint)point;
1617

1718
// View is an RCTRootView
1819
- (BOOL)isReactRootView;

ReactKit/ReactKit.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
13E067591A70F44B002CDEE1 /* UIView+ReactKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+ReactKit.m */; };
4242
830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; };
4343
832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; };
44+
835DD1321A7FDFB600D561F7 /* RCTText.m in Sources */ = {isa = PBXBuildFile; fileRef = 835DD1311A7FDFB600D561F7 /* RCTText.m */; };
4445
83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */; };
4546
83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4E1A601E3B00E9B192 /* RCTLog.m */; };
4647
83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA501A601E3B00E9B192 /* RCTUtils.m */; };
@@ -134,6 +135,8 @@
134135
830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = "<group>"; };
135136
830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = "<group>"; };
136137
830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = "<group>"; };
138+
835DD1301A7FDFB600D561F7 /* RCTText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTText.h; sourceTree = "<group>"; };
139+
835DD1311A7FDFB600D561F7 /* RCTText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTText.m; sourceTree = "<group>"; };
137140
83BEE46C1A6D19BC00B5863B /* RCTSparseArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSparseArray.h; sourceTree = "<group>"; };
138141
83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSparseArray.m; sourceTree = "<group>"; };
139142
83CBBA2E1A601D0E00E9B192 /* libReactKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReactKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -228,6 +231,8 @@
228231
137029581A6C197000575408 /* RCTRawTextManager.m */,
229232
13B07FFC1A6947C200A75B9A /* RCTShadowText.h */,
230233
13B07FFD1A6947C200A75B9A /* RCTShadowText.m */,
234+
835DD1301A7FDFB600D561F7 /* RCTText.h */,
235+
835DD1311A7FDFB600D561F7 /* RCTText.m */,
231236
13B080021A6947C200A75B9A /* RCTTextManager.h */,
232237
13B080031A6947C200A75B9A /* RCTTextManager.m */,
233238
13B0800C1A69489C00A75B9A /* RCTNavigator.h */,
@@ -416,6 +421,7 @@
416421
13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */,
417422
134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */,
418423
13E067591A70F44B002CDEE1 /* UIView+ReactKit.m in Sources */,
424+
835DD1321A7FDFB600D561F7 /* RCTText.m in Sources */,
419425
137029531A69923600575408 /* RCTImageDownloader.m in Sources */,
420426
83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */,
421427
83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */,

ReactKit/Views/RCTShadowText.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,5 @@ extern NSString *const RCTReactTagAttributeName;
2222
@property (nonatomic, assign) NSLineBreakMode truncationMode;
2323

2424
- (NSAttributedString *)attributedString;
25-
- (NSAttributedString *)reactTagAttributedString;
2625

2726
@end

0 commit comments

Comments
 (0)