I cannot querySelect input of ion-searchbar in Jasmine tests - ionic-framework

I'm running a simple test - enter a value into an Ionic search bar.
I cannot get a reference to the search bar input field in my jasmine tests.
I've set up a simple vanilla Ionic 4 app with a single test page.
The following tests all fail.
const el: HTMLElement = fixture.nativeElement;
const input = el.querySelector('input');
expect(input).not.toBeNull(); // FAILS
const bar = el.querySelector('ion-searchbar');
expect(bar.innerHTML).not.toEqual(''); // FAILS
expect(bar.shadowRoot).not.toBeNull(); // FAILS
I don't understand why?
I've written other tests successfully on Ionic lists to read values.
I tried putting in timers to give the component time to render the html.
If I put a simple HTML input field on the page it is picked up OK.
I thought the shadow DOM might be relevant here even though when I inspect the input field in the browser it doesn't seem to be in the shadow DOM, and the standard document.querySelector works just fine.
HTML Page
// test.page.html
<ion-header>
<ion-toolbar>
<ion-title>
Test Page
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<ion-searchbar></ion-searchbar>
</ion-content>
Component Class
// test.page.ts
import { Component } from '#angular/core';
#Component({
selector: 'app-test',
templateUrl: './test.page.html',
styleUrls: ['./test.page.scss'],
})
export class TestPage {
constructor() { }
}
Test Spec
// test.page.spec.ts
import { CUSTOM_ELEMENTS_SCHEMA } from '#angular/core';
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { TestPage } from './test.page';
describe('TestPage', () => {
let component: TestPage;
let fixture: ComponentFixture<TestPage>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TestPage ],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have a searchbar', () => {
const el: HTMLElement = fixture.nativeElement;
const bar = el.querySelector('ion-searchbar');
fixture.detectChanges();
expect(bar).not.toBeNull(); // PASSES
// adding some checks to see what is going on
expect(bar.children.length).toBeGreaterThan(0); // FAILS
expect(bar.innerHTML).not.toEqual(''); // FAILS
expect(bar.shadowRoot).not.toBeNull(); // FAILS
});
it('should have an input field', () => {
const el: HTMLElement = fixture.nativeElement;
const input = el.querySelector('input');
expect(input).not.toBeNull(); // FAILS
});
});

This is over a year late, but I think it's worth answering for others who might need it. For what it's worth, I'm running #ionic-native/core 4.15 with #angular/core 5.2.11 and ionic-angular 3.9.2.
I don't know why const input = el.querySelector('input'); isn't working for you. The following code works for me:
const toolbar = fixture.nativeElement.querySelector('ion-toolbar')
const searchBar = toolbar.querySelector('ion-searchbar');
const input = searchBar.querySelector('input');
Alternatively, you can make your query for the input more specific:
const input = searchBar.querySelector('div.searchbar-input-container > input.searchbar-input')
I like to look at the structure of HTMLElements and their component instances by console.loging them in the test. When I add this to my test:
console.log(searchBar);
The console in the captured test browser will output:
<ion-searchbar class="searchbar searchbar-md searchbar-left-aligned" ng-reflect-placeholder="Besmirch">
<div class="searchbar-input-container">
<button class="searchbar-md-cancel button button-md button-clear button-clear-md button-clear-md-dark" clear="" color="dark" ion-button="" mode="md" type="button" ng-reflect-color="dark" ng-reflect-mode="md" ng-reflect-clear=""><span class="button-inner"><ion-icon name="md-arrow-back" role="img" class="icon icon-md" aria-label="arrow back" ng-reflect-name="md-arrow-back"></ion-icon></span>
<div class="button-effect"></div>
</button>
<div class="searchbar-search-icon"></div>
<input class="searchbar-input" dir="auto" placeholder="Besmirch" type="search" autocomplete="off" autocorrect="off" spellcheck="false">
<button class="searchbar-clear-icon button button-md button-clear button-clear-md" clear="" ion-button="" type="button" ng-reflect-mode="md" ng-reflect-clear=""><span class="button-inner"></span>
<div class="button-effect"></div>
</button>
</div>
<button class="searchbar-ios-cancel button button-ios button-clear button-clear-ios" clear="" ion-button="" mode="ios" type="button" ng-reflect-mode="ios" ng-reflect-clear="" tabindex="-1"><span class="button-inner">Cancel</span>
<div class="button-effect"></div>
</button>
</ion-searchbar>
In this structure, we can see the input is in the .searchbar-input-container div.

Related

<select> inside react-popper inside react-bootstrap modal not working

I use a DatePicker with a select to switch the year and month. I use react-popper to show the DatePicker. But when I use the DatePicker in a react-bootstrap Modal the options of the select will popup for a second when I click it but then disappear again. I created a sandbox here to highlight the problem
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
import FocusTrap from 'focus-trap-react';
import { usePopper } from 'react-popper';
export default function Tooltip() {
const [referenceElement, setReferenceElement] = useState(null);
const [popperElement, setPopperElement] = useState(null);
const [showPicker, setShowPicker] = useState(false);
const popper = usePopper(referenceElement, popperElement, {
placement: 'bottom-start'
});
const openPopper = () => {
setShowPicker(true);
};
const closePopper = () => {
setShowPicker(false);
};
return <div className='datepicker-container'>
<a ref={setReferenceElement} className="btn btn-dark" onClick={openPopper}>Click to open select in Popper</a>
{showPicker && ReactDOM.createPortal(
<FocusTrap
active
focusTrapOptions={{
initialFocus: false,
allowOutsideClick: true,
clickOutsideDeactivates: true,
onDeactivate: closePopper
}}>
<div
tabIndex={-1}
style={popper.styles.popper}
className="dialog-sheet"
{...popper.attributes.popper}
ref={setPopperElement}
role="dialog">
<div>
<select>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
</select>
</div>
</div>
</FocusTrap>,
document.querySelector('#popup-container')
)}
</div>
}
The CSS looks like this:
.datepicker-container {
position: relative;
z-index: 100;
}
.dialog-sheet {
z-index: 2003;
background-color: #EEE;
}
Has anyone an idea how could I fix this. Is there something I'm missing in the css to fix this? Or has it maybe to do with the focus trap?
The problem was the portal I was using. Without ReactDOM.createPortal and just rendering it in place, works fine.

Pass data from home.html to home.ts

I'm new to Ionic and I would like to know how I can pass data that is in between the <p></p> HTML tag to my home.ts file. I have tried doing getElementById but that doesn't seem to work. Also ViewChild but with zero results. I'd like to have some help concerning this question.
From .ts -> html :
.ts-> varString : String = "Hallo world";
html-> {{varString}}
From html -> .ts depends of html tag
Ex. <input type=“text” name=“username” [(ngModel)]=“username”>
username is username : String = ""; and it changes avry time you update the input. Similar to other html tags. You can also get elements by id or class using Element of ionic angular
there is many way to pass data between html and ts, but you must have good understand about MVC design pattern. the MVC is the reason of why google introduced angular.
in angular (engine of ionic), your View(html in your project) knows everything about controller(ts file).
***** home.ts********
public MyName:string="jon";
MyFunc1()
{
alert(this.MyName);
}
MyFunc2()
{
this.MyName="other name";
alert(this.MyName);
}
*****home.html*******
<input type="text" [(ngModel)]="MyName" >
<p>{{MyName}}</p>
<button ion-button (click)="MyFunc1()" >MyFunc1</button>
<button ion-button (click)="MyFunc2()" >MyFunc2</button>
.
.
.
if you change the value of MyName in ts, then it will change automatically in html and also if you change you input value (that it is binded to MyName) it will change the MyName value in model (in ts).
Selecting DOM in ionic isn't a right way to change models value.
Taking the simple example of a Login page,
<ion-item>
<ion-label floating color="primary">Registered Email ID</ion-label>
<ion-input [(ngModel)]="login.email" name="email" type="text" #email="ngModel" spellcheck="false" autocapitalize="off"
required>
</ion-input>
</ion-item>
<ion-item>
<ion-label floating color="primary">Passcode</ion-label>
<ion-input [(ngModel)]="login.passcode" name="password" type="password" #password="ngModel" required>
</ion-input>
</ion-item>
The .ts code for this would be:
login= { username: '', email:'', mobile:'', passcode:'' };
That is literally it!
You can also go about without the login object and can declare variables like
username: any
email: any
and so on; and directly reference them in the html as
[(ngModel)]="username"
Hope it helps!
First try to understand that when you are working on Ionic, you are working on Angular but not Core Javascript.
and here is simple example that may help
home.html
import {Component} from '#angular/core';
import {NavController, AlertController} from 'ionic-angular';
#Component({
templateUrl: 'build/pages/home/home.html'
})
export class HomePage {
name: string;
constructor(public navCtrl: NavController, private alertController: AlertController) {
}
showName() {
console.log(this.name);
let alert = this.alertController.create({
title: 'Hello ' + this.name + '!',
buttons: ['OK']
});
alert.present();
}
}
home.ts
<ion-header>
<ion-navbar>
<ion-title>
Ionic Blank
</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<ion-label>{{ name }}</ion-label>
<ion-input [(ngModel)]="name"></ion-input>
<button (click)="showName()">Click Me</button>
</ion-content>

Ionic 2: View does not update based on model change

I have a very simple page with a couple of controls.
My issue is that the page does not pickup changes to the model when the icon in upper right corner is clicked. This toggles the showFilterPane variable, which again should show or hide a div based on *ngIf="showFilterPane".
I have another page just like this one working, and I can not figure out why this isn't.
Any tips?
(I've tried using the ChangeDetectorRef.detectChanges(); which works, but then the rangeslider will not work. The draggable point doesn't update, or does not move to where you tap.)
The page:
<ion-header>
<ion-navbar>
<ion-title>MY AO</ion-title>
<ion-buttons end>
<button ion-button (click)="toggleFilterPane()" icon-only>
<ion-icon name="options"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<div class="container">
<div class="left">
<div *ngIf="isSearching" class="spinner-container">
<ion-spinner></ion-spinner>
</div>
<!-- put content here -->
</div>
<div class="right" *ngIf="showFilterPane">
<ion-list inset>
<ion-list-header>BANA</ion-list-header>
<ion-item>
<ion-select multiple="true" [(ngModel)]="woTrackFilter">
<ion-option>1</ion-option>
<ion-option>2</ion-option>
<ion-option>3</ion-option>
<ion-option>4</ion-option>
<ion-option>5</ion-option>
</ion-select>
</ion-item>
</ion-list>
<ion-list inset>
<ion-list-header>TEKNIKSLAG</ion-list-header>
<ion-item>
<ion-select multiple="true" [(ngModel)]="woDisciplineFilter">
<ion-option>Signal</ion-option>
<ion-option>Bana</ion-option>
<ion-option>EL</ion-option>
<ion-option>Tele</ion-option>
</ion-select>
</ion-item>
</ion-list>
<ion-list inset>
<ion-list-header>DAGAR</ion-list-header>
<ion-item>
<ion-range min="10" max="80" step="4" [(ngModel)]="woDaysFilter">
<ion-label range-left>10</ion-label>
<ion-label range-right>80</ion-label>>
</ion-range>
</ion-item>
</ion-list>
<button ion-button (click)="doSearch()">Search</button>
</div>
</div>
</ion-content>
The component:
import { Component } from '#angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { WorkOrderDashboardPage } from "../work-order-dashboard/work-order-dashboard";
#Component({
selector: 'page-work-order-list',
templateUrl: 'work-order-list.html'
})
export class WorkOrderListPage {
private isSearching: boolean = false;
private showFilterPane: boolean=false;
private woTrackFilter: string[];
private woDisciplineFilter: string[];
private woDaysFilter: number;
constructor(public navCtrl: NavController, public navParams: NavParams) {
// Initialize storage providers here
}
ionViewDidLoad() {
console.log('ionViewDidLoad WorkOrderListPage');
}
toggleFilterPane(): void {
this.showFilterPane = !this.showFilterPane;
}
viewWorkOrder(event, workOrder): void {
this.navCtrl.push(WorkOrderDashboardPage, { workOrder: workOrder });
}
doSearch(): void {
console.log(this.woTrackFilter);
console.log(this.woDisciplineFilter);
console.log(this.woDaysFilter);
}
}
UPDATE: Found workaround
I tried creating a separate app, where the exact same code is working. That lead me to think something wasn't right on the LoginPage, the page that called setRoot() to the above page.
The login code looked like this:
WLAuthorizationManager.login("UserLogin", data).then(() => {
// Success
console.log("Logged in");
this.navCtrl.setRoot(WorkOrderListPage);
},
(err) => {
// failed
console.error(err);
this.showError("Username or password is incorrect");
})
I then figured it might be some Zone issue, and wrapped the setRoot call in zone.run() like this:
WLAuthorizationManager.login("UserLogin", data).then(() => {
// Success
console.log("Logged in");
this.zone.run(() =>
this.navCtrl.setRoot(WorkOrderListPage)
);
},
(err) => {
// failed
console.error(err);
this.showError("Username or password is incorrect");
})
After that the view started to respond as expected. I feel this is a bit of a hack. Can someone shed some light as to what is happening here?
Seems like you are basically using ngZone to make sure Angular knows you changed things so it will reload that part of the DOM to reflect the changes. I don't feel like it's a hack, because you are just making sure that it works as intended.
Angular 2 has some optimization features that help make your app run smoother and one of those is avoiding DOM updates whenever and wherever possible. By using zones (or ngZones) you are basically telling Angular "pay attention to this part, it changes and I need that change to be reflected in the DOM".
I've run into that sort of problem before myself and using zones is usually your best bet. Got into situations where a part of the interface would be stuck unless you touched a button or somesuch.
Another workaround (at least for range sliders) is using the AppllicationRef tick() method, which forces a DOM update. More info about it here.

load data in the view before loading it [duplicate]

This question already has an answer here:
Error if don't check if {{object.field}} exists
(1 answer)
Closed 6 years ago.
I have a problem, I am working with Ionic 2 and angular 2.
I have to show some datas in a view but I get these datas from an API in the same page so when I try to show them in the view, they are undefined yet. How can I wait to load the view after getting these datas?
I have tried onPageWillEnter but it does not work.
Thank you in advance.
You can wrap your template with *ngIf
template: `
<div *ngIf="data">
... <!-- content -->
</div>`
When data is set, the content will be shown.
You can also use the safe-navigation or Elvis operator to avoid error messages
template: `<div>{{data?.someProp}}</div>`
to avoid error messages when data is null
provider allow you get data from server
generate: ionic generate provider MyProvider --ts
#Injectable()
export class MenuProvider {
data: any = null;
constructor(public http: Http) { }
load() {
if (this.data) {
// already loaded data
return Promise.resolve(this.data);
}
// don't have the data yet
return new Promise(resolve => {
// We're using Angular Http provider to request the data,
// then on the response it'll map the JSON data to a parsed JS object.
// Next we process the data and resolve the promise with the new data.
this.http.get('asset/menu.json')
.map(res => res.json())
.subscribe(data => {
// we've got back the raw data, now generate the core schedule data
// and save the data for later reference
this.data = data;
resolve(this.data);
});
});
}
}
menu.ts
import {MenuProvider} from "../../providers/menu-provider/menu-provider";
import {Component, OnInit} from '#angular/core';
import {IONIC_DIRECTIVES, NavController, Modal} from 'ionic-angular';
import {AccountHistoryPage} from "../../pages/account-history/account-history";
/*
Generated class for the MenuComponent component.
See https://angular.io/docs/ts/latest/api/core/ComponentMetadata-class.html
for more info on Angular 2 Components.
*/
#Component({
selector: 'menu-component',
templateUrl: 'build/components/menu-component/menu-component.html',
directives: IONIC_DIRECTIVES,
providers: [MenuProvider]
})
export class MenuComponent {
// menu object
jsonMenu: any;
constructor(public nav: NavController, menuProvider: MenuProvider) {
// TODO: show progress
menuProvider.load().then(data => {
// TODO: turn off menu load json progress
// data after proccessed.
this.jsonMenu = data;
console.log(this.jsonMenu);
}, err => console.log(err));
}
itemClick(moduleNumber: number) {
console.log(moduleNumber);
let page: any = null;
if (moduleNumber == 210102) {
page = Modal.create(AccountSourcePage);
}
if (moduleNumber == 210103) {
page = Modal.create(AccountBeneficiatyPage);
}
if (moduleNumber == 210101) {
page = Modal.create(AccountHistoryPage);
}
this.nav.present(page);
}
}
after that use temple with ngFor, ngIf.... to display
<ion-item *ngFor="let menu of jsonMenu">
<!-- title -->
<div class="khungtieude">
<span class="tieudechinh">{{menu.title}}</span>
<span class="gachgiua"></span>
</div>
<!-- arrows -->
<div class="arrow-container" [hidden]="!(menu.contains.length > 1)">
<ion-icon class="arrow-back" name="ios-arrow-back"></ion-icon>
<ion-icon class="arrow-forw" name="ios-arrow-forward"></ion-icon>
</div>
<!-- slide -->
<!-- using template instead of fix code, i will add later -->
<ion-slides loop>
<!-- page 1 of side -->
<!-- i need loop in total/3 + (total%3==0? 0 : 1) -->
<ion-slide *ngFor="let temp of menu.contains">
<ion-row style="padding-top: 10px;">
<ion-col width-33 center *ngFor="let menuItem of temp.page" (click)="itemClick(menuItem.moduleId)">
<span class="icon-cknb main-menu-ic"></span>
<br/>
<div class="main-menu-text">
{{menuItem.displayName}}
</div>
</ion-col>
</ion-row>
</ion-slide>
</ion-slides>
</ion-item>

Angular 2.0 and Modal Dialog

I am trying to find some examples on how to do a Confirmation modal dialog in Angular 2.0. I have been using Bootstrap dialog for Angular 1.0 and unable to find any examples in the web for Angular 2.0. I also checked angular 2.0 docs with no luck.
Is there a way to use the Bootstrap dialog with Angular 2.0?
Angular 2 and up
Bootstrap css (animation is preserved)
NO JQuery
NO bootstrap.js
Supports custom modal content (just like accepted answer)
Recently added support for multiple modals on top of each other.
`
#Component({
selector: 'app-component',
template: `
<button type="button" (click)="modal.show()">test</button>
<app-modal #modal>
<div class="app-modal-header">
header
</div>
<div class="app-modal-body">
Whatever content you like, form fields, anything
</div>
<div class="app-modal-footer">
<button type="button" class="btn btn-default" (click)="modal.hide()">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</app-modal>
`
})
export class AppComponent {
}
#Component({
selector: 'app-modal',
template: `
<div (click)="onContainerClicked($event)" class="modal fade" tabindex="-1" [ngClass]="{'in': visibleAnimate}"
[ngStyle]="{'display': visible ? 'block' : 'none', 'opacity': visibleAnimate ? 1 : 0}">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<ng-content select=".app-modal-header"></ng-content>
</div>
<div class="modal-body">
<ng-content select=".app-modal-body"></ng-content>
</div>
<div class="modal-footer">
<ng-content select=".app-modal-footer"></ng-content>
</div>
</div>
</div>
</div>
`
})
export class ModalComponent {
public visible = false;
public visibleAnimate = false;
public show(): void {
this.visible = true;
setTimeout(() => this.visibleAnimate = true, 100);
}
public hide(): void {
this.visibleAnimate = false;
setTimeout(() => this.visible = false, 300);
}
public onContainerClicked(event: MouseEvent): void {
if ((<HTMLElement>event.target).classList.contains('modal')) {
this.hide();
}
}
}
To show the backdrop, you'll need something like this CSS:
.modal {
background: rgba(0,0,0,0.6);
}
The example now allows for multiple modals at the same time. (see the onContainerClicked() method).
For Bootstrap 4 css users, you need to make 1 minor change (because a css class name was updated from Bootstrap 3). This line:
[ngClass]="{'in': visibleAnimate}" should be changed to:
[ngClass]="{'show': visibleAnimate}"
To demonstrate, here is a plunkr
Here's a pretty decent example of how you can use the Bootstrap modal within an Angular2 app on GitHub.
The gist of it is that you can wrap the bootstrap html and jquery initialization in a component. I've created a reusable modal component that allows you to trigger an open using a template variable.
<button type="button" class="btn btn-default" (click)="modal.open()">Open me!</button>
<modal #modal>
<modal-header [show-close]="true">
<h4 class="modal-title">I'm a modal!</h4>
</modal-header>
<modal-body>
Hello World!
</modal-body>
<modal-footer [show-default-buttons]="true"></modal-footer>
</modal>
You just need to install the npm package and register the modal module in your app module:
import { Ng2Bs3ModalModule } from 'ng2-bs3-modal/ng2-bs3-modal';
#NgModule({
imports: [Ng2Bs3ModalModule]
})
export class MyAppModule {}
This is a simple approach that does not depend on jquery or any other library except Angular 2.
The component below (errorMessage.ts) can be used as a child view of any other component. It is simply a bootstrap modal that is always open or shown. It's visibility is governed by the ngIf statement.
errorMessage.ts
import { Component } from '#angular/core';
#Component({
selector: 'app-error-message',
templateUrl: './app/common/errorMessage.html',
})
export class ErrorMessage
{
private ErrorMsg: string;
public ErrorMessageIsVisible: boolean;
showErrorMessage(msg: string)
{
this.ErrorMsg = msg;
this.ErrorMessageIsVisible = true;
}
hideErrorMsg()
{
this.ErrorMessageIsVisible = false;
}
}
errorMessage.html
<div *ngIf="ErrorMessageIsVisible" class="modal fade show in danger" id="myModal" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h4 class="modal-title">Error</h4>
</div>
<div class="modal-body">
<p>{{ErrorMsg}}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" (click)="hideErrorMsg()">Close</button>
</div>
</div>
</div>
</div>
This is an example parent control (some non-relevant code has been omitted for brevity):
parent.ts
import { Component, ViewChild } from '#angular/core';
import { NgForm } from '#angular/common';
import {Router, RouteSegment, OnActivate, ROUTER_DIRECTIVES } from '#angular/router';
import { OnInit } from '#angular/core';
import { Observable } from 'rxjs/Observable';
#Component({
selector: 'app-application-detail',
templateUrl: './app/permissions/applicationDetail.html',
directives: [ROUTER_DIRECTIVES, ErrorMessage] // Note ErrorMessage is a directive
})
export class ApplicationDetail implements OnActivate
{
#ViewChild(ErrorMessage) errorMsg: ErrorMessage; // ErrorMessage is a ViewChild
// yada yada
onSubmit()
{
let result = this.permissionsService.SaveApplication(this.Application).subscribe(x =>
{
x.Error = true;
x.Message = "This is a dummy error message";
if (x.Error) {
this.errorMsg.showErrorMessage(x.Message);
}
else {
this.router.navigate(['/applicationsIndex']);
}
});
}
}
parent.html
<app-error-message></app-error-message>
// your html...
Now available as a NPM package
angular-custom-modal
#Stephen Paul continuation...
Angular 2 and up Bootstrap css (animation is preserved)
NO JQuery
NO bootstrap.js
Supports custom modal content
Support for multiple modals on top of each
other.
Moduralized
Disable scroll when modal is open
Modal gets destroyed when navigating away.
Lazy content initialization, which gets ngOnDestroy(ed) when the modal is exited.
Parent scrolling disabled when modal is visible
Lazy content initialization
Why?
In some cases you might not want to modal to retain its status after having been closed, but rather restored to the initial state.
Original modal issue
Passing the content straightforward into the view actually generates initializes it even before the modal gets it. The modal doesn't have a way to kill such content even if using a *ngIf wrapper.
Solution
ng-template. ng-template doesn't render until ordered to do so.
my-component.module.ts
...
imports: [
...
ModalModule
]
my-component.ts
<button (click)="reuseModal.open()">Open</button>
<app-modal #reuseModal>
<ng-template #header></ng-template>
<ng-template #body>
<app-my-body-component>
<!-- This component will be created only when modal is visible and will be destroyed when it's not. -->
</app-my-body-content>
<ng-template #footer></ng-template>
</app-modal>
modal.component.ts
export class ModalComponent ... {
#ContentChild('header') header: TemplateRef<any>;
#ContentChild('body') body: TemplateRef<any>;
#ContentChild('footer') footer: TemplateRef<any>;
...
}
modal.component.html
<div ... *ngIf="visible">
...
<div class="modal-body">
ng-container *ngTemplateOutlet="body"></ng-container>
</div>
References
I have to say that it wouldn't have been possible without the excellent official and community documentation around the net. It might help some of you too to understand better how ng-template, *ngTemplateOutlet and #ContentChild work.
https://angular.io/api/common/NgTemplateOutlet
https://blog.angular-university.io/angular-ng-template-ng-container-ngtemplateoutlet/
https://medium.com/claritydesignsystem/ng-content-the-hidden-docs-96a29d70d11b
https://netbasal.com/understanding-viewchildren-contentchildren-and-querylist-in-angular-896b0c689f6e
https://netbasal.com/understanding-viewchildren-contentchildren-and-querylist-in-angular-896b0c689f6e
Full copy-paste solution
modal.component.html
<div
(click)="onContainerClicked($event)"
class="modal fade"
tabindex="-1"
[ngClass]="{'in': visibleAnimate}"
[ngStyle]="{'display': visible ? 'block' : 'none', 'opacity': visibleAnimate ? 1 : 0}"
*ngIf="visible">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<ng-container *ngTemplateOutlet="header"></ng-container>
<button class="close" data-dismiss="modal" type="button" aria-label="Close" (click)="close()">×</button>
</div>
<div class="modal-body">
<ng-container *ngTemplateOutlet="body"></ng-container>
</div>
<div class="modal-footer">
<ng-container *ngTemplateOutlet="footer"></ng-container>
</div>
</div>
</div>
</div>
modal.component.ts
/**
* #Stephen Paul https://stackoverflow.com/a/40144809/2013580
* #zurfyx https://stackoverflow.com/a/46949848/2013580
*/
import { Component, OnDestroy, ContentChild, TemplateRef } from '#angular/core';
#Component({
selector: 'app-modal',
templateUrl: 'modal.component.html',
styleUrls: ['modal.component.scss'],
})
export class ModalComponent implements OnDestroy {
#ContentChild('header') header: TemplateRef<any>;
#ContentChild('body') body: TemplateRef<any>;
#ContentChild('footer') footer: TemplateRef<any>;
public visible = false;
public visibleAnimate = false;
ngOnDestroy() {
// Prevent modal from not executing its closing actions if the user navigated away (for example,
// through a link).
this.close();
}
open(): void {
document.body.style.overflow = 'hidden';
this.visible = true;
setTimeout(() => this.visibleAnimate = true, 200);
}
close(): void {
document.body.style.overflow = 'auto';
this.visibleAnimate = false;
setTimeout(() => this.visible = false, 100);
}
onContainerClicked(event: MouseEvent): void {
if ((<HTMLElement>event.target).classList.contains('modal')) {
this.close();
}
}
}
modal.module.ts
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
import { ModalComponent } from './modal.component';
#NgModule({
imports: [
CommonModule,
],
exports: [ModalComponent],
declarations: [ModalComponent],
providers: [],
})
export class ModalModule { }
I use ngx-bootstrap for my project.
You can find the demo here
The github is here
How to use:
Install ngx-bootstrap
Import to your module
// RECOMMENDED (doesn't work with system.js)
import { ModalModule } from 'ngx-bootstrap/modal';
// or
import { ModalModule } from 'ngx-bootstrap';
#NgModule({
imports: [ModalModule.forRoot(),...]
})
export class AppModule(){}
Simple static modal
<button type="button" class="btn btn-primary" (click)="staticModal.show()">Static modal</button>
<div class="modal fade" bsModal #staticModal="bs-modal" [config]="{backdrop: 'static'}"
tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title pull-left">Static modal</h4>
<button type="button" class="close pull-right" aria-label="Close" (click)="staticModal.hide()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
This is static modal, backdrop click will not close it.
Click <b>×</b> to close modal.
</div>
</div>
</div>
</div>
Here is my full implementation of modal bootstrap angular2 component:
I assume that in your main index.html file (with <html> and <body> tags) at the bottom of <body> tag you have:
<script src="assets/js/jquery-2.1.1.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
modal.component.ts:
import { Component, Input, Output, ElementRef, EventEmitter, AfterViewInit } from '#angular/core';
declare var $: any;// this is very importnant (to work this line: this.modalEl.modal('show')) - don't do this (becouse this owerride jQuery which was changed by bootstrap, included in main html-body template): let $ = require('../../../../../node_modules/jquery/dist/jquery.min.js');
#Component({
selector: 'modal',
templateUrl: './modal.html',
})
export class Modal implements AfterViewInit {
#Input() title:string;
#Input() showClose:boolean = true;
#Output() onClose: EventEmitter<any> = new EventEmitter();
modalEl = null;
id: string = uniqueId('modal_');
constructor(private _rootNode: ElementRef) {}
open() {
this.modalEl.modal('show');
}
close() {
this.modalEl.modal('hide');
}
closeInternal() { // close modal when click on times button in up-right corner
this.onClose.next(null); // emit event
this.close();
}
ngAfterViewInit() {
this.modalEl = $(this._rootNode.nativeElement).find('div.modal');
}
has(selector) {
return $(this._rootNode.nativeElement).find(selector).length;
}
}
let modal_id: number = 0;
export function uniqueId(prefix: string): string {
return prefix + ++modal_id;
}
modal.html:
<div class="modal inmodal fade" id="{{modal_id}}" tabindex="-1" role="dialog" aria-hidden="true" #thisModal>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header" [ngClass]="{'hide': !(has('mhead') || title) }">
<button *ngIf="showClose" type="button" class="close" (click)="closeInternal()"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
<ng-content select="mhead"></ng-content>
<h4 *ngIf='title' class="modal-title">{{ title }}</h4>
</div>
<div class="modal-body">
<ng-content></ng-content>
</div>
<div class="modal-footer" [ngClass]="{'hide': !has('mfoot') }" >
<ng-content select="mfoot"></ng-content>
</div>
</div>
</div>
</div>
And example of usage in client Editor component:
client-edit-component.ts:
import { Component } from '#angular/core';
import { ClientService } from './client.service';
import { Modal } from '../common';
#Component({
selector: 'client-edit',
directives: [ Modal ],
templateUrl: './client-edit.html',
providers: [ ClientService ]
})
export class ClientEdit {
_modal = null;
constructor(private _ClientService: ClientService) {}
bindModal(modal) {this._modal=modal;}
open(client) {
this._modal.open();
console.log({client});
}
close() {
this._modal.close();
}
}
client-edit.html:
<modal [title]='"Some standard title"' [showClose]='true' (onClose)="close()" #editModal>{{ bindModal(editModal) }}
<mhead>Som non-standart title</mhead>
Some contents
<mfoot><button calss='btn' (click)="close()">Close</button></mfoot>
</modal>
Ofcourse title, showClose, <mhead> and <mfoot> ar optional parameters/tags.
Check ASUI dialog which create at runtime. There is no need of hide and show logic. Simply service will create a component at runtime using AOT
ASUI NPM
try to use ng-window, it's allow developer to open and full control multiple windows in single page applications in simple way, No Jquery, No Bootstrap.
Avilable Configration
Maxmize window
Minimize window
Custom size,
Custom posation
the window is dragable
Block parent window or not
Center the window or not
Pass values to chield window
Pass values from chield window to parent window
Listening to closing chield window in parent window
Listen to resize event with your custom listener
Open with maximum size or not
Enable and disable window resizing
Enable and disable maximization
Enable and disable minimization
Angular 7 + NgBootstrap
A simple way of opening modal from main component and passing result back to it. is what I wanted. I created a step-by-step tutorial which includes creating a new project from scratch, installing ngbootstrap and creation of Modal. You can either clone it or follow the guide.
Hope this helps new to Angular.!
https://github.com/wkaczurba/modal-demo
Details:
modal-simple template (modal-simple.component.html):
<ng-template #content let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">Are you sure?</h4>
<button type="button" class="close" aria-label="Close" (click)="modal.dismiss('Cross click')">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p>You have not finished reading my code. Are you sure you want to close?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="modal.close('yes')">Yes</button>
<button type="button" class="btn btn-outline-dark" (click)="modal.close('no')">No</button>
</div>
</ng-template>
The modal-simple.component.ts:
import { Component, OnInit, ViewChild, Output, EventEmitter } from '#angular/core';
import { NgbModal } from '#ng-bootstrap/ng-bootstrap';
#Component({
selector: 'app-modal-simple',
templateUrl: './modal-simple.component.html',
styleUrls: ['./modal-simple.component.css']
})
export class ModalSimpleComponent implements OnInit {
#ViewChild('content') content;
#Output() result : EventEmitter<string> = new EventEmitter();
constructor(private modalService : NgbModal) { }
open() {
this.modalService.open(this.content, {ariaLabelledBy: 'modal-simple-title'})
.result.then((result) => { console.log(result as string); this.result.emit(result) },
(reason) => { console.log(reason as string); this.result.emit(reason) })
}
ngOnInit() {
}
}
Demo of it (app.component.html) - simple way of dealing with return event:
<app-modal-simple #mymodal (result)="onModalClose($event)"></app-modal-simple>
<button (click)="mymodal.open()">Open modal</button>
<p>
Result is {{ modalCloseResult }}
</p>
app.component.ts - onModalClosed is executed once modal is closed:
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
modalCloseResult : string;
title = 'modal-demo';
onModalClose(reason : string) {
this.modalCloseResult = reason;
}
}
Cheers