Angular2 NG-шаблон в отдельном файле

angular2 как использовать ng-шаблон из другого файла? Когда я помещаю ng-шаблон в тот же HTML, где я использую, он работает, но когда я перемещаю ng-шаблон в отдельный файл, он не работает. Есть ли способ переместить ng-шаблон в свой собственный файл и использовать его в другом файле HTML?

инфо-message.html

<ng-template #messageTemplate>
    Hi
</ng-template>

<ng-container *ngTemplateOutlet="messageTemplate;"></ng-container>

выше работает нормально, потому что NG-шаблон и использование находится в одном файле

сообщение-template.html

<ng-template #messageTemplate>
    Hi
</ng-template>

инфо-message.html

<ng-container *ngTemplateOutlet="messageTemplate;"></ng-container>

Это не работает. Есть ли способ использовать "messageTemplate", который находится в отдельном файле внутри другого HTML (например, info-message.html)

Заранее спасибо.

1 ответ

Такое поведение может быть достигнуто через "портал". Это полезный и довольно распространенный шаблон в приложениях Angular. Например, у вас может быть глобальная боковая панель, расположенная рядом с верхним уровнем приложения, и тогда дочерние компоненты могут указывать локальный <ng-template/>как часть их общего шаблона, который будет отображаться в этом месте.

Обратите внимание, что в то время как <ng-template/> может быть определен вне файла, где определен желаемый выход, все еще необходимо поместить <ng-template/> внутри шаблона какого- то компонента. Это может быть минималистский компонент, который отвечает только за <ng-template/>Однако это может быть и сложный компонент, где <ng-template/> представляет интерес только незначительную роль.

Этот код иллюстрирует одну возможную базовую реализацию портала.

@Directive({
  selector: '[appPortal]'
})
export class PortalDirective implements AfterViewInit {
  @Input() outlet: string;

  constructor(private portalService: PortalService, private templateRef: TemplateRef<any>) {}

  ngAfterViewInit(): void {
    const outlet: PortalOutletDirective = this.portalService.outlets[this.outlet];
    outlet.viewContainerRef.clear();
    outlet.viewContainerRef.createEmbeddedView(this.templateRef);
  }
}

@Directive({
  selector: '[appPortalOutlet]'
})
export class PortalOutletDirective implements OnInit {
  @Input() appPortalOutlet: string;

  constructor(private portalService: PortalService, public viewContainerRef: ViewContainerRef) {}

  ngOnInit(): void {
    this.portalService.registerOutlet(this);
  }
}

@Injectable({
  providedIn: 'root'
})
export class PortalService {
  outlets = new Map<string, PortalOutletDirective>();

  registerOutlet(outlet: PortalOutletDirective) {
    this.outlets[outlet.appPortalOutlet] = outlet;
  }
}

Он работает, используя три части:

  • Директива "портала". Это живет по желанию <ng-template/> и принимает в качестве входных данных название торговой точки, в которой должен отображаться контент.
  • Директива "Портал". Это живет на выходе, например <ng-container/>и определяет выход.
  • Сервис портала. Это предоставляется на корневом уровне и хранит ссылки на порталы портала, чтобы порталы могли получить к ним доступ.

Это может показаться большой работой для чего-то довольно простого, но как только эта сантехника будет на месте, ее будет легко (повторно) использовать.

<div class="container">
  <div class="row">
    <div class="col-6">
      <app-foo></app-foo>
    </div>
    <div class="col-6">
      <ng-container [appPortalOutlet]="'RightPanel'"></ng-container>
    </div>
  </div>
</div>

// foo.component.html
<h1>Foo</h1>
<ng-template appPortal [outlet]="'RightPanel'">
 <h1>RIGHT</h1>
</ng-template>

В общем, изобретать велосипед не очень хорошая идея, хотя, когда уже есть хорошо протестированные, документированные и стабильные реализации. В Angular CDK есть такая реализация, и я бы посоветовал использовать ее на практике, а не свою.

Вы видели это? https://github.com/angular/angular/issues/27503 Там есть пример, предоставленный dawidgarus

Предполагается, что если вы хотите повторно использовать свой шаблон в разных файлах, вам следует преобразовать то, что находится внутри шаблона, в отдельный компонент, а затем вы можете использовать этот компонент где угодно.

Если вы загружаете отдельный файл, вы можете определить компонент в отдельном файле (вместо <ng-template>). А затем введите весь компонент в<ng-container> используя *ngComponentOutlet.

Вы можете найти полную версию с примером здесь: /questions/53058488/vnedrenie-komponenta-v-ng-kontejner-zagruzit-ng-shablon-iz-fajla/53058496#53058496

Расширение ответа @peter554 по причинам объяснения и переносимости. Это позволит вам использовать шаблон во всех компонентах.

Использовать:

'app.module.ts'
import {NgModule} from '@angular/core';
import {
    IdcPortalDirective, IdcTemplatePortalDirective,
    PortalService
} from './idc-template-portal/idc-template-portal.component';

@NgModule({
    declarations: [
        IdcPortalDirective,
        IdcTemplatePortalDirective
    ],
    imports: [],
    exports: [],
    providers: [
        PortalService
    ],
    bootstrap: [AppComponent]
})
export class AppModule {}
'./idc-template-portal/idc-template-portal.component.ts'
import {
    AfterViewInit,
    Directive,
    Injectable,
    Input,
    OnInit, Output,
    TemplateRef,
    ViewContainerRef
} from '@angular/core';
/*** Input Template ***/
/*** <ng-template idcPortal [outlet]="'outletname'">Template Contents</ng-template> ***/
@Directive({
    selector: '[idcPortal]'
})
export class IdcPortalDirective implements OnInit {
    @Input() outlet: string;
    @Output() inlet: string = this.outlet;

    constructor(private portalService: PortalService, public templateRef: TemplateRef<any>) {}

    ngOnInit():void {
        this.portalService.registerInlet(this);
    }

}
/*** Output Container ***/
/*** <ng-container [idcPortalOutlet]="'outletname'"></ng-container> ***/
@Directive({
    selector: '[idcPortalOutlet]'
})
export class IdcTemplatePortalDirective implements OnInit, AfterViewInit {
    @Input() appPortalOutlet: string;
    @Output() outlet: string = this.appPortalOutlet;

    constructor(private portalService: PortalService, public viewContainerRef: ViewContainerRef) {}

    ngOnInit():void {
        this.portalService.registerOutlet(this);
    }

    ngAfterViewInit() {
        this.portalService.initializePortal(this.appPortalOutlet);
    }

}
@Injectable({
    providedIn: 'root'
})
export class PortalService {
    outlets = new Map<string, IdcTemplatePortalDirective>();
    inlets = new Map<string, IdcPortalDirective>();

    registerOutlet(outlet: IdcTemplatePortalDirective) {
        this.outlets[outlet.outlet] = outlet;
    }

    registerInlet(inlet: IdcPortalDirective) {
        this.inlets[inlet.inlet] = inlet;
    }

    initializePortal(portal:string) {
        const inlet: IdcPortalDirective = this.inlets[portal];
        const outlet: IdcTemplatePortalDirective = this.outlets[portal];
        outlet.viewContainerRef.clear();
        outlet.viewContainerRef.createEmbeddedView(inlet.templateRef);
    }
}

Он,@peter554, упоминает об изобретении велосипеда в отношении пакета порталов Angular CDK. Однако я считаю, что его / эта реализация имеет больше смысла в том, как она используется в потоке приложения, и в простоте, с которой шаблон может быть перенесен из компонента в другой компонент, который содержит выход портала (позволяя компоненту компонент-> шаблон портала Например, в шаблоне компонента, реализующем Angular Material MatBottomSheet (idcBottomSheet)).

Элемент input:

<!--
/*
  For example, perhaps you have a mobile view
  where a template is hidden (via css) and ported
  over to a MatBottomSheet component template to be 
  popped up when requested (by button click). 
*/
-->
<button #bottomsheetButton (click)="openBottomSheet(Notes)" mat-button>
    <mat-icon>notes</mat-icon>
</button>
<!--/* hidden in mobile view mode. */-->
<ng-content *ngTemplateOutlet="Notes"></ng-content>
<ng-template #Notes idcPortal [outlet]="'idcBottomSheet'"><!--/* template to port */-->
    <form>
        <mat-form-field class="w-100 h-100">
            <mat-label>A place for your thoughts:</mat-label>
            <textarea matInput
                      cdkTextareaAutosize
                      #autosize="cdkTextareaAutosize"
                      cdkAutosizeMinRows="10"
                      cdkAutosizeMaxRows="10"
                      placeholder="Angular. It makes me feel...">
            </textarea>
        </mat-form-field>
    </form>
</ng-template>

Элемент output (внутри шаблона компонента MatBottomSheet):

<ng-container [idcPortalOutlet]="'appIdcBottomSheet'"></ng-container>

Вы можете использовать что-то вроде этого (шаблон используется из другого компонента):

@Component(
    template: '<ng-container *ngTemplateOutlet="infoMessage.template;"></ng-container>'
)
export class MessageTemplate {
    infoMessage: InfoMessage;    
}

@Component(
    ....
)
export class InfoMessage{    
    @ContentChild('columnTemplate') template: TemplateRef<any>;

    constructor(private messageTemplate: MessageTemplate) {
        messageTemplate.infoMessage = this;
    }
}
Другие вопросы по тегам