Skip to content

Commit ac2b04a

Browse files
mheveryvicb
authored andcommitted
test(ivy): Add small_app spec for sprint #3 (angular#22018)
PR Close angular#22018
1 parent a63b764 commit ac2b04a

21 files changed

+699
-86
lines changed

Diff for: packages/core/src/render3/component.ts

+144-18
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {NG_HOST_SYMBOL, createError, createLView, createTView, directiveCreate,
1717
import {ComponentDef, ComponentType} from './interfaces/definition';
1818
import {LElementNode} from './interfaces/node';
1919
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
20+
import {RootContext} from './interfaces/view';
2021
import {notImplemented, stringify} from './util';
2122

2223

@@ -43,6 +44,19 @@ export interface CreateComponentOptions {
4344
* Example: PublicFeature is a function that makes the component public to the DI system.
4445
*/
4546
features?: (<T>(component: T, componentDef: ComponentDef<T>) => void)[];
47+
48+
/**
49+
* A function which is used to schedule change detection work in the future.
50+
*
51+
* When marking components as dirty, it is necessary to schedule the work of
52+
* change detection in the future. This is done to coalesce multiple
53+
* {@link markDirty} calls into a single changed detection processing.
54+
*
55+
* The default value of the scheduler is the `requestAnimationFrame` function.
56+
*
57+
* It is also useful to override this function for testing purposes.
58+
*/
59+
scheduler?: (work: () => void) => void;
4660
}
4761

4862

@@ -155,11 +169,22 @@ export const NULL_INJECTOR: Injector = {
155169
}
156170
};
157171

172+
/**
173+
* A permanent marker promise which signifies that the current CD tree is
174+
* clean.
175+
*/
176+
const CLEAN_PROMISE = Promise.resolve(null);
158177

159178
/**
160179
* Bootstraps a Component into an existing host element and returns an instance
161180
* of the component.
162181
*
182+
* Use this function to bootstrap a component into the DOM tree. Each invocation
183+
* of this function will create a separate tree of components, injectors and
184+
* change detection cycles and lifetimes. To dynamically insert a new component
185+
* into an existing tree such that it shares the same injection, change detection
186+
* and object lifetime, use {@link ViewContainer#createComponent}.
187+
*
163188
* @param componentType Component to bootstrap
164189
* @param options Optional parameters which control bootstrapping
165190
*/
@@ -170,15 +195,23 @@ export function renderComponent<T>(
170195
if (componentDef.type != componentType) componentDef.type = componentType;
171196
let component: T;
172197
const hostNode = locateHostElement(rendererFactory, opts.host || componentDef.tag);
198+
const rootContext: RootContext = {
199+
// Incomplete initialization due to circular reference.
200+
component: null !,
201+
scheduler: opts.scheduler || requestAnimationFrame,
202+
clean: CLEAN_PROMISE,
203+
};
173204
const oldView = enterView(
174205
createLView(
175-
-1, rendererFactory.createRenderer(hostNode, componentDef.rendererType), createTView()),
206+
-1, rendererFactory.createRenderer(hostNode, componentDef.rendererType), createTView(),
207+
null, rootContext),
176208
null !);
177209
try {
178210
// Create element node at index 0 in data array
179211
hostElement(hostNode, componentDef);
180212
// Create directive instance with n() and store at index 1 in data array (el is 0)
181-
component = getDirectiveInstance(directiveCreate(1, componentDef.n(), componentDef));
213+
component = rootContext.component =
214+
getDirectiveInstance(directiveCreate(1, componentDef.n(), componentDef));
182215
} finally {
183216
leaveView(oldView);
184217
}
@@ -188,27 +221,120 @@ export function renderComponent<T>(
188221
return component;
189222
}
190223

191-
export function detectChanges<T>(component: T) {
192-
ngDevMode && assertNotNull(component, 'detectChanges should be called with a component');
193-
const hostNode = (component as any)[NG_HOST_SYMBOL] as LElementNode;
194-
if (ngDevMode && !hostNode) {
195-
createError('Not a directive instance', component);
196-
}
224+
/**
225+
* Synchronously perform change detection on a component (and possibly its sub-components).
226+
*
227+
* This function triggers change detection in a synchronous way on a component. There should
228+
* be very little reason to call this function directly since a preferred way to do change
229+
* detection is to {@link markDirty} the component and wait for the scheduler to call this method
230+
* at some future point in time. This is because a single user action often results in many
231+
* components being invalidated and calling change detection on each component synchronously
232+
* would be inefficient. It is better to wait until all components are marked as dirty and
233+
* then perform single change detection across all of the components
234+
*
235+
* @param component The component which the change detection should be performed on.
236+
*/
237+
export function detectChanges<T>(component: T): void {
238+
const hostNode = _getComponentHostLElementNode(component);
197239
ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView');
198240
renderComponentOrTemplate(hostNode, hostNode.view, component);
199-
isDirty = false;
200241
}
201242

202-
let isDirty = false;
203-
export function markDirty<T>(
204-
component: T, scheduler: (fn: () => void) => void = requestAnimationFrame) {
205-
ngDevMode && assertNotNull(component, 'markDirty should be called with a component');
206-
if (!isDirty) {
207-
isDirty = true;
208-
scheduler(() => detectChanges(component));
243+
/**
244+
* Mark the component as dirty (needing change detection).
245+
*
246+
* Marking a component dirty will schedule a change detection on this
247+
* component at some point in the future. Marking an already dirty
248+
* component as dirty is a noop. Only one outstanding change detection
249+
* can be scheduled per component tree. (Two components bootstrapped with
250+
* separate `renderComponent` will have separate schedulers)
251+
*
252+
* When the root component is bootstrapped with `renderComponent` a scheduler
253+
* can be provided.
254+
*
255+
* @param component Component to mark as dirty.
256+
*/
257+
export function markDirty<T>(component: T) {
258+
const rootContext = getRootContext(component);
259+
if (rootContext.clean == CLEAN_PROMISE) {
260+
let res: null|((val: null) => void);
261+
rootContext.clean = new Promise<null>((r) => res = r);
262+
rootContext.scheduler(() => {
263+
detectChanges(rootContext.component);
264+
res !(null);
265+
rootContext.clean = CLEAN_PROMISE;
266+
});
209267
}
210268
}
211269

212-
export function getHostElement<T>(component: T): RElement {
213-
return ((component as any)[NG_HOST_SYMBOL] as LElementNode).native;
270+
/**
271+
* Retrieve the root component of any component by walking the parent `LView` until
272+
* reaching the root `LView`.
273+
*
274+
* @param component any component
275+
*/
276+
function getRootContext(component: any): RootContext {
277+
ngDevMode && assertNotNull(component, 'component');
278+
const lElementNode = _getComponentHostLElementNode(component);
279+
let lView = lElementNode.view;
280+
while (lView.parent) {
281+
lView = lView.parent;
282+
}
283+
const rootContext = lView.context as RootContext;
284+
ngDevMode && assertNotNull(rootContext, 'rootContext');
285+
return rootContext;
286+
}
287+
288+
function _getComponentHostLElementNode<T>(component: T): LElementNode {
289+
ngDevMode && assertNotNull(component, 'expecting component got null');
290+
const lElementNode = (component as any)[NG_HOST_SYMBOL] as LElementNode;
291+
ngDevMode && assertNotNull(component, 'object is not a component');
292+
return lElementNode;
293+
}
294+
295+
/**
296+
* Retrieve the host element of the component.
297+
*
298+
* Use this function to retrieve the host element of the component. The host
299+
* element is the element which the component is associated with.
300+
*
301+
* @param component Component for which the host element should be retrieved.
302+
*/
303+
export function getHostElement<T>(component: T): HTMLElement {
304+
return _getComponentHostLElementNode(component).native as any;
305+
}
306+
307+
/**
308+
* Retrieves the rendered text for a given component.
309+
*
310+
* This function retrieves the host element of a component and
311+
* and then returns the `textContent` for that element. This implies
312+
* that the text returned will include re-projected content of
313+
* the component as well.
314+
*
315+
* @param component The component to return the content text for.
316+
*/
317+
export function getRenderedText(component: any): string {
318+
const hostElement = getHostElement(component);
319+
return hostElement.textContent || '';
320+
}
321+
322+
/**
323+
* Wait on component until it is rendered.
324+
*
325+
* This function returns a `Promise` which is resolved when the component's
326+
* change detection is executed. This is determined by finding the scheduler
327+
* associated with the `component`'s render tree and waiting until the scheduler
328+
* flushes. If nothing is scheduled, the function returns a resolved promise.
329+
*
330+
* Example:
331+
* ```
332+
* await whenRendered(myComponent);
333+
* ```
334+
*
335+
* @param component Component to wait upon
336+
* @returns Promise which resolves when the component is rendered.
337+
*/
338+
export function whenRendered(component: any): Promise<null> {
339+
return getRootContext(component).clean;
214340
}

Diff for: packages/core/src/render3/index.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {createComponentRef, detectChanges, getHostElement, markDirty, renderComponent} from './component';
9+
import {createComponentRef, detectChanges, getHostElement, getRenderedText, markDirty, renderComponent, whenRendered} from './component';
1010
import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective, definePipe} from './definition';
1111
import {InjectFlags} from './di';
1212
import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveType} from './interfaces/definition';
1313

1414
export {InjectFlags, QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF, inject, injectElementRef, injectTemplateRef, injectViewContainerRef} from './di';
15+
export {CssSelector} from './interfaces/projection';
16+
1517

1618
// Naming scheme:
1719
// - Capital letters are for creating things: T(Text), E(Element), D(Directive), V(View),
@@ -106,6 +108,11 @@ export {
106108
defineComponent,
107109
defineDirective,
108110
definePipe,
111+
detectChanges,
112+
createComponentRef,
113+
getHostElement,
114+
getRenderedText,
115+
markDirty,
116+
renderComponent,
117+
whenRendered,
109118
};
110-
export {createComponentRef, detectChanges, getHostElement, markDirty, renderComponent};
111-
export {CssSelector} from './interfaces/projection';

Diff for: packages/core/src/render3/instructions.ts

+26-18
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,15 @@ let isParent: boolean;
7070
*/
7171
let tData: TData;
7272

73-
/** State of the current view being processed. */
74-
let currentView: LView;
75-
// The initialization has to be after the `let`, otherwise `createLView` can't see `let`.
76-
currentView = createLView(null !, null !, createTView());
73+
/**
74+
* State of the current view being processed.
75+
*
76+
* NOTE: we cheat here and initialize it to `null` even thought the type does not
77+
* contain `null`. This is because we expect this value to be not `null` as soon
78+
* as we enter the view. Declaring the type as `null` would require us to place `!`
79+
* in most instructions since they all assume that `currentView` is defined.
80+
*/
81+
let currentView: LView = null !;
7782

7883
let currentQueries: LQueries|null;
7984

@@ -131,21 +136,21 @@ const enum BindingDirection {
131136
*/
132137
export function enterView(newView: LView, host: LElementNode | LViewNode | null): LView {
133138
const oldView = currentView;
134-
data = newView.data;
135-
bindingIndex = newView.bindingStartIndex || 0;
136-
tData = newView.tView.data;
137-
creationMode = newView.creationMode;
139+
data = newView && newView.data;
140+
bindingIndex = newView && newView.bindingStartIndex || 0;
141+
tData = newView && newView.tView.data;
142+
creationMode = newView && newView.creationMode;
138143

139-
cleanup = newView.cleanup;
140-
renderer = newView.renderer;
144+
cleanup = newView && newView.cleanup;
145+
renderer = newView && newView.renderer;
141146

142147
if (host != null) {
143148
previousOrParentNode = host;
144149
isParent = true;
145150
}
146151

147152
currentView = newView;
148-
currentQueries = newView.queries;
153+
currentQueries = newView && newView.queries;
149154

150155
return oldView !;
151156
}
@@ -165,8 +170,8 @@ export function leaveView(newView: LView): void {
165170
}
166171

167172
export function createLView(
168-
viewId: number, renderer: Renderer3, tView: TView,
169-
template: ComponentTemplate<any>| null = null, context: any | null = null): LView {
173+
viewId: number, renderer: Renderer3, tView: TView, template: ComponentTemplate<any>| null,
174+
context: any | null): LView {
170175
const newView = {
171176
parent: currentView,
172177
id: viewId, // -1 for component views
@@ -300,7 +305,8 @@ export function renderTemplate<T>(
300305
host = createLNode(
301306
null, LNodeFlags.Element, hostNode,
302307
createLView(
303-
-1, providedRendererFactory.createRenderer(null, null), getOrCreateTView(template)));
308+
-1, providedRendererFactory.createRenderer(null, null), getOrCreateTView(template),
309+
null, null));
304310
}
305311
const hostView = host.data !;
306312
ngDevMode && assertNotNull(hostView, 'Host node should have an LView defined in host.data.');
@@ -406,7 +412,8 @@ export function elementStart(
406412
if (isHostElement) {
407413
const tView = getOrCreateTView(hostComponentDef !.template);
408414
componentView = addToViewTree(createLView(
409-
-1, rendererFactory.createRenderer(native, hostComponentDef !.rendererType), tView));
415+
-1, rendererFactory.createRenderer(native, hostComponentDef !.rendererType), tView,
416+
null, null));
410417
}
411418

412419
// Only component views should be added to the view tree directly. Embedded views are
@@ -556,7 +563,8 @@ export function locateHostElement(
556563
export function hostElement(rNode: RElement | null, def: ComponentDef<any>) {
557564
resetApplicationState();
558565
createLNode(
559-
0, LNodeFlags.Element, rNode, createLView(-1, renderer, getOrCreateTView(def.template)));
566+
0, LNodeFlags.Element, rNode,
567+
createLView(-1, renderer, getOrCreateTView(def.template), null, null));
560568
}
561569

562570

@@ -1114,8 +1122,8 @@ export function embeddedViewStart(viewBlockId: number): boolean {
11141122
enterView((existingView as LViewNode).data, previousOrParentNode as LViewNode);
11151123
} else {
11161124
// When we create a new LView, we always reset the state of the instructions.
1117-
const newView =
1118-
createLView(viewBlockId, renderer, getOrCreateEmbeddedTView(viewBlockId, container));
1125+
const newView = createLView(
1126+
viewBlockId, renderer, getOrCreateEmbeddedTView(viewBlockId, container), null, null);
11191127
if (lContainer.queries) {
11201128
newView.queries = lContainer.queries.enterView(lContainer.nextIndex);
11211129
}

Diff for: packages/core/src/render3/interfaces/view.ts

+29-2
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,11 @@ export interface LView {
160160
template: ComponentTemplate<{}>|null;
161161

162162
/**
163-
* For embedded views, the context with which to render the template.
163+
* - For embedded views, the context with which to render the template.
164+
* - For root view of the root component the context contains change detection data.
165+
* - `null` otherwise.
164166
*/
165-
context: {}|null;
167+
context: {}|RootContext|null;
166168

167169
/**
168170
* A count of dynamic views that are children of this view (indirectly via containers).
@@ -261,6 +263,31 @@ export interface TView {
261263
destroyHooks: HookData|null;
262264
}
263265

266+
/**
267+
* RootContext contains information which is shared for all components which
268+
* were bootstrapped with {@link renderComponent}.
269+
*/
270+
export interface RootContext {
271+
/**
272+
* A function used for scheduling change detection in the future. Usually
273+
* this is `requestAnimationFrame`.
274+
*/
275+
scheduler: (workFn: () => void) => void;
276+
277+
/**
278+
* A promise which is resolved when all components are considered clean (not dirty).
279+
*
280+
* This promise is overwritten every time a first call to {@link markDirty} is invoked.
281+
*/
282+
clean: Promise<null>;
283+
284+
/**
285+
* RootComponent - The component which was instantiated by the call to
286+
* {@link renderComponent}.
287+
*/
288+
component: {};
289+
}
290+
264291
/**
265292
* Array of hooks that should be executed for a view and their directive indices.
266293
*

Diff for: packages/core/test/bundling/hello_world/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ ts_library(
3434
srcs = ["domino_typings.d.ts"] + glob(["*_spec.ts"]),
3535
deps = [
3636
"//packages:types",
37+
"//packages/core/testing",
3738
],
3839
)
3940

0 commit comments

Comments
 (0)