Angular CDK : attach overlay to a clicked element
Asked Answered
B

2

8

I'm trying to make a custom popover for a table cell, in order to show a cell details when clicking on it, in a way similar to mdBoostrap popovers.

For now, I have the following app: https://stackblitz.com/edit/angular-m5rx6j

The Popup component shown under the main component, but I would like to show it over the table, just under the element I clicked.

I suppose I need to do the following :

  • Get the ElementRef of the 'td' where I click -> I don't know how
  • Attach the overlay to this element -> Already do that, but with the root element
Barnstorm answered 5/12, 2019 at 16:32 Comment(6)
You'd better start from some ready solution e.g. angular bootstrap popover.Kilpatrick
I do this in order to learn Angular, and didn't find any working example for Angular 8.2 (got some for Angular 5, but nothing more).Barnstorm
I tried to replace the ElementRef by the clicked td. No effect... Also if I change the positionStrategy to something else it doesn't work either. The problem comes probably from somewhere else See : stackblitz.com/edit/angular-q1bza9Moult
Also, this stackblitz is may be useful because containing something really near of what you want stackblitz.com/edit/custom-overlay-step-4Moult
Indeed, I think I already saw this Stackblitz on another question. While showing how to make a global overlay, I'm more looking to do something like this mdbootstrap.com/docs/angular/advanced/popovers.Barnstorm
I was looking at some similar stuff. Looking at ngx-popper might be a good place to learn a good Angular way to achieve that: github.com/MrFrankel/ngx-popper . Didn't look at it yet though.Sarcenet
D
19

There're a two amazing articles about using OverLay from CDK in Netanet Basal's Blog

  1. Creating Powerful Components with Angular CDK
  2. Context Menus Made Easy with Angular CDK

I try to simplyfied in this stackblitz

Basicaly you has a service that inject Overlay

constructor(private overlay: Overlay) { }

To open a template you pass the origin (I called him "origin"), the template (I called menu) and the viewContainerRef of your component

    this.overlayRef = this.overlay.create(
        this.getOverlayConfig({ origin: origin})
    );
    //I can pass "data" as implicit and "close" to close the menu
    this.overlayRef.attach(new TemplatePortal(menu, viewContainerRef, {
        $implicit: data, close:this.close
    }));

getOverLayConfig return a config some like

private getOverlayConfig({ origin}): OverlayConfig {
    return new OverlayConfig({
        hasBackdrop: false,
        backdropClass: "popover-backdrop",
        positionStrategy: this.getOverlayPosition(origin),
        scrollStrategy: this.overlay.scrollStrategies.close()
    });
}

And the position strategie is where you want to attach the template -an array with your preferered positions, e.g.

      [
        {
            originX: "center",
            originY: "bottom",
            overlayX: "center",
            overlayY: "top"
        },
      ]

Well, the other part of the code is about close the template element. I choose create in the service a function open that

1.-attach the element

2.-create a subscription of

this.sub = fromEvent<MouseEvent>(document, "click")

3.-return an observable that return null or the argument you pass in a function "close"(*)

NOTE: Don't forget incluyed in your css ~@angular/cdk/overlay-prebuilt.css

(*) this allow me my template like

<ng-template #tpl let-close="close" let-data>
  <div class="popover" >
    <h5>{{name}} {{data.data}}</h5> //<--name is a variable of component
                                    //data.data a variable you can pass
  And here's some amazing content. It's very engaging. Right?
  <div>
   <a (click)="close('uno')">Close</a> //<--this close and return 'uno'
  </div>
  </div>
</ng-template>

Update if we want to attach a component first we need remember that must be in the entryComponents of the module

@NgModule({
  imports:      [ BrowserModule, FormsModule,OverlayModule ],
  declarations: [ AppComponent,HelloComponent], //<--HERE
  bootstrap:    [ AppComponent ],
  entryComponents:[HelloComponent]  //<--and HERE

})

Well, to attach a component is simple change the attach and use ComponentPortal, e.g.

const ref=this.overlayRef.attach(new ComponentPortal(HelloComponent,viewContainerRef))

then, if our component has somes inputs, e.g.

  @Input() name="Angular";
  @Input() obj={count:0};

We can use ref.instance to access to the component, e.g

  ref.instance.name="New Name"

But as we want maintain the service the most general use, I want to use the argument "data" to give values to the variables, so our function "open" becomes

open(origin: any, component: any, viewContainerRef: ViewContainerRef, data: any) {
        this.close(null);
        this.overlayRef = this.overlay.create(
            this.getOverlayConfig({ origin: origin})
        );
        const ref=this.overlayRef.attach(new ComponentPortal(component, viewContainerRef));
    for (let key in data) //here pass all the data to our component
    {
       ref.instance[key]=data[key]
    } 
    ...rest of code...
}

As always, if we pass an object, all the changes in the component change the properties of the object, so in our main component can be make some like

obj={count:2}

open(origin:any,menu:any,index:number)
  {
    this.popupService.open(origin,HelloComponent,this.viewContainerRef,
        {name:'new Name'+index,obj:this.obj})
    .subscribe(res=>{
      console.log(res)
    })
  }

See that, as I pass as obj an object any change in the component change the propertie of the object, in my case the component is very simple

@Component({
  selector: 'component',
  template:`Hello {{name}}
    <button (click)="obj.count=obj.count+1">click</button>
  `
})
export class HelloComponent  {
  @Input() name="Angular";
  @Input() obj={count:0};
}

You can see in a new stackblitz

Update2 To close the panel from the HelloComponent we need inject the service as public and use close. more or less, a button

<button (click)="popupService.close(4)">close</button>

where you inject the service

constructor(public popupService: MenuContextualService){}
Daisey answered 10/12, 2019 at 17:9 Comment(1)
Thank for the answer, it worked. Would it be possible to add an example with a ComponentPortal which takes an input?Barnstorm
S
1

To get the ref of an element, you can use a template identifier: #this_element.

You can directly use the value inside the component template, or get a Typescript variable from @ViewChild / @ViewChildren.

For example in your code:

<td #this_element>
  <div (click)="open(this_element)">Overlay Host1</div>
</td>

And your open function would now read open(element: any) with element having a type depending on where you put #this_element.

And you can also get a hold of this element in your component code through

@ViewChild('this_element', { static: true }) element;

There is a bit of warning in the Angular docs if you need to access nativeElement though, so beware if you run inside a web worker. You might want to use Renderer2 instead in this case.

Sarcenet answered 10/12, 2019 at 11:13 Comment(2)
Would it be compatible with a table which tbody is populated by a "ngFor" ?Barnstorm
Yes, in this case you need to use @ViewChildren, which dinamically gives you a list of the elements corresponding to the template name you give.Sarcenet

© 2022 - 2024 — McMap. All rights reserved.