Обнаружение изменений Angular2: ngOnChanges не срабатывает для вложенного объекта

129

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

<div class="col-sm-5">
    <laps
        [lapsData]="rawLapsData"
        [selectedTps]="selectedTps"
        (lapsHandler)="lapsHandler($event)">
    </laps>
</div>

<map
    [lapsData]="rawLapsData"
    class="col-sm-7">
</map>

В контроллере rawLapsdataвремя от времени происходит мутация.

В laps, данные выводятся как HTML в табличном формате. Это меняется всякий раз, когда rawLapsdataменяется.

Мой mapкомпонент нужно использовать ngOnChangesв качестве триггера для перерисовки маркеров на карте Google. Проблема в том, что ngOnChanges не срабатывает при rawLapsDataизменении родительского объекта . Что я могу сделать?

import {Component, Input, OnInit, OnChanges, SimpleChange} from 'angular2/core';

@Component({
    selector: 'map',
    templateUrl: './components/edMap/edMap.html',
    styleUrls: ['./components/edMap/edMap.css']
})
export class MapCmp implements OnInit, OnChanges {
    @Input() lapsData: any;
    map: google.maps.Map;

    ngOnInit() {
        ...
    }

    ngOnChanges(changes: { [propName: string]: SimpleChange }) {
        console.log('ngOnChanges = ', changes['lapsData']);
        if (this.map) this.drawMarkers();
    }

Обновление: ngOnChanges не работает, но похоже, что lapsData обновляется. В ngInit есть прослушиватель событий для изменения масштаба, который также вызывает this.drawmarkers. Когда я меняю масштаб, я действительно вижу изменение маркеров. Так что единственная проблема в том, что я не получаю уведомления в момент изменения входных данных.

В родителе у меня эта строчка. (Напомним, изменение отражается на кругах, но не на карте).

this.rawLapsData = deletePoints(this.rawLapsData, this.selectedTps);

Обратите внимание, что this.rawLapsDataэто указатель на середину большого json-объекта.

this.rawLapsData = this.main.data.TrainingCenterDatabase.Activities[0].Activity[0].Lap;
Саймон Х
источник
Ваш код не показывает, как данные обновляются или какого типа данные. Назначен ли новый экземпляр или это просто свойство измененного значения?
Günter Zöchbauer
@ GünterZöchbauer Я добавил строку из родительского компонента
Simon H
Я думаю, что обертывание этой строки zone.run(...)должно сделать это тогда.
Günter Zöchbauer
1
Ваш массив (ссылка) не меняется, поэтому ngOnChanges()не будет вызываться. Вы можете использовать ngDoCheck()и реализовать свою собственную логику, чтобы определить, изменилось ли содержимое массива. lapsDataобновляется, потому что он имеет / является ссылкой на тот же массив, что и rawLapsData.
Марк Райкок
1
1) В компоненте laps ваш код / ​​шаблон перебирает каждую запись в массиве lapsData и отображает содержимое, поэтому есть привязки Angular к каждому отображаемому фрагменту данных. 2) Даже если Angular не обнаруживает никаких изменений (проверка ссылок) во входных свойствах компонента, он все равно (по умолчанию) проверяет все привязки шаблона. Вот как круги улавливают изменения. 3) Компонент карт, вероятно, не имеет привязки в своем шаблоне к свойству ввода lapsData, верно? Это объяснило бы разницу.
Марк Райкок,

Ответы:

154

rawLapsData продолжает указывать на тот же массив, даже если вы изменяете содержимое массива (например, добавляете элементы, удаляете элементы, изменяете элемент).

Во время обнаружения изменений, когда Angular проверяет входные свойства компонентов на предмет изменений, он использует (по существу) ===для грязной проверки. Для массивов это означает, что ссылки на массив (только) проверяются грязно. Поскольку rawLapsDataссылка на массив не меняется, ngOnChanges()не будет вызываться.

Я могу придумать два возможных решения:

  1. Реализуйте ngDoCheck()и выполните собственную логику обнаружения изменений, чтобы определить, изменилось ли содержимое массива. (В документе Lifecycle Hooks есть пример .)

  2. Назначьте новый массив rawLapsDataвсякий раз, когда вы вносите какие-либо изменения в содержимое массива. Затем ngOnChanges()будет вызываться, потому что массив (ссылка) появится как изменение.

В своем ответе вы пришли к другому решению.

Повторяя некоторые комментарии здесь к OP:

Я до сих пор не понимаю, как я lapsмогу уловить это изменение (наверняка он должен использовать что-то эквивалентное самому ngOnChanges()себе?), А mapне могу.

  • В lapsкомпоненте ваш код / ​​шаблон перебирает каждую запись в lapsDataмассиве и отображает содержимое, поэтому для каждого отображаемого фрагмента данных есть привязки Angular.
  • Даже когда Angular не обнаруживает никаких изменений входных свойств компонента (с помощью ===проверки), он по-прежнему (по умолчанию) грязно проверяет все привязки шаблона. Когда любое из этих изменений изменится, Angular обновит DOM. Вот что вы видите.
  • mapsКомпонент , вероятно , не имеет каких - либо привязок в своем шаблоне его lapsDataввода недвижимости, не так ли? Это объяснило бы разницу.

Обратите внимание, что lapsDataв обоих компонентах и rawLapsDataв родительском компоненте все указывают на один и тот же / один массив. Таким образом, даже если Angular не замечает никаких (ссылочных) изменений во lapsDataвходных свойствах, компоненты «получают» / видят любые изменения содержимого массива, потому что все они совместно используют / ссылаются на этот один массив. Нам не нужен Angular для распространения этих изменений, как в случае с примитивным типом (строка, число, логическое значение). Но с примитивным типом любое изменение значения всегда будет запускаться ngOnChanges()- это то, что вы используете в своем ответе / решении.

Как вы, наверное, уже догадались, свойства ввода объекта ведут себя так же, как свойства ввода массива.

Марк Райкок
источник
Да, я мутировал глубоко вложенный объект, и я думаю, что это затруднило Angular обнаружение изменений в структуре. Не я предпочитаю мутировать, но это переведенный XML, и я не могу позволить себе потерять какие-либо окружающие данные, так как я хочу снова воссоздать xml в конце
Саймон Х
7
@SimonH, «Angular трудно заметить изменения в структуре» - для ясности, Angular даже не смотрит внутрь входных свойств, которые являются массивами или объектами для изменений. Он только проверяет, изменилось ли значение - для объектов и массивов значение является ссылкой. Для примитивных типов значение ... значение. (Я не уверен, что у меня есть весь жаргон, но вы
поняли
11
Отличный ответ. Команде Angular2 отчаянно нужно опубликовать подробный авторитетный документ о внутреннем устройстве обнаружения изменений.
Если я выполняю функции в doCheck, в моем случае проверка на выполнение вызывает столько раз. Подскажите, пожалуйста, другой способ?
Mr_Perfect
@MarkRajcok вы можете , пожалуйста , помогите мне решить эту проблему stackoverflow.com/questions/50166996/...
Никсона
27

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

   rawLapsData = Object.assign({}, rawLapsData);

Думаю, я бы предпочел этот подход реализации вашего собственного, ngDoCheck()но, возможно, кто-то вроде @ GünterZöchbauer сможет вмешаться.

Дэвид
источник
Если вы не уверены, поддерживает ли целевой браузер <Object.assign ()>, вы также можете преобразовать его в строку json и выполнить синтаксический анализ до json, который также создаст новый объект ...
Гунтрам,
1
@Guntram Или полифилл?
Дэвид
12

В случае с массивами это можно сделать так:

В .tsфайле (родительский компонент), в котором вы обновляете, rawLapsDataсделайте это так:

rawLapsData = somevalue; // change detection will not happen

Решение:

rawLapsData = {...somevalue}; //change detection will happen

и ngOnChangesбудет вызываться дочерний компонент

Датский Дуллу
источник
10

Как расширение второго решения Марка Райкока

Назначьте новый массив для rawLapsData всякий раз, когда вы вносите какие-либо изменения в содержимое массива. Затем будет вызываться ngOnChanges (), потому что массив (ссылка) появится как изменение

вы можете клонировать содержимое массива следующим образом:

rawLapsData = rawLapsData.slice(0);

Я говорю об этом, потому что

rawLapsData = Object.assign ({}, rawLapsData);

у меня не сработало. Надеюсь, это поможет.

Decebal
источник
8

Если данные поступают из внешней библиотеки, вам может потребоваться запустить оператор обновления данных внутри zone.run(...). Залить zone: NgZoneдля этого. Если вы уже можете запустить экземпляр внешней библиотеки внутри zone.run(), то, возможно, вам не понадобится zone.run()позже.

Гюнтер Цохбауэр
источник
Как отмечено в комментариях к OP, изменения были не внешними, а глубоко внутри объекта json
Саймон Х
1
Как говорится в вашем ответе, нам все же нужно запустить что-то для синхронизации в Angular2, например, angular1 $scope.$apply?
Pankaj Parkar
1
Если что-то запускается извне Angular, API, пропатченный по зоне, не используется, и Angular не получает уведомления о возможных изменениях. Да, zone.runпохоже на $scope.apply.
Günter Zöchbauer
6

Используется, ChangeDetectorRef.detectChanges()чтобы указать Angular запускать обнаружение изменений при редактировании вложенного объекта (который он пропускает из-за грязной проверки).

Тим Филлипс
источник
Но как ? Я пытаюсь использовать это, я хочу вызвать changeDetection в дочернем элементе после того, как родитель нажал новый элемент с двусторонним ограничением [(Коллекция)].
Deunz
Проблема не в том, что обнаружение изменений не происходит, а в том, что обнаружение изменений не обнаруживает изменений! так что это не похоже на решение! почему этот ответ получил пять голосов?
Шахрияр Салджуги
5

У меня есть 2 решения вашей проблемы

  1. Используется ngDoCheckдля определения objectизмененных данных или нет
  2. Назначьте objectновый адрес памяти object = Object.create(object)от родительского компонента.
giapnh
источник
2
Будет ли какая-нибудь заметная разница между Object.create (object) и Object.assign ({}, object)?
Daniel Galarza
3

Мое "хакерское" решение

   <div class="col-sm-5">
        <laps
            [lapsData]="rawLapsData"
            [selectedTps]="selectedTps"
            (lapsHandler)="lapsHandler($event)">
        </laps>
    </div>
    <map
        [lapsData]="rawLapsData"
        [selectedTps]="selectedTps"   // <--------
        class="col-sm-7">
    </map>

selectedTps изменяется одновременно с rawLapsData, и это дает карте еще один шанс обнаружить изменение с помощью более простого объектного примитивного типа. Это НЕ элегантно, но работает.

Саймон Х
источник
Мне трудно отслеживать все изменения в различных компонентах синтаксиса шаблонов, особенно в средних и крупных приложениях. Обычно я использую общий эмиттер событий и подписку для передачи данных (быстрое решение) или реализую для этого шаблон Redux (через Rx.Subject) (когда есть время планировать) ...
Sasxa
3

Обнаружение изменений не запускается, когда вы изменяете свойство объекта (включая вложенный объект). Одним из решений было бы переназначить новую ссылку на объект с помощью функции clone () lodash.

import * as _ from 'lodash';

this.foo = _.clone(this.foo);
Антон Никпрелай
источник
2

Вот хак, который избавил меня от проблем с этим.

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

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

Грэм Филлипс
источник
1
Тьфу! Подход array.slice (0) намного чище.
Крис Хейнс,
2

Я наткнулся на ту же потребность. И я много читал об этом, так что вот моя статья по этой теме.

Если вы хотите, чтобы обнаружение изменений происходило при нажатии, тогда оно будет, когда вы измените значение объекта внутри, верно? И у вас также будет это, если вы каким-то образом удалите объекты.

Как уже было сказано, использование changeDetectionStrategy.onPush

Допустим, у вас есть этот компонент, который вы создали с помощью changeDetectionStrategy.onPush:

<component [collection]="myCollection"></component>

Затем вы нажимаете элемент и запускаете обнаружение изменений:

myCollection.push(anItem);
refresh();

или вы удалите элемент и вызовете обнаружение изменений:

myCollection.splice(0,1);
refresh();

или вы бы изменили значение attrbibute для элемента и инициировали обнаружение изменений:

myCollection[5].attribute = 'new value';
refresh();

Содержание обновления:

refresh() : void {
    this.myCollection = this.myCollection.slice();
}

Метод slice возвращает тот же самый массив, а знак [=] создает новую ссылку на него, вызывая обнаружение изменений каждый раз, когда это необходимо. Легко и читабельно :)

С Уважением,

Deunz
источник
2

Я пробовал все решения, упомянутые здесь, но по какой-то причине ngOnChanges()у меня все еще не сработало. Итак, я вызвал его this.ngOnChanges()после вызова службы, которая повторно заполняет мои массивы, и она сработала .... правильно? возможно нет. Аккуратные? конечно нет. Работает? да!

Язан Халайле
источник
1

хорошо, поэтому мое решение для этого было:

this.arrayWeNeed.DoWhatWeNeedWithThisArray();
const tempArray = [...arrayWeNeed];
this.arrayWeNeed = [];
this.arrayWeNeed = tempArray;

И это вызывает у меня ngOnChanges

DanielWaw
источник
0

Пришлось создать для него хак -

I created a Boolean Input variable and toggled it whenever array changed, which triggered change detection in the child component, hence achieving the purpose
Ришаб Вадхва
источник
0

В моем случае это были изменения значения объекта, которые ngOnChangeне фиксировались. Некоторые значения объектов изменяются в ответ на вызов API. Повторная инициализация объекта устранила проблему и вызвалаngOnChange срабатывание в дочернем компоненте.

Что-то вроде

 this.pagingObj = new Paging(); //This line did the magic
 this.pagingObj.pageNumber = response.PageNumber;
Кайлас
источник