Skip to content

Commit ec65a3e

Browse files
authored
Add more documentation (#101)
There's been a lack of documentation in some key parts of the repo, especially under the `@angular-react/core` library. This PR aims to add some much-necessary internal repo documentation, and in addition to #40 should cover most of the foreseen needs in this regard.
1 parent 96753d2 commit ec65a3e

File tree

6 files changed

+118
-13
lines changed

6 files changed

+118
-13
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
SimpleChanges,
1717
TemplateRef,
1818
Type,
19+
AfterContentInit,
1920
} from '@angular/core';
2021
import classnames from 'classnames';
2122
import toStyle from 'css-to-style';
@@ -77,7 +78,7 @@ const defaultWrapperComponentOptions: WrapperComponentOptions = {
7778
* Simplifies some of the handling around passing down props and CSS styling on the host component.
7879
*/
7980
// NOTE: TProps is not used at the moment, but a preparation for a potential future change.
80-
export abstract class ReactWrapperComponent<TProps extends {}> implements AfterViewInit, OnChanges {
81+
export abstract class ReactWrapperComponent<TProps extends {}> implements AfterContentInit, AfterViewInit, OnChanges {
8182
private _contentClass: Many<ContentClassValue>;
8283
private _contentStyle: ContentStyleValue;
8384

@@ -302,7 +303,7 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterV
302303
const nativeElement: HTMLElement = this.elementRef.nativeElement;
303304

304305
// We want to wait until child elements are rendered
305-
setTimeout(() => {
306+
afterRenderFinished(() => {
306307
if (nativeElement.firstElementChild) {
307308
const rootChildDisplay = getComputedStyle(nativeElement.firstElementChild).display;
308309
nativeElement.style.display = rootChildDisplay;

libs/core/src/lib/renderer/react-content.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ export const CHILDREN_TO_APPEND_PROP = 'children-to-append';
1313
*/
1414
export interface ReactContentProps {
1515
/**
16-
* Experimental rendering mode.
16+
* Use the legacy rendering mode.
17+
*
1718
* Uses a similar approach to `router-outlet`, where the child elements are added to the parent, instead of this node, and this is hidden.
19+
*
1820
* @default false
1921
*/
2022
legacyRenderMode?: boolean;
@@ -67,6 +69,7 @@ export class ReactContent extends React.PureComponent<InternalReactContentProps>
6769
}
6870

6971
render() {
72+
// TODO: See if we can just render React.Fragment and the children within it, having no extra DOM nodes.
7073
return React.createElement('react-content', !this.props.legacyRenderMode && { style: { display: 'none' } });
7174
}
7275
}

libs/core/src/lib/renderer/react-node.ts

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import * as React from 'react';
55
import * as ReactDOM from 'react-dom';
6+
67
import { StringMap } from '../declarations/string-map';
78
import removeUndefinedProperties from '../utils/object/remove-undefined-properties';
89
import { CHILDREN_TO_APPEND_PROP } from './react-content';
@@ -14,11 +15,15 @@ export function isReactNode(node: any): node is ReactNode {
1415
return (<ReactNode>node).setRenderPendingCallback !== undefined;
1516
}
1617

18+
/**
19+
* Logical representation of everything needed to render a React element in the
20+
* DOM, with the needed methods to do so.
21+
*/
1722
export class ReactNode {
1823
// Access to these properties are restricted through setters and functions
1924
// so that the dirty "render pending" state of this object can be properly
2025
// tracked and all nodes with "render pending" can be flushed at the end
21-
// of render operation.
26+
// of a render operation.
2227
private _props = {};
2328
private _comment: string;
2429
private _text: string;
@@ -70,28 +75,66 @@ export class ReactNode {
7075
this._isRenderPending = true;
7176
}
7277

78+
/**
79+
* Marks the node to be removed from the DOM in the next render cycle.
80+
*/
7381
destroyNode() {
7482
this.setRenderPending();
7583
this._isDestroyPending = true;
7684
}
7785

78-
setAttribute(name: string, value: any) {
86+
/**
87+
* Sets an attribute on the node.
88+
* @note the value can only be a `string`. See `setProperty` for other use-cases.
89+
* @see `Renderer2#setAttribute`.
90+
*
91+
* @param name The attribute name.
92+
* @param value The new value.
93+
*/
94+
setAttribute(name: string, value: string) {
7995
this.setAttributes({ [name]: value });
8096
}
8197

82-
setAttributes(attributes: StringMap) {
98+
/**
99+
* Set attributes on this node.
100+
* Note that values can only be of type `string`. See `setProperties` for other use-cases.
101+
* @see `Renderer2#setAttribute`.
102+
*
103+
* @param attributes the attributes to set.
104+
*/
105+
setAttributes(attributes: StringMap<string>) {
83106
this.setProperties(attributes);
84107
}
85108

109+
/**
110+
* Sets a prop in the underlying React element.
111+
* @see `Renderer2#setProperty`.
112+
*
113+
* @param name The property name.
114+
* @param value The new value.
115+
*/
86116
setProperty(name: string, value: any) {
87117
this.setProperties({ [name]: value });
88118
}
89119

120+
/**
121+
* Like `setProperty` but for multiple props at once.
122+
*
123+
* @param properties An object with the props.
124+
*/
90125
setProperties(properties: StringMap) {
91126
this.setRenderPending();
92127
Object.assign(this._props, removeUndefinedProperties(properties));
93128
}
94129

130+
/**
131+
* Remove a prop or an attribute from the underlying React element.
132+
* @see `Renderer2#removeAttribute`.
133+
*
134+
* @param name The property name.
135+
* @param childName _Optional_ A property of `name` to remove instead.
136+
* @returns the deleted property
137+
*/
95138
removeProperty(name: string, childName?: string) {
96139
this.setRenderPending();
97140
if (childName) {
@@ -101,23 +144,49 @@ export class ReactNode {
101144
return delete this._props[name];
102145
}
103146

147+
/**
148+
* Add a direct child of this node.
149+
* @see `Renderer2#addChild`.
150+
*
151+
* @param node The node to add.
152+
*/
104153
addChild(node: ReactNode) {
105154
this.setRenderPending();
106155
this._children.push(node);
107156
}
108157

158+
/**
159+
* Remove a direct child of this node.
160+
* @see `Renderer2#removeChild`.
161+
*
162+
* @param node The node to remove.
163+
*/
109164
removeChild(node: ReactNode) {
110165
this.setRenderPending();
111166
this._children = this._children.filter(child => child !== node);
112167
}
113168

169+
/**
170+
* Cast the node to a comment node.
171+
* @see `Renderer2#createComment`.
172+
*
173+
* @param value the text in the comment to render.
174+
* @returns itself.
175+
*/
114176
asComment(value: string) {
115177
this.setRenderPending();
116178
this.type = undefined;
117179
this._comment = value;
118180
return this;
119181
}
120182

183+
/**
184+
* Cast the node to a text node.
185+
* @see `Renderer2#createText`.
186+
*
187+
* @param value the text to render.
188+
* @returns itself.
189+
*/
121190
asText(value: string) {
122191
this.setRenderPending();
123192
this.type = undefined;
@@ -132,6 +201,11 @@ export class ReactNode {
132201
return this;
133202
}
134203

204+
/**
205+
* Render the node to the DOM, or unmount it, as necessary.
206+
*
207+
* @returns itself.
208+
*/
135209
render(): ReactNode {
136210
// Only complete render operations for ReactNodes that are parented by HTMLElements.
137211
// Those that are parented by other ReactNodes will be rendered recursively by their
@@ -159,7 +233,14 @@ export class ReactNode {
159233
return this;
160234
}
161235

162-
// This is called by Angular core when projected content is being added.
236+
/**
237+
* Appends a child.
238+
*
239+
* @see `Renderer2#appendChild`.
240+
* @note This is called by Angular core when projected content is being added.
241+
*
242+
* @param projectedContent the content to project.
243+
*/
163244
appendChild(projectedContent: HTMLElement) {
164245
if (DEBUG) {
165246
console.error(
@@ -172,6 +253,9 @@ export class ReactNode {
172253
this._childrenToAppend.push(projectedContent);
173254
}
174255

256+
/**
257+
* @note for easier debugging.
258+
*/
175259
toString(): string {
176260
if (this._typeName) {
177261
return `[${this._typeName} ReactNode]`;
@@ -203,6 +287,14 @@ export class ReactNode {
203287
this._props['key'] = key;
204288
}
205289

290+
// Just having some props on a React element can cause it to
291+
// behave undesirably, and since the templates are hard-coded to pass
292+
// all Inputs all the time, they pass `undefined` values too.
293+
// This ensures these are removed.
294+
// Additionally, there are some things that Angular templating forbids,
295+
// and stops at-compile time (with errors), such as `Input`s being prefixed with `on`.
296+
// Since React does not have the notion of `Output`s as Angular (they are just props of type function, essentially callbacks).
297+
// To work around this, we, by convention, prefix any PascalCased prop with `on` here, after the template has already been compiled.
206298
const clearedProps = this._transformProps(removeUndefinedProperties(this._props));
207299

208300
if (DEBUG) {
@@ -249,6 +341,8 @@ export class ReactNode {
249341
this._typeName = this.type as string;
250342

251343
// Attempt to resolve the type as a React Element class name/type.
344+
// Since Angular templates are just strings, we can't include types in them.
345+
// Therefore, we use the component registry to resolve the type of a component from a string.
252346
if (typeof this.type === 'string') {
253347
this.type = getComponentClass(this.type);
254348
}

libs/core/src/lib/renderer/react-template.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ const TEMPLATE_DETECT_CHANGES_THROTTLE_MS = 250;
1515
*/
1616
export interface ReactTemplateProps {
1717
/**
18-
* Experimental rendering mode.
18+
* Use the legacy rendering mode.
19+
*
1920
* Uses a similar approach to `router-outlet`, where the child elements are added to the parent, instead of this node, and this is hidden.
21+
*
2022
* @default false
2123
*/
2224
legacyRenderMode?: boolean;
@@ -104,6 +106,7 @@ export class ReactTemplate<TContext extends object | void> extends React.Compone
104106
}
105107

106108
render() {
109+
// TODO: See if we can just render React.Fragment and the children within it, having no extra DOM nodes.
107110
return React.createElement('react-template', !this.props.legacyRenderMode && { style: { display: 'none' } });
108111
}
109112
}

libs/core/src/lib/renderer/registry.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ const camelCaseSplit = /([a-z0-9])([A-Z])/g;
1010

1111
/**
1212
* Register an element to be renderer when the renderer sees the tag.
13-
* @param elementName the tag
14-
* @param resolver A resolver to the React component
13+
* @param elementName the tag to be used to get the component type when rendering.
14+
* @param resolver A resolver to the React component.
1515
*/
1616
export function registerElement(elementName: string, resolver: ComponentResolver): void {
1717
if (elementMap.has(elementName)) {

libs/core/src/lib/renderer/renderer.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,18 @@ export class AngularReactRendererFactory extends ɵDomRendererFactory2 {
2525
public reactRootNodes: Set<ReactNode> = new Set();
2626

2727
private isRenderPending: boolean;
28+
2829
// This flag can only be set to true from outside. It can only be reset
2930
// to false from inside. This value is reset on "end" when the pending
3031
// renders are flushed.
31-
public setRenderPendingCallback = () => {
32+
public readonly setRenderPendingCallback = () => {
3233
this.isRenderPending = true;
3334
};
3435

3536
constructor(eventManager: EventManager, sharedStylesHost: ɵDomSharedStylesHost) {
3637
super(eventManager, sharedStylesHost);
3738

39+
// tslint:disable-next-line: no-use-before-declare
3840
this.defaultReactRenderer = new ReactRenderer(this);
3941
}
4042

@@ -57,6 +59,7 @@ export class AngularReactRendererFactory extends ɵDomRendererFactory2 {
5759
this.reactRootNodes
5860
);
5961
}
62+
6063
// Flush any pending React element render updates. This cannot be done
6164
// earlier (as is done for DOM elements) because React element props
6265
// are ReadOnly.
@@ -120,7 +123,7 @@ export class ReactRenderer implements Renderer2 {
120123
}
121124

122125
appendChild(parent: HTMLElement | ReactNode, node: ReactNode): void {
123-
// Only append a child if there is a child.
126+
// Only append a child if there is a child to append.
124127
if (!node) {
125128
return;
126129
}
@@ -146,6 +149,7 @@ export class ReactRenderer implements Renderer2 {
146149
if (DEBUG) {
147150
console.warn('Renderer > appendChild > asReact > parent:', parent.toString(), 'node:', node.toString());
148151
}
152+
149153
node.setRenderPendingCallback = () => parent.setRenderPending();
150154
parent.addChild(node);
151155
node.parent = parent;
@@ -304,7 +308,7 @@ export class ReactRenderer implements Renderer2 {
304308
}
305309
target.setProperty(event, callback);
306310

307-
// NEEDS WORK: Implement prevent default callback behavior.
311+
// TODO: NEEDS WORK: Implement prevent default callback behavior.
308312
// return <() => void>this.eventManager.addEventListener(
309313
// target, event, decoratePreventDefault(callback)) as() => void;
310314

0 commit comments

Comments
 (0)