Como ignoro a carga inicial ao assistir alterações de modelo no AngularJS?

184

Eu tenho uma página da web que serve como editor de uma única entidade, que fica como um gráfico profundo na propriedade $ scope.fieldcontainer. Depois de receber uma resposta da minha API REST (via $ resource), adiciono um relógio ao 'fieldcontainer'. Estou usando este relógio para detectar se a página / entidade está "suja". No momento, estou fazendo o botão salvar saltar, mas realmente quero tornar o botão invisível até que o usuário direcione o modelo.

O que estou obtendo é um único gatilho do relógio, o que acho que está acontecendo porque a atribuição .fieldcontainer = ... ocorre imediatamente após a criação do relógio. Eu estava pensando em usar apenas uma propriedade "dirtyCount" para absorver o alarme falso inicial, mas isso parece muito hacky ... e achei que deveria haver uma maneira "angular idiomática" de lidar com isso - eu não sou o único usando um relógio para detectar um modelo sujo.

Aqui está o código em que eu ajusto meu relógio:

 $scope.fieldcontainer = Message.get({id: $scope.entityId },
            function(message,headers) {
                $scope.$watch('fieldcontainer',
                    function() {
                        console.log("model is dirty.");
                        if ($scope.visibility.saveButton) {
                            $('#saveMessageButtonRow').effect("bounce", { times:5, direction: 'right' }, 300);
                        }
                    }, true);
            });

Eu continuo pensando que deve haver uma maneira mais limpa de fazer isso do que guardar meu código "UI dirtying" com um "if (dirtyCount> 0)" ...

Kevin Hoffman
fonte
Também preciso recarregar o $ scope.fieldcontainer com base em determinados botões (por exemplo, carregar uma versão anterior da entidade). Para isso, eu precisaria suspender o relógio, recarregar e retomar o relógio.
Kevin Hoffman
Você consideraria alterar minha resposta como a resposta aceita? Aqueles que demoram um tempo para ler até o final tendem a concordar que é a solução correta.
trixtur
@trixtur Eu tenho o mesmo problema de OP, mas no meu caso sua resposta não funciona, porque meu antigo valor não é undefined. Ele tem um valor padrão, necessário no caso de minha atualização de modelo, não apresentar todas as informações. Portanto, alguns valores não mudam, mas precisam ser acionados.
precisa saber é
@oliholz ​​Então, em vez de verificar contra indefinido, retire o "typeof" e verifique o old_fieldcontainer com o seu valor padrão.
trixtur
@ KevinHoffman, você deve marcar a resposta da MW na resposta correta para outras pessoas que encontrarem essa pergunta. É uma solução muito melhor. Obrigado!
Dev01

Respostas:

118

defina um sinalizador imediatamente antes do carregamento inicial,

var initializing = true

e quando o primeiro $ watch for disparado, faça

$scope.$watch('fieldcontainer', function() {
  if (initializing) {
    $timeout(function() { initializing = false; });
  } else {
    // do whatever you were going to do
  }
});

A sinalização será removida no final do ciclo de resumo atual, para que a próxima alteração não seja bloqueada.

reescrito
fonte
17
Sim, isso funcionará, mas a comparação da abordagem de valor antigo e novo sugerida pelo @MW. é ao mesmo tempo mais simples e mais idiomático angular. Você pode atualizar a resposta aceita?
Gerryster #
2
Sempre definir o oldValue para igualar o newValue no primeiro fogo foi adicionada especificamente para evitar ter de fazer esta bandeira corte
Charlie Martin
2
A resposta da MW está realmente incorreta, pois o novo valor para o valor antigo não trata do caso em que o valor antigo é indefinido.
trixtur
1
Para comentaristas: esse não é um modelo divino, nada impede que você coloque a variável 'inicializada' dentro do escopo de um decorador e depois decore seu relógio "normal" com ele. De qualquer forma, isso está completamente fora do escopo desta questão.
reescrito
1
Consulte plnkr.co/edit/VrWrHHWwukqnxaEM3J5i para obter uma comparação dos métodos propostos, configurando observadores no retorno de chamada .get () e na inicialização do escopo.
reescrita
483

Na primeira vez que o ouvinte é chamado, o valor antigo e o novo valor serão idênticos. Então, faça o seguinte:

$scope.$watch('fieldcontainer', function(newValue, oldValue) {
  if (newValue !== oldValue) {
    // do whatever you were going to do
  }
});

Essa é realmente a maneira que os documentos do Angular recomendam :

Depois que um observador é registrado com o escopo, o ouvinte fn é chamado de forma assíncrona (via $ evalAsync) para inicializar o observador. Em casos raros, isso é indesejável porque o ouvinte é chamado quando o resultado da watchExpression não foi alterado. Para detectar esse cenário no ouvinte fn, você pode comparar o newVal e o oldVal. Se esses dois valores forem idênticos (===), o ouvinte foi chamado devido à inicialização

MW.
fonte
3
Desta forma é mais idiomática de resposta da @ reescrito
Jiří Vypědřík
25
Isso não está correto. O ponto é ignorar a alteração de nullpara o valor inicial carregado, para não ignorar a alteração quando não houver alteração.
reescrito
1
Bem, a função de ouvinte sempre será acionada quando o relógio for criado. Isso ignora a primeira ligação, que é o que acredito que o solicitante queria. Se ele quer especificamente ignorar a alteração quando o valor antigo era nulo, é claro que ele pode comparar oldValue com null ... mas acho que não é isso que ele queria fazer.
MW.
O OP está perguntando especificamente sobre observadores de recursos (dos serviços REST). Os recursos são criados primeiro, depois os observadores aplicados e, em seguida, os atributos da resposta são gravados, e somente então o Angular faz um ciclo. Ignorar o primeiro ciclo com um sinalizador "inicializando" fornece uma maneira de aplicar um observador apenas em recursos inicializados, mas ainda observando alterações de / para null.
reescrita
7
Isso está errado, oldValueserá nulo na primeira volta.
Kevin C.
42

Sei que esta pergunta foi respondida, no entanto, tenho uma sugestão:

$scope.$watch('fieldcontainer', function (new_fieldcontainer, old_fieldcontainer) {
    if (typeof old_fieldcontainer === 'undefined') return;

    // Other code for handling changed object here.
});

O uso de flags funciona, mas tem um pouco de cheiro de código , você não acha?

trixtur
fonte
1
Este é o caminho a percorrer. No Coffeescript, é tão simples quanto return unless oldValue?(? É o operador existencial no CS).
9134 Kevin C.
1
Adereços na palavra cheiro de código! também confira deus objeto lol. bom demais.
Matthew Harwood
1
Embora essa não seja a resposta aceita, isso fez com que meu código funcionasse ao preencher um objeto de uma API e desejo assistir diferentes estados de salvamento. Muito agradável.
Lucas Reppe Welander
1
Graças - isso ajudou para mim
Wand Criador
1
Isso é ótimo, no entanto, no meu caso, instanciei os dados como uma matriz vazia antes de fazer a chamada AJAX para minha API; portanto, verifique o tamanho disso em vez do tipo. Mas esta resposta mostra definitivamente o método mais eficaz, pelo menos.
Saborknight 31/03
4

Durante o carregamento inicial dos valores atuais, o campo de valor antigo é indefinido. Portanto, o exemplo abaixo ajuda você a excluir carregamentos iniciais.

$scope.$watch('fieldcontainer', 
  function(newValue, oldValue) {
    if (newValue && oldValue && newValue != oldValue) {
      // here what to do
    }
  }), true;
Tahsin Turkoz
fonte
Você provavelmente deseja usar! == desde que nulle undefinedcorresponderá a essa situação, ou ( '1' == 1etc) #
Jamie Pate
em geral, você está certo. Mas, nesse caso, newValue e oldValue não podem ser esses valores de canto devido às verificações anteriores. Ambos os valores têm o mesmo tipo também porque pertencem ao mesmo campo. Obrigado.
Tahsin Turkoz
3

Apenas valide o estado do novo valor:

$scope.$watch('fieldcontainer',function(newVal) {
      if(angular.isDefined(newVal)){
          //Do something
      }
});
Luis Saraza
fonte