NgFor не обновляет данные с помощью Pipe в Angular2

114

В этом сценарии я показываю в представлении список студентов (массив) с помощью ngFor:

<li *ngFor="#student of students">{{student.name}}</li>

Замечательно, что он обновляется всякий раз, когда я добавляю другого ученика в список.

Однако, когда я даю ему , pipeчтобы filterпо имени студента,

<li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>

Список не обновляется до тех пор, пока я не введу что-нибудь в поле имени учащегося.

Вот ссылка на plnkr .

Hello_world.html

<h1>Students:</h1>
<label for="newStudentName"></label>
<input type="text" name="newStudentName" placeholder="newStudentName" #newStudentElem>
<button (click)="addNewStudent(newStudentElem.value)">Add New Student</button>
<br>
<input type="text" placeholder="Search" #queryElem (keyup)="0">
<ul>
    <li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>
</ul>

sort_by_name_pipe.ts

import {Pipe} from 'angular2/core';

@Pipe({
    name: 'sortByName'
})
export class SortByNamePipe {

    transform(value, [queryString]) {
        // console.log(value, queryString);
        return value.filter((student) => new RegExp(queryString).test(student.name))
        // return value;
    }
}

Чу Сон
источник
14
Добавьте pure:falseв свой канал и changeDetection: ChangeDetectionStrategy.OnPushв свой компонент.
Эрик Мартинес,
2
Спасибо @EricMartinez. Оно работает. Но не могли бы вы немного объяснить?
Чу Сон
2
Кроме того, я бы предложил НЕ использовать .test()в вашей функции фильтра. Это потому, что если пользователь вводит строку, которая включает символы специального значения, такие как: *или +т.д., ваш код сломается. Я думаю, вам следует использовать .includes()или избегать строки запроса с помощью настраиваемой функции.
Eggy
6
Добавление pure:falseи изменение состояния канала решит проблему. Изменять ChangeDetectionStrategy не нужно.
pixelbits
2
Для всех, кто читает это, документация для Angular Pipes стала намного лучше и охватывает многие из тех вещей, которые обсуждались здесь. Проверить это.
0xcaff

Ответы:

154

Чтобы полностью понять проблему и возможные решения, нам нужно обсудить обнаружение изменений Angular - для каналов и компонентов.

Обнаружение замены трубы

Каналы без сохранения состояния / чистые

По умолчанию каналы не имеют состояния / чистые. Каналы без сохранения состояния / чистые каналы просто преобразуют входные данные в выходные данные. Они ничего не помнят, поэтому у них нет никаких свойств - только transform()метод. Следовательно, Angular может оптимизировать обработку каналов без сохранения состояния / чистых каналов: если их входные данные не меняются, каналы не нужно запускать во время цикла обнаружения изменений. Для канала, такого как {{power | exponentialStrength: factor}}, powerи factorявляются входами.

Для этого вопроса, "#student of students | sortByName:queryElem.value", studentsи queryElem.valueявляются входами, а труба sortByNameимеет состояние / чистая. studentsэто массив (ссылка).

  • Когда добавляется студент, ссылка на массив не изменяется - studentsне изменяется - следовательно, канал без сохранения состояния / чистый канал не выполняется.
  • Когда что-то вводится во вход фильтра, queryElem.valueдействительно изменяется, поэтому выполняется канал без сохранения состояния / чистый.

Один из способов решить проблему с массивом - это изменять ссылку на массив каждый раз, когда добавляется ученик, то есть создавать новый массив каждый раз, когда добавляется ученик. Мы могли бы сделать это с помощью concat():

this.students = this.students.concat([{name: studentName}]);

Хотя это работает, наш addNewStudent()метод не должен быть реализован определенным образом только потому, что мы используем канал. Мы хотим использовать push()для добавления в наш массив.

Каналы с отслеживанием состояния

У каналов с отслеживанием состояния есть состояние - обычно у них есть свойства, а не только transform()метод. Их может потребоваться оценка, даже если их входные данные не изменились. Когда мы указываем, что канал имеет состояние / не является чистым - pure: falseтогда всякий раз, когда система обнаружения изменений Angular проверяет компонент на наличие изменений, и этот компонент использует канал с сохранением состояния, он будет проверять вывод канала, изменился ли его вход или нет.

Это похоже на то, что мы хотим, хотя это менее эффективно, поскольку мы хотим, чтобы канал выполнялся, даже если studentsссылка не изменилась. Если мы просто сделаем канал с отслеживанием состояния, мы получим ошибку:

EXCEPTION: Expression 'students | sortByName:queryElem.value  in HelloWorld@7:6' 
has changed after it was checked. Previous value: '[object Object],[object Object]'. 
Current value: '[object Object],[object Object]' in [students | sortByName:queryElem.value

Согласно ответу @drewmoore , «эта ошибка возникает только в режиме разработки (который включен по умолчанию с бета-версии 0). Если вы вызовете enableProdMode()при загрузке приложения, ошибка не будет выдана». В документах дляApplicationRef.tick() государства:

В режиме разработки tick () также выполняет второй цикл обнаружения изменений, чтобы гарантировать, что дальнейшие изменения не будут обнаружены. Если во время этого второго цикла будут внесены дополнительные изменения, привязки в приложении будут иметь побочные эффекты, которые невозможно устранить за один проход обнаружения изменений. В этом случае Angular выдает ошибку, поскольку приложение Angular может иметь только один проход обнаружения изменений, во время которого должно завершиться обнаружение всех изменений.

В нашем сценарии я считаю ошибку ложной / вводящей в заблуждение. У нас есть конвейер с отслеживанием состояния, и вывод может меняться каждый раз при его вызове - это может иметь побочные эффекты, и это нормально. NgFor оценивается после канала, поэтому он должен работать нормально.

Однако мы не можем развиваться с этой ошибкой, поэтому один способ обхода - добавить свойство массива (то есть состояние) в реализацию конвейера и всегда возвращать этот массив. См. Ответ @ pixelbits для этого решения.

Однако мы можем быть более эффективными, и, как мы увидим, нам не понадобится свойство массива в реализации конвейера, и нам не понадобится обходной путь для обнаружения двойного изменения.

Обнаружение изменения компонентов

По умолчанию при каждом событии браузера обнаружение изменений Angular проходит через каждый компонент, чтобы узнать, изменился ли он - проверяются входные данные и шаблоны (и, возможно, другие вещи?).

Если мы знаем, что компонент зависит только от своих входных свойств (и событий шаблона), и что входные свойства неизменны, мы можем использовать гораздо более эффективную onPushстратегию обнаружения изменений. В этой стратегии, вместо проверки каждого события браузера, компонент проверяется только при изменении входных данных и при срабатывании событий шаблона. И, видимо, Expression ... has changed after it was checkedс этой настройкой мы не получаем эту ошибку. Это связано с тем, что onPushкомпонент не проверяется снова, пока он снова не будет "отмечен" ( ChangeDetectorRef.markForCheck()). Таким образом, привязки шаблонов и выходы канала с сохранением состояния выполняются / оцениваются только один раз. Каналы без сохранения состояния / чистые каналы по-прежнему не выполняются, если их входные данные не меняются. Так что нам по-прежнему нужен канал с отслеживанием состояния.

Это решение, предложенное @EricMartinez: канал с отслеживанием состояния с onPushобнаружением изменений. См. Ответ @caffinatedmonkey для этого решения.

Обратите внимание, что с этим решением transform()методу не нужно каждый раз возвращать один и тот же массив. Я нахожу это немного странным: канал с отслеживанием состояния без состояния. Подумав еще об этом ... канал с отслеживанием состояния, вероятно, всегда должен возвращать один и тот же массив. В противном случае его можно было бы использовать только с onPushкомпонентами в режиме разработки.


Итак, после всего этого, я думаю, мне нравится комбинация ответов @ Eric и @ pixelbits: конвейер с отслеживанием состояния, который возвращает одну и ту же ссылку на массив, с onPushобнаружением изменений, если компонент это позволяет. Поскольку канал с отслеживанием состояния возвращает ту же ссылку на массив, канал по-прежнему можно использовать с компонентами, которые не настроены с помощью onPush.

Plunker

Вероятно, это станет идиомой Angular 2: если массив передает канал, и массив может измениться (элементы в массиве, а не ссылка на массив), нам нужно использовать канал с отслеживанием состояния.

Марк Райкок
источник
Спасибо за подробное объяснение проблемы. Как ни странно, обнаружение изменений массивов реализовано как идентификация вместо сравнения на равенство. Можно ли решить эту проблему с помощью Observable?
Мартин Новак
@MartinNowak, если вы спрашиваете, был ли массив Observable, а канал не имел состояния ... Я не знаю, я этого не пробовал.
Марк Райкок 09
Другим решением может быть чистый канал, в котором выходной массив отслеживает изменения во входном массиве с помощью прослушивателей событий.
Tuupertunut 02
Я только что форкнул ваш Plunker, и он работает у меня без onPushобнаружения изменений plnkr.co/edit/gRl0Pt9oBO6038kCXZPk?p=preview
Дэн
28

Как отметил Эрик Мартинес в комментариях, добавление pure: falseв ваш Pipeдекоратор и changeDetection: ChangeDetectionStrategy.OnPushв ваш Componentдекоратор решит вашу проблему. Вот рабочий плункр. Переход на ChangeDetectionStrategy.Always, тоже работает. Вот почему.

Согласно руководству angular2 по трубам :

По умолчанию каналы не имеют состояния. Мы должны объявить канал с отслеживанием состояния, установив для pureсвойства @Pipeдекоратора значение false. Этот параметр указывает системе обнаружения изменений Angular проверять вывод этого конвейера каждый цикл, независимо от того, изменился ли его ввод или нет.

Что касается ChangeDetectionStrategy, по умолчанию, все привязки проверяются каждый цикл. При pure: falseдобавлении трубы, я считаю , что метод обнаружения изменений меняется от CheckAlwaysдо CheckOnceпо соображениям производительности. С OnPushпривязки для Компонента проверяются только при изменении свойства ввода или при запуске события. Для получения дополнительной информации о детекторах изменений, важной части angular2, перейдите по следующим ссылкам:

0xcaff
источник
«Когда pure: falseдобавляется канал, я считаю, что метод обнаружения изменений меняется с CheckAlways на CheckOnce» - это не согласуется с тем, что вы процитировали из документов. Насколько я понимаю, входные данные канала без сохранения состояния по умолчанию проверяются каждый цикл. Если есть изменение, transform()вызывается метод канала для (повторного) генерации вывода. transform()Метод канала с отслеживанием состояния вызывается каждый цикл, и его выходные данные проверяются на наличие изменений. См. Также stackoverflow.com/a/34477111/215945 .
Марк Райкок
@MarkRajcok, Ой, ты прав, но если это так, почему срабатывает изменение стратегии обнаружения изменений?
0xcaff
1
Отличный вопрос. Казалось бы, когда стратегия обнаружения изменений изменена на OnPush(т. Е. Компонент помечен как «неизменяемый»), выходной канал с отслеживанием состояния не должен «стабилизироваться» - т. Е. Кажется, что transform()метод запускается только один раз (возможно, все обнаружение изменений для компонента запускается только один раз). Сравните это с ответом @ pixelbits, где transform()метод вызывается несколько раз, и он должен стабилизироваться (согласно другому ответу pixelbits ), следовательно, необходимо использовать одну и ту же ссылку на массив.
Марк Райкок
@MarkRajcok Если то, что вы говорите, правильно с точки зрения эффективности, это, вероятно, лучший способ в данном конкретном случае использования, потому что он transformвызывается только тогда, когда новые данные отправляются, а не несколько раз, пока вывод не стабилизируется, верно?
0xcaff
1
Наверное, увидите мой пространный ответ. Я бы хотел, чтобы в Angular 2 было больше документации об обнаружении изменений.
Марк Райкок
22

Демо Plunkr

Вам не нужно изменять ChangeDetectionStrategy. Реализации канала с отслеживанием состояния достаточно, чтобы все заработало.

Это канал с отслеживанием состояния (других изменений не было):

@Pipe({
  name: 'sortByName',
  pure: false
})
export class SortByNamePipe {
  tmp = [];
  transform (value, [queryString]) {
    this.tmp.length = 0;
    // console.log(value, queryString);
    var arr = value.filter((student)=>new RegExp(queryString).test(student.name));
    for (var i =0; i < arr.length; ++i) {
        this.tmp.push(arr[i]);
     }

    return this.tmp;
  }
}
pixelbits
источник
Я получил эту ошибку, не изменив changeDectection на onPush: [ plnkr.co/edit/j1VUdgJxYr6yx8WgzCId?p=preview ]( Plnkr)Expression 'students | sortByName:queryElem.value in HelloWorld@7:6' has changed after it was checked. Previous value: '[object Object],[object Object],[object Object]'. Current value: '[object Object],[object Object],[object Object]' in [students | sortByName:queryElem.value in HelloWorld@7:6]
Chu Son
1
Можете ли вы привести пример, почему вам нужно сохранять значения в локальной переменной в SortByNamePipe, а затем возвращать их в функции преобразования @pixelbits? Я также заметил, что Angular дважды перебирает функцию преобразования с помощью changeDetetion.OnPush и 4 раза без нее
Chu Son
@ChuSon, вы, наверное, смотрите логи из предыдущей версии. Я считаю, что вместо того, чтобы возвращать массив значений, мы возвращаем ссылку на массив значений, которые могут быть обнаружены изменениями. Pixelbits, ваш ответ имеет больше смысла.
0xcaff
Для удобства других читателей, относительно ошибки ChuSon, упомянутой выше в комментариях, и необходимости использования локальной переменной, был создан другой вопрос , на который ответил пиксельбит.
Марк Райкок
19

Из документации angular

Чистые и нечистые трубы

Есть две категории труб: чистые и нечистые. Трубы по умолчанию чистые. Каждая трубка, которую вы видели до сих пор, была чистой. Вы делаете канал нечистым, устанавливая его чистый флаг в false. Вы можете сделать FlyingHeroesPipe нечистым следующим образом:

@Pipe({ name: 'flyingHeroesImpure', pure: false })

Прежде чем это сделать, разберитесь в разнице между чистым и нечистым, начиная с чистой трубы.

Чистые каналы Angular выполняет чистый канал только тогда, когда обнаруживает чистое изменение входного значения. Чистое изменение - это либо изменение примитивного входного значения (String, Number, Boolean, Symbol), либо измененная ссылка на объект (Date, Array, Function, Object).

Angular игнорирует изменения внутри (составных) объектов. Он не будет вызывать чистый канал, если вы измените входной месяц, добавите во входной массив или обновите свойство входного объекта.

Это может показаться ограничительным, но это также быстро. Проверка ссылки на объект выполняется быстро - намного быстрее, чем глубокая проверка различий, - поэтому Angular может быстро определить, может ли он пропустить как выполнение конвейера, так и обновление представления.

По этой причине предпочтительнее использовать чистый канал, если вы можете жить со стратегией обнаружения изменений. Когда нет, можно использовать нечистую трубку.

Сумит Джайсвал
источник
Ответ правильный, но было бы неплохо приложить немного больше усилий при его размещении
Стефан
2

Вместо того, чтобы делать чисто: ложь. Вы можете глубоко скопировать и заменить значение в компоненте на this.students = Object.assign ([], NEW_ARRAY); где NEW_ARRAY - модифицированный массив.

Он работает для angular 6 и должен работать и для других угловых версий.

идеи
источник
0

Обходной путь: вручную импортируйте канал в конструктор и вызовите метод преобразования с помощью этого канала.

constructor(
private searchFilter : TableFilterPipe) { }

onChange() {
   this.data = this.searchFilter.transform(this.sourceData, this.searchText)}

На самом деле тебе даже труба не нужна

Ричард Луо
источник
0

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

пусть пункт пунктов | pipe: param

Ник Быстров
источник
0

В этом случае я использовал свой канал в ts-файле для фильтрации данных. Это намного лучше с точки зрения производительности, чем использование чистых труб. Используйте в ts так:

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

  YourFunction(value) {
    this.pipe.transform(value, 'pipeFilter');
  }
}
Андрис
источник
0

создание нечистых труб обходится дорого. поэтому не создавайте нечистые каналы, а лучше измените ссылку на переменную данных, создав копию данных при изменении данных и переназначив ссылку копии в исходной переменной данных.

            emp=[];
            empid:number;
            name:string;
            city:string;
            salary:number;
            gender:string;
            dob:string;
            experience:number;

            add(){
              const temp=[...this.emps];
              const e={empid:this.empid,name:this.name,gender:this.gender,city:this.city,salary:this.salary,dob:this.dob,experience:this.experience};
              temp.push(e); 
              this.emps =temp;
              //this.reset();
            } 
Амит Бхандари
источник