AngularJS yoluna benzer şekilde @Input olarak alt bileşene açısal geri arama işlevi


228

AngularJS, direktifin geri çağrısını iletebileceğiniz & parametrelerine sahiptir (örn. AngularJS geri çağrıların yolu .) @InputBir Açısal Bileşen (aşağıdaki gibi bir şey) için geri çağrıyı iletmek mümkün mü ? AngularJS yapar mı?

@Component({
    selector: 'suggestion-menu',
    providers: [SuggestService],
    template: `
    <div (mousedown)="suggestionWasClicked(suggestion)">
    </div>`,
    changeDetection: ChangeDetectionStrategy.Default
})
export class SuggestionMenuComponent {
    @Input() callback: Function;

    suggestionWasClicked(clickedEntry: SomeModel): void {
        this.callback(clickedEntry, this.query);
    }
}


<suggestion-menu callback="insertSuggestion">
</suggestion-menu>

6
gelecekteki okuyucular için @Inputyol benim kod spagetti yapılan ve bakımı kolay değil .. @Outputs istediğim yapmak çok daha doğal bir yoludur. Sonuç olarak kabul edilen cevabı değiştirdim
Michail Michailidis

@IanS sorusu AngularJS'e benzer şekilde Angular'da bir şeylerin nasıl yapıldığı hakkında mı? başlık neden yanıltıcı?
Michail Michailidis

Açısal AngularJS'den çok farklıdır. Açısal 2+ sadece Açısaldır.
Ian S

1
Başlığınız düzeltildi;)
Ian S

1
@IanS Teşekkürler! şimdi soru olsa da angularJs hakkında - olsa eklediğiniz etiketi ile.
Michail Michailidis

Yanıtlar:


297

Bence bu kötü bir çözüm. Eğer bir Fonksiyon ile bileşene geçmek istiyorsanız @Input(), @Output()dekoratör aradığınız şeydir.

export class SuggestionMenuComponent {
    @Output() onSuggest: EventEmitter<any> = new EventEmitter();

    suggestionWasClicked(clickedEntry: SomeModel): void {
        this.onSuggest.emit([clickedEntry, this.query]);
    }
}

<suggestion-menu (onSuggest)="insertSuggestion($event[0],$event[1])">
</suggestion-menu>

45
Kesin olarak, işlevi geçmiyorsunuz, daha ziyade çıkışa bir dinleyici olay dinleyicisi bağlıyorsunuz. Neden çalıştığını anlamak için yararlı.
Jens

13
Bu harika bir yöntem, ama bu cevabı okuduktan sonra bir sürü sorum kaldı. Daha derinlemesine olacağını @Outputve açıklayan bir bağlantıya sahip olacağını umuyordum EventEmitter. İşte, ilgilenenler için @Output için Açısal belgeler .
WebWanderer

9
Bu tek yönlü ciltleme için iyidir. Çocuğun etkinliğine bağlanabilirsin. Ancak alt öğeye geri arama işlevi iletemez ve geri aramanın dönüş değerini analiz etmesine izin veremezsiniz. Aşağıdaki cevap buna izin verir.
kale

3
"Bunun kötü bir çözüm olduğunu düşünüyorum" yerine neden bir yolla diğerini tercih etmenin daha fazla açıklanacağını umuyorum.
Fidan Hakaj

6
Muhtemelen vakaların% 80'i için iyidir, ancak bir alt bileşen bir geri çağırma olup olmadığına bağlı olarak görselleştirme istediğinde değil.
John Freeman

117

GÜNCELLEME

Bu cevap, Açısal 2 hala alfadayken ve özelliklerin çoğu kullanılamadığında / belgelenmediğinde gönderilmiştir. Aşağıdakiler hala çalışacak olsa da, bu yöntem artık tamamen eski. Aşağıdaki cevapları kesinlikle tavsiye ediyorum .

Orijinal Yanıt

Evet, aslında, ancak doğru bir şekilde kapsamlandığından emin olmak isteyeceksiniz. Bunun için thisne istediğim anlamına gelir sağlamak için bir özellik kullandım .

@Component({
  ...
  template: '<child [myCallback]="theBoundCallback"></child>',
  directives: [ChildComponent]
})
export class ParentComponent{
  public theBoundCallback: Function;

  public ngOnInit(){
    this.theBoundCallback = this.theCallback.bind(this);
  }

  public theCallback(){
    ...
  }
}

@Component({...})
export class ChildComponent{
  //This will be bound to the ParentComponent.theCallback
  @Input()
  public myCallback: Function; 
  ...
}

1
Bu işe yaradı! Teşekkürler! Keşke belgeler bir yerde olsaydı :)
Michail Michailidis

1
İsterseniz statik bir yöntem kullanabilirsiniz, ancak daha sonra bileşenin örnek üyelerine erişiminiz olmazdı. Yani muhtemelen kullanım durumunuz değil. Ama evet, bunu da geçmeniz gerekecekParent -> Child
SnareChops

3
Mükemmel cevap! Gerçi bağlayıcı olsa işlev genellikle yeniden adlandırmayın. içinde ngOnInitsadece kullanırsınız: this.theCallback = this.theCallback.bind(this)ve sonra aktarabilirler theCallbackyerine theBoundCallback.
Zack,

1
@MichailMichailidis Evet, çözümünüze katılıyorum ve cevabımı insanları daha iyi bir şekilde yönlendirmek için bir notla güncelledim. Buna dikkat ettiğiniz için teşekkürler.
SnareChops

7
@Output ve EventEmitter tek yönlü ciltleme için uygundur. Çocuğun etkinliğine bağlanabilir, ancak çocuğa geri arama işlevi iletemez ve geri aramanın dönüş değerini analiz etmesine izin veremezsiniz. Bu cevap buna izin veriyor.
kale

31

SnareChops'un verdiği cevaba bir alternatif.

Aynı etkiyi elde etmek için şablonunuzdaki .bind (this) öğesini kullanabilirsiniz. O kadar temiz olmayabilir ama birkaç satır tasarruf sağlar. Şu anda açısal 2.4.0 kullanıyorum

@Component({
  ...
  template: '<child [myCallback]="theCallback.bind(this)"></child>',
  directives: [ChildComponent]
})
export class ParentComponent {

  public theCallback(){
    ...
  }
}

@Component({...})
export class ChildComponent{
  //This will be bound to the ParentComponent.theCallback
  @Input()
  public myCallback: Function; 
  ...
}

2
Diğerleri şablonda bind (this) ifadesini yorumladığından, ileride kullanımdan kaldırılacak / desteklenmeyecek şekilde hiçbir yerde belgelenmemiştir. Artı tekrar @Inputkod spagetti ve kullanma olmaya neden olan @Outputdaha doğal / untangled süreçte sonuçları
Michail Michailidis

1
Bind () öğesini şablona yerleştirdiğinizde, Angular bu ifadeyi her değişiklik algılamasında yeniden değerlendirir. Diğer çözüm - şablonun dışına bağlanma - daha az özlüdür, ancak bu sorunu yoktur.
Chris

soru: .bind (this) yaparken, çocuk veya üst ile bağlama yöntemi bağlama? Bence çocukla beraber. Ama mesele şu ki, bağ denildiğinde, her zaman onu çağıran çocuktur, bu yüzden doğru olduğumda bu bağ gerekli görünmüyor.
ChrisZ

Ana bileşenle bağlanır. Bunun nedeni, theCallBack () çağrıldığında, muhtemelen kendi içinde bir şey yapmak isteyecektir ve "bu" ana bileşen değilse, bağlamın dışında kalacaktır ve bu nedenle kendi yöntemlerine ve değişkenlerine ulaşamaz. artık.
Max Fahl

29

Bazı durumlarda, bir üst bileşen tarafından gerçekleştirilmesi için iş mantığına ihtiyacınız olabilir. Aşağıdaki örnekte, üst bileşen tarafından sağlanan mantığa bağlı olarak tablo satırını işleyen bir alt bileşenimiz var:

@Component({
  ...
  template: '<table-component [getRowColor]="getColor"></table-component>',
  directives: [TableComponent]
})
export class ParentComponent {

 // Pay attention on the way this function is declared. Using fat arrow (=>) declaration 
 // we can 'fixate' the context of `getColor` function
 // so that it is bound to ParentComponent as if .bind(this) was used.
 getColor = (row: Row) => {
    return this.fancyColorService.getUserFavoriteColor(row);
 }

}

@Component({...})
export class TableComponent{
  // This will be bound to the ParentComponent.getColor. 
  // I found this way of declaration a bit safer and convenient than just raw Function declaration
  @Input('getRowColor') getRowColor: (row: Row) => Color;

  renderRow(){
    ....
    // Notice that `getRowColor` function holds parent's context because of a fat arrow function used in the parent
    const color = this.getRowColor(row);
    renderRow(row, color);
  }
}

Burada 2 şey göstermek istedim:

  1. Doğru bağlamı tutmak için .bind (this) yerine yağ oku (=>) işlev görür;
  2. Alt bileşende geri çağırma işlevinin normal bildirimi.

1
Yağ okunun kullanımının yerini almak için harika bir açıklama.bind(this)
TYMG

6
Kullanım ipucu: koymak emin olun [getRowColor]="getColor"ve [getRowColor]="getColor()";-)
Simon_Weaver

Güzel. Tam da aradığım şey buydu. Basit ve etkili.
BrainSlugs83

7

Örnek olarak, kalıcı pencerenin üst öğe olduğu, giriş formunun alt öğe olduğu ve oturum açma düğmesinin kalıcı üst öğenin kapatma işlevini geri çağırdığı bir oturum açma yöntemi penceresi kullanıyorum.

Ana mod, modun kapatılması işlevini içerir. Bu üst öğe close işlevini login alt bileşenine iletir.

import { Component} from '@angular/core';
import { LoginFormComponent } from './login-form.component'

@Component({
  selector: 'my-modal',
  template: `<modal #modal>
      <login-form (onClose)="onClose($event)" ></login-form>
    </modal>`
})
export class ParentModalComponent {
  modal: {...};

  onClose() {
    this.modal.close();
  }
}

Alt oturum açma bileşeni oturum açma formunu gönderdikten sonra, üst öğenin geri arama işlevini kullanarak ana yöntemi kapatır

import { Component, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'login-form',
  template: `<form (ngSubmit)="onSubmit()" #loginForm="ngForm">
      <button type="submit">Submit</button>
    </form>`
})
export class ChildLoginComponent {
  @Output() onClose = new EventEmitter();
  submitted = false;

  onSubmit() {
    this.onClose.emit();
    this.submitted = true;
  }
}

7

Max Fahl'ın verdiği cevaba bir alternatif.

Geri çağırma işlevini, bağlamanız gerekmeyecek şekilde ana bileşendeki bir ok işlevi olarak tanımlayabilirsiniz.

@Component({
  ...
  // unlike this, template: '<child [myCallback]="theCallback.bind(this)"></child>',
  template: '<child [myCallback]="theCallback"></child>',
  directives: [ChildComponent]
})
export class ParentComponent {

   // unlike this, public theCallback(){
   public theCallback = () => {
    ...
  }
}

@Component({...})
export class ChildComponent{
  //This will be bound to the ParentComponent.theCallback
  @Input()
  public myCallback: Function; 
  ...
}


5

Şablonda .bind kullanarak bağımsız değişkenli yöntem geçirme

@Component({
  ...
  template: '<child [action]="foo.bind(this, 'someArgument')"></child>',
  ...
})
export class ParentComponent {
  public foo(someParameter: string){
    ...
  }
}

@Component({...})
export class ChildComponent{

  @Input()
  public action: Function; 

  ...
}

Cevabınız aslında bununla aynı değil: stackoverflow.com/a/42131227/986160 ?
Michail Michailidis

Bu yoruma cevap
veriyor


0

Başka bir alternatif.

OP bir geri arama kullanmanın yolunu sordu. Bu durumda özellikle @serginho tarafından kabul edilen cevap olarak kabul edilecek bir olayı (örneğinde: bir tıklama olayını) işleyen bir işleve atıfta bulundu: @Outputve ile EventEmitter.

Bununla birlikte, geri arama ve olay arasında bir fark vardır: Geri arama ile alt bileşeniniz, üst öğeden bazı geri bildirimler veya bilgiler alabilir, ancak bir etkinlik yalnızca bir şeyin herhangi bir geri bildirim beklemeden olduğunu bildirebilir.

Geri bildirimin gerekli olduğu kullanım durumları vardır, ör. bir renk veya bileşenin işlemesi gereken öğelerin bir listesini edinin. Bazı yanıtların önerdiği gibi ilişkili işlevleri kullanabilirsiniz veya arabirimleri kullanabilirsiniz (bu her zaman benim tercihimdir).

Misal

Diyelim ki bu alanlara sahip tüm veritabanı tablolarınızla kullanmak istediğiniz {id, name} öğelerinin bir listesi üzerinde çalışan genel bir bileşene sahipsiniz. Bu bileşen:

  • bir dizi öğeyi (sayfa) alma ve bunları bir listede gösterme
  • bir öğenin kaldırılmasına izin ver
  • bir öğenin tıklandığını bildirir, böylece üst öğe bazı işlemler yapabilir.
  • öğelerin sonraki sayfasını almayı sağlar.

Alt Bileşen

Normal bağlanma kullanıldığında 1 @Input()ve 3 @Output()parametreye ihtiyacımız olurdu (ancak ana öğeden geri bildirim alınmadan). Ör. <list-ctrl [items]="list" (itemClicked)="click($event)" (itemRemoved)="removeItem($event)" (loadNextPage)="load($event)" ...>, ancak bir arayüz oluşturmak için yalnızca bir taneye ihtiyacımız olacak @Input():

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

export interface IdName{
  id: number;
  name: string;
}

export interface IListComponentCallback<T extends IdName> {
    getList(page: number, limit: number): Promise< T[] >;
    removeItem(item: T): Promise<boolean>;
    click(item: T): void;
}

@Component({
    selector: 'list-ctrl',
    template: `
      <button class="item" (click)="loadMore()">Load page {{page+1}}</button>
      <div class="item" *ngFor="let item of list">
          <button (click)="onDel(item)">DEL</button>
          <div (click)="onClick(item)">
            Id: {{item.id}}, Name: "{{item.name}}"
          </div>
      </div>
    `,
    styles: [`
      .item{ margin: -1px .25rem 0; border: 1px solid #888; padding: .5rem; width: 100%; cursor:pointer; }
      .item > button{ float: right; }
      button.item{margin:.25rem;}
    `]
})
export class ListComponent implements OnInit {
    @Input() callback: IListComponentCallback<IdName>; // <-- CALLBACK
    list: IdName[];
    page = -1; 
    limit = 10;

    async ngOnInit() {
      this.loadMore();
    }
    onClick(item: IdName) {
      this.callback.click(item);   
    }
    async onDel(item: IdName){ 
        if(await this.callback.removeItem(item)) {
          const i = this.list.findIndex(i=>i.id == item.id);
          this.list.splice(i, 1);
        }
    }
    async loadMore(){
      this.page++;
      this.list = await this.callback.getList(this.page, this.limit); 
    }
}

Üst Bileşen

Artık üst öğedeki liste bileşenini kullanabiliriz.

import { Component } from "@angular/core";
import { SuggestionService } from "./suggestion.service";
import { IdName, IListComponentCallback } from "./list.component";

type Suggestion = IdName;

@Component({
  selector: "my-app",
  template: `
    <list-ctrl class="left" [callback]="this"></list-ctrl>
    <div class="right" *ngIf="msg">{{ msg }}<br/><pre>{{item|json}}</pre></div>
  `,
  styles:[`
    .left{ width: 50%; }
    .left,.right{ color: blue; display: inline-block; vertical-align: top}
    .right{max-width:50%;overflow-x:scroll;padding-left:1rem}
  `]
})
export class ParentComponent implements IListComponentCallback<Suggestion> {
  msg: string;
  item: Suggestion;

  constructor(private suggApi: SuggestionService) {}

  getList(page: number, limit: number): Promise<Suggestion[]> {
    return this.suggApi.getSuggestions(page, limit);
  }
  removeItem(item: Suggestion): Promise<boolean> {
    return this.suggApi.removeSuggestion(item.id)
      .then(() => {
        this.showMessage('removed', item);
        return true;
      })
      .catch(() => false);
  }
  click(item: Suggestion): void {
    this.showMessage('clicked', item);
  }
  private showMessage(msg: string, item: Suggestion) {
    this.item = item;
    this.msg = 'last ' + msg;
  }
}

Not <list-ctrl>alır thisgeri nesnesi olarak (ana bileşen). Ek bir avantaj, üst örneği göndermenin gerekli olmamasıdır, kullanım durumunuz izin veriyorsa, bir hizmet veya arabirimi uygulayan herhangi bir nesne olabilir.

Bunun tam örneği bu stackblitz'de .


-3

Mevcut cevap basitleştirilebilir ...

@Component({
  ...
  template: '<child [myCallback]="theCallback"></child>',
  directives: [ChildComponent]
})
export class ParentComponent{
  public theCallback(){
    ...
  }
}

@Component({...})
export class ChildComponent{
  //This will be bound to the ParentComponent.theCallback
  @Input()
  public myCallback: Function; 
  ...
}

yani açıkça bağlanmaya gerek yok mu?
Michail Michailidis

3
Without .bind(this)sonra thiscallback'inde iç olacak windowolursa olsun kullanım durumuna bağlı olarak kısıtlayıcı önlemler alabilir. Ancak thisgeri aramada hiç varsa , o .bind(this)zaman gereklidir. Bunu yapmazsanız, bu basitleştirilmiş sürüm gitmenin yoludur.
SnareChops

3
Geri aramayı her zaman bileşenle bağlamanızı öneririm, çünkü eninde sonunda thisgeri arama işlevinin içinde kullanacaksınız . Sadece hataya yatkın.
Alexandre Junges

Bu Açısal 2 antipatternine bir örnektir.
Serginho

Bir anti-desen olması gerekmez. Tam olarak bunu istediğiniz durumlar var. Bileşene, görünümle ilgili olmayan bir şeyi NASIL yapmasını söylemek nadir değildir. Bu mantıklı ve bu cevabın neden bu kadar nefret ettiğini anlamıyorum.
Lazar Ljubenović
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.