Unverified Commit 542ae6c8 authored by Robert Knight's avatar Robert Knight Committed by GitHub

Merge pull request #1766 from hypothesis/avoid-ng-lookup-for-services

Decouple `wrapReactComponent` from Angular `$injector`
parents e279908b dff1f428
...@@ -251,6 +251,8 @@ function startAngularApp(config) { ...@@ -251,6 +251,8 @@ function startAngularApp(config) {
.register('$rootScope', { value: $rootScope }); .register('$rootScope', { value: $rootScope });
} }
const wrapComponent = component => wrapReactComponent(component, container);
angular angular
.module('h', [angularRoute, angularToastr]) .module('h', [angularRoute, angularToastr])
...@@ -259,36 +261,33 @@ function startAngularApp(config) { ...@@ -259,36 +261,33 @@ function startAngularApp(config) {
// UI components // UI components
.component('annotation', annotation) .component('annotation', annotation)
.component('annotationBody', wrapReactComponent(AnnotationBody)) .component('annotationBody', wrapComponent(AnnotationBody))
.component('annotationHeader', wrapReactComponent(AnnotationHeader)) .component('annotationHeader', wrapComponent(AnnotationHeader))
.component('annotationActionBar', wrapReactComponent(AnnotationActionBar)) .component('annotationActionBar', wrapComponent(AnnotationActionBar))
.component('annotationLicense', wrapReactComponent(AnnotationLicense)) .component('annotationLicense', wrapComponent(AnnotationLicense))
.component('annotationOmega', wrapReactComponent(AnnotationOmega)) .component('annotationOmega', wrapComponent(AnnotationOmega))
.component( .component(
'annotationPublishControl', 'annotationPublishControl',
wrapReactComponent(AnnotationPublishControl) wrapComponent(AnnotationPublishControl)
) )
.component('annotationQuote', wrapReactComponent(AnnotationQuote)) .component('annotationQuote', wrapComponent(AnnotationQuote))
.component('annotationThread', annotationThread) .component('annotationThread', annotationThread)
.component('annotationViewerContent', annotationViewerContent) .component('annotationViewerContent', annotationViewerContent)
.component('helpPanel', wrapReactComponent(HelpPanel)) .component('helpPanel', wrapComponent(HelpPanel))
.component('loggedOutMessage', wrapReactComponent(LoggedOutMessage)) .component('loggedOutMessage', wrapComponent(LoggedOutMessage))
.component('moderationBanner', wrapReactComponent(ModerationBanner)) .component('moderationBanner', wrapComponent(ModerationBanner))
.component('searchStatusBar', wrapReactComponent(SearchStatusBar)) .component('searchStatusBar', wrapComponent(SearchStatusBar))
.component('focusedModeHeader', wrapReactComponent(FocusedModeHeader)) .component('focusedModeHeader', wrapComponent(FocusedModeHeader))
.component('selectionTabs', wrapReactComponent(SelectionTabs)) .component('selectionTabs', wrapComponent(SelectionTabs))
.component('sidebarContent', sidebarContent) .component('sidebarContent', sidebarContent)
.component('sidebarContentError', wrapReactComponent(SidebarContentError)) .component('sidebarContentError', wrapComponent(SidebarContentError))
.component( .component('shareAnnotationsPanel', wrapComponent(ShareAnnotationsPanel))
'shareAnnotationsPanel',
wrapReactComponent(ShareAnnotationsPanel)
)
.component('streamContent', streamContent) .component('streamContent', streamContent)
.component('svgIcon', wrapReactComponent(SvgIcon)) .component('svgIcon', wrapComponent(SvgIcon))
.component('tagEditor', wrapReactComponent(TagEditor)) .component('tagEditor', wrapComponent(TagEditor))
.component('tagList', wrapReactComponent(TagList)) .component('tagList', wrapComponent(TagList))
.component('threadList', threadList) .component('threadList', threadList)
.component('topBar', wrapReactComponent(TopBar)) .component('topBar', wrapComponent(TopBar))
.directive('hAutofocus', hAutofocusDirective) .directive('hAutofocus', hAutofocusDirective)
.directive('hBranding', hBrandingDirective) .directive('hBranding', hBrandingDirective)
.directive('hOnTouch', hOnTouchDirective) .directive('hOnTouch', hOnTouchDirective)
...@@ -315,7 +314,6 @@ function startAngularApp(config) { ...@@ -315,7 +314,6 @@ function startAngularApp(config) {
.service('session', () => container.get('session')) .service('session', () => container.get('session'))
.service('streamer', () => container.get('streamer')) .service('streamer', () => container.get('streamer'))
.service('streamFilter', () => container.get('streamFilter')) .service('streamFilter', () => container.get('streamFilter'))
.service('tags', () => container.get('tags'))
// Redux store // Redux store
.service('store', () => container.get('store')) .service('store', () => container.get('store'))
......
...@@ -64,7 +64,7 @@ export function withServices(Component) { ...@@ -64,7 +64,7 @@ export function withServices(Component) {
function Wrapper(props) { function Wrapper(props) {
// Get the current dependency injector instance that is provided by a // Get the current dependency injector instance that is provided by a
// `ServiceContext.Provider` somewhere higher up the component tree. // `ServiceContext.Provider` somewhere higher up the component tree.
const $injector = useContext(ServiceContext); const injector = useContext(ServiceContext);
// Inject services, unless they have been overridden by props passed from // Inject services, unless they have been overridden by props passed from
// the parent component. // the parent component.
...@@ -80,7 +80,7 @@ export function withServices(Component) { ...@@ -80,7 +80,7 @@ export function withServices(Component) {
} }
if (!(service in props)) { if (!(service in props)) {
services[service] = $injector.get(service); services[service] = injector.get(service);
} }
} }
return <Component {...services} {...props} />; return <Component {...services} {...props} />;
......
...@@ -3,6 +3,7 @@ import { Component, createElement } from 'preact'; ...@@ -3,6 +3,7 @@ import { Component, createElement } from 'preact';
import { useContext } from 'preact/hooks'; import { useContext } from 'preact/hooks';
import propTypes from 'prop-types'; import propTypes from 'prop-types';
import { Injector } from '../../../shared/injector';
import { createDirective } from '../../directive/test/util'; import { createDirective } from '../../directive/test/util';
import { ServiceContext } from '../service-context'; import { ServiceContext } from '../service-context';
import wrapReactComponent from '../wrap-react-component'; import wrapReactComponent from '../wrap-react-component';
...@@ -58,11 +59,15 @@ describe('wrapReactComponent', () => { ...@@ -58,11 +59,15 @@ describe('wrapReactComponent', () => {
return { element, onClick, onDblClick }; return { element, onClick, onDblClick };
} }
let servicesInjector;
beforeEach(() => { beforeEach(() => {
const servicesInjector = new Injector();
servicesInjector.register('theme', { value: 'dark' });
angular angular
.module('app', []) .module('app', [])
.component('btn', wrapReactComponent(Button)) .component('btn', wrapReactComponent(Button, servicesInjector));
.value('theme', 'dark');
angular.mock.module('app'); angular.mock.module('app');
}); });
...@@ -73,7 +78,7 @@ describe('wrapReactComponent', () => { ...@@ -73,7 +78,7 @@ describe('wrapReactComponent', () => {
}); });
it('derives Angular component "bindings" from React "propTypes"', () => { it('derives Angular component "bindings" from React "propTypes"', () => {
const ngComponent = wrapReactComponent(Button); const ngComponent = wrapReactComponent(Button, servicesInjector);
assert.deepEqual(ngComponent.bindings, { assert.deepEqual(ngComponent.bindings, {
label: '<', label: '<',
isDisabled: '<', isDisabled: '<',
...@@ -141,7 +146,7 @@ describe('wrapReactComponent', () => { ...@@ -141,7 +146,7 @@ describe('wrapReactComponent', () => {
angular angular
.module('app', []) .module('app', [])
.component('parent', parentComponent) .component('parent', parentComponent)
.component('child', wrapReactComponent(ChildComponent)); .component('child', wrapReactComponent(ChildComponent, servicesInjector));
angular.mock.module('app'); angular.mock.module('app');
// Render the component with the child initially visible. // Render the component with the child initially visible.
...@@ -160,7 +165,7 @@ describe('wrapReactComponent', () => { ...@@ -160,7 +165,7 @@ describe('wrapReactComponent', () => {
return <div>Hello world</div>; return <div>Hello world</div>;
} }
assert.throws( assert.throws(
() => wrapReactComponent(TestComponent), () => wrapReactComponent(TestComponent, servicesInjector),
'React component TestComponent does not specify its inputs using "propTypes"' 'React component TestComponent does not specify its inputs using "propTypes"'
); );
}); });
...@@ -231,7 +236,7 @@ describe('wrapReactComponent', () => { ...@@ -231,7 +236,7 @@ describe('wrapReactComponent', () => {
angular angular
.module('app', []) .module('app', [])
.component('parent', parentComponent) .component('parent', parentComponent)
.component('child', wrapReactComponent(Child)); .component('child', wrapReactComponent(Child, servicesInjector));
angular.mock.module('app'); angular.mock.module('app');
const element = createDirective(document, 'parent'); const element = createDirective(document, 'parent');
......
...@@ -6,6 +6,10 @@ function useExpressionBinding(propName) { ...@@ -6,6 +6,10 @@ function useExpressionBinding(propName) {
return propName.match(/^on[A-Z]/); return propName.match(/^on[A-Z]/);
} }
/**
* @typedef {import('../../shared/injector').Injector} Injector
*/
/** /**
* Controller for an Angular component that wraps a React component. * Controller for an Angular component that wraps a React component.
* *
...@@ -14,14 +18,16 @@ function useExpressionBinding(propName) { ...@@ -14,14 +18,16 @@ function useExpressionBinding(propName) {
* has been created. * has been created.
*/ */
class ReactController { class ReactController {
constructor($element, $injector, $scope, type) { constructor($element, services, $scope, type) {
/** The DOM element where the React component should be rendered. */ /** The DOM element where the React component should be rendered. */
this.domElement = $element[0]; this.domElement = $element[0];
/** /**
* The Angular service injector, used by this component and its descendants. * The services injector, used by this component and its descendants.
*
* @type {Injector}
*/ */
this.$injector = $injector; this.services = services;
/** The React component function or class. */ /** The React component function or class. */
this.type = type; this.type = type;
...@@ -83,10 +89,10 @@ class ReactController { ...@@ -83,10 +89,10 @@ class ReactController {
updateReactComponent() { updateReactComponent() {
// Render component, with a `ServiceContext.Provider` wrapper which // Render component, with a `ServiceContext.Provider` wrapper which
// provides access to Angular services via `withServices` or `useContext` // provides access to services via `withServices` or `useContext`
// in child components. // in child components.
render( render(
<ServiceContext.Provider value={this.$injector}> <ServiceContext.Provider value={this.services}>
<this.type {...this.props} /> <this.type {...this.props} />
</ServiceContext.Provider>, </ServiceContext.Provider>,
this.domElement this.domElement
...@@ -103,14 +109,16 @@ class ReactController { ...@@ -103,14 +109,16 @@ class ReactController {
* one-way ('<') bindings except for those with names matching /^on[A-Z]/ which * one-way ('<') bindings except for those with names matching /^on[A-Z]/ which
* are assumed to be callbacks that use expression ('&') bindings. * are assumed to be callbacks that use expression ('&') bindings.
* *
* If the React component needs access to an Angular service, it can get at * If the React component needs access to a service, it can get at
* them using the `withServices` wrapper from service-context.js. * them using the `withServices` wrapper from service-context.js.
* *
* @param {Function} type - The React component class or function * @param {Function} type - The React component class or function
* @param {Injector} services
* Dependency injection container providing services that components use.
* @return {Object} - * @return {Object} -
* An AngularJS component spec for use with `angular.component(...)` * An AngularJS component spec for use with `angular.component(...)`
*/ */
export default function wrapReactComponent(type) { export default function wrapReactComponent(type, services) {
if (!type.propTypes) { if (!type.propTypes) {
throw new Error( throw new Error(
`React component ${type.name} does not specify its inputs using "propTypes"` `React component ${type.name} does not specify its inputs using "propTypes"`
...@@ -122,8 +130,8 @@ export default function wrapReactComponent(type) { ...@@ -122,8 +130,8 @@ export default function wrapReactComponent(type) {
* component being wrapped. * component being wrapped.
*/ */
// @ngInject // @ngInject
function createController($element, $injector, $scope) { function createController($element, $scope) {
return new ReactController($element, $injector, $scope, type); return new ReactController($element, services, $scope, type);
} }
const bindings = {}; const bindings = {};
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment