Skip to content

Implement new content reprojection #2529

Closed

Description

@tbosch

Overview

The current shadow dom emulation also contains content reprojection, but has the following limiations:

  1. it is not very performant, as it tries to stay relatively close to the ShadowDOM spec (see e.g. Performance regression for tables that use shadow dom emulation #2298).
  2. it does not allow moving the projected content programmatically somewhere else than the shadow dom of the component that contains the <content> tag (see Renderer: Is it possible to get a reference to transcluded content? #2320).

The main idea is:

  1. attach component views already during compilation
  2. reproject content also already during compilation

With this we have

  1. performance gain as everything happens during compilation, which might be executed in a build step
  2. reprojected nodes can be moved via directives out of the Shadow DOM to any place, as the nodes will be copied literally to the place of the <content> tag during compilation. I.e. a directive can use a ProtoViewRef + an ElementRef to also move projected content (see Support for manual content reprojection #1989).

Example for implementing TabGroup

<tab-group>
  <!-- Tab label can be set via attribute with the content directly inside of <tab> -->
  <tab label="Vegetables">Carrots, Beets </tab>
  <!-- Tab label can also be a full element -->
  <tab>
    <tab-label> Fruits </tab-label>
    <tab-body>Apples, Cherries</tab-body>
  </tab>
</tab-group>
@View({
  template: `
  <div class="header">
    <button *ng-for="#tab of tabs" (click)="selected = tab">
      <template ng-outlet="tab.labelTemplate"/>
    </button>
  </div>
  <div class="content">
      <template ng-outlet="selected.bodyTemplate"/>
  <div>
`
})
@Component()
class TabGroup {
  selected:Tab = null;
  constructor(@Query(Tab) tabs) {}
}

@View({
  template: `
    <template ng-template-ref>
      {{labelAttribute}}
      <ng-content select="tab-label"/>
    </template>
    <template ng-template-ref>
      <ng-content select="tab-body"/>
      <ng-content select="*"/>
    </span>
`
})
@Component()
class Tab {
  constructor(
    public @Attribute('label') labelAttribute:string,
    private @ViewQuery(TemplateRef) templates
  ) {}
  onInit() {
    this.labelTemplate = this.templates.get(0);
    this.bodyTemplate = this.templates.get(1);
  }
}

See #1931

Rules

  • ng-content can only support projecting nodes directly under a host element.
    Locating elements further down does not work e.g. if NgFor is placed on a parent.
  • <ng-content select="*"> is to be treated as "everything that has not been matched by other ng-content tags".
  • If an <ng-content> element is destroyed, its nodes will not overflow to a <ng-content select="*">, as the reprojection happens during compile time.
  • light dom (projected content) has to be instantiated exactly once, regardeless of whether projection actually happens via ng-content (e.g. ng-content is used via NgIf), as the usage of the content in the light dom could have side effects
  • trying to instantiate a ProtoView that contains a <ng-content> element multiple times needs to fail, as the light dom might have side effects and we cannot allow these to happen multiple times.
  • trying to instantiate a ProtoView with an ElementRef that does not match the place where the ProtoView should not be allowed, as this would be very confusing to users.

Combination with native ShadowDOM

The only thing that Angular needs to do to support native ShadowDOM is to place the view content into a shadow root instead of the element children. A component can declare this like this:

@View({
  useShadowDom: true,
  template: '...'
})
...

Angular will then still already attach component views and project content during compilation. However, when component views are instantiated that have the flag useShadowDom set, Angular will move the child nodes of Elements into the shadow root of the Element.

Notes

  • cycles: ProtoViews can contain cycles. Attaching component views can stop when they encounter a nested <template> (or *... or template attribute), as instantiation also stops at these places.
  • for now append templates of nested components only on render side, as we get the most benefits there by cloning a single time. Also keep the View abstraction (instantiating a component would automatically also instantiate child Views and cache them).

Activity

  1. tbosch commented on Jun 13, 2015

    @tbosch
    ContributorAuthor

    @mhevery: I deviated from the initial plan to have a completely different implementation when shadow dom i used, as I don't think we need it (see section above). Essentially, we can't do a short path, but need to implement the whole thing so that users can use it.

  2. tbosch commented on Jun 13, 2015

    @tbosch
    ContributorAuthor
  3. jelbourn commented on Jun 15, 2015

    @jelbourn
    Contributor

    I think this is good. Definitely something that needs very clear documentation in the end, but seems pretty learnable to me.

  4. added this to the alpha-29 milestone on Jun 15, 2015
  5. self-assigned this
    on Jun 15, 2015
  6. tbosch commented on Jun 16, 2015

    @tbosch
    ContributorAuthor

    Side goals:

    • simplify Renderer: Remove attachViewInContainer, and replace with attachViewNextToElement / attachViewNextToView, i.e. the renderer should no more need to track the content of ViewContainers.
    • allow to use content reprojection on all renderers (e.g. DOM, NativeScript, ReactNative)
  7. modified the milestones: alpha-30, alpha-29 on Jul 1, 2015
  8. 15 remaining items

  9. rolandjitsu commented on Jul 23, 2015

    @rolandjitsu

    @tbosch so how do I use native shadow DOM now? Do I add the useShadowDom property on the @View() annotation? Or has it been named to something else? Because I can only find useNativeShadowDom and there is not much docs on this.

  10. angular-automatic-lock-bot commented on Sep 6, 2019

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  11. locked and limited conversation to collaborators on Sep 6, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions