From 0a875790f58c7de2842b21216c1ceaa5d1185547 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Tue, 16 Jun 2015 09:08:16 -0700 Subject: [PATCH] [Navigator]: Allow developer to observe the focus change events from the owner or the children of the navigator component. Summary: Per offline discussion with @evv, we'd like to deprecate the `onDidFocus` and `onWillFocus` API that makes it really hard for the descendent children of a navigator to observe its focus change events. @public Since for now the descendent children do have access to the navigator via `this.props.navigator`, this diff makes it easy to observe the focus change event by doing: ``` this.props.navigator.addListener('willfocus', this._onFocus); ``` The goal is to make the event system in navigator more useful and maintainable. Test Plan: Test Video: https://www.facebook.com/pxlcld/mrzS 1. jest: ./Libraries/FBReactKit/js/runTests.js NavigationEventEmitter 2. Load UI Explorer: , see console logs that shows the focus change events fires. --- .../Navigator/NavigationBarSample.js | 25 ++++++ .../UIExplorer/Navigator/NavigatorExample.js | 29 +++++++ .../Navigator/Navigation/NavigationContext.js | 76 ++++++++++++++++++ .../Navigator/Navigation/NavigationEvent.js | 21 +++++ .../Navigation/NavigationEventEmitter.js | 70 +++++++++++++++++ .../__tests__/NavigationEventEmitter-test.js | 78 +++++++++++++++++++ .../CustomComponents/Navigator/Navigator.js | 25 +++++- 7 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js create mode 100644 Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js create mode 100644 Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js create mode 100644 Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js diff --git a/Examples/UIExplorer/Navigator/NavigationBarSample.js b/Examples/UIExplorer/Navigator/NavigationBarSample.js index 2b3f8e250..545f76b82 100644 --- a/Examples/UIExplorer/Navigator/NavigationBarSample.js +++ b/Examples/UIExplorer/Navigator/NavigationBarSample.js @@ -92,6 +92,31 @@ function newRandomRoute() { var NavigationBarSample = React.createClass({ + componentWillMount: function() { + var navigator = this.props.navigator; + + var callback = (event) => { + console.log( + `NavigationBarSample : event ${event.type}`, + { + route: JSON.stringify(event.data.route), + target: event.target, + type: event.type, + } + ); + }; + + // Observe focus change events from this component. + this._listeners = [ + navigator.navigationContext.addListener('willfocus', callback), + navigator.navigationContext.addListener('didfocus', callback), + ]; + }, + + componentWillUnmount: function() { + this._listeners && this._listeners.forEach(listener => listener.remove()); + }, + render: function() { return ( listener.remove()); + }, + + _setNavigatorRef: function(navigator) { + if (navigator !== this._navigator) { + this._navigator = navigator; + + if (navigator) { + var callback = (event) => { + console.log( + `TabBarExample: event ${event.type}`, + { + route: JSON.stringify(event.data.route), + target: event.target, + type: event.type, + } + ); + }; + // Observe focus change events from the owner. + this._listeners = [ + navigator.navigationContext.addListener('willfocus', callback), + navigator.navigationContext.addListener('didfocus', callback), + ]; + } + } + }, }); var styles = StyleSheet.create({ diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js new file mode 100644 index 000000000..8169415eb --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @providesModule NavigationContext + * @flow + */ +'use strict'; + +var NavigationEventEmitter = require('NavigationEventEmitter'); +var emptyFunction = require('emptyFunction'); + +type EventSubscription = { + remove: Function +}; + +/** + * Class that contains the info and methods for app navigation. + */ +class NavigationContext { + _eventEmitter: ?NavigationEventEmitter; + + constructor() { + this._eventEmitter = new NavigationEventEmitter(this); + } + + addListener( + eventType: string, + listener: Function, + context: ?Object + ): EventSubscription { + var emitter = this._eventEmitter; + if (emitter) { + return emitter.addListener(eventType, listener, context); + } else { + return {remove: emptyFunction}; + } + } + + emit(eventType: String, data: any): void { + var emitter = this._eventEmitter; + if (emitter) { + emitter.emit(eventType, data); + } + } + + dispose() { + var emitter = this._eventEmitter; + if (emitter) { + emitter.removeAllListeners(); + this._eventEmitter = null; + } + } +} + +module.exports = NavigationContext; diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js new file mode 100644 index 000000000..b6923b4f2 --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js @@ -0,0 +1,21 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule NavigationEvent + * @flow + */ +'use strict'; + +class NavigationEvent { + type: String; + target: Object; + data: any; + + constructor(type: String, target: Object, data: any) { + this.type = type; + this.target = target; + this.data = data; + } +} + +module.exports = NavigationEvent; diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js new file mode 100644 index 000000000..db9e78554 --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @providesModule NavigationEventEmitter + * @flow + */ +'use strict'; + +var EventEmitter = require('EventEmitter'); +var NavigationEvent = require('NavigationEvent'); + +type EventParams = { + eventType: String; + data: any; +}; + +class NavigationEventEmitter extends EventEmitter { + _emitQueue: Array; + _emitting: boolean; + _target: Object; + + constructor(target: Object) { + super(); + this._emitting = false; + this._emitQueue = []; + this._target = target; + } + + emit(eventType: String, data: any): void { + if (this._emitting) { + // An event cycle that was previously created hasn't finished yet. + // Put this event cycle into the queue and will finish them later. + this._emitQueue.push({eventType, data}); + return; + } + + this._emitting = true; + var event = new NavigationEvent(eventType, this._target, data); + super.emit(eventType, event); + this._emitting = false; + + while (this._emitQueue.length) { + var arg = this._emitQueue.shift(); + this.emit(arg.eventType, arg.data); + } + } +} + +module.exports = NavigationEventEmitter; diff --git a/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js new file mode 100644 index 000000000..518fe0724 --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +'use strict'; + +jest + .dontMock('EventEmitter') + .dontMock('NavigationEvent') + .dontMock('NavigationEventEmitter'); + +var NavigationEventEmitter = require('NavigationEventEmitter'); + +describe('NavigationEventEmitter', () => { + it('emit event', () => { + var target = {}; + var emitter = new NavigationEventEmitter(target); + var focusCounter = 0; + var focusTarget; + + emitter.addListener('focus', (event) => { + focusCounter++; + focusTarget = event.target; + }); + + emitter.emit('focus'); + emitter.emit('blur'); + + expect(focusCounter).toBe(1); + expect(focusTarget).toBe(target); + }); + + it('put nested emit call in queue', () => { + var target = {}; + var emitter = new NavigationEventEmitter(target); + var logs = []; + + emitter.addListener('one', () => { + logs.push(1); + emitter.emit('two'); + logs.push(2); + }); + + emitter.addListener('two', () => { + logs.push(3); + emitter.emit('three'); + logs.push(4); + }); + + emitter.addListener('three', () => { + logs.push(5); + }); + + emitter.emit('one'); + + expect(logs).toEqual([1, 2, 3, 4, 5]); + }); +}); diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index af10348fc..93610e973 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -30,11 +30,11 @@ var AnimationsDebugModule = require('NativeModules').AnimationsDebugModule; var Dimensions = require('Dimensions'); var InteractionMixin = require('InteractionMixin'); +var NavigationContext = require('NavigationContext'); var NavigatorBreadcrumbNavigationBar = require('NavigatorBreadcrumbNavigationBar'); var NavigatorNavigationBar = require('NavigatorNavigationBar'); var NavigatorSceneConfigs = require('NavigatorSceneConfigs'); var PanResponder = require('PanResponder'); -var Platform = require('Platform'); var React = require('React'); var StaticContainer = require('StaticContainer.react'); var StyleSheet = require('StyleSheet'); @@ -203,11 +203,17 @@ var Navigator = React.createClass({ initialRouteStack: PropTypes.arrayOf(PropTypes.object), /** + * @deprecated + * Use `navigationContext.addListener('willfocus', callback)` instead. + * * Will emit the target route upon mounting and before each nav transition */ onWillFocus: PropTypes.func, /** + * @deprecated + * Use `navigationContext.addListener('didfocus', callback)` instead. + * * Will be called with the new route of each scene after the transition is * complete or after the initial mounting */ @@ -321,7 +327,10 @@ var Navigator = React.createClass({ }, componentWillUnmount: function() { - + if (this._navigationContext) { + this._navigationContext.dispose(); + this._navigationContext = null; + } }, /** @@ -461,12 +470,16 @@ var Navigator = React.createClass({ }, _emitDidFocus: function(route) { + this.navigationContext.emit('didfocus', {route: route}); + if (this.props.onDidFocus) { this.props.onDidFocus(route); } }, _emitWillFocus: function(route) { + this.navigationContext.emit('willfocus', {route: route}); + var navBar = this._navBar; if (navBar && navBar.handleWillFocus) { navBar.handleWillFocus(route); @@ -1139,6 +1152,14 @@ var Navigator = React.createClass({ ); }, + + // Getter for `navigationContext`. + get navigationContext() { + if (!this._navigationContext) { + this._navigationContext = new NavigationContext(); + } + return this._navigationContext; + } }); module.exports = Navigator;