Skip to content

Commit

Permalink
2015-02-05 updates
Browse files Browse the repository at this point in the history
- [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
  • Loading branch information
vjeux committed Feb 6, 2015
1 parent fd8b7de commit 4f613e2
Show file tree
Hide file tree
Showing 26 changed files with 787 additions and 259 deletions.
18 changes: 8 additions & 10 deletions Examples/UIExplorer/TextExample.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,20 @@ var AttributeToggler = React.createClass({
render: function() {
var curStyle = {fontSize: this.state.fontSize};
return (
<View>
<Text>
<Text style={curStyle}>
Tap the controls below to change attributes.
</Text>
<Text>
<Text>
See how it will even work on{' '}
<Text style={curStyle}>
this nested text
</Text>
See how it will even work on{' '}
<Text style={curStyle}>
this nested text
</Text>
<Text onPress={this.increaseSize}>
{'>> Increase Size <<'}
</Text>
</Text>
<Text onPress={this.increaseSize}>
{'>> Increase Size <<'}
</Text>
</View>
</Text>
);
}
});
Expand Down
290 changes: 263 additions & 27 deletions Libraries/Components/Subscribable.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,270 @@
*/
'use strict';

var Subscribable = {
Mixin: {
componentWillMount: function() {
this._subscriptions = [];
},
componentWillUnmount: function() {
this._subscriptions.forEach((subscription) => subscription.remove());
this._subscriptions = null;
},

/**
* Special form of calling `addListener` that *guarantees* that a
* subscription *must* be tied to a component instance, and therefore will
* be cleaned up when the component is unmounted. It is impossible to create
* the subscription and pass it in - this method must be the one to create
* the subscription and therefore can guarantee it is retained in a way that
* will be cleaned up.
*
* @param {EventEmitter} eventEmitter emitter to subscribe to.
* @param {string} eventType Type of event to listen to.
* @param {function} listener Function to invoke when event occurs.
* @param {object} context Object to use as listener context.
*/
addListenerOn: function(eventEmitter, eventType, listener, context) {
this._subscriptions.push(
eventEmitter.addListener(eventType, listener, context)
);
/**
* Subscribable wraps EventEmitter in a clean interface, and provides a mixin
* so components can easily subscribe to events and not worry about cleanup on
* unmount.
*
* Also acts as a basic store because it records the last data that it emitted,
* and provides a way to populate the initial data. The most recent data can be
* fetched from the Subscribable by calling `get()`
*
* Advantages over EventEmitter + Subscibable.Mixin.addListenerOn:
* - Cleaner usage: no strings to identify the event
* - Lifespan pattern enforces cleanup
* - More logical: Subscribable.Mixin now uses a Subscribable class
* - Subscribable saves the last data and makes it available with `.get()`
*
* Legacy Subscribable.Mixin.addListenerOn allowed automatic subscription to
* EventEmitters. Now we should avoid EventEmitters and wrap with Subscribable
* instead:
*
* ```
* AppState.networkReachability = new Subscribable(
* RCTDeviceEventEmitter,
* 'reachabilityDidChange',
* (resp) => resp.network_reachability,
* RKReachability.getCurrentReachability
* );
*
* var myComponent = React.createClass({
* mixins: [Subscribable.Mixin],
* getInitialState: function() {
* return {
* isConnected: AppState.networkReachability.get() !== 'none'
* };
* },
* componentDidMount: function() {
* this._reachSubscription = this.subscribeTo(
* AppState.networkReachability,
* (reachability) => {
* this.setState({ isConnected: reachability !== 'none' })
* }
* );
* },
* render: function() {
* return (
* <Text>
* {this.state.isConnected ? 'Network Connected' : 'No network'}
* </Text>
* <Text onPress={() => this._reachSubscription.remove()}>
* End reachability subscription
* </Text>
* );
* }
* });
* ```
*/

var EventEmitter = require('EventEmitter');

var invariant = require('invariant');
var logError = require('logError');

var SUBSCRIBABLE_INTERNAL_EVENT = 'subscriptionEvent';


class Subscribable {
/**
* Creates a new Subscribable object
*
* @param {EventEmitter} eventEmitter Emitter to trigger subscription events.
* @param {string} eventName Name of emitted event that triggers subscription
* events.
* @param {function} eventMapping (optional) Function to convert the output
* of the eventEmitter to the subscription output.
* @param {function} getInitData (optional) Async function to grab the initial
* data to publish. Signature `function(successCallback, errorCallback)`.
* The resolved data will be transformed with the eventMapping before it
* gets emitted.
*/
constructor(eventEmitter, eventName, eventMapping, getInitData) {

this._internalEmitter = new EventEmitter();
this._eventMapping = eventMapping || (data => data);

eventEmitter.addListener(
eventName,
this._handleEmit,
this
);

// Asyncronously get the initial data, if provided
getInitData && getInitData(this._handleInitData.bind(this), logError);
}

/**
* Returns the last data emitted from the Subscribable, or undefined
*/
get() {
return this._lastData;
}

/**
* Add a new listener to the subscribable. This should almost never be used
* directly, and instead through Subscribable.Mixin.subscribeTo
*
* @param {object} lifespan Object with `addUnmountCallback` that accepts
* a handler to be called when the component unmounts. This is required and
* desirable because it enforces cleanup. There is no easy way to leave the
* subsciption hanging
* {
* addUnmountCallback: function(newUnmountHanlder) {...},
* }
* @param {function} callback Handler to call when Subscribable has data
* updates
* @param {object} context Object to bind the handler on, as "this"
*
* @return {object} the subscription object:
* {
* remove: function() {...},
* }
* Call `remove` to terminate the subscription before unmounting
*/
subscribe(lifespan, callback, context) {
invariant(
typeof lifespan.addUnmountCallback === 'function',
'Must provide a valid lifespan, which provides a way to add a ' +
'callback for when subscription can be cleaned up. This is used ' +
'automatically by Subscribable.Mixin'
);
invariant(
typeof callback === 'function',
'Must provide a valid subscription handler.'
);

// Add a listener to the internal EventEmitter
var subscription = this._internalEmitter.addListener(
SUBSCRIBABLE_INTERNAL_EVENT,
callback,
context
);

// Clean up subscription upon the lifespan unmount callback
lifespan.addUnmountCallback(() => {
subscription.remove();
});

return subscription;
}

/**
* Callback for the initial data resolution. Currently behaves the same as
* `_handleEmit`, but we may eventually want to keep track of the difference
*/
_handleInitData(dataInput) {
var emitData = this._eventMapping(dataInput);
this._lastData = emitData;
this._internalEmitter.emit(SUBSCRIBABLE_INTERNAL_EVENT, emitData);
}

/**
* Handle new data emissions. Pass the data through our eventMapping
* transformation, store it for later `get()`ing, and emit it for subscribers
*/
_handleEmit(dataInput) {
var emitData = this._eventMapping(dataInput);
this._lastData = emitData;
this._internalEmitter.emit(SUBSCRIBABLE_INTERNAL_EVENT, emitData);
}
}


Subscribable.Mixin = {

/**
* @return {object} lifespan Object with `addUnmountCallback` that accepts
* a handler to be called when the component unmounts
* {
* addUnmountCallback: function(newUnmountHanlder) {...},
* }
*/
_getSubscribableLifespan: function() {
if (!this._subscribableLifespan) {
this._subscribableLifespan = {
addUnmountCallback: (cb) => {
this._endSubscribableLifespanCallbacks.push(cb);
},
};
}
return this._subscribableLifespan;
},

_endSubscribableLifespan: function() {
this._endSubscribableLifespanCallbacks.forEach(cb => cb());
},

/**
* Components use `subscribeTo` for listening to Subscribable stores. Cleanup
* is automatic on component unmount.
*
* To stop listening to the subscribable and end the subscription early,
* components should store the returned subscription object and invoke the
* `remove()` function on it
*
* @param {Subscribable} subscription to subscribe to.
* @param {function} listener Function to invoke when event occurs.
* @param {object} context Object to bind the handler on, as "this"
*
* @return {object} the subscription object:
* {
* remove: function() {...},
* }
* Call `remove` to terminate the subscription before unmounting
*/
subscribeTo: function(subscribable, handler, context) {
invariant(
subscribable instanceof Subscribable,
'Must provide a Subscribable'
);
return subscribable.subscribe(
this._getSubscribableLifespan(),
handler,
context
);
},

componentWillMount: function() {
this._endSubscribableLifespanCallbacks = [];

// DEPRECATED addListenerOn* usage:
this._subscribableSubscriptions = [];
},

componentWillUnmount: function() {
// Resolve the lifespan, which will cause Subscribable to clean any
// remaining subscriptions
this._endSubscribableLifespan && this._endSubscribableLifespan();

// DEPRECATED addListenerOn* usage uses _subscribableSubscriptions array
// instead of lifespan
this._subscribableSubscriptions.forEach(
(subscription) => subscription.remove()
);
this._subscribableSubscriptions = null;
},

/**
* DEPRECATED - Use `Subscribable` and `Mixin.subscribeTo` instead.
* `addListenerOn` subscribes the component to an `EventEmitter`.
*
* Special form of calling `addListener` that *guarantees* that a
* subscription *must* be tied to a component instance, and therefore will
* be cleaned up when the component is unmounted. It is impossible to create
* the subscription and pass it in - this method must be the one to create
* the subscription and therefore can guarantee it is retained in a way that
* will be cleaned up.
*
* @param {EventEmitter} eventEmitter emitter to subscribe to.
* @param {string} eventType Type of event to listen to.
* @param {function} listener Function to invoke when event occurs.
* @param {object} context Object to use as listener context.
*/
addListenerOn: function(eventEmitter, eventType, listener, context) {
this._subscribableSubscriptions.push(
eventEmitter.addListener(eventType, listener, context)
);
}
};

Expand Down
4 changes: 2 additions & 2 deletions ReactKit/Base/RCTTouchHandler.m
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ - (void)_recordNewTouches:(NSSet *)touches

RCTAssert(targetView.reactTag && targetView.userInteractionEnabled,
@"No react view found for touch - something went wrong.");

// Get new, unique touch id
const NSUInteger RCTMaxTouches = 11; // This is the maximum supported by iDevices
NSInteger touchID = ([_reactTouches.lastObject[@"target"] integerValue] + 1) % RCTMaxTouches;
Expand All @@ -97,7 +97,7 @@ - (void)_recordNewTouches:(NSSet *)touches

// Create touch
NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:9];
reactTouch[@"target"] = targetView.reactTag;
reactTouch[@"target"] = [targetView reactTagAtPoint:[touch locationInView:targetView]];
reactTouch[@"identifier"] = @(touchID);
reactTouch[@"touches"] = [NSNull null]; // We hijack this touchObj to serve both as an event
reactTouch[@"changedTouches"] = [NSNull null]; // and as a Touch object, so making this JIT friendly.
Expand Down
1 change: 1 addition & 0 deletions ReactKit/Base/RCTViewNodeProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- (void)insertReactSubview:(id<RCTViewNodeProtocol>)subview atIndex:(NSInteger)atIndex;
- (void)removeReactSubview:(id<RCTViewNodeProtocol>)subview;
- (NSMutableArray *)reactSubviews;
- (NSNumber *)reactTagAtPoint:(CGPoint)point;

// View is an RCTRootView
- (BOOL)isReactRootView;
Expand Down
6 changes: 6 additions & 0 deletions ReactKit/ReactKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
13E067591A70F44B002CDEE1 /* UIView+ReactKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+ReactKit.m */; };
830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; };
832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; };
835DD1321A7FDFB600D561F7 /* RCTText.m in Sources */ = {isa = PBXBuildFile; fileRef = 835DD1311A7FDFB600D561F7 /* RCTText.m */; };
83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */; };
83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4E1A601E3B00E9B192 /* RCTLog.m */; };
83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA501A601E3B00E9B192 /* RCTUtils.m */; };
Expand Down Expand Up @@ -134,6 +135,8 @@
830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = "<group>"; };
830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = "<group>"; };
830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = "<group>"; };
835DD1301A7FDFB600D561F7 /* RCTText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTText.h; sourceTree = "<group>"; };
835DD1311A7FDFB600D561F7 /* RCTText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTText.m; sourceTree = "<group>"; };
83BEE46C1A6D19BC00B5863B /* RCTSparseArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSparseArray.h; sourceTree = "<group>"; };
83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSparseArray.m; sourceTree = "<group>"; };
83CBBA2E1A601D0E00E9B192 /* libReactKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReactKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -228,6 +231,8 @@
137029581A6C197000575408 /* RCTRawTextManager.m */,
13B07FFC1A6947C200A75B9A /* RCTShadowText.h */,
13B07FFD1A6947C200A75B9A /* RCTShadowText.m */,
835DD1301A7FDFB600D561F7 /* RCTText.h */,
835DD1311A7FDFB600D561F7 /* RCTText.m */,
13B080021A6947C200A75B9A /* RCTTextManager.h */,
13B080031A6947C200A75B9A /* RCTTextManager.m */,
13B0800C1A69489C00A75B9A /* RCTNavigator.h */,
Expand Down Expand Up @@ -416,6 +421,7 @@
13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */,
134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */,
13E067591A70F44B002CDEE1 /* UIView+ReactKit.m in Sources */,
835DD1321A7FDFB600D561F7 /* RCTText.m in Sources */,
137029531A69923600575408 /* RCTImageDownloader.m in Sources */,
83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */,
83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */,
Expand Down
1 change: 0 additions & 1 deletion ReactKit/Views/RCTShadowText.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,5 @@ extern NSString *const RCTReactTagAttributeName;
@property (nonatomic, assign) NSLineBreakMode truncationMode;

- (NSAttributedString *)attributedString;
- (NSAttributedString *)reactTagAttributedString;

@end
Loading

0 comments on commit 4f613e2

Please sign in to comment.