ExpressionChangedAfterItHasBeenCheckedError Explained


308

Explique-me por que continuo recebendo este erro: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.

Obviamente, eu só o obtenho no modo dev, isso não acontece na minha produção, mas é muito chato e simplesmente não entendo os benefícios de ter um erro no meu ambiente dev que não aparece no produto - Provavelmente por causa da minha falta de entendimento.

Normalmente, a correção é fácil, apenas envolvo o erro que causa o código em um setTimeout como este:

setTimeout(()=> {
    this.isLoading = true;
}, 0);

Ou force a detecção de alterações com um construtor como este constructor(private cd: ChangeDetectorRef) {}:

this.isLoading = true;
this.cd.detectChanges();

Mas por que constantemente encontro esse erro? Quero entender para evitar essas correções hacky no futuro.


Respostas:


121

Eu tive uma questão semelhante. Observando a documentação dos ganchos do ciclo de vida , mudei ngAfterViewInitpara ngAfterContentInite funcionou.


@ PhilipEnc, meu problema estava relacionado a uma alteração acionada pela alteração do DOM. Quando o DOM mudava, o objeto QueryList (que vinha de uma propriedade @ContentChildren) era atualizado e, dentro do método que a atualização chamava, alterava a propriedade vinculada bidirecional. Isso criou o problema que eu estava tendo. A quebra dessa alteração nas duas propriedades, setTimeoutcomo você mostrou acima, fez o truque. Obrigado!
Kbpontius

1
No meu caso, eu havia colocado algum código que alterava o valor da matriz de grade primeng em ngAfterContentInit, coloquei o código em ngOnInit e funcionou.
Vibhu

ngAfterContentCheckedfunciona aqui enquanto ngAfterContentInitainda gera erro.
ashubuntu 3/03/19

ngAfterContentChecked use, mas projeto carregado muito lentamente
Ghotekar Rahul 17/02

101

Este erro indica um problema real no seu aplicativo, portanto, faz sentido lançar uma exceção.

Na devModedetecção de alterações, é adicionada uma volta adicional após cada execução regular de detecção de alterações para verificar se o modelo foi alterado.

Se o modelo mudou entre o turno regular e o turno adicional de detecção de alterações, isso indica que

  • a própria detecção de alterações causou uma mudança
  • um método ou método retorna um valor diferente toda vez que é chamado

que são ruins, porque não está claro como proceder, pois o modelo pode nunca se estabilizar.

Se as execuções angulares alterarem a detecção até o modelo estabilizar, ele poderá ser executado para sempre. Se o Angular não executar a detecção de alterações, a visualização poderá não refletir o estado atual do modelo.

Consulte também Qual é a diferença entre o modo de produção e desenvolvimento no Angular2?


4
Como posso evitar esse erro no futuro? Existe uma maneira diferente de pensar no meu código para garantir que eu não cometa os mesmos erros?
precisa saber é o seguinte

25
Geralmente, isso é causado por alguns retornos de chamada do ciclo de vida como ngOnInitou ngOnChangespara modificar o modelo (alguns retornos de chamada do ciclo de vida permitem modificar o modelo que outros não, não me lembro exatamente de qual deles faz ou não). Não vincule a métodos ou funções na exibição; em vez disso, vincule a campos e atualize os campos nos manipuladores de eventos. Se você deve ligar-se aos métodos, verifique se eles sempre retornam a mesma instância de valor, desde que não haja realmente uma alteração. A detecção de alterações chamará muito esses métodos.
Günter Zöchbauer

Para quem chega aqui que recebe esse erro usando a biblioteca ngx-toaster, aqui está o relatório de erro: github.com/scttcper/ngx-toastr/issues/160
rmcsharry

2
Não é necessariamente um problema com o aplicativo. O fato de chamar changeRef.detectChanges()é uma solução / suprime o erro, é uma prova disso. É como modificando dentro estado $scope.$watch()em angular 1.
Kevin Beal

1
Não conheço muito bem o Angular 1, mas a detecção de alterações no Angular 2 funciona de maneira bem diferente. Você está certo de que isso não é necessariamente um problema, mas geralmente cdRef.detectChanges()é necessário apenas em alguns casos estranhos e você deve olhar atentamente quando precisar, para entender corretamente o porquê.
Günter Zöchbauer 6/11

83

Muita compreensão veio depois que eu entendi os Ganchos do Ciclo de Vida Angular e sua relação com a detecção de alterações.

Eu estava tentando fazer com que o Angular atualizasse um sinalizador global vinculado ao *ngIfde um elemento e estava tentando alterar esse sinalizador dentro do ngOnInit()gancho do ciclo de vida de outro componente.

De acordo com a documentação, esse método é chamado depois que o Angular já detectou alterações:

Chamado uma vez, após o primeiro ngOnChanges ().

Portanto, atualizar o sinalizador dentro de ngOnChanges()não iniciará a detecção de alterações. Então, quando a detecção de alterações for acionada novamente, o valor do sinalizador será alterado e o erro será gerado.

No meu caso, mudei isso:

constructor(private globalEventsService: GlobalEventsService) {

}

ngOnInit() {
    this.globalEventsService.showCheckoutHeader = true;
}

Para isso:

constructor(private globalEventsService: GlobalEventsService) {
    this.globalEventsService.showCheckoutHeader = true;
}

ngOnInit() {

}

e resolveu o problema :)


3
Meu problema foi semelhante. Que cometi um erro após longas horas e defini uma variável fora da função e construtor ngOnInit. Isso recebe alterações de dados de um observável, que é colocado na função de inicialização. Fez a mesma coisa que você para corrigir o erro.
Rave10 13/02/19

1
Muito semelhante ao redor, mas eu estava tentando rolar ( router.navigate) após o carregamento para um fragmento, se presente no URL. Esse código foi inicialmente colocado no AfterViewInitlocal em que eu estava recebendo o erro, depois mudei como você diz para o construtor, mas não respeitava o fragmento. Movendo-se para ngOnInitresolvido :) obrigado!
Joel Balmer

E se meu html estiver vinculado a um tempo de retorno de getter como "HH: MM" via get ClockValue () {return DateTime.TimeAMPM (new Date ())}, ele eventualmente desarmará quando os minutos mudarem enquanto a detecção estiver em execução, como posso conserte isso?
Meryan

O mesmo aqui. Também constatou que a quebra de linha setInterval()também funciona se precisar disparar após outro código de evento vitalício.
Rick Strahl

39

Atualizar

Eu recomendo começar primeiro com a auto-resposta do OP : pense corretamente no que pode ser feito no constructorvs, no que deve ser feito ngOnChanges().

Original

Esta é mais uma observação do que uma resposta, mas pode ajudar alguém. Eu me deparei com esse problema ao tentar fazer a presença de um botão depender do estado do formulário:

<button *ngIf="form.pristine">Yo</button>

Até onde eu sei, essa sintaxe leva o botão a ser adicionado e removido do DOM com base na condição. O que por sua vez leva ao ExpressionChangedAfterItHasBeenCheckedError.

A correção no meu caso (embora eu não pretenda entender todas as implicações da diferença), era usar display: none:

<button [style.display]="form.pristine ? 'inline' : 'none'">Yo</button>

6
Meu entendimento da diferença entre o ngIf e o estilo é que o ngIf não inclui o HTML na página até que a condição seja verdadeira, reduzindo assim o "peso da página" um pouquinho, enquanto a técnica de estilo faz com que o HTML sempre seja na página e é simplesmente oculto ou mostrado com base no valor de form.pristine.
precisa saber é o seguinte

4
Você também pode usar em [hidden]vez da parte muito detalhada [style.display]. :)
Philipp Meissner

2
Por que não. Embora, como mencionado por @Simon_Weaver em outro comentário nesta página, [hidden] não vai ter sempre o mesmo comportamento comodisplay: none
Arnaud P

1
Eu estava mostrando dois botões diferentes (logout / login) com * ngIf em cada botão e isso estava causando o problema.
GoTo 7/10

construtor foi o lugar certo para mim, o lançamento de um material Snack-Bar
Austin

31

Havia respostas interessantes, mas não achei uma que atendesse às minhas necessidades, a mais próxima de @ chittrang-mishra, que se refere apenas a uma função específica e não a várias alternâncias, como no meu aplicativo.

Eu não queria usar [hidden]a vantagem de *ngIfnem fazer parte do DOM, então encontrei a solução a seguir, que pode não ser a melhor para todos, pois suprime o erro em vez de corrigi-lo, mas no meu caso, onde eu conheço o resultado final está correto, parece ok para o meu aplicativo.

O que eu fiz foi implementar AfterViewChecked, adicionar constructor(private changeDetector : ChangeDetectorRef ) {}e depois

ngAfterViewChecked(){
  this.changeDetector.detectChanges();
}

Espero que isso ajude outros, como muitos outros me ajudaram.


3
isso não acionará um loop infinito de detecção de alterações? Quero dizer, você está detectando alterações depois de verificar.
Manuel Azar

@ManuelAzar Aparentemente não. Esta é a única solução que funcionou para mim. Pelo menos um pouco de silêncio no meu console. Eu estava tão cansado de todos esses "erros" irrelevantes de detecção de alterações.
Jeremy Thille

31

O Angular executa a detecção de alterações e, quando descobrir que alguns valores que foram passados ​​para o componente filho foram alterados, o Angular lança o erro:

ExpressionChangedAfterItHasBeenCheckedError clique para mais

Para corrigir isso, podemos usar o AfterContentCheckedgancho do ciclo de vida e

import { ChangeDetectorRef, AfterContentChecked} from '@angular/core';

  constructor(
  private cdref: ChangeDetectorRef) { }

  ngAfterContentChecked() {

    this.cdref.detectChanges();

  }

Embora isso possa resolver o problema, isso não é muito extenso e exagerado no CD?
Nicky

Eu acho que essa é a única resposta que soluciona esse erro causado pela passagem de valores para uma criança. Obrigado!
java-addict301

@Nicky Sim. Toda vez que você toca na tela, em qualquer lugar, ngAfterContentChecked () é chamado
Mert Mertce

25

No meu caso, tive esse problema no meu arquivo de especificação, enquanto executava meus testes.

Eu tive que mudar ngIf para [hidden]

<app-loading *ngIf="isLoading"></app-loading>

para

<app-loading [hidden]="!isLoading"></app-loading>


2
A diferença aqui é que *ngIfaltera o DOM, adicionando e removendo o elemento da página, enquanto [hidden]altera a visibilidade do item, sem removê-lo do DOM.
Grungondola 30/10

5
Mas, isso realmente não resolveu o problema real ...?
Rafael13

23

Siga os passos abaixo:

1. Use 'ChangeDetectorRef' importando-o de @ angular / core da seguinte maneira:

import{ ChangeDetectorRef } from '@angular/core';

2. Implemente-o no construtor () da seguinte maneira:

constructor(   private cdRef : ChangeDetectorRef  ) {}

3. Adicione à sua função o seguinte método que você está chamando em um evento, como o clique do botão. Então fica assim:

functionName() {   
    yourCode;  
    //add this line to get rid of the error  
    this.cdRef.detectChanges();     
}

23

Eu estava usando ng2-carouselamos (Angular 8 e Bootstrap 4)

Abaixo corrigi meu problema:

O que eu fiz:

1. implement AfterViewChecked,  
2. add constructor(private changeDetector : ChangeDetectorRef ) {} and then 
3. ngAfterViewChecked(){ this.changeDetector.detectChanges(); }

Ajudou. Surpreendente!!
Pathik Vejani

Você salvou meu dia ... Obrigado!
omostan 14/03

19

Eu estava enfrentando o mesmo problema que o valor estava mudando em uma das matrizes no meu componente. Mas, em vez de detectar as alterações na alteração de valor, mudei a estratégia de detecção de alteração de componente para onPush(que detectará alterações na alteração de objeto e não na alteração de valor).

import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';

@Component({
    changeDetection: ChangeDetectionStrategy.OnPush
    selector: -
    ......
})

Isso pareceu funcionar para adicionar / remover dinamicamente controles. Existem desvantagens?
21419 Ricardo Saracino

Funciona como um encanto na situação que tenho em mãos, obrigado! Um componente foi vinculado a um objeto "global" que foi alterado em outro lugar e causou o erro. Esse componente já tinha um manipulador de atualização para quando o objeto vinculado foi atualizado, agora esse manipulador de eventos chama changeDetectorRef.detectChanges () em combinação com o ChangeDetectionStrategy.OnPush, isso funciona como desejado sem o erro.
Bernoulli IT

@RicardoSaracino, você encontrou algum inconveniente? Eu estava pensando a mesma coisa. Eu sei como o OnPush de detecção de alterações funciona, mas me pergunto se há uma pegadinha que talvez esteja faltando. Eu não quero ter que voltar.
mtpultz

@ RicardoSaracino, Sim, tem algumas desvantagens, você pode consultar este link detalhado blog.angular-university.io/onpush-change-detection-how-it-works
Dheeraj

@BernoulliIT Obrigado, estou feliz que funcionou para você.
Dheeraj 15/01

17

Referindo-se ao artigo https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4

Portanto, a mecânica por trás da detecção de alterações realmente funciona de maneira que os resumos de detecção e verificação de alterações sejam executados de forma síncrona. Isso significa que, se atualizarmos as propriedades de forma assíncrona, os valores não serão atualizados quando o loop de verificação estiver em execução e não obteremos ExpressionChanged...erros. A razão pela qual obtemos esse erro é que, durante o processo de verificação, o Angular vê valores diferentes dos registrados na fase de detecção de alterações. Então, para evitar isso ....

1) Use changeDetectorRef

2) use setTimeOut. Isso executará seu código em outra VM como uma tarefa macro. A Angular não verá essas alterações durante o processo de verificação e você não receberá esse erro.

 setTimeout(() => {
        this.isLoading = true;
    });

3) Se você realmente deseja executar seu código na mesma VM, use como

Promise.resolve(null).then(() => this.isLoading = true);

Isso criará uma micro-tarefa. A fila de micro-tarefas é processada após a conclusão do código síncrono atual, portanto, a atualização da propriedade ocorrerá após a etapa de verificação.


Você pode usar a opção 3 com uma expressão de estilo? Eu tenho uma expressão de estilo para altura que deve ser avaliada por último, porque é baseada no conteúdo injetado.
N-ate

1
Desculpe, acabei de ver seu comentário, sim, eu não vejo nenhuma razão para não. Portanto, isso também deve funcionar com mudanças de estilo.
ATHER

4

@HostBinding pode ser uma fonte confusa desse erro.

Por exemplo, digamos que você tenha a seguinte ligação de host em um componente

// image-carousel.component.ts
@HostBinding('style.background') 
style_groupBG: string;

Para simplificar, digamos que essa propriedade seja atualizada através da seguinte propriedade de entrada:

@Input('carouselConfig')
public set carouselConfig(carouselConfig: string) 
{
    this.style_groupBG = carouselConfig.bgColor;   
}

No componente pai, você o está definindo programaticamente ngAfterViewInit

@ViewChild(ImageCarousel) carousel: ImageCarousel;

ngAfterViewInit()
{
    this.carousel.carouselConfig = { bgColor: 'red' };
}

Aqui está o que acontece:

  • Seu componente pai é criado
  • O componente ImageCarousel é criado e atribuído a carousel(via ViewChild)
  • Não podemos acessar carouselaté ngAfterViewInit()(será nulo)
  • Atribuímos a configuração, que define style_groupBG = 'red'
  • Isso, por sua vez, define background: redo componente ImageCarousel do host
  • Esse componente é 'de propriedade' do seu componente pai; portanto, quando ele verifica alterações, encontra uma alteração carousel.style.backgrounde não é inteligente o suficiente para saber que isso não é um problema, e lança a exceção.

Uma solução é introduzir outro ImageCarousel, um div de wrapper, e definir a cor do plano de fundo, mas você não obtém alguns dos benefícios de usar HostBinding(como permitir que o pai controle os limites completos do objeto).

A melhor solução, no componente pai, é adicionar detectChanges () depois de definir a configuração.

ngAfterViewInit()
{
    this.carousel.carouselConfig = { ... };
    this.cdr.detectChanges();
}

Isso pode parecer bastante óbvio assim, e muito semelhante a outras respostas, mas há uma diferença sutil.

Considere o caso em que você não adiciona @HostBindingaté mais tarde durante o desenvolvimento. De repente, você recebe esse erro e parece não fazer sentido.


2

Aqui estão os meus pensamentos sobre o que está acontecendo. Não li a documentação, mas tenho certeza de que isso faz parte do motivo pelo qual o erro é mostrado.

*ngIf="isProcessing()" 

Ao usar * ngIf, ele altera fisicamente o DOM adicionando ou removendo o elemento toda vez que a condição é alterada. Portanto, se a condição mudar antes de ser renderizada na visualização (o que é altamente possível no mundo do Angular), o erro será gerado. Veja a explicação aqui entre os modos de desenvolvimento e produção.

[hidden]="isProcessing()"

Ao usá- [hidden]lo, não altera fisicamente o, DOMmas apenas o oculta elementda vista, provavelmente usando CSSna parte de trás. O elemento ainda está lá no DOM, mas não é visível, dependendo do valor da condição. É por isso que o erro não ocorrerá ao usar [hidden].


Se isProcessing()está fazendo a coisa certa, você deve usar !isProcessing()para o[hidden]
Matthieu Charbonnier

hiddennão "usa CSS nas costas", é uma propriedade HTML comum. developer.mozilla.org/pt-BR/docs/Web/HTML/Global_attributes/…
Lazar Ljubenović

1

Para o meu problema, eu estava lendo o github - "ExpressionChangedAfterItHasBeenCheckedError ao alterar o valor 'não modelo' de um componente no afterViewInit" e decidi adicionar o ngModel

<input type="hidden" ngModel #clientName />

Corrigiu o meu problema, espero que ajude alguém.


1
Onde nesse site diz para adicionar ngModel. E você poderia explicar por que isso deve ser útil?
Peter Wippermann

Quando eu estava rastreando esse problema, ele me levou a investigar o link. Depois de ler o artigo, adicionei o atributo e ele corrigiu o meu problema. É útil se alguém tiver o mesmo problema.
Demodave 23/07/19

1

Dicas de depuração

Este erro pode ser bastante confuso e é fácil fazer uma suposição errada sobre exatamente quando está ocorrendo. Acho útil adicionar muitas instruções de depuração como essa nos componentes afetados nos locais apropriados. Isso ajuda a entender o fluxo.

No pai, coloque instruções como esta (a string exata 'EXPRESSIONCHANGED' é importante), mas, além disso, são apenas exemplos:

    console.log('EXPRESSIONCHANGED - HomePageComponent: constructor');
    console.log('EXPRESSIONCHANGED - HomePageComponent: setting config', newConfig);
    console.log('EXPRESSIONCHANGED - HomePageComponent: setting config ok');
    console.log('EXPRESSIONCHANGED - HomePageComponent: running detectchanges');

Nos retornos de chamada filho / serviços / timer:

    console.log('EXPRESSIONCHANGED - ChildComponent: setting config');
    console.log('EXPRESSIONCHANGED - ChildComponent: setting config ok');

Se você executar detectChangesmanualmente, adicione o log para isso também:

    console.log('EXPRESSIONCHANGED - ChildComponent: running detectchanges');
    this.cdr.detectChanges();

Em seguida, no depurador do Chrome, basta filtrar por 'EXPRESSIONCHANGES'. Isso mostrará exatamente o fluxo e a ordem de tudo o que é definido, e também exatamente em que ponto o Angular lança o erro.

insira a descrição da imagem aqui

Você também pode clicar nos links cinza para inserir pontos de interrupção.

Outra coisa a se observar se você tiver nomeado propriedades semelhantes em todo o aplicativo (como style.background), verifique se está depurando o que você pensa - configurando-o para um valor de cor obscuro.


1

No meu caso, eu tinha uma propriedade assíncrona em LoadingService com um BehavioralSubjectisLoading

O uso do modelo [oculto] funciona, mas * ngIf falha

    <h1 [hidden]="!(loaderService.isLoading | async)">
        THIS WORKS FINE
        (Loading Data)
    </h1>

    <h1 *ngIf="!(loaderService.isLoading | async)">
        THIS THROWS ERROR
        (Loading Data)
    </h1>

1

Uma solução que funcionou para mim usando rxjs

import { startWith, tap, delay } from 'rxjs/operators';

// Data field used to populate on the html
dataSource: any;

....

ngAfterViewInit() {
  this.yourAsyncData.
      .pipe(
          startWith(null),
          delay(0),
          tap((res) => this.dataSource = res)
      ).subscribe();
}

qual foi o código problemático? qual é a solução aqui?
mkb 28/02

Oi @mkb o problema ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.aconteceu quando as alterações de valor são acionadas quando o DOM muda
Sandeep K Nair

Olá, quero dizer o que você fez aqui para superar o problema. Você não usou rxjs antes ou adicionou delay () ou adicionou startWith ()? Eu já uso rxjs com vários métodos rxjs, mas ainda assim recebo o erro, espero resolver o mistério :(
mkb

Os adicionados delayfazem o erro desaparecer. Funciona de maneira semelhante a setTimeout.
Lazar Ljubenović 30/03

1

Eu tive esse tipo de erro no Ionic3 (que usa o Angular 4 como parte de sua pilha de tecnologia).

Para mim, estava fazendo isso:

<ion-icon [name]="getFavIconName()"></ion-icon>

Então, eu estava tentando alterar condicionalmente o tipo de ícone de íon de a pinpara a remove-circle, de acordo com o modo em que a tela estava operando.

Acho que vou ter que adicionar um *ngIf.


1

Meu problema foi manifesto quando adicionei, *ngIfmas essa não foi a causa. O erro foi causado pela alteração do modelo nas {{}}tags e, em seguida, pela tentativa de exibir o modelo alterado na *ngIfinstrução. Aqui está um exemplo:

<div>{{changeMyModelValue()}}</div> <!--don't do this!  or you could get error: ExpressionChangedAfterItHasBeenCheckedError-->
....
<div *ngIf="true">{{myModel.value}}</div>

Para corrigir o problema, mudei de local changeMyModelValue()para onde fazia mais sentido.

Na minha situação, eu queria changeMyModelValue()ligar sempre que um componente filho alterava os dados. Isso exigiu que eu criasse e emitisse um evento no componente filho para que o pai pudesse lidar com ele (chamando changeMyModelValue(). Consulte https://angular.io/guide/component-interaction#parent-listens-for-child-event


0

Espero que isso ajude alguém vindo aqui: Fazemos chamadas de serviço ngOnInitda seguinte maneira e usamos uma variável displayMainpara controlar a montagem dos elementos no DOM.

component.ts

  displayMain: boolean;
  ngOnInit() {
    this.displayMain = false;
    // Service Calls go here
    // Service Call 1
    // Service Call 2
    // ...
    this.displayMain = true;
  }

e component.html

<div *ngIf="displayMain"> <!-- This is the Root Element -->
 <!-- All the HTML Goes here -->
</div>

0

Eu recebi esse erro porque estava usando uma variável em component.html que não foi declarada em component.ts. Depois que eu removi a peça em HTML, esse erro se foi.


0

Eu recebi esse erro porque estava despachando ações de redux no modal e o modal não foi aberto naquele momento. Eu estava despachando ações no momento em que o componente modal recebe entrada. Então, coloquei setTimeout lá para garantir que o modal seja aberto e as ações sejam corrigidas.


0

Para quem está lutando com isso. Aqui está uma maneira de depurar corretamente esse erro: https://blog.angular-university.io/angular-debugging/

No meu caso, na verdade, me livrei desse erro usando esse hack [oculto] em vez de * ngIf ...

Mas o link que eu forneci me permitiu encontrar THE GUILTY * ngIf :)

Aproveitar.


Usar em hiddenvez de ngIfnão é um hack, nem aborda o núcleo do problema. Você está apenas escondendo o problema.
Lazar Ljubenović 30/03

-2

A solução ... services e rxjs ... emissores de eventos e associação de propriedades usam o rxjs .. é melhor implementá-lo por conta própria, mais controle e mais fácil de depurar. Lembre-se de que os emissores de eventos estão usando rxjs. Simplesmente, crie um serviço e, dentro de um observável, faça com que cada componente assine o observador e passe novo valor ou valor de consumo, conforme necessário


1
Além de não responder à pergunta, também é um péssimo conselho. Gente, por favor, não reimplemente o rxjs sozinho, porque você está recebendo os erros de CD do Angular. :)
Lazar Ljubenović
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.