File

projects/common/lib/components/consent-modal/consent-modal.component.ts

Description

ConsentModalComponent, aka the "Information Collection Notice", is a modal designed to be shown at the beginning of an application. It is a boilerplate checkbox to consent to information collection.

Currently this component requires the body to be manually set as a child element (via transclusion)

TODO - Set default body if none is passed in.

Extends

AbstractHttpService

Implements

ControlValueAccessor OnInit AfterViewInit

Example

<common-consent-modal #consentModal [isUnderMaintenance]="false"
       title="Information collection notice"
       agreeLabel="I have read and understand this information"
       processName="processName"
       (accept)="accountLetterApplication.infoCollectionAgreement = $event;  saveApplication($event)">
   <p><strong>Keep your personal information secure – especially when using a shared device like a computer at a library, school or café.</strong> To delete any information that was entered, either complete the application and submit it or, if you don’t finish, close the web browser.</p><p><strong>Need to take a break and come back later?</strong> The data you enter on this form is saved locally to the computer or device you are using until you close the web browser or submit your application.</p><p><strong>Information in this application is collected by the Ministry of Health</strong> under section 26(a), (c) and (e) of the Freedom of Information and Protection of Privacy Act and will be used to determine eligibility for provincial health care benefits in BC and administer Premium Assistance. Should you have any questions about the collection of this personal information please <a href="http://www2.gov.bc.ca/gov/content/health/health-drug-coverage/msp/bc-residents-contact-us" target="_blank">contact Health Insurance BC <i class="fa fa-external-link" aria-hidden="true"></i></a>.</p>
</common-consent-modal>


// Component code - Remove backticks when copying (necessary to render docs)
`@ViewChild('consentModal') consentModal: ConsentModalComponent`
...
openModal(){
this.consentModal.showFullSizeView();
}

Metadata

providers { provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => ConsentModalComponent) }
selector common-consent-modal
styleUrls ./consent-modal.component.scss
templateUrl ./consent-modal.component.html
viewProviders { provide: ControlContainer, useExisting: forwardRef(() => NgForm) }

Index

Properties
Methods
Inputs
Outputs
HostListeners

Constructor

constructor(http: HttpClient, logService: CommonLogger)
Parameters :
Name Type Optional
http HttpClient No
logService CommonLogger No

Inputs

agreeLabel
Type : string
Default value : 'I have read and understand this info'
body
Type : string
continueButton
Type : string
Default value : 'Continue'
disableContinue
Type : boolean
Default value : false

Used in cases where we have custom form controls inside NgContent that we wish to be satisifed before user can continue through modal.

isUnderMaintenance
Type : boolean
Default value : false

If isUnderMaintenance is true, then this will automatically try and make a request to the SPA ENV server to determine if it's in a maintenance window. If your application determines this manually, leave this alone.

maintenanceFlag
Type : string
Default value : 'false'
processName
Type : string
title
Type : string
url
Type : string
Default value : '/msp/api/env'

Outputs

accept
Type : EventEmitter
close
Type : EventEmitter
cutOffDate
Type : EventEmitter<ISpaEnvResponse>

HostListeners

window:keydown
Arguments : '$event'
window:keydown(event: KeyboardEvent)

Methods

continue
continue()
Returns : void
Protected handleError
handleError(error: HttpErrorResponse)
Parameters :
Name Type Optional
error HttpErrorResponse No
Returns : any
handleTab
handleTab()
Returns : void
handleTabBackwards
handleTabBackwards()
Returns : void
inMaintenance
inMaintenance()
Returns : void
isContinueDisabled
isContinueDisabled()
Returns : boolean
ngAfterViewInit
ngAfterViewInit()
Returns : void
ngOnInit
ngOnInit()
Returns : void
registerOnChange
registerOnChange(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
registerOnTouched
registerOnTouched(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
sendSpaEnvServer
sendSpaEnvServer(rapidResponseCode: string)
Parameters :
Name Type Optional
rapidResponseCode string No
Returns : Observable<any>
showFullSizeView
showFullSizeView()

Call this method to display the modal.

Returns : void
writeValue
writeValue(value: any)
Parameters :
Name Type Optional
value any No
Returns : void
Protected generateUUID
generateUUID()
Inherited from AbstractHttpService
Returns : any
Protected get
get(url, queryParams?: HttpParams)
Inherited from AbstractHttpService
Type parameters :
  • T

Makes a GET request to the specified URL, using headers and HTTP options specified in their respective methods.

Parameters :
Name Type Optional Description
url No

Target URL to make the GET request

queryParams HttpParams Yes
Returns : Observable<T>
Protected Abstract handleError
handleError(error: HttpErrorResponse)
Inherited from AbstractHttpService

Handles all failed requests that throw either a server error (400/500) or a client error (e.g. lost internet).

Parameters :
Name Type Optional
error HttpErrorResponse No
Returns : any
Protected post
post(url, body)
Inherited from AbstractHttpService
Type parameters :
  • T
Parameters :
Name Optional
url No
body No
Returns : Observable<T>
Protected setupRequest
setupRequest(observable: Observable)
Inherited from AbstractHttpService
Type parameters :
  • T
Parameters :
Name Type Optional
observable Observable<any> No
Returns : Observable<T>
Protected uploadAttachment
uploadAttachment(relativeUrl: string, attachment: CommonImage)
Inherited from AbstractHttpService

Uploads an individual attachment. All you need to do is set the url. Note: urls often include UUIDs, so this must be an application decision.

Parameters :
Name Type Optional Description
relativeUrl string No

URL to hit, must include UUIDs of application and CommonImage

attachment CommonImage No

CommonImage to upload

Returns : any

Properties

Private _applicationHeaderMap
Type : Map<string | string>
Default value : new Map([ ['ACL', '{"SPA_ENV_ACL_MAINTENANCE_FLAG":"","SPA_ENV_ACL_MAINTENANCE_MESSAGE":""}'], ['MSP', '{"SPA_ENV_MSP_MAINTENANCE_FLAG":"","SPA_ENV_MSP_MAINTENANCE_MESSAGE":""}'], ['PA', '{"SPA_ENV_PACUTOFF_MAINTENANCE_START":"","SPA_ENV_PACUTOFF_MAINTENANCE_END":"","SPA_ENV_NOW":"","SPA_ENV_PACUTOFF_MAINTENANCE_FLAG":"","SPA_ENV_PACUTOFF_MAINTENANCE_MESSAGE":""}'], ['SUPPBEN', '{"SPA_ENV_SUPPBEN_MAINTENANCE_START":"","SPA_ENV_SUPPBEN_MAINTENANCE_END":"","SPA_ENV_NOW":"","SPA_ENV_SUPPBEN_MAINTENANCE_FLAG":"","SPA_ENV_SUPPBEN_MAINTENANCE_MESSAGE":"","SPA_ENV_PACUTOFF_MAINTENANCE_START":"","SPA_ENV_PACUTOFF_MAINTENANCE_END":""}'], ])
Protected _headers
Type : HttpHeaders
Default value : new HttpHeaders()
Public _onChange
Default value : () => {...}
Public _onTouched
Default value : () => {...}
agreeCheck
Type : boolean
Default value : false
Protected closed
Type : boolean
Default value : false
Public continueButtonRef
Type : ElementRef
Decorators :
@ViewChild('continueButtonRef')
Protected focusableEls
Type : HTMLElement[]
Protected focusedEl
Type : HTMLElement
Public fullSizeViewModal
Type : ModalDirective
Decorators :
@ViewChild('fullSizeViewModal')
Public maintenanceMessage
Type : string
Public modalContents
Type : ElementRef
Decorators :
@ViewChild('modalContents')
Public spaEnvRes
Type : ISpaEnvResponse
Default value : {} as any
Protected Abstract _headers
Type : HttpHeaders
Inherited from AbstractHttpService

The headers to send along with every GET and POST.

Protected logHTTPRequestsToConsole
Type : boolean
Default value : false
Inherited from AbstractHttpService
import { forwardRef, Component, EventEmitter, Input, Output, ViewChild, OnInit, HostListener, AfterViewInit, ElementRef} from '@angular/core';
import { ModalDirective } from 'ngx-bootstrap/modal';
import { Observable, of } from 'rxjs';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Response } from '@angular/http';
import { CommonLogger } from '../../services/logger.service';
import { AbstractHttpService } from '../../services/abstract-api-service';
import { ControlContainer, ControlValueAccessor, NgForm, NG_VALUE_ACCESSOR } from '@angular/forms';


/**
 * Consent Modal is a Modal with the Information or Notice. It can be used to get the User's consent an
 * then proceed with the application. It also makes an API call to the SPA-ENV server to see if the app is under
 * maintenance.
 *
 *
 * @example
*       	<common-consent-modal #mspConsentModal body='Body Of Consent'
*               title='Notice' [application]="mspAccountApp"
*               processName='MSP'
*               agreeLabel='I have read and understand this info'
*               (onClose)="addressChangeChkBx.focus()">
*           </common-consent-modal>
 * @export
 */

export interface ISpaEnvResponse {
  SPA_ENV_MSP_MAINTENANCE_FLAG: string;
  SPA_ENV_MSP_MAINTENANCE_MESSAGE: string;
  SPA_ENV_ACL_MAINTENANCE_FLAG: string;
  SPA_ENV_ACL_MAINTENANCE_MESSAGE: string;
  SPA_ENV_SUPPBEN_MAINTENANCE_FLAG: string;
  SPA_ENV_SUPPBEN_MAINTENANCE_MESSAGE: string;
  SPA_ENV_SUPPBEN_MAINTENANCE_START: string;
  SPA_ENV_SUPPBEN_MAINTENANCE_END: string;
  SPA_ENV_PACUTOFF_MAINTENANCE_FLAG: string;
  SPA_ENV_PACUTOFF_MAINTENANCE_MESSAGE: string;
  SPA_ENV_PACUTOFF_MAINTENANCE_START: string;
  SPA_ENV_NOW: string;
  SPA_ENV_PACUTOFF_MAINTENANCE_END: string;
}

// Disabling tslint for @example below.
// tslint:disable:max-line-length

/**
 * ConsentModalComponent, aka the "Information Collection Notice", is a modal
 * designed to be shown at the beginning of an application. It is a boilerplate
 * checkbox to consent to information collection.
 *
 * Currently this component requires the body to be manually set as a child
 * element (via transclusion)
 *
 * TODO - Set default body if none is passed in.
 *
 * @example
 *       <common-consent-modal #consentModal [isUnderMaintenance]="false"
 *                          title="Information collection notice"
 *                          agreeLabel="I have read and understand this information"
 *                          processName="processName"
 *                          (accept)="accountLetterApplication.infoCollectionAgreement = $event;  saveApplication($event)">
 *                      <p><strong>Keep your personal information secure – especially when using a shared device like a computer at a library, school or café.</strong> To delete any information that was entered, either complete the application and submit it or, if you don’t finish, close the web browser.</p><p><strong>Need to take a break and come back later?</strong> The data you enter on this form is saved locally to the computer or device you are using until you close the web browser or submit your application.</p><p><strong>Information in this application is collected by the Ministry of Health</strong> under section 26(a), (c) and (e) of the Freedom of Information and Protection of Privacy Act and will be used to determine eligibility for provincial health care benefits in BC and administer Premium Assistance. Should you have any questions about the collection of this personal information please <a href="http://www2.gov.bc.ca/gov/content/health/health-drug-coverage/msp/bc-residents-contact-us" target="_blank">contact Health Insurance BC <i class="fa fa-external-link" aria-hidden="true"></i></a>.</p>
 *       </common-consent-modal>
 *
 *
 *        // Component code - Remove backticks when copying (necessary to render docs)
 *        `@ViewChild('consentModal') consentModal: ConsentModalComponent`
 *        ...
 *        openModal(){
 *          this.consentModal.showFullSizeView();
 *        }
 */
@Component({
  selector: 'common-consent-modal',
  templateUrl: './consent-modal.component.html',
  styleUrls: ['./consent-modal.component.scss'],
  viewProviders: [
    { provide: ControlContainer, useExisting: forwardRef(() => NgForm ) }
  ],
  providers: [
    { provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => ConsentModalComponent )}
  ]
})

export class ConsentModalComponent extends AbstractHttpService implements ControlValueAccessor, OnInit, AfterViewInit  {

  protected _headers: HttpHeaders = new HttpHeaders();
  @Input() processName: string;

  /**
   * If `isUnderMaintenance` is true, then this will automatically try and
   * make a request to the SPA ENV server to determine if it's in a
   * maintenance window.  If your application determines this manually, leave
   * this alone.
   */
  @Input() isUnderMaintenance: boolean = false;
  @Input() title: string;
  @Input() body: string; // = '<p><strong>Keep your personal information secure – especially when using a shared device like a computer at a library, school or café.</strong> To delete any information that was entered, either complete the application and submit it or, if you don’t finish, close the web browser.</p><p><strong>Need to take a break and come back later?</strong> The data you enter on this form is saved locally to the computer or device you are using until you close the web browser or submit your application.</p><p><strong>Information in this application is collected by the Ministry of Health</strong> under section 26(a), (c) and (e) of the Freedom of Information and Protection of Privacy Act and will be used to determine eligibility for provincial health care benefits in BC and administer Premium Assistance. Should you have any questions about the collection of this personal information please <a href="http://www2.gov.bc.ca/gov/content/health/health-drug-coverage/msp/bc-residents-contact-us" target="_blank">contact Health Insurance BC <i class="fa fa-external-link" aria-hidden="true"></i></a>.</p>';
  @Input() agreeLabel: string = 'I have read and understand this info';
  @Input() continueButton: string = 'Continue';
  @Input() maintenanceFlag: string = 'false';
  @Input() url: string = '/msp/api/env';
  @ViewChild('fullSizeViewModal') public fullSizeViewModal: ModalDirective;
  @ViewChild('modalContents') public modalContents: ElementRef;
  @ViewChild('continueButtonRef') public continueButtonRef: ElementRef;
  @Output() close = new EventEmitter<void>();
  @Output() cutOffDate: EventEmitter<ISpaEnvResponse> = new EventEmitter<ISpaEnvResponse>();
  @Output() accept = new EventEmitter<boolean>();

  /**
   * Used in cases where we have custom form controls inside NgContent that we
   * wish to be satisifed before user can continue through modal.
   */
  @Input() disableContinue: boolean = false;

  public spaEnvRes: ISpaEnvResponse = {} as any;
  public maintenanceMessage: string;
  protected focusableEls: HTMLElement[];
  protected focusedEl: HTMLElement;
  protected closed: boolean = false;

  // TODO: This should eventually be pulled out of the common library as it pertains to MSP-specific code.
  // tslint:disable-next-line:max-line-length
  private _applicationHeaderMap: Map<string, string> = new Map([
    ['ACL', '{"SPA_ENV_ACL_MAINTENANCE_FLAG":"","SPA_ENV_ACL_MAINTENANCE_MESSAGE":""}'],
    ['MSP', '{"SPA_ENV_MSP_MAINTENANCE_FLAG":"","SPA_ENV_MSP_MAINTENANCE_MESSAGE":""}'],
    ['PA', '{"SPA_ENV_PACUTOFF_MAINTENANCE_START":"","SPA_ENV_PACUTOFF_MAINTENANCE_END":"","SPA_ENV_NOW":"","SPA_ENV_PACUTOFF_MAINTENANCE_FLAG":"","SPA_ENV_PACUTOFF_MAINTENANCE_MESSAGE":""}'],
    ['SUPPBEN', '{"SPA_ENV_SUPPBEN_MAINTENANCE_START":"","SPA_ENV_SUPPBEN_MAINTENANCE_END":"","SPA_ENV_NOW":"","SPA_ENV_SUPPBEN_MAINTENANCE_FLAG":"","SPA_ENV_SUPPBEN_MAINTENANCE_MESSAGE":"","SPA_ENV_PACUTOFF_MAINTENANCE_START":"","SPA_ENV_PACUTOFF_MAINTENANCE_END":""}'],
  ]);
  agreeCheck: boolean = false;

  public _onChange = (_: any) => {};
  public _onTouched = () => {};

  constructor(protected http: HttpClient,  private logService: CommonLogger) {
      super(http);
  }

  ngOnInit(): void {
    // Called after ngOnInit when the component's or directive's content has been initialized.
    // Add 'implements AfterContentInit' to the class.
    if (this.isUnderMaintenance) {
      this.inMaintenance();
    }
  }

  ngAfterViewInit(): void {
    // Create an array of focusable elements from the contents of the modal
    this.focusableEls = Array.from(this.modalContents.nativeElement.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]'));
    // Initialize to the first focusable element
    this.focusedEl = this.focusableEls[0];
    this.focusedEl.focus();
    this.fullSizeViewModal.config.backdrop = true;

  }

  @HostListener('window:keydown', ['$event'])
  handleKeyDown(event: KeyboardEvent) {
    // Check that the modal is open
    if (!this.closed) {
      // Handle tabbing
      if (event.key === 'Tab') {
        // Prevent usual tabbing, manually set focus
        event.preventDefault();
        if (event.shiftKey) {
          this.handleTabBackwards();
        } else {
          this.handleTab();
        }

      // Stop users from being able to escape the modal
      } else if (event.key === 'Escape') {
        event.preventDefault();
      }
    }
  }

  // Api callout to get the message from the Rapid code
  sendSpaEnvServer(rapidResponseCode: string): Observable<any> {
    this._headers = new HttpHeaders({
      'SPA_ENV_NAME': rapidResponseCode
    });
    return this.post<any>(this.url, null);
  }

  // Move to next focusable element, if at last element, move to first
  handleTab() {
    const position = this.focusableEls.indexOf(this.focusedEl);
    if (position === this.focusableEls.length - 1) {
      this.focusedEl = this.focusableEls[0];
    } else {
      this.focusedEl = this.focusableEls[position + 1];
    }
    this.focusedEl.focus();
  };

  // Move to next focusable element, if at last element, move to first
  handleTabBackwards() {
    const position = this.focusableEls.indexOf(this.focusedEl);
    if (position === 0) {
      this.focusedEl = this.focusableEls[this.focusableEls.length - 1];
    } else {
      this.focusedEl = this.focusableEls[position - 1];
    }
    this.focusedEl.focus();
  };

  /**
   * Call this method to display the modal.
   */
  showFullSizeView() {
      this.fullSizeViewModal.config.keyboard = false;
      this.fullSizeViewModal.show();
  }

  continue() {
      this.accept.emit(true);
      this.fullSizeViewModal.hide();
      this.close.emit();
      this.closed = true;
      this._onChange(true);
      this._onTouched();
  }

    protected handleError(error: HttpErrorResponse) {
      if (error.error instanceof ErrorEvent) {
          // Client-side / network error occured
          console.error('MspMaintenanceService error: ', error.error.message);
      } else {
          // The backend returned an unsuccessful response code
          console.error(`MspMaintenanceService Backend returned error code: ${error.status}.  Error body: ${error.error}`);
      }

    // A user facing erorr message /could/ go here; we shouldn't log dev info through the throwError observable
    return of(error);
  }


  inMaintenance() {
        const headerName = this._applicationHeaderMap.get(this.processName);

        this.sendSpaEnvServer(headerName)
                .subscribe(response => {
                    this.spaEnvRes = <ISpaEnvResponse> response;
                    // TODO: This should eventually be pulled out of the common library as it pertains to MSP-specific code.
                    if (this.spaEnvRes.SPA_ENV_ACL_MAINTENANCE_FLAG === 'true') {
                        this.maintenanceFlag = 'true';
                        this.maintenanceMessage = this.spaEnvRes.SPA_ENV_ACL_MAINTENANCE_MESSAGE;
                    } else if (this.spaEnvRes.SPA_ENV_MSP_MAINTENANCE_FLAG === 'true') {
                        this.maintenanceFlag = 'true';
                        this.maintenanceMessage =  this.spaEnvRes.SPA_ENV_MSP_MAINTENANCE_MESSAGE;
                    } else if (this.spaEnvRes.SPA_ENV_PACUTOFF_MAINTENANCE_FLAG === 'true') {
                        this.maintenanceFlag = 'true';
                        this.maintenanceMessage = this.spaEnvRes.SPA_ENV_PACUTOFF_MAINTENANCE_MESSAGE;
                    } else if (this.spaEnvRes.SPA_ENV_SUPPBEN_MAINTENANCE_FLAG === 'true') {
                        this.maintenanceFlag = 'true';
                        this.maintenanceMessage = this.spaEnvRes.SPA_ENV_SUPPBEN_MAINTENANCE_MESSAGE;
                    }
                    if (this.spaEnvRes.SPA_ENV_PACUTOFF_MAINTENANCE_START) {
                        this.cutOffDate.emit(this.spaEnvRes);
                    }

            }, (error: Response | any) => {
                this.logService.log({
                  event: 'ACL - SPA Env System Error',
                  success: false,
                  errMsg: 'ACL - SPA Env Rapid Response Error' + error
                });
        }

      );
  }

  registerOnChange(fn: any): void {
    this.accept.emit(fn) ;
    this._onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this._onTouched = fn;
  }

  writeValue(value: any): void {}

  isContinueDisabled(): boolean {
    const disabled = !this.agreeCheck || this.disableContinue;

    const hasTabbableContinue = this.focusableEls ? this.focusableEls.indexOf(this.continueButtonRef.nativeElement) !== -1 : false;

    // If it is tabbable but no longer should be, remove it
    if (hasTabbableContinue && disabled) this.focusableEls.pop();
    //  If it's not tabbable but it now should be, add it
    if (!hasTabbableContinue && !disabled) this.focusableEls.push(this.continueButtonRef.nativeElement);

    return disabled;
  }
}
<div bsModal #fullSizeViewModal="bs-modal" class="modal fade" tabindex="-1" role="dialog"
      data-keyboard="false"
     aria-labelledby="myLargeModalLabel" aria-hidden="true"
     [config]="{backdrop: 'static', ignoreBackdropClick: true}">

  <div class="modal-dialog" style="margin-top: 90px;">
    <div #modalContents class="modal-content">
      <!--  Header -->
      <div class="modal-header modal-header-primary">
        <h2 id="myLargeModalLabel">{{maintenanceFlag == 'true' ? 'Maintenance notice' : title}}</h2>
      </div>

      <!--   Modal Body   -->
      <div class="modal-body">

        <div *ngIf="maintenanceFlag == 'true'">
          <h4>{{maintenanceMessage}}</h4>
        </div>
        <div *ngIf="maintenanceFlag != 'true'">
            <ng-content></ng-content>
        <br>
        <div *ngIf="maintenanceFlag != 'true'">
          <div class="form-check form-check-inline">
            <input [ngModelOptions]="{standalone: true}" class="input-lg form-check-input" id="agree" type="checkbox"
              [(ngModel)]="agreeCheck" (ngModelChange)='accept.emit($event);'  >
            <label class="form-check-label" for="agree"><strong>{{agreeLabel}}</strong></label>
          </div>
        </div>
      </div>

      <!-- Footer -->
      <div *ngIf="maintenanceFlag != 'true'" class="modal-footer">
        <button #continueButtonRef [disabled]="isContinueDisabled()" class="btn btn-block btn-primary pull-left"
                type="submit" (click)="continue()">{{continueButton}}</button>

      </div>
    </div>
  </div>
</div>
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""