WorldMaker/angular-pharkas

Document the "bag in box" patterns

Closed this issue · 1 comments

I try to avoid Angular's bidirectional bindings ("bag in box" bindings), but sometimes they are required. They can be handled entirely by documentation, but there may be a call for helper functions.

Creating an Input/Output pair that supports "bag in box" binding should be easy enough:

@Component({
// …
})
export class MyExample extends PharkasComponent<MyExample> {
  @Input() set test(value: string | Observable<string>) { this.setInput('test', value) }
  @Output() readonly testChange = EventEmitter()

  constructor(ref: ChangeDetectorRef) {
    super(ref)
    const test = this.useInput('test')
    this.bindOutput(this.testChange, test)
  }
}

On the other hand the direct way to create a template binding to consume some other component, won't work without a new helper:

@Component({
// …
  template: `<example-component test=[(test)]></example-component>
})
export class MyExample extends PharkasComponent<MyExample> {
  get test { return this.bindable<string>('test') }
  set test(value: string | Observable<string>) { this.setInput('test', value) }

  constructor(ref: ChangeDetectorRef) {
    super(ref)
    // Depending on order, one of these bindings will fail with an already bound error
    const test = this.useInput('test')
    this.bind('test', someObservable)
  }
}

Alternatively, this can be easily accomplished eschewing the "bag in box" "shortcut" operator and documenting the "preferred" Pharkas approach, which may currently be better aligned as:

@Component({
// …
  template: `<example-component [test]="test" (testChange)="handleTestChange"></example-component>
})
export class MyExample extends PharkasComponent<MyExample> {
  get test { return this.bindable<string>('test') }
  handleTestChange: (value: string) => void

  constructor(ref: ChangeDetectorRef) {
    super(ref)

    this.handleTestChange = this.createCallback('handleTestChange')
    const testChange = this.useCallback('handleTestChange')

    const someObservable = combineLatest([testChange, /* … */]).pipe(
      map(([test], /* … */) => { /* … */ })
    )

    this.bind('test', someObservable)
  }
}

Action items:

  1. Should probably document these even if no helper added
  2. Should there be a helper function here? Would it be more like a useBidiCallback or bindBidi?

Leaning more and more towards the documentation side of the fence, especially because everywhere I encounter a "bag-in-box" I end up dropping it for more native-feeling Observable patterns anyway. Case in point: dropping the update Input and updateChanged Output from a Highcharts wrapper (#16) because it seemed entirely redundant when the Highcharts options were already in an observable and directly pushing "hey, here's an update".

I don't want to add it to the current README, because it would feel entirely like bloat to me, so this should be a sub-task of #9.