Hani Hani - 2 months ago 22
TypeScript Question

Angular2, generic overlay component for all other components inside the app

I have the following two components:

overlay

@Component({
selector: 'overlay',
template: '<div class="check"><ng-content></ng-content></div>'
})
export class Overlay {
save(params) {
//bunch of stuff that are commonly used
}
}


myComponent (there are many more like this)

@Component({
selector: 'myComponent',
directives: [Overlay],
template: '<overlay><form (ngSubmit)="save({ name: 'sam', lastName: 'jones' })">I'm the component. Click <input type="submit">Here</input> to save me!</form></overlay>'
})
export class MyComponent {

}


This solutions doesn't work with no errors thrown, Angular2 simply skips both components initialization. Anyways, i think you get the idea here. There's a common component which will need to wrap a lot of other components hence it needs to be generic. It has a
template
so services don't do the job. I haven't worked much with custom annotations, maybe they could do something like this? Any thoughts on how to achieve such a functionality?

note: the overlay contains logic and template, both of which are needed for any other component that needs to use it. The template is a complex modal dialog with animations, messages, fades etc... that is common across the solution.

Update

Found this: Angular2: progress/loading overlay directive

Answer

I created one for this specific task and thought about embedding bootstrap modal as template but came across a couple of issues such as the fact that the only useful piece out of bootstrap modal would be the class names. Not surprisingly, didn't want that for this specific task since I needed to distinguish between window modals and progress modals on components.

Overlay

@Component({
    selector: 'overlay',
    template:
    `<div [ngClass]="isOpen ? 'opened' : 'closed'">
         <div class="modal" role="dialog">
            <div class="modalBody">
                <div *ngIf="isSaving">
                    <span class="text-success text-bold">
                        Saving...
                    </span>
                </div>
                <div *ngIf="isSaved">
                    <span class="text-success text-bold">
                        Saved.
                    </span>
                </div>
            </div>
        </div>
    </div>`,
    styles: [
        '.modal { position:absolute; width: 100%; height: 100%; margin: -30px; background-color: rgba(255, 255, 255, 0.7); z-index: 1000; text-align: center; }',
        '.closed { visibility: hidden; }',
        '.opened { visibility: visible; }',
        '.modalBody { top: 45%; left: 25%; width: 50%; position: absolute; }',
        '.text-bold { font-weight: 800; font-size: 1.5em; }'
    ]
})
export class Overlay implements OnChanges, AfterContentInit {
    @Input() isSaving: boolean = false;
    @Input() isSaved: boolean = false;
    @Input() containerElement: HTMLElement;

    isOpen = false;

    private modalElement;

    constructor(private element: ElementRef, private animationBuilder: AnimationBuilder) { }

    ngOnChanges() {
        if (this.modalElement) {
            if (this.isSaving == true || this.isSaved == true) {
                this.toggleAnimation(true);
            }
            else if (this.isSaving == false && this.isSaved == false) {
                this.toggleAnimation(false);
            }
        }
    }

    ngAfterContentInit() {
        this.containerElement.style.position = 'relative';
        this.modalElement = this.element.nativeElement.querySelector('.modal');
    }

    private toggleAnimation(isOpen) {
        var startCss = { backgroundColor: 'rgba(255, 255, 255, 0)' };
        var endCss = { backgroundColor: 'rgba(255, 255, 255, 0.7)' };

        if (isOpen) {
            this.isOpen = true

            this.animation(
                true,
                this.modalElement,
                400,
                startCss,
                endCss,
                null
            );
        }
        else {
            this.animation(
                isOpen,
                this.modalElement,
                400,
                startCss,
                endCss,
                () => {
                    this.isOpen = false;
                }
            );
        }
    }

    private animation(isStart, element, duration, startCss, endCss, finishedCallback) {
        var animation = this.animationBuilder.css();

        animation.setDuration(duration);

        if (isStart) {
            animation.setFromStyles(startCss).setToStyles(endCss);
        } else {
            animation.setFromStyles(endCss).setToStyles(startCss)
        }

        animation.start(element);

        if (finishedCallback) {
            setTimeout(finishedCallback, duration);
        }
    }
}

Usage

As is, you'll need a relative container in order for overlay to fill its parent. The css definitely needs modifications to accommodate for a few scenarios such as mobile devices and non-positioned containers. Here's how it's currently used:

HTML

<form action="/edit" method="post" #myForm="ngForm" (ngSubmit)="save ajax method that will update the isSaving and isSaved accordingly" novalidate>
    <div style="position: relative;" #overlayContainer>
        <overlay [isSaving]="isSaving" [isSaved]="isSaved" [containerElement]="overlayContainer"></overlay>
    </div>
</form>

After saving the form, an overlay is shown within the containerElement for 400ms and is faded out and goes hidden afterwards until the next save attempt. The isSaving and isSaved binding values are the responsibility of the any component that wishes to use the overlay.

Comments