We have been using @ng-bootstrap/ng-bootstrap library in our project for some behaviours/components like Modal. Recently i wanted to eliminate that dependency and implemented the bootstrap modal behaviour with Angular. It was actually pretty easy. Let me shortly tell you how it works:
I have a modal-service, and a modal-component. Service creates the modal component dynamically via ComponentFactoryResolver (details can be seen in this SO post) and add in DOM. By closing the modal, modal just calls the callback function which was defined from service as well, and this just destroys the component, removes from DOM.
So: i have 2 animation states for this modal component, enter and leave. Enter works well. As soon as the component appears in dom, the predefined :enter state is triggered and my animation works. But :leave does not.
This is exactly how closing modal works: Modal is open, you click on the close button or anywhere else on the modal-backdrop. This just calls close function, which is defined as an input, and given from service during the creation.
@Input() closeCallback: Function;
And service just removes the component from DOM.
Since the component is removed as soon as the close button is clicked, the animation does not have the time it needs i think. So :leave does not work.
I thought to put a timeout (delay) on close, and trigger the animation manually, but since i want to use the predefined behaviours :enter and :leave, i could not figure it out how it possible. So how can i make my leaving animation work? (with or without :leave)
Service-Code:
@Injectable()
export class ModalService implements OnDestroy {
private renderer: Renderer2;
private componentRef: ComponentRef<ModalComponent>;
constructor(private rendererFactory: RendererFactory2,
private componentFactoryResolver: ComponentFactoryResolver,
private appRef: ApplicationRef,
private injector: Injector) {
this.renderer = rendererFactory.createRenderer(null, null);
}
ngOnDestroy() {
this.componentRef.destroy();
}
open(content: string, titel: string, primaryButtonLabel: string, secondaryButtonLabel?: string, primaryButtonCallback?: Function, secondaryButtonCallback?: Function) {
// 1. Create a component reference from the component
this.componentRef = this.componentFactoryResolver
.resolveComponentFactory(ModalComponent)
.create(this.injector);
this.componentRef.instance.content = content;
this.componentRef.instance.titel = titel;
this.componentRef.instance.primaryButtonLabel = primaryButtonLabel;
this.componentRef.instance.secondaryButtonLabel = secondaryButtonLabel;
this.componentRef.instance.primaryButtonCallback = primaryButtonCallback;
this.componentRef.instance.secondaryButtonCallback = secondaryButtonCallback;
this.componentRef.instance.closeCallback = (() => {
this.close();
});
// 2. Attach component to the appRef so that it's inside the ng component tree
this.appRef.attachView(this.componentRef.hostView);
// 3. Get DOM element from component
const domElem = (this.componentRef.hostView as EmbeddedViewRef<any>)
.rootNodes[0] as HTMLElement;
// 4. Append DOM element to the body
this.renderer.appendChild(document.body, domElem);
this.renderer.addClass(document.body, 'modal-open');
}
close() {
this.renderer.removeClass(document.body, 'modal-open');
this.appRef.detachView(this.componentRef.hostView);
this.componentRef.destroy();
}
}
Modal-Component.ts:
@Component({
selector: '[modal]',
templateUrl: './modal.component.html',
styleUrls: ['./modal.component.scss'],
animations: [
trigger('modalSlideInOut', [
transition(':enter', [
style({opacity: 0, transform: 'translateY(-100%)'}),
animate('0.3s ease-in', style({'opacity': '1', transform: 'translateY(0%)'}))
]) ,
transition(':leave', [
style({opacity: 1, transform: 'translateY(0%)'}),
animate('0.3s ease-out', style({'opacity': '0', transform: 'translateY(-100%)'}))
])
])
]
})
export class ModalComponent implements AfterViewInit {
....
@Input() closeCallback: Function;
constructor() { }
close() {
this.closeCallback();
}
}
Modal-HTML is not very relevant but you can imagine sth like this:
<div [@modalSlideInOut] role="document" class="modal-dialog">
<div ....
<button (click)="close()">
CLOSE
</button>
...
</div>
</div>