Imagine the following component hierarchy in an Angular 2 application.
app.component
└── node.component
├── header.component
├── body.component
└── footer.component
The app will render a tree of nodes. There is a single 'root' node held in ngrx/store
to reflect the state of the tree.
The app.component
gets a "slice" of the AppState
held in ngrx/store
, i.e. an Observable
on the root node.
export class AppComponent {
rootNode: Observable<Node>;
constructor(private readonly store: Store<AppState>) {
this.rootNode = this.store.select(state => state.nodes);
}
}
The app.component
renders the rootNode
using async
pipe and the node.component
(see hierarchy of components above):
<app-node [node]="rootNode | async"></app-node>
The node.component
receives the Node
as Input()
and renders it using three components: header.component
, body.component
, footer.component
.
@Component({
selector: 'app-node',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NodeComponent {
@Input() node: Node;
constructor() {}
}
Note, the node.component
view does NOT use async
pipe since it's not rendering an Observable
, but a normal Node object:
<node-header [node]="node"></node-header>
<node-body [node]="node"></node-body>
<node-footer [node]="node"></node-footer>
Although these child components only render a subset of the node's data, they receive, as @Input(),
the entire Node in order make decisions about how it should be rendered.
Important detail: All three components create a FormGroup
allowing the node to be edited. However, they also render a view of the same values that can be edited, i.e.
<input formControlName="title">
<p>Title {{ node.title }}</p>
Since header.component
, body.component
and footer.component
all do pretty much the same thing, I'll just provide the outline of header.component
:
@Component({
selector: 'node-header',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderComponent implements OnInit {
@Input() node: Node;
@Input() headerForm: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.headerForm = this.fb.group({
title: [this.node.title]
});
}
}
A FormBuilder
creates a FormGroup
and binds to the title
property of the Node received from @Input()
. The <node-header>
will render both the form control for editing, and the value:
<div [formGroup]="headerForm">
<input formControlName="title"name="title">
</div>
<p>Title {{ node.title }}</p>
Both the node.component
and header.component
use OnPush
Change Detection strategy since the number of nodes can grow very large.
Question / Problem
{{ node.title }}
is not updated when the input
control is edited. I suspect this is because the value in ngrx/store
hasn't changed and so the Observable in app.component
doesn't get notified, and change detection doesn't run.
<!-- Not updated when input control edited. -->
<app-node [node]="rootNode | async"></app-node>
I can't remember where I read it, but I thought change detection wasn't needed if a component updated it's (own) data-bound properties. However, in this example, header.component
binds to a property of node - therefore the view, {{ node.title }}
, isn't updated. To be clearer, I've added some comments to the header.component
:
@Component({
selector: 'node-header',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderComponent implements OnInit {
@Input() node: Node;
@Input() headerForm: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.headerForm = this.fb.group({
// Binds to property of node.
// {{node.title}} won't update even thought it's "local".
title: [this.node.title]
});
}
}
What can I do here to ensure the view is updated instantly as the user makes edits, and only later, push the changes to ngrx/store
?