Component Communication

AngularDart provides several ways for components to communicate with each other.

Parent to Child: @Input()

Use @Input() to pass data from a parent component to a child component:

// child_component.dart
@Component(
  selector: 'child-component',
  template: '<p>Hello, {{ name }}!</p>',
)
class ChildComponent {
  @Input()
  String name = '';
}

// parent_component.dart
@Component(
  selector: 'parent-component',
  template: '<child-component [name]="userName"></child-component>',
  directives: [ChildComponent],
)
class ParentComponent {
  String userName = 'Alice';
}

Input with alias

@Input('user-name')
String internalName = '';

Usage: <child-component [user-name]="name"></child-component>

Child to Parent: @Output()

Use @Output() with a StreamController to emit events from child to parent:

// child_component.dart
@Component(
  selector: 'child-component',
  template: '<button (click)="notify()">Notify Parent</button>',
)
class ChildComponent {
  @Output()
  final messageSent = StreamController<String>.broadcast();

  void notify() {
    messageSent.add('Hello from child!');
  }
}

// parent_component.dart
@Component(
  selector: 'parent-component',
  template: '<child-component (messageSent)="onMessage($event)"></child-component>'
      '<p>Message: {{ receivedMessage }}</p>',
  directives: [ChildComponent],
)
class ParentComponent {
  String receivedMessage = '';

  void onMessage(String message) {
    receivedMessage = message;
  }
}

Two-Way Binding

Combine @Input() and @Output() for two-way binding:

@Component(
  selector: 'my-input',
  template: '<input [value]="value" (input)="onInput($event)">',
)
class MyInputComponent {
  @Input()
  String value = '';

  @Output()
  final valueChange = StreamController<String>.broadcast();

  void onInput(Event event) {
    final target = event.target as HtmlInputElement;
    valueChange.add(target.value);
  }
}

Usage with banana-in-a-box syntax:

<my-input [(value)]="userName"></my-input>

Parent Accesses Child: @ViewChild

Use @ViewChild to access a child component or element from the parent:

@Component(
  selector: 'parent-component',
  template: '<child-component #child></child-component>'
      '<button (click)="callChildMethod()">Call Child</button>',
  directives: [ChildComponent],
)
class ParentComponent implements AfterViewInit {
  @ViewChild('child')
  ChildComponent? child;

  @override
  void ngAfterViewInit() {
    child?.doSomething();
  }

  void callChildMethod() {
    child?.doSomething();
  }
}

Multiple children with @ViewChildren

@Component(
  selector: 'parent-component',
  template: '<child-component *ngFor="let item of items"></child-component>',
  directives: [ChildComponent, NgFor],
)
class ParentComponent implements AfterViewInit {
  @ViewChildren(ChildComponent)
  List<ChildComponent>? children;

  @override
  void ngAfterViewInit() {
    children?.forEach((child) => child.initialize());
  }
}

Content Projection: @ContentChild

Use @ContentChild to access projected content:

// panel-header directive
@Directive(selector: '[panelHeader]')
class PanelHeaderDirective {}

@Component(
  selector: 'panel',
  template: '<div class="panel">'
      '<ng-content select="[panelHeader]"></ng-content>'
      '<ng-content></ng-content>'
      '</div>',
)
class PanelComponent implements AfterContentInit {
  @ContentChild(PanelHeaderDirective)
  PanelHeaderDirective? header;

  @override
  void ngAfterContentInit() {
    if (header != null) {
      print('Panel has a header');
    }
  }
}

Usage:

<panel>
  <div panelHeader>My Header</div>
  <p>Panel content</p>
</panel>

@ContentChildren

@Component(
  selector: 'tab-container',
  template: '<ng-content select="tab"></ng-content>',
)
class TabContainerComponent implements AfterContentInit {
  @ContentChildren(TabComponent)
  List<TabComponent>? tabs;

  @override
  void ngAfterContentInit() {
    tabs?.first.isActive = true;
  }
}

Component Interaction via Services

For complex communication, use a shared service:

@Injectable()
class MessageService {
  final _messages = StreamController<String>.broadcast();
  Stream<String> get messages => _messages.stream;

  void send(String message) {
    _messages.add(message);
  }
}

@Component(
  selector: 'sender',
  template: '<button (click)="send()">Send</button>',
  providers: [],
)
class SenderComponent {
  final MessageService _service;
  SenderComponent(this._service);

  void send() {
    _service.send('Hello!');
  }
}

@Component(
  selector: 'receiver',
  template: '<p>{{ message }}</p>',
)
class ReceiverComponent implements OnInit, OnDestroy {
  final MessageService _service;
  String message = '';
  StreamSubscription? _sub;

  ReceiverComponent(this._service);

  @override
  void ngOnInit() {
    _sub = _service.messages.listen((msg) => message = msg);
  }

  @override
  void ngOnDestroy() {
    _sub?.cancel();
  }
}

Best Practices

  • Use @Input() for parent-to-child communication
  • Use @Output() for child-to-parent events
  • Use services for sibling or distant component communication
  • Use @ViewChild sparingly; prefer data binding
  • Always unsubscribe from streams in ngOnDestroy