I have an angular2 component which I have included below. I generate a list of chapters which I then display with an *ngFor= tag, but I want to be able to individually target these in my ng2 component (so I can highlight the selected chapter). I would of thought the below code would generate something like this:
<p class="chapter 1" #1>1. My First Chapter</p>
However, I don't get the #1, hence my selector doesn't work and I can't by default set the first chapter in the list to be selected.
import { Component, ViewChild, ElementRef, AfterViewInit } from '#angular/core';
#Component({
selector: 'tutorial',
template: `
<div class="content row">
<div class="chapters col s3">
<h3>Chapters:</h3>
<p *ngFor="let chapter of chapters" class="chapter" #{{chapter.number}}>{{chapter.number}}. {{chapter.title}}</p>
</div>
</div>
`
})
export class bookComponent implements AfterViewInit {
public chapters = _chapters;
#ViewChild('2') el:ElementRef;
ngAfterViewInit() {
this.el.nativeElement.className += " clicked";
}
}
What should I do to be able to individually select my generated <p> tags?
For you use case this might be a more angulary way
<p *ngFor="let chapter of chapters; let i=index" (click)="clickedItem = i" [class.clicked]="i == clickedItem" class="chapter" #{{chapter.number}}>{{chapter.number}}. {{chapter.title}}</p>
export class bookComponent implements AfterViewInit {
public chapters = _chapters;
clickedItem: number;
}
Updating the model and binding the view to make Angular reflect the model to the view is the preferred way instead of imperatively modifying the DOM.
I would let the NgFor loop control adding or removing the clicked class:
<p *ngFor="let chapter of chapters" class="chapter"
[class.clicked]="chapter.number === selectedChapterNumber">
{{chapter.number}}. {{chapter.title}}
</p>
Then just set selectedChapterNumber appropriately in your component logic.
export class bookComponent {
public chapters = _chapters;
private selectedChapterNumber = 1;
}
You can use directive with HostListener to select an element as shown below.
Working Demo : http://plnkr.co/edit/mtmCKg7kPgZoerqT0UIO?p=preview
import {Directive, Attribute,ElementRef,Renderer,Input,HostListener} from '#angular/core';
#Directive({
selector: '[myAttr]'
})
export class myDir {
constructor(private el:ElementRef,private rd: Renderer){
console.log('fired');
console.log(el.nativeElement);
}
#HostListener('click', ['$event.target'])
onClick(btn) {
if(this.el.nativeElement.className=="selected"){
this.el.nativeElement.className ="";
}else{
this.el.nativeElement.className ="selected";
}
}
}
//our root app component
import {Component} from '#angular/core';
#Component({
selector: 'my-app',
directives:[myDir],
template:
`
<style>
.selected{
color:red;
background:yellow;
}
</style>
<div class="content row">
<div class="chapters col s3">
<h3>Chapters:</h3>
<p myAttr *ngFor="let chapter of chapters" class="chapter" #{{chapter.number}}>{{chapter.number}}. {{chapter.title}}</p>
</div>
</div>
`
})
export class App {
chapters=[{number:1,title:"chapter1"},{number:2,title:"chapter2"},{number:3,title:"chapter3"}]
}
Related
I'm looking to transfer the value of an array between two components. My application is a basic crud application that will store accounts with an id, name and date.
I have a service that has 2 basic functions and an array to pull from -
The array is holding the accounts. It is acting as my database for right now.
account-crud.service.ts
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http'
import { Account } from '../models/Account';
import { AddAccountComponent } from '../dashboard/add-account/add-account.component';
#Injectable({
providedIn: 'root'
})
export class AccountCrudService {
private accounts:Account[] = [];
addAccount(account: Account){
this.accounts.push(account);
};
getAccounts(): Account[] {
console.log(this.getAccounts)
return this.accounts;
}
}
This service is used by 2 components. The desired behavior is for me to use the AddAccountComponent to add components, which will then push to the array in our service and then display all the accounts in the AllAccountsComponent.
Here is the add account component -
add-account.component.ts
#Component({
selector: 'app-add-account',
standalone: true,
imports: [FormsModule, ReactiveFormsModule, JsonPipe, RouterModule],
providers: [AccountCrudService],
templateUrl: './add-account.component.html',
styleUrls: ['./add-account.component.scss']
})
export class AddAccountComponent {
constructor(titleService:Title, private AccountCrudService: AccountCrudService) {
titleService.setTitle("Add Account")
}
public printAccounts() {
console.log('accounts :');
console.log(this.AccountCrudService.getAccounts());
}
private account:Account = {id: 0, name:'', date:''};
public get Account():Account{
return this.account;
}
public addAccount(){
const currentAccount:Account = {
id: this.account.id,
name: this.account.name,
date: this.account.date
};
this.AccountCrudService.addAccount(currentAccount);
}
And here is the HTML for the add account component -
add-account.component.html
<form #addAccountForm="ngForm">
<label for="name">Account Name</label>
<input name="accountName" type="text" [(ngModel)]="Account.name">
<br>
<label for="date">Initial Email Date</label>
<input name="date" [(ngModel)]="Account.date">
<br>
<button type="submit" (click)="addAccount()">Submit</button>
<button type="button" (click)="printAccounts()">console log accts</button>
<button type="button" routerLink="/dashboard">back</button>
<!-- this is for debugging purposes -->
{{ addAccountForm.value | json }}
</form>
And this component is where I plan to display all the accounts from that array. The line that I suspect is not working is the
this.accounts = this.AccountCrudService.getAccounts();
line in the getAllAccounts() function. -
all-accounts.component.ts
#Component({
selector: 'app-all-accounts',
templateUrl: './all-accounts.component.html',
styleUrls: ['./all-accounts.component.scss'],
imports: [AccountCardComponent, RouterModule, CommonModule, AddAccountComponent, FormsModule],
providers: [AccountCrudService],
standalone: true
})
export class AllAccountsComponent {
accounts: Account[];
constructor(titleService:Title, public AccountCrudService: AccountCrudService) {
titleService.setTitle("Dashboard")
this.getAllAccounts();
}
//this is a debugging function
public printAccounts(){
console.log('accounts :');
console.log(this.AccountCrudService.getAccounts());
}
public getAllAccounts() {
//I want the value of accounts to be equal to the value of getAccounts()
this.accounts = this.AccountCrudService.getAccounts();
return this.accounts;
}
}
all-accounts.component.html
<div class="all-accounts-header">
<h1>All Accounts</h1>
<div class="all-accounts-options">
<button class="all-accounts-addBtn" routerLink="/add-account">Add</button>
</div>
</div>
<br>
<div class="all-accounts-cards">
<app-account-card [accounts]='accounts'></app-account-card>
</div>
I send this array to the account-card component where it uses an ngFor to display all the accounts. This functionality works when given a full array of accounts manually.
from account-card.component
<div *ngFor="let account of accounts" class="account-card-container">
<div class="account-card-name">
<div class="account-card-header">
<h2>{{ account.name }}</h2>
<div class="account-card-date">
<p></p>
</div>
</div>
</div>
And here is the .ts file.
account-card.component.ts
#Component({
selector: 'app-account-card',
templateUrl: './account-card.component.html',
styleUrls: ['./account-card.component.scss'],
imports: [AllAccountsComponent, CommonModule],
providers: [AccountCrudService],
standalone: true
})
export class AccountCardComponent {
#Input() accounts: any[];
}
When I console log the array in the add-account component, it displays my array just fine. But once I do it anywhere else, the whole array is gone.
I have used #Input() to try and input the array directly from the service, but that didn't work.
Any help is appreciated. Thanks!
While working on Angular 2 form submit I run into a problem. When I create an object inside a component everything works well and my form gets submit via post method. But when I am using an object from a class outside the component my form sends a get request with url http://localhost:4200/blog?title=sss&content=ssssss
Does anyone know why this is happening?
Template:
<form (ngSubmit)="onSubmit()" #f="ngForm">
<!-- <form #f="ngForm" (ngSubmit)="onSubmit(f)">-->
<div class="form-group">
<label for="title">Tytuł</label>
<textarea class="form-control" id="title" rows="1"
ngModel name = "title" required minlength="3" #title="ngModel"></textarea>
<span class="help-block" *ngIf="!title.valid && title.touched">Wprowadzono za krótki tekst (minum to 3 znaki).</span>
<label for="content">Zawartość:</label>
<textarea class="form-control" id="content" rows="3"
ngModel name = "content" required minlength="3" #content="ngModel"></textarea>
<span class="help-block" *ngIf="!content.valid && content.touched">Wprowadzono za krótki tekst (minum to 3 znaki).</span>
</div>
<button type="submit" class="btn btn-primary"
[disabled] ="!f.valid"
>Wyślij</button>
</form>
Component:
import {Component, OnInit, ViewChild} from '#angular/core';
import {NgForm} from "#angular/forms";
import {Blog} from "../../shared/blog";
import {BlogService} from "../../services/blog.service";
#Component({
selector: 'app-blog-form',
templateUrl: './blog-form.component.html',
styleUrls: ['./blog-form.component.css']
})
export class BlogFormComponent implements OnInit {
#ViewChild('f') form: NgForm;
errorMessage: string;
/* this works well
blog = {
title: '',
content: '',
dateCreated: ''
}*/
//this doesn't work
blog: Blog;
ngOnInit(){}
onSubmit(){
this.blog.title = this.form.value.title;
this.blog.content = this.form.value.content;
}
}
The Blog class. I tried both this:
export class Blog {
constructor(public title = '', public content = '', public dateCreated = ''){}}
And this:
export class Blog {
constructor(public title : string, public content : string, public dateCreated : string){}}
Thanks for any help :)
I am not sure why this is happening but try not using this.form.value.
onSubmit(){
this.blog.title = this.form.title;
this.blog.content = this.form.content;
console.log(this.blog);
}
Use of value posts back your page. Now this code should work.
I'm trying achieve a nested form with validation in Angular 2, I've seen posts and followed the documentation but I'm really struggling, hope you can point me in the right direction.
What I am trying to achieve is having a validated form with multiple children components. These children components are a bit complex, some of them have more children components, but for the sake of the question I think we can attack the problem having a parent and a children.
What am I trying to accomplish
Having a form that works like this:
<div [formGroup]="userForm" novalidate>
<div>
<label>User Id</label>
<input formControlName="userId">
</div>
<div>
<label>Dummy</label>
<input formControlName="dummyInput">
</div>
</div>
This requires having a class like this:
private userForm: FormGroup;
constructor(private fb: FormBuilder){
this.createForm();
}
private createForm(): void{
this.userForm = this.fb.group({
userId: ["", Validators.required],
dummyInput: "", Validators.required]
});
}
This works as expected, but now I want to decouple the code, and put the "dummyInput" functionality in a separate, different component. This is where I get lost. This is what I tried, I think I'm not far from getting the answer, but I'm really out of ideas, I'm fairly new to the scene:
parent.component.html
<div [formGroup]="userForm" novalidate>
<div>
<label>User Id</label>
<input formControlName="userId">
</div>
<div>
<dummy></dummy>
</div>
</div>
parent.component.ts
private createForm(): void{
this.userForm = this.fb.group({
userId: ["", Validators.required],
dummy: this.fb.group({
dummyInput: ["", Validators.required]
})
});
children.component.html
<div [formGroup]="dummyGroup">
<label>Dummy Input: </label>
<input formControlName="dummyInput">
</div>
children.component.ts
private dummyGroup: FormGroup;
I know something is not right with the code, but I'm really in a roadblock. Any help would be aprreciated.
Thanks.
you can add an Input in your children component to pass the FormGroup to it.and use FormGroupName to pass the name of your FormGroup :)
children.component.ts
#Input('group');
private dummyGroup: FormGroup;
parent.component.html
<div [formGroup]="userForm" novalidate>
<div>
<label>User Id</label>
<input formControlName="userId">
</div>
<div formGroupName="dummy">
<dummy [group]="userForm.controls['dummy']"></dummy>
</div>
</div>
Not going to lie, don't know how I didn't find this post earlier.
Angular 2: Form containing child component
The solution is to bind the children component to the same formGroup, by passing the formGroup from the parent to the children as an Input.
If anyone shares a piece of code to solve the problem in other way, I'll gladly accept it.
The main idea is that you have to treat the formGroup and formControls as variables, mainly javascript objects and arrays.
So I'll put some code in to make my point. The code below is somewhat like what you have. The form is constructed dynamically, just that it is split into sections, each section containing its share of fields and labels.
The HTML is backed up by typescript classes. Those are not here as they do not have much special. Just the FormSchemaUI, FormSectionUI and FormFieldUI are important.
Treat each piece of code as its own file.
Also please take note that formSchema: FormSchema is a JSON object that I receive from a service. Any properties of the UI classes that you do not see defined are inherited from their base Data clases. Those are not presented here.
The hierarchy is: FormSchema contains multiple sections. A section contains multiple fields.
<form (ngSubmit)="onSubmit()" #ciRegisterForm="ngForm" [formGroup]="formSchemaUI.MainFormGroup">
<button kendoButton (click)="onSubmit(ciRegisterForm)" [disabled]="!canSubmit()"> Register {{registerPageName}} </button>
<br /><br />
<app-ci-register-section *ngFor="let sectionUI of formSchemaUI.SectionsUI" [sectionUI]="sectionUI">
</app-ci-register-section>
<button kendoButton (click)="onSubmit(ciRegisterForm)" [disabled]="!canSubmit()"> Register {{registerPageName}} </button>
</form>
=============================================
<div class="row" [formGroup]="sectionUI.MainFormGroup">
<div class="col-md-12 col-lg-12" [formGroupName]="sectionUI.SectionDisplayId">
<fieldset class="section-border">
<legend class="section-border">{{sectionUI.Title}}</legend>
<ng-container *ngFor='let fieldUI of sectionUI.FieldsUI; let i=index; let even = even;'>
<div class="row" *ngIf="even">
<ng-container>
<div class="col-md-6 col-lg-6" app-ci-field-label-tuple [fieldUI]="fieldUI">
</div>
</ng-container>
<ng-container *ngIf="sectionUI.Fields[i+1]">
<div class="col-md-6 col-lg-6" app-ci-field-label-tuple [fieldUI]="sectionUI.FieldsUI[i+1]">
</div>
</ng-container>
</div>
</ng-container>
</fieldset>
</div>
</div>
=============================================
{{fieldUI.Label}}
=============================================
<ng-container>
<div class="row">
<div class="col-md-4 col-lg-4 text-right">
<label for="{{fieldUI.FieldDisplayId}}"> {{fieldUI.Label}} </label>
</div>
<div class="col-md-8 col-lg-8">
<div app-ci-field-edit [fieldUI]="fieldUI" ></div>
</div>
</div>
</ng-container>
=============================================
<ng-container [formGroup]="fieldUI.ParentSectionFormGroup">
<ng-container *ngIf="fieldUI.isEnabled">
<ng-container [ngSwitch]="fieldUI.ColumnType">
<input *ngSwitchCase="'HIDDEN'" type="hidden" id="{{fieldUI.FieldDisplayId}}" [value]="fieldUI.Value" />
<ci-field-textbox *ngSwitchDefault
[fieldUI]="fieldUI"
(valueChange)="onValueChange($event)"
class="fullWidth" style="width:100%">
</ci-field-textbox>
</ng-container>
</ng-container>
</ng-container>
=============================================
export class FormSchemaUI extends FormSchema {
SectionsUI: Array<FormSectionUI>;
MainFormGroup: FormGroup;
static fromFormSchemaData(formSchema: FormSchema): FormSchemaUI {
let formSchemaUI = new FormSchemaUI(formSchema);
formSchemaUI.SectionsUI = new Array<FormSectionUI>();
formSchemaUI.Sections.forEach(section => {
let formSectionUI = FormSectionUI.fromFormSectionData(section);
formSchemaUI.SectionsUI.push(formSectionUI);
});
formSchemaUI.MainFormGroup = FormSchemaUI.buildMainFormGroup(formSchemaUI);
return formSchemaUI;
}
static buildMainFormGroup(formSchemaUI: FormSchemaUI): FormGroup {
let obj = {};
formSchemaUI.SectionsUI.forEach(sectionUI => {
obj[sectionUI.SectionDisplayId] = sectionUI.SectionFormGroup;
});
let sectionFormGroup = new FormGroup(obj);
return sectionFormGroup;
}
}
=============================================
export class FormSectionUI extends FormSection {
constructor(formSection: FormSection) {
this.SectionDisplayId = 'section' + this.SectionId.toString();
}
SectionDisplayId: string;
FieldsUI: Array<FormFieldUI>;
HiddenFieldsUI: Array<FormFieldUI>;
SectionFormGroup: FormGroup;
MainFormGroup: FormGroup;
ParentFormSchemaUI: FormSchemaUI;
static fromFormSectionData(formSection: FormSection): FormSectionUI {
let formSectionUI = new FormSectionUI(formSection);
formSectionUI.FieldsUI = new Array<FormFieldUI>();
formSectionUI.HiddenFieldsUI = new Array<FormFieldUI>();
formSectionUI.Fields.forEach(field => {
let fieldUI = FormFieldUI.fromFormFieldData(field);
if (fieldUI.ColumnType != 'HIDDEN')
formSectionUI.FieldsUI.push(fieldUI);
else formSectionUI.HiddenFieldsUI.push(fieldUI);
});
formSectionUI.SectionFormGroup = FormSectionUI.buildFormSectionFormGroup(formSectionUI);
return formSectionUI;
}
static buildFormSectionFormGroup(formSectionUI: FormSectionUI): FormGroup {
let obj = {};
formSectionUI.FieldsUI.forEach(fieldUI => {
obj[fieldUI.FieldDisplayId] = fieldUI.FieldFormControl;
});
let sectionFormGroup = new FormGroup(obj);
return sectionFormGroup;
}
}
=============================================
export class FormFieldUI extends FormField {
constructor(formField: FormField) {
super();
this.FieldDisplayId = 'field' + this.FieldId.toString();
this.ListItems = new Array<SelectListItem>();
}
public FieldDisplayId: string;
public FieldFormControl: FormControl;
public ParentSectionFormGroup: FormGroup;
public MainFormGroup: FormGroup;
public ParentFormSectionUI: FormSectionUI;
public ValueChange: EventEmitter<any> = new EventEmitter<any>();
static buildFormControl(formFieldUI:FormFieldUI): FormControl {
let nullValidator = Validators.nullValidator;
let fieldKey: string = formFieldUI.FieldDisplayId;
let fieldValue: any;
switch (formFieldUI.ColumnType) {
default:
fieldValue = formFieldUI.Value;
break;
}
let isDisabled = !formFieldUI.IsEnabled;
let validatorsArray: ValidatorFn[] = new Array<ValidatorFn>();
let asyncValidatorsArray: AsyncValidatorFn[] = new Array<AsyncValidatorFn>();
let formControl = new FormControl({ value: fieldValue, disabled: isDisabled }, validatorsArray, asyncValidatorsArray);
return formControl;
}
}
To get a reference to the parent form simply use this (maybe not available in Angular 2. I've tested it with Angular 6):
TS
import {
FormGroup,
ControlContainer,
FormGroupDirective,
} from "#angular/forms";
#Component({
selector: "app-leveltwo",
templateUrl: "./leveltwo.component.html",
styleUrls: ["./leveltwo.component.sass"],
viewProviders: [
{
provide: ControlContainer,
useExisting: FormGroupDirective
}
]
})
export class NestedLevelComponent implements OnInit {
//form: FormGroup;
constructor(private parent: FormGroupDirective) {
//this.form = form;
}
}
HTML
<input type="text" formControlName="test" />
import { Directive } from '#angular/core';
import { ControlContainer, NgForm } from '../../../node_modules/#angular/forms';
#Directive({
selector: '[ParentProvider]',
providers: [
{
provide: ControlContainer,
useFactory: function (form: NgForm) {
return form;
},
deps: [NgForm]
}`enter code here`
]
})
export class ParentProviderDirective {
constructor() { }
}
<div ParentProvider >
for child
</div>
An alternative to the FormGroupDirective (as described in #blacksheep's answer) is the use of ControlContainer like so:
import { FormGroup, ControlContainer } from "#angular/forms";
export class ChildComponent implements OnInit {
formGroup: FormGroup;
constructor(private controlContainer: ControlContainer) {}
ngOnInit() {
this.formGroup = <FormGroup>this.controlContainer.control;
}
The formGroup can be set in the direct parent or further up (parent's parent, for example). This makes it possible to pass a from group over various nested components, without the need for a chain of #Input()s to pass the formGroup along. In any parent set the formGroup to make it available via ControlContainer in the child:
<... [formGroup]="myFormGroup">
I have a button that trigger a modal using ng-bootstrap modal
<button (click)="openModal()">Open</button>
the template of modal.html with sortable component inside is:
<template #modalContent let-c="close" let-d="dismiss">
<bs-sortable #sortableComponent [(ngModel)]="array
[itemTemplate]="itemTemplate"></bs-sortable>
</template>
<template #itemTemplate let-item="item" let-index="index">
<div>{{item | json}}
<span class="fa fa-trash" (click)="removeItem(array,index)"></span>
</div>
</template>
the class will be:
import { NgbModal } from "#ng-bootstrap/ng-bootstrap";
export class sortableModal{
#Input() public array: [];
#ViewChild("modalContent") public modalContent: NgbModalModule;
#ViewChild("sortableComponent") sortableComponent: SortableComponent;
constructor( public modalService: NgbModal ){
}
openModal(){
this.modalService.open(this.modalContent);
}
removeItem(arr,i){
if(i===undefined) i = -1;
arr.splice(i,1);
this.sortableComponent.writeValue(arr);
//this.sortableComponent is undefined; why is that?
}
}
I still don't know why this is happening. But I solved it by wrap ng2-bootstrap sortable component into your own component, MySortableComponent, which has sortable component in. It works.
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