Вспомогательное поле для флажка с тремя состояниями (пере) оценивается слишком часто - PullRequest
0 голосов
/ 24 мая 2018

У меня есть «наивная» реализация выбора флажка с тремя состояниями для нескольких строк, а также GMail и аналогичных приложений.Вы можете выбрать отдельные строки, и есть флажок верхнего уровня с тремя состояниями, который указывает:

  1. Состояние «Все строки проверены»
  2. Состояние «Некоторые, но не все строки проверены«(промежуточный)
  3. Состояние« Нулевые строки проверены »

Я говорю« наивно », потому что поле, поддерживающее этот флажок верхнего уровня, переоценивается слишком часто, иЯ чувствую, что мне нужно, возможно, Subject или Observable поля для его поддержки.

Вот репродукция моей текущей реализации.

  1. ng new obstest --minimal (Angular 5 CLI)
  2. cd obstest
  3. ng generate service search и добавьте его в app.module providers
  4. Добавьте этот метод макета в службу:

    search(query: Observable<string>) {
      // Fake search, ignore query for demo
      return of<any[]>([
        { isSelected: false, id: 1, txt: 'item 1' },
        { isSelected: false, id: 2, txt: 'item 2' },
        { isSelected: false, id: 3, txt: 'item 3' },
      ]);
    }
    

    Обычно для получения результатов из конечной точки API поиска используется HttpClient.

  5. Добавьте это в файл app.component.ts:

    enum TriState {
      NothingSelected = '[ ]',
      IntermediateSelection = '[-]',
      EverythingSelected = '[X]',
    }
    
  6. Измените оформление компонента следующим образом:

    @Component({
      selector: 'app-root',
      template: `
        <div><input (keyup)="query$.next($event.target.value)"></div>
        <div (click)="onMultiSelectChange()">{{selectionState}}</div>
        <ul *ngFor="let item of results">
          <li (click)='item.isSelected = !item.isSelected'>
            {{item.isSelected ? '[X]' : '[ ]'}} {{item.txt}}
          </li>
        </ul>`,
    })
    
  7. Замените код компонента следующим:

    export class AppComponent {
      results: any[];
      query$ = new Subject<string>();
    
      public get selectionCount() {
        console.warn('Getting count at', new Date().toISOString());
        return this.results.filter(r => r.isSelected).length;
      }
    
      public get selectionState() {
        console.warn('Getting state at', new Date().toISOString());
        if (this.selectionCount === 0) { return TriState.NothingSelected; }
        if (this.selectionCount === this.results.length) { return TriState.EverythingSelected; }
        return TriState.IntermediateSelection;
      }
    
      constructor (service: SearchService) { 
        service.search(of('fake query')).subscribe(r => this.results = r);
      }
    
      onMultiSelectChange() {
        if (this.selectionState === TriState.EverythingSelected) {
          this.results.forEach(r => r.isSelected = false);
        } else {
          this.results.forEach(r => r.isSelected = true);
        }
      }
    }
    
  8. import выпускДля каждого файла

  9. ng serve --open

(Пере) загрузите приложение с открытым окном консоли и:

  • Result : восемь предупреждений на консоли (в моем реальном приложении это даже больше) и постоянный поток при выборе / отмене выбора элементов, даже когда наблюдаемые изменения не имеют отношения / эффекта.
  • Ожидаемые : два предупреждения на консоли при нагрузке, два на соответствующие изменения в других полях.

В KnockoutJS я знал, как это сделать, используя «вычисляемые наблюдаемые» (возможно, вычисленные pure ), и я уверен, что это можно сделать с помощью Angular 5+ (возможно, с помощью rxjs?).Я просто не знаю, как.

Как бы я изменил selectionCount и selectionState таким образом, чтобы представление могло связываться с ними, но они только (повторно) оцениваются при необходимости?

Может кто-нибудь рассказать мне о идиоматическом решении Angular и / или RxJs?

1 Ответ

0 голосов
/ 24 мая 2018

this.results начинается с null, поэтому у него есть два назначения в течение жизненного цикла: сначала null, затем предоставленный вами массив [ ... mock data ... ].

Исследование ваших получателей:

  public get selectionCount() {
    console.warn('Getting count at', new Date().toISOString());
    return this.results.filter(r => r.isSelected).length;
  }

  public get selectionState() {
    console.warn('Getting state at', new Date().toISOString());
    if (this.selectionCount === 0) { return TriState.NothingSelected; }
    if (this.selectionCount === this.results.length) { return TriState.EverythingSelected; }
    return TriState.IntermediateSelection;
  }

Когда вызывается selectionState, он вызывает предупреждение, затем дважды вызывает selectionCount, так что три вызова вызываются за вызов selectionState.Angular не выполняет кэширование получателей.Они вызываются дважды в течение всего жизненного цикла из-за двух назначений this.results, что составляет шесть предупреждений о нагрузке.Я не уверен, откуда берутся оставшиеся два.

Более RxJS способ написать этот класс - избежать мутаций состояний и делать все с наблюдаемым, что-то вроде:

export class AppComponent {
  results$: Observable<any[]>;
  selections$ = new BehaviorSubject<boolean[]>([]);
  selectionCount$: Observable<number>;
  selectionState$: Observable<TriState>;
  query$ = new Subject<string>();

  constructor (service: SearchService) { 
    this.results$ = service.search(of('fake query')).pipe(shareReplay(1));
    this.selectionCount$ = combineLatest(this.results$, this.selections$).pipe(
       map(([results, selections]) => results.filter((result, i) => selections[i])),
       map(results => results.length),
    );
    this.selectionState$ = of(TriState.IntermediateSelection).pipe(concat(this.results.pipe(map(
      results => {
          if (this.selectionCount === 0) { return TriState.NothingSelected; }
          if (this.selectionCount === this.results.length) { return TriState.EverythingSelected; }
      }))));
  }

  toggle(i) {
    selections$.value[i] = !selections$.value[i];
    selections$.next(selections$.value);
  }

  toggleAll() {
    combineLatest(this.selectionState$, this.results$).pipe(
      first(),
      map(([state, results]) => {
        return results.map(() => state === TriState.EverythingSelected);
      }))
      .subscribe(this.selections$);
  }
}

Вероятно, есть ошибки выше, я не проверял это, но, надеюсь, это передает идею.Для шаблона вам нужно будет использовать канал | async, что-то вроде:

@Component({
  selector: 'app-root',
  template: `
    <div><input (keyup)="query$.next($event.target.value)"></div>
    <div (click)="toggleAll()">{{selectionState | async}}</div>
    <ul *ngFor="let item of results | async">
      <li (click)='toggle($index)'>
        {{item.isSelected ? '[X]' : '[ ]'}} {{item.txt}}
      </li>
    </ul>`,
})

К сожалению, Angular не обеспечивает какого-либо стандартизированного управления состояниями, такого как Redux, для реализации этого шаблона, поэтомувы либо должны быть достаточно дисциплинированными, чтобы делать это самостоятельно, либо не бояться дополнительных вызовов.

В качестве альтернативы, вы также можете иметь компонент-оболочку, который обрабатывает Observable и связанное с ним состояние, а не шаблон и иметь дочерний элемент.компонент только визуализирует состояние.Это позволит избежать всех преобразований состояния, и вам нужно будет только async получить наблюдаемые результаты.Я думаю, что это называется узором тяжелых / легких компонентов?Это довольно популярная модель, позволяющая избегать повсеместного обращения с наблюдаемыми, однако я думаю, что я неправильно понял название.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...