Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

The Recipe for Angular 2 in a Java EE Environment: Frontend 2

DZone's Guide to

The Recipe for Angular 2 in a Java EE Environment: Frontend 2

In this article, we go over creating the Angular UI using the HTTP Service, with its types, the Detailview, with its validations, and then run a few tests.

· Web Dev Zone
Free Resource

Should you build your own web experimentation solution? Download this whitepaper by Optimizely to find out.

This is the fourth article in our "The Recipe for Angular 2 in a Java EE Environment" series, with a more detailed description of the Frontend of the Angular2 and JavaEE Project. It follows my third article about Angular 2 and Java EE that gave a detailed overview of the Frontend infrastructure of the Car Rental Project.

In this article about the Angular UI, we'll go over the HTTP Service with its Types, the Detailview with its Validations, and 2 Tests, which are shown.

The Service uses Observables that have Typescript Interfaces to type the responses. Observables are used because they can process single responses and streams of responses. 

The DetailView has Input Validation that is not declarative in order to get the flexibility to validate over several values and write your own flexible validation functions. 

The Test checks the validation function with its input values. The other Validation is tested to show that multiple values can be validated at once. The component is run in a testbed and tested for its values.

The Tests are executed by Angular CLI with Karma(ng test). In the Car Rental Project, the tests are executed in Maven on PhantomJS during the build.

RestService

The RestService is in crrest.service.ts and the Types are in crTypes.ts

@Injectable()
export class CrRestService {

    private _crTableUrlProd = '/rest/model/crTable/mietNr/{mietNr}';  // URL to web api
    private _crDetailUrlProd = '/rest/model/crDetail/mietNr/{mietNr}/jahr/{jahr}';
    private _crTableUrlDev = 'http://localhost:8080/carrental-web/rest/model/crTable/mietNr/{mietNr}';  // URL to web api
    private _crDetailUrlDev = 'http://localhost:8080/carrental-web/rest/model/crDetail/mietNr/{mietNr}/jahr/{jahr}';
    private _reqOptionsArgs: RequestOptionsArgs = { headers: new Headers() };

    constructor( private http: Http, private pl: PlatformLocation ) {
        this._reqOptionsArgs.headers.set( 'Content-Type', 'application/json' );
    }

    getCrTableRows( policeNr: string ) {
        let url = environment.production ? this.pl.getBaseHrefFromDOM() + this._crTableUrlProd : this._crTableUrlDev;
        console.log( url );
        if ( environment.production ) {
            let start = url.indexOf( "/crlist/" );
            console.log( "start=" + start );
            if ( start > -1 ) {
                let end = url.indexOf( "/rest/" );
                url = url.substring( 0, start ) + url.substring( end, url.length );
            } else {
                url = this._crTableUrlProd.substring( 1 );
                console.log( url );
            }
        }
        url = url.replace( "{mietNr}", policeNr );
        return this.http.get( url, this._reqOptionsArgs ).map( res => this.unpackTableResponse( res ) ).catch( this.handleError );
    }

    getCrDetail( policeNr: string, jahr: string ) {
        let url = environment.production ? this.pl.getBaseHrefFromDOM() + this._crDetailUrlProd : this._crDetailUrlDev;
        console.log( url );
        if ( environment.production ) {
            let start = url.indexOf( "/crdetail/" );
            console.log( "start=" + start );
            if ( start > -1 ) {
                let end = url.indexOf( "/rest/" );
                url = url.substring( 0, start ) + url.substring( end, url.length );
            } else {
                url = this._crDetailUrlProd.substring( 1 );
                console.log( url );
            }
        }
        url = url.replace( "{mietNr}", policeNr ).replace( "{jahr}", jahr );
        return this.http.get( url, this._reqOptionsArgs ).map( res => this.unpackDetailResponse( res ) ).catch( this.handleError );
    }

    private unpackTableResponse( res: Response ): CrTableRow[] {
        //console.log(res);     
        return <CrTableRow[]>res.json();
    }

    private unpackDetailResponse( res: Response ): CrDetail {
        //console.log(res);     
        return <CrDetail>res.json();
    }

    private handleError( error: Response ) {
        // in a real world app, we may send the error to some remote logging infrastructure
        // instead of just logging it to the console
        console.error( JSON.stringify( error ) );
        return Observable.throw( error );
    }
}

Line 1: the Annotation Injectable() does what the name says. You can use the service in your components as a constructor argument.  

Lines 4-7: the URLs that the RestService uses for the dev environment and prod environment. It is possible to do the configuration in Angular CLI(Proxy config) but this demonstrates how to use the environment. 

Lines 10-12: the Constructor gets the HTTP Service and the PlatformLocation injected by Angular. The required Content-Type is set in Line 11.

Lines 14-30: We see here the method that gets the Values for the ListView. In Line 15, we see the switch between prod and dev URLs. In the Lines 16-27, some weird results of getBaseHrefFromDOM() on one Browser in the prod environment are dealt with. In Line 28, the placeholder mietnr is replaced with the number. 

Line 29: the Observer that calls the RestService. It gets the URL with the HTTP Options and maps the results in typed Objects. Exceptions are caught and logged to the Console with the JSON String. 

Line 32-48: does the same as getTableRows(...), but for the DetailView.

DetailView

The Values Component is in the Files crvalues.component.ts and crvalues.component.html.

<form novalidate [formGroup]="form">
    <table class="table borderless">
        <tr>
            <th></th>
            <th>{{'ANZFAHRZEUGE' | translate}}</th>
            <th>{{'MIETEGEPLANT' | translate}}</th>
            <th>{{'MIETEABGERECHNET' | translate}}</th>
        </tr>
        <tr>
            <td>{{'FRAUEN' | translate}}</td>
            <td>
                <div [ngClass]="{'form-group': true, 'has-error': !form.valid}">
                    <input type="text" class="form-control" formControlName="anzahlPkw" />
                </div>
            </td>
            <td>
                <div class="divboxmodel">{{crvalues.mieteGeplantPkw}}</div>
            </td>
            <td>
                <div [ngClass]="{'form-group': true, 'has-error': !form.valid}">
                    <input type="text" class="form-control" formControlName="mieteAbgerechnetPkw" />
                </div>
            </td>
        </tr>
        <tr>
            <td>{{'MAENNER' | translate}}</td>
            <td>
                <div [ngClass]="{'form-group': true, 'has-error': !form.valid}">
                    <input type="text" class="form-control" formControlName="anzahlLkw" />
                </div>
            </td>
            <td>
                <div class="divboxmodel">{{crvalues.mieteGeplantPkw}}</div>
            </td>
            <td>
                <div [ngClass]="{'form-group': true, 'has-error': !form.valid}">
                    <input type="text" class="form-control" formControlName="mieteAbgerechnetLkw" />
                </div>
            </td>
        </tr>
        <tr>
            <td>{{'TOTAL' | translate}}</td>
            <td>
                <div class="divboxmodel">{{crvalues.anzahlTotal}}</div>
            </td>
            <td>
                <div class="divboxmodel">{{crvalues.mieteGeplantTotal}}</div>
            </td>
            <td>
                <div class="divboxmodel">{{crvalues.mieteAbgerechnetTotal}}</div>
            </td>
        </tr>
    </table>
</form>
{{form.valid}}

Line 1: the formGroup is defined. The form is the FormGroup defined in the Component for Validation. 

Line 10: we have the translated Heading. 

Line 11: we have the div with "'has-error': !form.valid" that shows the div as invalid(red border) if the input does not validate. The 'formControlName="anzahlPkw"' in the input element connects the input to the formcontrol for validation. 

Line 12: shows the number of planned rented cars.

Line 29: shows the value of the  form.valid that is the boolean logic that shows if the form is valid or not. 

@Component({    
    selector: 'app-crvalues',
    templateUrl: './crvalues.component.html',
    styleUrls: ['./crvalues.component.css']         
})
export class CrValuesComponent implements OnInit, OnDestroy {
    form: FormGroup;
    fcNames = ['anzahlPkw', 'anzahlLkw', 'mieteAbgerechnetPkw', 'mieteAbgerechnetLkw'];
    @Input() crvalues: CrPortfolio;
    updateTotalsSub: Subscription[] = [];

    constructor(fb: FormBuilder) {
        this.form = fb.group({
            anzahlPkw: ['', CrValuesValidators.positiveIntValidator],
            mieteAbgerechnetPkw: ['', CrValuesValidators.positiveIntValidator],
            anzahlLkw: ['', CrValuesValidators.positiveIntValidator],
            mieteAbgerechnetLkw: ['', CrValuesValidators.positiveIntValidator]
        },{
            validator: this.validate.bind(this)
        });
    }

    ngOnInit() {
        let fc = <FormControl>this.form.controls[this.fcNames[0]];
        fc.setValue(this.crvalues.anzahlPkw);        
        this.updateTotalsSub.push(fc.valueChanges.subscribe((value) => this.updateTotals(value)));      
        fc = <FormControl>this.form.controls[this.fcNames[1]];
        fc.setValue(this.crvalues.anzahlLkw);        
        this.updateTotalsSub.push(fc.valueChanges.subscribe((value) => this.updateTotals(value)));
        fc = <FormControl>this.form.controls[this.fcNames[2]];
        fc.setValue(this.crvalues.mieteAbgerechnetPkw);
        this.updateTotalsSub.push(fc.valueChanges.subscribe((value) => this.updateTotals(value)));        
        fc = <FormControl>this.form.controls[this.fcNames[3]];
        fc.setValue(this.crvalues.mieteAbgerechnetLkw);  
        this.updateTotalsSub.push(fc.valueChanges.subscribe((value) => this.updateTotals(value)));    
        this.crvalues.mieteGeplantTotal = this.crvalues.mieteGeplantPkw + this.crvalues.mieteGeplantLkw;
        this.updateTotals(null);
    }

    ngOnDestroy() {
        for(let sub of this.updateTotalsSub) {
            sub.unsubscribe();
        }
    }

    updateTotals(value: any): void {
        this.crvalues.anzahlTotal = parseInt(this.form.controls[this.fcNames[0]].value)  + parseInt(this.form.controls[this.fcNames[1]].value);
        this.crvalues.mieteAbgerechnetTotal = parseInt(this.form.controls[this.fcNames[2]].value) + parseInt(this.form.controls[this.fcNames[3]].value);
        //console.log("updateTotals("+value+") called.");
    }

    validate(group: FormGroup): boolean {
        let myFcNames = ['anzahlPkw', 'anzahlLkw', 'mieteAbgerechnetPkw', 'mieteAbgerechnetLkw'];
//        myFcNames.forEach(key => console.log(group.controls[key].value));
//        console.log('-----------------------');
        let invalidFcs =  myFcNames.filter(fcn => !CrValuesComponent.validNumber(group.controls[fcn].value));
        let valid = invalidFcs.length === 0;
        return valid;
    }

    static validNumber(value: AbstractControl): boolean {
        if(!value) return false;
        let myValue = parseInt(value.value, 20);
        let ret = isNaN(myValue);
        if (!ret && myValue >= 0) {
            ret = true;
        } else {
            ret = false;
        }
        return ret;
    }
}

Line 7: defines the FormGroup.

Line 8: defines an Array of the names of the Formcontrols.

Line 9: defines the Input Value of the Component that provides the initial values.

Line 10: defines the Array for the Subscriptions for the ValueChanges.

Lines 12-13: defines a Constructor that gets a Formbuilder injected to create a Form with the FormControls. 

Line 14: defines a FormControl 'anzahlPkw' with the initial value '' and the Validator CrValuesValidators.positiveIntValidator. The Validator checks if the number is a positive integer. If one Validation of a Form fails the hole Form is invalid. 

Line 19: defines a FormGroup Validator that can access all of the form controls.

Line 21: defines ngOnInit() that is the Method to do the initialization of a Component.

Line 22: gets the Formcontrol 'anzahlPkw' of the form.

Line 23: sets the Value of the Input Parameter of the Component as the value of the Formcontrol. 

Line 24: subscribes the updateTotals method to the Valuechanges of the Formcontrol  to recalculate the total after each value change. The subscription is pushed to the updateTotalsSub Array.

Lines 38-42: defines the ngDestroy() method to unsubscribe the Valuechange Subscribtions. That is necessary because Angular cannot clean them up automatically.

Lines 44-48: defines the updateTotals() method that calculates the Totals of the values of the Formcontrols.

Line 53: defines a Validator that gets the FormGroup as Parameter. That enables Validators to check dependencies between FormControls during validation.

Line 57: filters for all FormControls that contain invalid numbers. 

Line 58: checks if the list of invalid numbers is empty.

Lines 62-72: defines the static method validNumber that checks the validity of the number.

Validation Test

The Values Component is tested for the validation with the crvalues.component.spec.ts.

describe('Component: CrValues', () => {
  it('validate should return false', () => {
    let component = new CrValuesComponent(new FormBuilder());
    let param = new CrPortfolioImpl(null, null, null, null);
    component.crvalues = param;
    component.ngOnInit();
    expect(!component.form.valid).toBeTruthy();
  });

  it('validate should return true', () => {
    let component = new CrValuesComponent(new FormBuilder());
    let param = new CrPortfolioImpl(1, 1, 1, 1);
    component.crvalues = param;
    component.ngOnInit();
    expect(component.form.valid).toBeTruthy();
  });

  it('validate should return false', () => {
    let component = new CrValuesComponent(new FormBuilder());
    let param = new CrPortfolioImpl(-1, -1, -1, -1);
    component.crvalues = param;
    component.ngOnInit();
    expect(!component.form.valid).toBeTruthy();
  });

//  it('make tests fail', () => {
//      expect(false).toBeTruthy();
//  })
}); 

class CrPortfolioImpl implements CrPortfolio {

    constructor(anzahlPkw: number, anzahlLkw: number, mieteAbgerechnetPkw: number, mieteAbgerechnetLkw: number) {
        this.anzahlPkw = anzahlPkw;
        this.anzahlLkw = anzahlLkw;
        this.mieteAbgerechnetPkw = mieteAbgerechnetPkw;
        this.mieteAbgerechnetLkw = mieteAbgerechnetLkw;
    }

    id: string;  
    bezeichnung: string; 
    anzahlPkw: number;
    anzahlLkw: number;
    anzahlTotal: number;
    mieteGeplantPkw: number;
    mieteGeplantLkw: number;
    mieteGeplantTotal: number;
    mieteAbgerechnetPkw: number;
    mieteAbgerechnetLkw: number;
    mieteAbgerechnetTotal: number;
}

Line 1: sets up the testsuite with describe.

Line 2: sets up the test with it.

Line 3: creates the Angular Component to be tested. The Constructor needs the Formbuilder that would be injected by Angular.

Line 4: creates the Input Parameter of the Component with the CrPortfolioImpl Class. 

Line 5: sets the Input Parameter of the Component.

Line 6: calls ngOnInit() of the Component to initialize the Form and FormControls.

Line 7: checks that the form is invalid. 

The next two Tests are similar.

Testing the Root Component

The app.component.ts Component has a test too. It is app.component.spec.ts. I will not describe it here. It is just an example that the Root Component can be tested too. 

Conclusion

The RestServices can be called in a Typesave way with Observables. Angular provides a flexible Validation infrastructure that can be used to write Form Validations that validate values dependent on each other. The Validations can be tested by Angular CLI and the Maven build. This time it is not so similar, but Observables are available in RxJava for Java Developers. The Code still looks familiar and the Concepts of Angular are easy to understand. The Gap to the Java World is small.

Upcoming

This is the end of the of the description of the Angular UI of the Angular2AndJavaEE Project. In the next Article will describe the Maven build of the Project that is integrated with Angular/CLI and PhantomJS.

Angular2AndJavaEE now has a Project Diary that shows the current state of the Project and the features we have planned for the future.

Implementing an Experimentation Solution: Choosing whether to build or buy?

Topics:
java ee ,angular 2 ,typescript ,maven ,web dev

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}