Skip to content

Commit 4b3c3d1

Browse files
author
Ben Grynhaus
committed
core changes
2 parents 8fdef0d + da5efbd commit 4b3c3d1

File tree

6 files changed

+111
-23
lines changed

6 files changed

+111
-23
lines changed

libs/core/package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/core/package.json

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
{
22
"$schema": "../../node_modules/ng-packagr/package.schema.json",
33
"name": "@angular-react/core",
4-
"version": "0.1.13-alpha6",
4+
"version": "0.2.0-6",
55
"ngPackage": {
66
"lib": {
77
"languageLevel": [
88
"dom",
99
"es2017"
1010
],
11+
"embedded": [
12+
"css-to-style"
13+
],
1114
"entryFile": "public-api.ts",
1215
"umdModuleIds": {
1316
"react": "React",
14-
"react-dom": "ReactDOM"
17+
"react-dom": "ReactDOM",
18+
"css-to-style": "toStyle"
1519
},
1620
"comments": "none"
1721
},
22+
"whitelistedNonPeerDependencies": [
23+
"tslib",
24+
"css-to-style"
25+
],
1826
"dest": "../../@angular-react/core"
1927
},
2028
"description": "Use React components inside Angular",
@@ -39,9 +47,14 @@
3947
"@angular/core": "^5.2.7",
4048
"@angular/platform-browser-dynamic": "^5.2.7",
4149
"@angular/platform-browser": "^5.2.7",
42-
"@types/react-dom": "^16.0.4",
43-
"@types/react": "^16.3.4",
4450
"react-dom": "^16.3.1",
4551
"react": "^16.3.1"
52+
},
53+
"dependencies": {
54+
"css-to-style": "^1.2.0"
55+
},
56+
"devDependencies": {
57+
"@types/react-dom": "^16.0.4",
58+
"@types/react": "^16.3.4"
4659
}
4760
}

libs/core/src/components/wrapper-component.ts

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
1-
import { AfterViewInit, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, TemplateRef, Type } from "@angular/core";
1+
import { AfterViewInit, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, TemplateRef, Type, ChangeDetectorRef, OnChanges, SimpleChanges, HostBinding, Input } from "@angular/core";
22
import { isReactNode } from "../renderer/react-node";
33
import { renderComponent, renderFunc, renderTemplate } from "../renderer/renderprop-helpers";
44
import { unreachable } from "../utils/types/unreachable";
5+
import toStyle from 'css-to-style';
56

6-
const blacklistedAttributesAsProps = [
7-
'class',
8-
'style'
7+
type PropMapper = (value: any) => [string, any];
8+
9+
type AttributeNameAlternative = [string, string | undefined];
10+
11+
const forbiddenAttributesAsProps: ReadonlyArray<AttributeNameAlternative> = [
12+
['key', null],
13+
['class', 'rClass'],
14+
['style', 'rStyle'],
915
];
1016

11-
const blacklistedAttributeMatchers = [
17+
const ignoredAttributeMatchers = [
1218
/^_?ng-?.*/
1319
];
1420

21+
const ngClassRegExp = /^ng-/;
22+
1523
export interface RenderComponentOptions<TContext extends object> {
1624
readonly componentType: Type<TContext>;
1725
readonly factoryResolver: ComponentFactoryResolver;
@@ -31,16 +39,30 @@ export type JsxRenderFunc<TContext> = (context: TContext) => JSX.Element;
3139
* Simplifies some of the handling around passing down props and setting CSS on the host component.
3240
*/
3341
// NOTE: TProps is not used at the moment, but a preparation for a potential future change.
34-
export abstract class ReactWrapperComponent<TProps extends {}> implements AfterViewInit {
42+
export abstract class ReactWrapperComponent<TProps extends {}> implements AfterViewInit, OnChanges {
3543

3644
protected abstract reactNodeRef: ElementRef;
3745

46+
@Input() set rClass(value: string) {
47+
if (isReactNode(this.reactNodeRef.nativeElement)) {
48+
this.reactNodeRef.nativeElement.setProperty('className', value);
49+
this.changeDetectorRef.detectChanges();
50+
}
51+
}
52+
53+
@Input() set rStyle(value: string) {
54+
if (isReactNode(this.reactNodeRef.nativeElement)) {
55+
this.reactNodeRef.nativeElement.setProperty('style', toStyle(value));
56+
this.changeDetectorRef.detectChanges();
57+
}
58+
}
59+
3860
/**
3961
* Creates an instance of ReactWrapperComponent.
4062
* @param elementRef The host element.
4163
* @param setHostDisplay Whether the host's `display` should be set to the root child node's `display`. defaults to `false`
4264
*/
43-
constructor(public readonly elementRef: ElementRef, private readonly setHostDisplay: boolean = false) { }
65+
constructor(public readonly elementRef: ElementRef, private readonly changeDetectorRef: ChangeDetectorRef, private readonly setHostDisplay: boolean = false) { }
4466

4567
ngAfterViewInit() {
4668
this._passAttributesAsProps();
@@ -50,6 +72,18 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterV
5072
}
5173
}
5274

75+
ngOnChanges(changes: SimpleChanges) {
76+
this._passAttributesAsProps();
77+
}
78+
79+
protected detectChanges() {
80+
if (isReactNode(this.reactNodeRef.nativeElement)) {
81+
this.reactNodeRef.nativeElement.setRenderPending();
82+
}
83+
84+
this.changeDetectorRef.markForCheck();
85+
}
86+
5387
/**
5488
* Create an JSX renderer for an `@Input` property.
5589
* @param input The input property
@@ -104,15 +138,19 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterV
104138
(this.elementRef.nativeElement as HTMLElement).attributes
105139
);
106140

107-
const whitelistedHostAttributes = hostAttributes.filter(attr => !this._isBlacklistedAttribute(attr));
108-
if (!whitelistedHostAttributes || whitelistedHostAttributes.length === 0) {
109-
return;
110-
}
111-
112141
if (!this.reactNodeRef || !isReactNode(this.reactNodeRef.nativeElement)) {
113142
throw new Error('reactNodeRef must hold a reference to a ReactNode');
114143
}
115144

145+
// Ensure there are no blacklisted props. Suggest alternative as error if there is any
146+
hostAttributes.forEach(attr => {
147+
const [forbidden, alternativeAttrName] = this._isForbiddenAttribute(attr);
148+
if (forbidden) {
149+
throw new Error(`[${(this.elementRef.nativeElement as HTMLElement).tagName.toLowerCase()}] React wrapper components cannot have the '${attr.name}' attribute set. Use the following alternative: ${alternativeAttrName || ''}`);
150+
}
151+
});
152+
153+
const whitelistedHostAttributes = hostAttributes.filter(attr => !this._isIgnoredAttribute(attr));
116154
const props = whitelistedHostAttributes.reduce((acc, attr) => ({
117155
...acc,
118156
[attr.name]: attr.value,
@@ -124,7 +162,7 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterV
124162
private _setHostDisplay() {
125163
const nativeElement: HTMLElement = this.elementRef.nativeElement;
126164

127-
// setTimeout since we want to wait until child elements are rendered
165+
// We want to wait until child elements are rendered
128166
setTimeout(() => {
129167
if (nativeElement.firstElementChild) {
130168
const rootChildDisplay = getComputedStyle(nativeElement.firstElementChild).display;
@@ -133,11 +171,23 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterV
133171
});
134172
}
135173

136-
private _isBlacklistedAttribute(attr: Attr) {
137-
if (blacklistedAttributesAsProps.includes(attr.name)) {
138-
return true;
174+
private _isIgnoredAttribute(attr: Attr) {
175+
return ignoredAttributeMatchers.some(regExp => regExp.test(attr.name));
176+
}
177+
178+
private _isForbiddenAttribute(attr: Attr): [boolean, string | undefined] {
179+
const { name, value } = attr;
180+
181+
if (name === 'key') return [true, undefined];
182+
if (name === 'class' && value.split(' ').some(className => !ngClassRegExp.test(className))) return [true, 'rClass'];
183+
if (name === 'style') {
184+
const style = toStyle(value);
185+
// Only allowing style if it's something that changes the display - setting anything else should be done on the child component directly (via the `styles` attribute in fabric for example)
186+
if (Object.entries(style).filter(([key, value]) => value || key !== 'display').length > 0) {
187+
return [true, 'rStyle'];
188+
}
139189
}
140190

141-
return blacklistedAttributeMatchers.some(regExp => regExp.test(attr.name));
191+
return [false, undefined];
142192
}
143193
}

libs/core/src/renderer/registry.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ export function registerElement(
1212
resolver: ComponentResolver
1313
): void {
1414
if (elementMap.has(elementName)) {
15-
throw new Error(`Element for ${elementName} already registered.`);
15+
// Ignore multiple register attempts for the same component.
16+
// Angular doesn't allow sharing whole NgModule instances (in this case, an @NgModule for React-wrapped components) with lazy-loaded @NgModules (in the app),
17+
// To keep the API simple, allow multiple calls to `registerElement`.
18+
// Disadvantage is that you can't replace (React) component implementations at runtime. This sounds far-fetched, but solvable with a `static forRoot()` pattern for every
19+
// React-wrapper components' @NgModule, ensuring that `registerElement` is only called once.
20+
return;
1621
} else {
1722
const entry = { resolver: resolver };
1823
elementMap.set(elementName, entry);

libs/core/src/renderer/renderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class AngularReactRendererFactory extends ɵDomRendererFactory2 {
5656
}
5757

5858
class ReactRenderer implements Renderer2 {
59-
data: { [key: string]: any } = Object.create(null);
59+
readonly data: { [key: string]: any } = Object.create(null);
6060

6161
constructor(private rootRenderer: AngularReactRendererFactory) { }
6262

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
declare module 'css-to-style' {
3+
4+
function toStyle(cssText: string) : CSSStyleDeclaration;
5+
6+
export default toStyle;
7+
}

0 commit comments

Comments
 (0)