NgFor не оновлює дані за допомогою Pipe in Angular2


113

У цьому сценарії я відображаю список студентів (масив) для перегляду за допомогою 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()або уникати рядок запиту зі спеціальною функцією.
Еггі

6
Додавання pure:falseта надання вашої труби стаціонарним вирішить проблему. Змінювати ChangeDetectionStrategy не потрібно.
пікселі

2
Для всіх, хто читає це, документація на кутові труби стала набагато кращою і переглядає багато тих же речей, про які йшлося тут. Перевір.
0xcaff

Відповіді:


153

Щоб повністю зрозуміти проблему та можливі рішення, нам потрібно обговорити виявлення кутових змін - для труб та компонентів.

Виявлення зміни труби

Труби без громадянства

За замовчуванням труби без стану / чисті. Труби без стану / чисті труби просто перетворюють вхідні дані у вихідні дані. Вони нічого не пам’ятають, тому не мають жодних властивостей - лише transform()метод. Таким чином, кутовий може оптимізувати обробку труб без стану / чистого стану: якщо їх вхід не змінюється, труби не потрібно виконувати під час циклу виявлення змін. Для таких труб, як {{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() держави:

У режимі розробки галочка () також виконує другий цикл виявлення змін, щоб гарантувати, що подальших змін не буде виявлено. Якщо під час цього другого циклу з’являються додаткові зміни, прив’язки в додатку можуть мати побічні ефекти, які неможливо вирішити за один пропуск виявлення змін. У цьому випадку Angular видає помилку, оскільки кутова програма може мати лише один пропуск виявлення змін, під час якого слід виявити всі зміни.

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

Однак ми не можемо реально розвиватися, коли ця помилка викидається, тому одним вирішенням є додавання властивості масиву (тобто стану) до реалізації труби та завжди повертати цей масив. Дивіться відповідь @ pixelbits для цього рішення.

Однак ми можемо бути ефективнішими, і, як ми побачимо, нам не знадобиться властивість масиву в реалізації труби, і нам не знадобиться рішення для виявлення подвійних змін.

Виявлення зміни компонентів

За замовчуванням у будь-якій події браузера виявлення кутових змін проходить через кожен компонент, щоб побачити, чи змінилось - перевіряються входи та шаблони (а може бути й інші речі?)

Якщо ми знаємо, що компонент залежить тільки від його вхідних властивостей (і подій шаблону), і що властивості введення незмінні, ми можемо використовувати набагато ефективнішу onPushстратегію виявлення змін. За допомогою цієї стратегії замість перевірки кожної події веб-переглядача компонент перевіряється лише тоді, коли змінюються входи та коли спрацьовують події шаблону. І, мабуть, ми не отримуємо цієї Expression ... has changed after it was checkedпомилки з цим налаштуванням. Це тому, що onPushкомпонент не перевіряється знову, поки він не буде "позначений" ( ChangeDetectorRef.markForCheck()) знову. Таким чином, прив'язки шаблонів і стаціонарні виходи труб виконуються / оцінюються лише один раз. Труби без стану / чисті труби досі не виконуються, якщо їх вхід не зміниться. Тож нам ще потрібна велика труба.

Це рішення @EricMartinez запропонував: стаціонарна труба з onPushвиявленням змін. Дивіться відповідь @ caffinedmonkey на це рішення.

Зауважте, що за допомогою цього рішення transform()методу не потрібно щоразу повертати один і той же масив. Я вважаю це трохи дивним: велика труба, яка не має штату. Якщо подумати про це ще трохи ... стан, який, напевно, повинен завжди повертати той же масив. В іншому випадку його можна використовувати лише з onPushкомпонентами в режимі розробки.


Отже, після всього цього, я думаю, що мені подобається поєднання відповідей @ Еріка та @ піксельбіта: стаціонарна труба, яка повертає ту саму посилання масиву, з onPushвиявленням змін, якщо компонент це дозволяє. Оскільки стаціонарна труба повертає ту саму посилання на масив, труба все ще може використовуватися з компонентами, які не налаштовані onPush.

Plunker

Це, мабуть, стане ідіомою Angular 2: якщо масив подає трубу, і масив може змінитися (елементи масиву, тобто не посилання на масив), нам потрібно використовувати стаціонарну трубу.


Дякуємо за детальне пояснення проблеми. Дивно, хоча виявлення змін масивів реалізується як ідентичність замість порівняння рівності. Чи можна було б це вирішити за допомогою «Спостережуваного»?
Мартін Новак

@MartinNowak, якщо ви запитуєте, чи масив був спостережуваним, а труба без стану ... я не знаю, я цього не пробував.
Марк Райчкок

Іншим рішенням буде чиста труба, де вихідний масив відстежує зміни у вхідному масиві разом із слухачами подій.
Tuupertunut

Я просто роздвоєний свій Plunker, і це працює для мене без onPushвиявлення зміни plnkr.co/edit/gRl0Pt9oBO6038kCXZPk?p=preview
Dan

27

Як зазначив Ерік Мартінес у коментарях, додавання pure: falseдо вашого Pipeдекоратора та changeDetection: ChangeDetectionStrategy.OnPushдо вашого Componentдекоратора вирішить вашу проблему. Ось робочий планк. Перехід на ChangeDetectionStrategy.Always, також працює. Ось чому.

Відповідно до посібника angular2 на трубах :

Труби за замовчуванням не мають стану. Ми повинні оголосити трубу справжньою, встановивши pureвластивість @Pipeдекоратора false. Цей параметр повідомляє системі виявлення змін Angular перевіряти вихід цієї труби кожен цикл, змінився чи ні його вхід.

Що стосується ChangeDetectionStrategy, за замовчуванням, всі прив'язки перевіряються на кожному циклі. Коли pure: falseдодається труба, я вважаю, що метод виявлення змін змінюється на CheckAlwaysна CheckOnceз причин продуктивності. З OnPush, прив'язки для Компонента перевіряються лише тоді, коли змінюється властивість введення або коли спрацьовує подія. Для отримання додаткової інформації про детектори змін, важливу частину angular2, перегляньте наступні посилання:


"Коли pure: falseдодається труба, я вважаю, що метод виявлення змін змінюється з CheckAlways на CheckOnce" - це не узгоджується з тим, що ви цитували в документах. Моє розуміння таке: входи труби без стану перевіряються кожен цикл за замовчуванням. Якщо є зміна, transform()метод труби викликається (пере) генерувати вихід. Метод стаціонарної труби transform()називається кожен цикл, і його вихід перевіряється на зміни. Дивіться також stackoverflow.com/a/34477111/215945 .
Марк Райкок

@MarkRajcok, На жаль, ви праві, але якщо це так, то чому зміна стратегії виявлення змін працює?
0xcaff

1
Чудове запитання. Здавалося б, що коли стратегія виявлення змін змінена на OnPush(тобто компонент позначений як "незмінний"), стаціонарний вихід труби не повинен "стабілізуватися" - тобто, здається, transform()метод запускається лише один раз (напевно, всі Виявлення змін для компонента виконується лише один раз). На відміну від відповіді @ pixelbits, де transform()метод викликається кілька разів, і він повинен стабілізуватися (згідно з іншою відповіддю пікселів ), отже, потрібно використовувати одне і те ж посилання на масив.
Марк Райкок

@MarkRajcok Якщо те, що ви говорите, є правильним, з точки зору ефективності, це, мабуть, кращий спосіб, в даному конкретному випадку використання, оскільки transformвикликається лише тоді, коли нові дані висуваються замість декількох разів, поки вихід стабілізується, правда?
0xcaff

1
Напевно, дивіться мою довготривалу відповідь. Мені б хотілося, щоб було більше документації щодо виявлення змін у
куті

22

Демо Плункр

Вам не потрібно змінювати 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;
  }
}

Я отримав цю помилку, не змінюючи changeDectection на onPush: [ plnkr.co/edit/j1VUdgJxYr6yx8WgzCId?p=previewSense(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? Я також зауважив, що кутовий цикл через функцію перетворення двічі зі зміноюDetetion.OnPush і 4 рази без нього
Chu Son

@ChuSon, напевно, ви дивитеся на журнали попередньої версії. Замість того, щоб повертати масив значень, ми повертаємо посилання на масив значень, який можна змінити, виявивши, я вважаю. Пікселіти, ваша відповідь має більше сенсу.
0xcaff

На користь інших читачів, щодо помилки ChuSon, згаданої вище в коментарях, та необхідності використання локальної змінної, було створено ще одне питання , на яке відповіли піксельбіти.
Марк Райчкок

19

З кутової документації

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

Існує дві категорії труб: чисті та нечисті. Труби за замовчуванням чисті. Кожна труба, яку ви бачили досі, була чистою. Ви робите нечистоту, встановлюючи її чистий прапор значення false. Ви можете зробити FlyingHeroesPipe таким чистим:

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

Перш ніж це зробити, зрозумійте різницю між чистим і нечистим, починаючи з чистої труби.

Чисті труби Angular виконує чисту трубу лише тоді, коли виявляє чисту зміну вхідного значення. Чиста зміна - це або зміна примітивного вхідного значення (String, Number, Boolean, Symbol), або змінене посилання на об'єкт (Date, Array, Function, Object).

Кутова ігнорує зміни всередині (складених) об'єктів. Чиста труба не зателефонує, якщо змінити місяць введення, додати до вхідного масиву або оновити властивість об'єкта введення.

Це може здатися обмежуючим, але це також швидко. Перевірка посилання на об'єкт швидка - набагато швидша, ніж глибока перевірка відмінностей - тому Angular може швидко визначити, чи може вона пропустити як виконання труби, так і оновлення перегляду.

З цієї причини чиста труба є кращою, коли ви можете жити зі стратегією виявлення змін. Коли ви не можете, ви можете використовувати нечисту трубу.


Відповідь правильна, але трохи більше зусиль при публікації було б непогано
Стефан

2

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

Він працює для кутових 6 і повинен працювати і для інших кутових версій.


0

Вирішення: імпорт труби вручну в конструктор та метод перетворення виклику за допомогою цієї труби

constructor(
private searchFilter : TableFilterPipe) { }

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

Насправді вам навіть труба не потрібна


0

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

нехай пункт предметів | труба: парам


0

У цьому випадку використання я використовував файл Pipe in 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();
            } 
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.