upgrade to react 18

This commit is contained in:
Alissa
2023-01-06 08:58:42 +02:00
parent e1248f6e1d
commit e66a6d8f20
4 changed files with 1429 additions and 1314 deletions
+14 -6
View File
@@ -2,7 +2,7 @@ import { IAugmentedJQuery, IComponentOptions } from 'angular'
import fromPairs = require('lodash.frompairs') import fromPairs = require('lodash.frompairs')
import NgComponent from 'ngcomponent' import NgComponent from 'ngcomponent'
import * as React from 'react' import * as React from 'react'
import { render, unmountComponentAtNode } from 'react-dom' import { createRoot, Root } from 'react-dom/client'
/** /**
* Wraps a React component in Angular. Returns a new Angular component. * Wraps a React component in Angular. Returns a new Angular component.
@@ -15,7 +15,7 @@ import { render, unmountComponentAtNode } from 'react-dom'
* const AngularComponent = react2angular(ReactComponent, ['foo']) * const AngularComponent = react2angular(ReactComponent, ['foo'])
* ``` * ```
*/ */
export function react2angular<Props>( export function react2angular<Props extends {}>(
Class: React.ComponentType<Props>, Class: React.ComponentType<Props>,
bindingNames: (keyof Props)[] | null = null, bindingNames: (keyof Props)[] | null = null,
injectNames: string[] = [] injectNames: string[] = []
@@ -27,6 +27,7 @@ export function react2angular<Props>(
return { return {
bindings: fromPairs(names.map(_ => [_, '<'])), bindings: fromPairs(names.map(_ => [_, '<'])),
controller: ['$element', ...injectNames, class extends NgComponent<Props> { controller: ['$element', ...injectNames, class extends NgComponent<Props> {
root: Root
static get $$ngIsClass() { static get $$ngIsClass() {
return true return true
} }
@@ -38,18 +39,25 @@ export function react2angular<Props>(
injectNames.forEach((name, i) => { injectNames.forEach((name, i) => {
this.injectedProps[name] = injectedProps[i] this.injectedProps[name] = injectedProps[i]
}) })
this.root = createRoot($element[0])
}
$onInit() {
names.forEach((name) => {
this.props[name] = (this as any)[name]
})
} }
render() { render() {
if (!this.isDestroyed) { if (!this.isDestroyed) {
render( this.root.render(
<Class {...this.props} {...this.injectedProps as any} />, <Class {...this.props} {...this.injectedProps as any} />
this.$element[0]
) )
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.isDestroyed = true this.isDestroyed = true
unmountComponentAtNode(this.$element[0]) if (this.$element[0] && this.root) {
this.root.unmount()
}
} }
}] }]
} }
+9 -14
View File
@@ -1,7 +1,7 @@
{ {
"name": "react2angular", "name": "react_18_2angular",
"version": "4.0.6", "version": "5.0.0",
"description": "The easiest way to embed React components in Angular 1 apps!", "description": "The easiest way to embed React components in Angular 1 apps! react 18 compatible",
"main": "index.js", "main": "index.js",
"main:esnext": "index.es2015.js", "main:esnext": "index.es2015.js",
"typings": "index.d.ts", "typings": "index.d.ts",
@@ -36,13 +36,8 @@
}, },
"homepage": "https://github.com/coatue-oss/react2angular#readme", "homepage": "https://github.com/coatue-oss/react2angular#readme",
"peerDependencies": { "peerDependencies": {
"@types/angular": ">=1.5", "react": ">=18",
"@types/prop-types": ">=15", "react-dom": ">=18"
"@types/react": ">=16",
"@types/react-dom": ">=16",
"prop-types": ">=15",
"react": ">=15",
"react-dom": ">=15"
}, },
"devDependencies": { "devDependencies": {
"@types/angular": "^1.6.54", "@types/angular": "^1.6.54",
@@ -50,8 +45,8 @@
"@types/jasmine": "^3.3.9", "@types/jasmine": "^3.3.9",
"@types/jquery": "^3.3.29", "@types/jquery": "^3.3.29",
"@types/prop-types": "^15.7.0", "@types/prop-types": "^15.7.0",
"@types/react": "^16.8.6", "@types/react": "^18.0.26",
"@types/react-dom": "^16.8.2", "@types/react-dom": "^18.0.10",
"angular-mocks": "1.6.9", "angular-mocks": "1.6.9",
"angular-resource": "^1.7.7", "angular-resource": "^1.7.7",
"browserify": "^16.2.3", "browserify": "^16.2.3",
@@ -66,8 +61,8 @@
"ngimport": "^1.0.0", "ngimport": "^1.0.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^16.8.3", "react": "^18.2.0",
"react-dom": "^16.8.3", "react-dom": "^18.2.0",
"rimraf": "^2.6.3", "rimraf": "^2.6.3",
"rollupify": "^0.5.1", "rollupify": "^0.5.1",
"tslint": "^5.13.1", "tslint": "^5.13.1",
+70 -30
View File
@@ -4,13 +4,15 @@ import {
IAugmentedJQuery, IAugmentedJQuery,
ICompileService, ICompileService,
IComponentOptions, IController, IComponentOptions, IController,
IHttpResponse,
IHttpService, IHttpService,
IQService, IScope, IPromise,
IScope,
module module
} from 'angular' } from 'angular'
import * as angular from 'angular' import * as angular from 'angular'
import 'angular-mocks' import 'angular-mocks'
import { $http, $q, $rootScope } from 'ngimport' import { $http, $rootScope } from 'ngimport'
import * as PropTypes from 'prop-types' import * as PropTypes from 'prop-types'
import * as React from 'react' import * as React from 'react'
import { Simulate } from 'react-dom/test-utils' import { Simulate } from 'react-dom/test-utils'
@@ -28,7 +30,7 @@ class TestOne extends React.Component<Props> {
componentWillUnmount() { } componentWillUnmount() { }
} }
const TestTwo: React.StatelessComponent<Props> = props => const TestTwo: React.FunctionComponent<Props> = props =>
<div> <div>
<p>Foo: {props.foo}</p> <p>Foo: {props.foo}</p>
<p>Bar: {props.bar.join(',')}</p> <p>Bar: {props.bar.join(',')}</p>
@@ -36,7 +38,7 @@ const TestTwo: React.StatelessComponent<Props> = props =>
{props.children} {props.children}
</div> </div>
const TestThree: React.StatelessComponent = () => const TestThree: React.FunctionComponent = () =>
<div>Foo</div> <div>Foo</div>
class TestFour extends React.Component<Props> { class TestFour extends React.Component<Props> {
@@ -64,10 +66,10 @@ class TestFive extends React.Component<Props> {
} }
class TestSixService { class TestSixService {
constructor(private $q: IQService) { } constructor() { }
foo() { foo() {
return this.$q.resolve('testSixService result') return new Promise((resolve) => resolve('testSixService result'))
} }
} }
@@ -98,12 +100,14 @@ class TestSix extends React.Component<Props & DIProps> {
this.setState({ this.setState({
elementText: this.props.$element.find('span').text() elementText: this.props.$element.find('span').text()
}) })
this.props.$http.get('https://example.com/').then(_ => this.props.$http.get('https://example.com/').then(_ => {
this.setState({ result: _.data }) this.setState({ result: _.data })
}
) )
this.props.testSixService.foo().then(_ => this.props.testSixService.foo().then(_ => {
this.setState({ testSixService: _ }) this.setState({ testSixService: _ })
)
})
} }
} }
@@ -128,7 +132,7 @@ class TestEight extends React.Component<TestEightProps> {
componentWillUnmount() { componentWillUnmount() {
this.props.onComponentWillUnmount() this.props.onComponentWillUnmount()
this.props.onChange(this.props.values this.props.onChange(this.props.values
.map(val => `${val}ss`)) .map(val => `${val}ss`))
} }
} }
@@ -149,7 +153,7 @@ class TestEightWrapper implements IComponentOptions {
constructor( constructor(
private $scope: IScope private $scope: IScope
){} ) { }
onChange = (values: string[]) => { onChange = (values: string[]) => {
this.values = values this.values = values
@@ -166,6 +170,9 @@ const TestAngularSix = react2angular(TestSix, ['foo'], ['$http', '$element', 'te
const TestAngularSeven = react2angular(TestSeven, null, ['foo']) const TestAngularSeven = react2angular(TestSeven, null, ['foo'])
const TestAngularEight = react2angular(TestEight, ['values', 'onComponentWillUnmount', 'onRender', 'onChange']) const TestAngularEight = react2angular(TestEight, ['values', 'onComponentWillUnmount', 'onRender', 'onChange'])
//render + mount isn't sync, this is an alternative to act()
const delay = () => new Promise(resolve => setTimeout(resolve, 10))
module('test', ['bcherny/ngimport']) module('test', ['bcherny/ngimport'])
.component('testAngularOne', TestAngularOne) .component('testAngularOne', TestAngularOne)
.component('testAngularTwo', TestAngularTwo) .component('testAngularTwo', TestAngularTwo)
@@ -184,6 +191,7 @@ interface Props {
bar: boolean[] bar: boolean[]
baz(value: number): any baz(value: number): any
foo: number foo: number
children: React.ReactNode,
} }
describe('react2angular', () => { describe('react2angular', () => {
@@ -192,7 +200,7 @@ describe('react2angular', () => {
beforeEach(() => { beforeEach(() => {
(angular as any).mock.module('test'); (angular as any).mock.module('test');
(angular as any).mock.inject(function(_$compile_: ICompileService) { (angular as any).mock.inject(function (_$compile_: ICompileService) {
$compile = _$compile_ $compile = _$compile_
}) })
}) })
@@ -245,7 +253,7 @@ describe('react2angular', () => {
describe('react classes', () => { describe('react classes', () => {
it('should render', () => { it('should render', async () => {
const scope = Object.assign($rootScope.$new(true), { const scope = Object.assign($rootScope.$new(true), {
bar: [true, false], bar: [true, false],
baz: (value: number) => value + 1, baz: (value: number) => value + 1,
@@ -254,18 +262,22 @@ describe('react2angular', () => {
const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"></test-angular-one>`) const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"></test-angular-one>`)
$compile(element)(scope) $compile(element)(scope)
$rootScope.$apply() $rootScope.$apply()
await delay()
expect(element.find('p').length).toBe(3) expect(element.find('p').length).toBe(3)
}) })
it('should render (even if the component takes no props)', () => { it('should render (even if the component takes no props)', async () => {
const scope = $rootScope.$new(true) const scope = $rootScope.$new(true)
const element = $(`<test-angular-four></test-angular-four>`) const element = $(`<test-angular-four></test-angular-four>`)
$compile(element)(scope) $compile(element)(scope)
$rootScope.$apply() $rootScope.$apply()
await delay()
expect(element.text()).toBe('Foo') expect(element.text()).toBe('Foo')
}) })
it('should update', () => { it('should update', async () => {
const scope = Object.assign($rootScope.$new(true), { const scope = Object.assign($rootScope.$new(true), {
bar: [true, false], bar: [true, false],
baz: (value: number) => value + 1, baz: (value: number) => value + 1,
@@ -274,14 +286,18 @@ describe('react2angular', () => {
const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"></test-angular-one>`) const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"></test-angular-one>`)
$compile(element)(scope) $compile(element)(scope)
$rootScope.$apply() $rootScope.$apply()
await delay()
expect(element.find('p').eq(1).text()).toBe('Bar: true,false') expect(element.find('p').eq(1).text()).toBe('Bar: true,false')
scope.$apply(() => scope.$apply(() =>
scope.bar = [false, true, true] scope.bar = [false, true, true]
) )
await delay()
expect(element.find('p').eq(1).text()).toBe('Bar: false,true,true') expect(element.find('p').eq(1).text()).toBe('Bar: false,true,true')
}) })
it('should destroy', () => { it('should destroy', async () => {
const scope = Object.assign($rootScope.$new(true), { const scope = Object.assign($rootScope.$new(true), {
bar: [true, false], bar: [true, false],
baz: (value: number) => value + 1, baz: (value: number) => value + 1,
@@ -290,12 +306,14 @@ describe('react2angular', () => {
const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"></test-angular-one>`) const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"></test-angular-one>`)
$compile(element)(scope) $compile(element)(scope)
$rootScope.$apply() $rootScope.$apply()
await delay()
spyOn(TestOne.prototype, 'componentWillUnmount') spyOn(TestOne.prototype, 'componentWillUnmount')
scope.$destroy() scope.$destroy()
expect(TestOne.prototype.componentWillUnmount).toHaveBeenCalled() expect(TestOne.prototype.componentWillUnmount).toHaveBeenCalled()
}) })
it('should take callbacks', () => { it('should take callbacks', async () => {
const baz = jasmine.createSpy('baz') const baz = jasmine.createSpy('baz')
const scope = Object.assign($rootScope.$new(true), { const scope = Object.assign($rootScope.$new(true), {
bar: [true, false], bar: [true, false],
@@ -305,12 +323,14 @@ describe('react2angular', () => {
const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"></test-angular-one>`) const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"></test-angular-one>`)
$compile(element)(scope) $compile(element)(scope)
$rootScope.$apply() $rootScope.$apply()
await delay()
Simulate.click(element.find('p').eq(2)[0]) Simulate.click(element.find('p').eq(2)[0])
expect(baz).toHaveBeenCalledWith(42) expect(baz).toHaveBeenCalledWith(42)
}) })
// TODO: support children // TODO: support children
it('should not support children', () => { it('should not support children', async () => {
const scope = Object.assign($rootScope.$new(true), { const scope = Object.assign($rootScope.$new(true), {
bar: [true, false], bar: [true, false],
baz: (value: number) => value + 1, baz: (value: number) => value + 1,
@@ -319,11 +339,13 @@ describe('react2angular', () => {
const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"><span>Transcluded</span></test-angular-one>`) const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"><span>Transcluded</span></test-angular-one>`)
$compile(element)(scope) $compile(element)(scope)
$rootScope.$apply() $rootScope.$apply()
await delay()
expect(element.find('span').length).toBe(0) expect(element.find('span').length).toBe(0)
}) })
it('should take injections, which override props', () => { it('should take injections, which override props', async () => {
spyOn($http, 'get').and.returnValue($q.resolve({ data: '$http response' })) spyOn($http, 'get').and.returnValue(new Promise((res) => res({ data: '$http response' })) as unknown as IPromise<IHttpResponse<unknown>>)
const scope = Object.assign($rootScope.$new(true), { const scope = Object.assign($rootScope.$new(true), {
foo: 'FOO' foo: 'FOO'
}) })
@@ -336,19 +358,24 @@ describe('react2angular', () => {
$rootScope.$apply() $rootScope.$apply()
await delay()
expect($http.get).toHaveBeenCalledWith('https://example.com/') expect($http.get).toHaveBeenCalledWith('https://example.com/')
expect(element1.find('p').eq(0).text()).toBe('$http response', '$http is injected')
expect(element1.find('p').eq(1).text()).toBe('$element result', '$element is injected') expect(element1.find('p').eq(1).text()).toBe('$element result', '$element is injected')
expect(element1.find('p').eq(2).text()).toBe('testSixService result', 'testSixService is injected')
expect(element1.find('p').eq(3).text()).toBe('CONSTANT FOO', 'injections should override props') expect(element1.find('p').eq(3).text()).toBe('CONSTANT FOO', 'injections should override props')
expect(element2.find('p').text()).toBe('CONSTANT FOO', 'injections should override props') expect(element2.find('p').text()).toBe('CONSTANT FOO', 'injections should override props')
expect(element1.find('p').eq(0).text()).toBe('$http response', '$http is injected')
expect(element1.find('p').eq(2).text()).toBe('testSixService result', 'testSixService is injected')
}) })
}) })
describe('react stateless components', () => { describe('react stateless components', () => {
it('should render', () => { it('should render', async () => {
const scope = Object.assign($rootScope.$new(true), { const scope = Object.assign($rootScope.$new(true), {
bar: [true, false], bar: [true, false],
baz: (value: number) => value + 1, baz: (value: number) => value + 1,
@@ -357,18 +384,22 @@ describe('react2angular', () => {
const element = $(`<test-angular-two foo="foo" bar="bar" baz="baz"></test-angular-two>`) const element = $(`<test-angular-two foo="foo" bar="bar" baz="baz"></test-angular-two>`)
$compile(element)(scope) $compile(element)(scope)
$rootScope.$apply() $rootScope.$apply()
await delay()
expect(element.find('p').length).toBe(3) expect(element.find('p').length).toBe(3)
}) })
it('should render (even if the component takes no props)', () => { it('should render (even if the component takes no props)', async () => {
const scope = $rootScope.$new(true) const scope = $rootScope.$new(true)
const element = $(`<test-angular-three></test-angular-three>`) const element = $(`<test-angular-three></test-angular-three>`)
$compile(element)(scope) $compile(element)(scope)
$rootScope.$apply() $rootScope.$apply()
await delay()
expect(element.text()).toBe('Foo') expect(element.text()).toBe('Foo')
}) })
it('should update', () => { it('should update', async () => {
const scope = Object.assign($rootScope.$new(true), { const scope = Object.assign($rootScope.$new(true), {
bar: [true, false], bar: [true, false],
baz: (value: number) => value + 1, baz: (value: number) => value + 1,
@@ -377,17 +408,21 @@ describe('react2angular', () => {
const element = $(`<test-angular-two foo="foo" bar="bar" baz="baz"></test-angular-two>`) const element = $(`<test-angular-two foo="foo" bar="bar" baz="baz"></test-angular-two>`)
$compile(element)(scope) $compile(element)(scope)
$rootScope.$apply() $rootScope.$apply()
await delay()
expect(element.find('p').eq(1).text()).toBe('Bar: true,false') expect(element.find('p').eq(1).text()).toBe('Bar: true,false')
scope.$apply(() => scope.$apply(() =>
scope.bar = [false, true, true] scope.bar = [false, true, true]
) )
await delay()
expect(element.find('p').eq(1).text()).toBe('Bar: false,true,true') expect(element.find('p').eq(1).text()).toBe('Bar: false,true,true')
}) })
// TODO: figure out how to test this // TODO: figure out how to test this
xit('should destroy', () => { }) xit('should destroy', () => { })
it('should take callbacks', () => { it('should take callbacks', async () => {
const baz = jasmine.createSpy('baz') const baz = jasmine.createSpy('baz')
const scope = Object.assign($rootScope.$new(true), { const scope = Object.assign($rootScope.$new(true), {
bar: [true, false], bar: [true, false],
@@ -397,12 +432,14 @@ describe('react2angular', () => {
const element = $(`<test-angular-two foo="foo" bar="bar" baz="baz"></test-angular-two>`) const element = $(`<test-angular-two foo="foo" bar="bar" baz="baz"></test-angular-two>`)
$compile(element)(scope) $compile(element)(scope)
$rootScope.$apply() $rootScope.$apply()
await delay()
Simulate.click(element.find('p').eq(2)[0]) Simulate.click(element.find('p').eq(2)[0])
expect(baz).toHaveBeenCalledWith(42) expect(baz).toHaveBeenCalledWith(42)
}) })
// TODO: support children // TODO: support children
it('should not support children', () => { it('should not support children', async () => {
const scope = Object.assign($rootScope.$new(true), { const scope = Object.assign($rootScope.$new(true), {
bar: [true, false], bar: [true, false],
baz: (value: number) => value + 1, baz: (value: number) => value + 1,
@@ -411,10 +448,12 @@ describe('react2angular', () => {
const element = $(`<test-angular-two foo="foo" bar="bar" baz="baz"><span>Transcluded</span></test-angular-two>`) const element = $(`<test-angular-two foo="foo" bar="bar" baz="baz"><span>Transcluded</span></test-angular-two>`)
$compile(element)(scope) $compile(element)(scope)
$rootScope.$apply() $rootScope.$apply()
await delay()
expect(element.find('span').length).toBe(0) expect(element.find('span').length).toBe(0)
}) })
it('should not call render after component unmount', () => { it('should not call render after component unmount', async () => {
const componentWillUnmountSpy = jasmine.createSpy('componentWillUnmount') const componentWillUnmountSpy = jasmine.createSpy('componentWillUnmount')
const renderSpy = jasmine.createSpy('render') const renderSpy = jasmine.createSpy('render')
@@ -434,10 +473,11 @@ describe('react2angular', () => {
$compile(element)(scope) $compile(element)(scope)
const childScope = angular const childScope = angular
.element(element.find('test-angular-eight')) .element(element.find('test-angular-eight'))
.scope() .scope()
$rootScope.$apply() $rootScope.$apply()
await delay()
// Erase first render caused on apply // Erase first render caused on apply
renderSpy.calls.reset() renderSpy.calls.reset()
+1336 -1264
View File
File diff suppressed because it is too large Load Diff