آنلاین

Content projection با ng-content

اغلب لازم است componentهایی بسازید که مثل container برای انواع مختلف محتوا عمل کنند. مثلا ممکن است بخواهید یک component کارت سفارشی بسازید:

ts
@Component({
  selector: 'custom-card',
  template: '<div class="card-shadow"> <!-- card content goes here --> </div>',
})
export class CustomCard {
  /* ... */
}

می‌توانید از element مربوط به <ng-content> به‌عنوان placeholder استفاده کنید تا مشخص کنید محتوا باید کجا قرار بگیرد:

ts
@Component({
  selector: 'custom-card',
  template: '<div class="card-shadow"> <ng-content/> </div>',
})
export class CustomCard {
  /* ... */
}

وقتی componentی با <ng-content> استفاده می‌کنید، هر child مربوط به host element آن component در محل همان <ng-content> render یا project می‌شود:

ts
// Component source
@Component({
  selector: 'custom-card',
  template: `
    <div class="card-shadow">
      <ng-content />
    </div>
  `,
})
export class CustomCard {
  /* ... */
}
html
<!-- Using the component -->
<custom-card>
  <p>This is the projected content</p>
</custom-card>
html
<!-- The rendered DOM -->
<custom-card>
  <div class="card-shadow">
    <p>This is the projected content</p>
  </div>
</custom-card>

Angular به هر child که به این شکل به یک component پاس داده شود content آن component می‌گوید. این با view component فرق دارد؛ view به elementهایی اشاره می‌کند که در template خود component تعریف شده‌اند.

element مربوط به <ng-content> نه component است و نه element مربوط به DOM. در عوض، یک placeholder ویژه است که به Angular می‌گوید محتوا را کجا render کند. compiler Angular همه elementهای <ng-content> را در build-time پردازش می‌کند. نمی‌توانید در runtime یک <ng-content> را insert، remove یا modify کنید. همچنین نمی‌توانید directive، style یا attribute دلخواه به <ng-content> اضافه کنید.

چند placeholder برای content

Angular از project کردن چند element متفاوت در placeholderهای متفاوت <ng-content> بر اساس CSS selector پشتیبانی می‌کند. اگر مثال کارت بالا را گسترش دهید، می‌توانید با استفاده از attribute مربوط به select، دو placeholder برای عنوان کارت و بدنه کارت بسازید:

ts
@Component({
  selector: 'card-title',
  template: `<ng-content>card-title</ng-content>`,
})
export class CardTitle {}

@Component({
  selector: 'card-body',
  template: `<ng-content>card-body</ng-content>`,
})
export class CardBody {}
ts
<!-- Component template -->
@Component({
  selector: 'custom-card',
  template: `
  <div class="card-shadow">
    <ng-content select="card-title" />
    <div class="card-divider"></div>
    <ng-content select="card-body" />
  </div>
  `,
})
export class CustomCard {}
ts
<!-- Using the component -->
@Component({
  selector: 'app-root',
  imports: [CustomCard, CardTitle, CardBody],
  template: `
    <custom-card>
      <card-title>Hello</card-title>
      <card-body>Welcome to the example</card-body>
    </custom-card>
`,
})
export class App {}
html
<!-- Rendered DOM -->
<custom-card>
  <div class="card-shadow">
    <card-title>Hello</card-title>
    <div class="card-divider"></div>
    <card-body>Welcome to the example</card-body>
  </div>
</custom-card>

placeholder مربوط به <ng-content> از همان CSS selectorهایی پشتیبانی می‌کند که selectorهای component پشتیبانی می‌کنند.

اگر یک یا چند placeholder از نوع <ng-content> با attribute مربوط به select داشته باشید و یک placeholder از نوع <ng-content> بدون attribute مربوط به select هم داشته باشید، دومی همه elementهایی را capture می‌کند که با attributeهای select match نشده‌اند:

html
<!-- Component template -->
<div class="card-shadow">
  <ng-content select="card-title" />
  <div class="card-divider"></div>
  <!-- capture anything except "card-title" -->
  <ng-content />
</div>
html
<!-- Using the component -->
<custom-card>
  <card-title>Hello</card-title>
  <img src="/..." />
  <p>Welcome to the example</p>
</custom-card>
html
<!-- Rendered DOM -->
<custom-card>
  <div class="card-shadow">
    <card-title>Hello</card-title>
    <div class="card-divider"></div>
    <img src="/..." />
    <p>Welcome to the example</p>
  </div>
</custom-card>

اگر یک component هیچ placeholder از نوع <ng-content> بدون attribute مربوط به select نداشته باشد، هر elementی که با یکی از placeholderهای component match نشود در DOM render نمی‌شود.

محتوای fallback

Angular می‌تواند برای placeholder مربوط به <ng-content> در یک component، زمانی که آن component هیچ child content مطابقی ندارد، fallback content نمایش دهد. می‌توانید fallback content را با اضافه کردن child content به خود element مربوط به <ng-content> مشخص کنید.

html
<!-- Component template -->
<div class="card-shadow">
  <ng-content select="card-title">Default Title</ng-content>
  <div class="card-divider"></div>
  <ng-content select="card-body">Default Body</ng-content>
</div>
html
<!-- Using the component -->
<custom-card>
  <card-title>Hello</card-title>
  <!-- No card-body provided -->
</custom-card>
html
<!-- Rendered DOM -->
<custom-card>
  <div class="card-shadow">
    <card-title>Hello</card-title>
    <div class="card-divider"></div>
    Default Body
  </div>
</custom-card>

Alias کردن content برای projection

Angular از یک attribute ویژه به نام ngProjectAs پشتیبانی می‌کند که اجازه می‌دهد یک CSS selector روی هر element مشخص کنید. هر زمان elementی با ngProjectAs در برابر یک placeholder از نوع <ng-content> بررسی شود، Angular به‌جای identity خود element، مقدار ngProjectAs را مقایسه می‌کند:

html
<!-- Component template -->
<div class="card-shadow">
  <ng-content select="card-title" />
  <div class="card-divider"></div>
  <ng-content />
</div>
html
<!-- Using the component -->
<custom-card>
  <h3 ngProjectAs="card-title">Hello</h3>

  <p>Welcome to the example</p>
</custom-card>
html
<!-- Rendered DOM -->
<custom-card>
  <div class="card-shadow">
    <h3>Hello</h3>
    <div class="card-divider"></div>
    <p>Welcome to the example</p>
  </div>
</custom-card>

ngProjectAs فقط از مقدارهای static پشتیبانی می‌کند و نمی‌توان آن را به expressionهای dynamic bind کرد.

نکات احتیاطی

محتوای projected در view والد زندگی می‌کند

با اینکه محتوای projected داخل component دریافت‌کننده render می‌شود، همچنان مالکیت آن با componentی است که آن را declare کرده است. Angular آن را به‌عنوان بخشی از view والد track می‌کند، و این چند اثر جانبی دارد که دانستنشان مفید است.

Change detection: محتوای projected زمانی check می‌شود که والد change detection را اجرا کند. اگر component دریافت‌کننده از OnPush استفاده کند، Angular می‌تواند بررسی template خود آن component را skip کند؛ اما محتوای projected را skip نمی‌کند، چون متعلق به والد است.

html
<!-- Parent template (default change detection) -->
<onpush-wrapper>
  <!-- Still checked on every parent cycle, OnPush doesn't help here -->
  <expensive-component />
</onpush-wrapper>

Dependency injection: محتوای projected dependencyهای خود را از injector والد می‌گیرد، نه از viewProviders مربوط به component دریافت‌کننده. برای جزئیات، Providers and viewProviders را ببینید.

بعضی componentهای کتابخانه‌ای از childهای projected پشتیبانی نمی‌کنند

برخی componentها، مثل menuها، tabها و listها، از ContentChildren استفاده می‌کنند تا childهای خود را پیدا کنند و behaviorهایی مثل keyboard navigation، focus management یا attributeهای ARIA را وصل کنند. این componentها با این فرض نوشته شده‌اند که childهای خود را مستقیم در اختیار دارند؛ بنابراین project کردن محتوای بیرونی داخل آن‌ها معمولا چیزهایی را به‌شکل ظریف خراب می‌کند.

برای مثال، wrap کردن elementهای <mat-menu-item> در یک لایه اضافه و project کردن آن‌ها داخل <mat-menu> می‌تواند keyboard navigation و پشتیبانی screen reader را بی‌صدا خراب کند. query همچنان itemها را پیدا می‌کند، اما setup داخلی که آن‌ها را interactive می‌کند ممکن است وقتی itemها از یک view context متفاوت می‌آیند درست کار نکند.

اگر یک component کتابخانه‌ای behavior childهای خود را مدیریت می‌کند، پیش از استفاده از content projection مستنداتش را بررسی کنید؛ ممکن است پشتیبانی نشده باشد.