3

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?

Jack
  • 10,313
  • 15
  • 75
  • 118
  • When you type into the input you're actually changing the value inside the FormControl 'title' from the form group. The binding to node.title is showing the object from state, so I would say it works as expected. – Adrian Fâciu Aug 17 '17 at 13:17
  • If you bind the template to the value from the headerForm it should update as you type. Something like {{ headerForm.controls.title.value }} – Adrian Fâciu Aug 17 '17 at 13:19

0 Answers0