Skip to content

Instantly share code, notes, and snippets.

@petyosi
Last active August 30, 2022 08:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save petyosi/088a41fddbd981b62a90b8e56b0432e9 to your computer and use it in GitHub Desktop.
Save petyosi/088a41fddbd981b62a90b8e56b0432e9 to your computer and use it in GitHub Desktop.
crud-app-final
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {HttpModule} from '@angular/http';
import {FormsModule} from '@angular/forms';
import {AgGridModule} from 'ag-grid-angular';
import {AthleteService} from './services/athlete.service';
import {StaticDataService} from './services/static-data.service';
import {AppComponent} from './app.component';
import {GridComponent} from './grid/grid.component';
import {AthleteEditScreenComponent} from './athlete-edit-screen/athlete-edit-screen.component';
@NgModule({
declarations: [
AppComponent,
GridComponent,
AthleteEditScreenComponent
],
imports: [
BrowserModule,
HttpModule,
FormsModule,
AgGridModule.withComponents([])
],
providers: [
AthleteService,
StaticDataService
],
bootstrap: [AppComponent]
})
export class AppModule {
}
.input-panel {
position: absolute;
border-radius: 5px;
border: solid lightgray 1px;
padding: 15px;
background-color: whitesmoke;
}
.submit-pane {
overflow: hidden;
margin-top: 10px;
}
<div class="input-panel" [style.width]="width" [style.top]="top" [style.left]="left" #panel>
<div style="display: inline-block">
<div style="float: left">
Name: <input [(ngModel)]="name"/>
</div>
<div style="float: left; padding-left: 10px">
Country:
<select [(ngModel)]="country" [compareWith]="countryComparator">
<option disabled selected>Country...</option>
<option *ngFor="let country of countries" [ngValue]="country">{{ country.name }}</option>
</select>
</div>
</div>
<div>
<button (click)="insertNewResult()" class="action-button">Insert New Result</button>
<ag-grid-angular style="width: 100%; height: 200px;"
class="ag-theme-balham"
[columnDefs]="columnDefs"
[rowData]="rowData"
(gridReady)="onGridReady($event)"
(rowValueChanged)="onRowValueChanged($event)">
</ag-grid-angular>
</div>
<div class="submit-pane">
<button (click)="saveAthlete()" [disabled]="!isValidAthlete()" class="action-button" style="float: right">Save
Athlete
</button>
</div>
</div>
import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
import {ColDef, ColumnApi, GridApi} from 'ag-grid';
import {StaticDataService} from '../services/static-data.service';
import {Result} from '../model/result.model';
import {Sport} from '../model/sport.model';
import {Country} from '../model/country.model';
import {Athlete} from '../model/athlete.model';
@Component({
selector: 'app-athlete-edit-screen',
templateUrl: './athlete-edit-screen.component.html',
styleUrls: ['./athlete-edit-screen.component.css']
})
export class AthleteEditScreenComponent implements OnInit {
// gridApi and columnApi
private api: GridApi;
private columnApi: ColumnApi;
// static data
private sports: Sport[];
private countries: Country[];
// interim/form data
private name: string;
private country: Country;
private rowData: Result[] = [];
// the results sub-table columns
private columnDefs: ColDef[];
@Input() containerCoords: any = null;
@Input() athlete: Athlete = null;
@Output() onAthleteSaved = new EventEmitter<Athlete>();
// to position this component relative to the containing component
@ViewChild('panel', {read: ElementRef}) public panel;
private width: any;
private left: any;
private top: any;
constructor(staticDataService: StaticDataService) {
staticDataService.countries().subscribe(
countries => this.countries = countries.sort(StaticDataService.alphabeticalSort()),
error => console.log(error)
);
staticDataService.sports().subscribe(
sports => {
// store reference to sports, after sorting alphabetically
this.sports = sports.sort(StaticDataService.alphabeticalSort());
// create the column defs
this.columnDefs = this.createColumnDefs(this.sports)
},
error => console.log(error)
);
}
ngOnInit() {
this.setPanelCoordinates();
if (this.athlete) {
this.name = this.athlete.name;
this.country = this.athlete.country;
this.rowData = this.athlete.results.slice(0);
}
}
countryComparator(c1: Country, c2: Country): boolean {
return c1 && c2 ? c1.id === c2.id : false;
}
insertNewResult() {
// insert a blank new row, providing the first sport as a default in the sport column
const updates = this.api.updateRowData(
{
add: [{
sport: this.sports[0]
}]
}
);
this.api.startEditingCell({
rowIndex: updates.add[0].rowIndex,
colKey: 'age'
});
}
isValidAthlete() {
return this.name && this.name !== '' &&
this.country;
}
saveAthlete() {
const athlete = new Athlete();
athlete.id = this.athlete ? this.athlete.id : null;
athlete.version = this.athlete ? this.athlete.version : undefined;
athlete.name = this.name;
athlete.country = this.country;
athlete.results = [];
this.api.forEachNode((node) => {
const {data} = node;
athlete.results.push(<Result> {
id: data.id,
version: data.version,
age: data.age,
year: data.year,
date: data.date,
bronze: data.bronze,
silver: data.silver,
gold: data.gold,
sport: data.sport
});
});
this.onAthleteSaved.emit(athlete);
}
onGridReady(params): void {
this.api = params.api;
this.columnApi = params.columnApi;
this.api.sizeColumnsToFit();
// temp fix until AG-1181 is fixed
this.api.hideOverlay();
}
// create some simple column definitions
private createColumnDefs(sports: Sport[]) {
return [
{
field: 'age',
editable: true
},
{
field: 'year',
editable: true
},
{
field: 'date',
editable: true
},
{
field: 'bronze',
editable: true
},
{
field: 'silver',
editable: true
},
{
field: 'gold',
editable: true
},
{
field: 'sport',
cellRenderer: (params) => params.data.sport.name,
editable: true,
cellEditor: 'richSelect',
cellEditorParams: {
values: sports,
cellRenderer: (params) => params.value.name
}
}
]
}
private setPanelCoordinates() {
// make our width 100pixels smaller than the container
this.width = (this.containerCoords.width - 100);
// set our left position to be the container left position plus half the difference in widths between this
// component and the container, minus the 15px padding
this.left = Math.floor(this.containerCoords.left + (this.containerCoords.width - this.width) / 2 - 15) + 'px';
// set our left position to be the container top position plus half the difference in height between this
// component and the container
this.top = Math.floor(this.containerCoords.top + (this.containerCoords.height - this.panel.nativeElement.offsetHeight) / 2) + 'px';
// add the px suffix back in (omitted above so that maths can work)
this.width = this.width + 'px'
}
}
import {Country} from './country.model';
import {Result} from './result.model';
export class Athlete {
id: number;
version: number;
name: string;
country: Country;
results: Result[];
}
import {Injectable} from '@angular/core';
import {Athlete} from '../model/athlete.model';
import {Headers, Http, RequestOptions, Response} from '@angular/http';
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/catch';
import {Observable} from 'rxjs/Observable';
@Injectable()
export class AthleteService {
static REQUEST_OPTIONS: RequestOptions = new RequestOptions({headers: new Headers({'Content-Type': 'application/json'})});
private apiRootUrl = 'http://localhost:8080';
private findAllUrl = this.apiRootUrl + '/athletes';
private findByIdUrl = this.apiRootUrl + '/athlete';
private saveUpdateUrl = this.apiRootUrl + '/saveAthlete';
private deleteUrl = this.apiRootUrl + '/deleteAthlete';
constructor(private http: Http) {
}
findAll(): Observable<Athlete[]> {
return this.http.get(this.findAllUrl)
.map((response: Response) => response.json())
.catch(this.defaultErrorHandler());
}
findById(id: number): Observable<Athlete> {
return this.http.get(this.findByIdUrl + '/' + id)
.map((response: Response) => response.json())
.catch(this.defaultErrorHandler());
}
save(athlete: Athlete): Observable<Athlete> {
return this.http.post(this.saveUpdateUrl, athlete, AthleteService.REQUEST_OPTIONS)
.map((response: Response) => response.json())
.catch(this.defaultErrorHandler());
}
delete(athlete: Athlete): Observable<boolean> {
return this.http.post(this.deleteUrl, athlete.id, AthleteService.REQUEST_OPTIONS)
.map((response: Response) => response.json())
.catch(this.defaultErrorHandler());
}
private defaultErrorHandler() {
return (error: any) => {
console.log(error);
return Observable.throw(error.json().error || 'Server error')
};
}
}
import {StaticData} from './static-data.model';
export class Country implements StaticData {
id: number;
name: string;
}
<div>
<button (click)="insertNewRow()" [disabled]="editInProgress" class="action-button">Insert New Row</button>
<button (click)="deleteSelectedRows()" [disabled]="!rowsSelected() || editInProgress" class="action-button">Delete
Selected Row
</button>
</div>
<div>
<ag-grid-angular style="width: 100%; height: 500px;"
class="ag-theme-balham"
#grid
[columnDefs]="columnDefs"
[rowData]="rowData"
rowSelection="multiple"
suppressRowClickSelection
suppressHorizontalScroll
suppressClickEdit
[getRowNodeId]="getRowNodeId"
(gridReady)="onGridReady($event)"
(rowDoubleClicked)="onRowDoubleClicked($event)">
</ag-grid-angular>
</div>
<ng-template [ngIf]="editInProgress">
<app-athlete-edit-screen [athlete]="athleteBeingEdited"
[containerCoords]="containerCoords"
(onAthleteSaved)="onAthleteSaved($event)"></app-athlete-edit-screen>
</ng-template>
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/forkJoin';
import {ColDef, ColumnApi, GridApi} from 'ag-grid';
import {AthleteService} from '../services/athlete.service';
import {Athlete} from '../model/athlete.model';
import {StaticDataService} from '../services/static-data.service';
import {Country} from '../model/country.model';
// we need to import this as we're making use of enterprise features, such as the richSelect cell editor
import 'ag-grid-enterprise';
@Component({
selector: 'app-grid',
templateUrl: './grid.component.html',
styleUrls: ['./grid.component.css']
})
export class GridComponent {
// row data and column definitions
private rowData: Athlete[];
private columnDefs: ColDef[];
// gridApi and columnApi
private api: GridApi;
private columnApi: ColumnApi;
private editInProgress: boolean = false;
private athleteBeingEdited: Athlete = null;
private containerCoords: {} = null;
@ViewChild('grid', {read: ElementRef}) public grid;
// inject the athleteService
constructor(private athleteService: AthleteService,
staticDataService: StaticDataService) {
staticDataService.countries().subscribe(
countries => this.columnDefs = this.createColumnDefs(countries),
error => console.log(error)
);
this.athleteService.findAll().subscribe(
athletes => this.rowData = athletes,
error => console.log(error)
)
}
getRowNodeId(params) {
return params.id;
}
onAthleteSaved(athleteToSave: Athlete) {
this.athleteService.save(athleteToSave)
.subscribe(
savedAthlete => {
console.log('Athlete saved', savedAthlete.name);
const added = [];
const updated = [];
if (athleteToSave.id) {
updated.push(savedAthlete);
} else {
added.push(savedAthlete);
}
this.api.updateRowData(
{
add: added,
update: updated
}
);
},
error => console.log(error)
);
this.athleteBeingEdited = null;
this.editInProgress = false;
}
// one grid initialisation, grab the APIs and auto resize the columns to fit the available space
onGridReady(params): void {
this.api = params.api;
this.columnApi = params.columnApi;
this.api.sizeColumnsToFit();
}
// create some simple column definitions
private createColumnDefs(countries: Country[]) {
return [
{
field: 'name',
editable: true,
checkboxSelection: true
},
{
field: 'country',
cellRenderer: (params) => params.data.country.name,
editable: true,
cellEditor: 'richSelect',
cellEditorParams: {
values: countries,
cellRenderer: (params) => params.value.name
}
},
{
field: 'results',
valueGetter: (params) => params.data.results.length
}
]
}
onRowDoubleClicked(params: any) {
if (this.editInProgress) {
return;
}
this.updateContainerCoords();
this.athleteBeingEdited = <Athlete>params.data;
this.editInProgress = true;
}
insertNewRow() {
this.updateContainerCoords();
this.editInProgress = true;
}
private updateContainerCoords() {
this.containerCoords = {
top: this.grid.nativeElement.offsetTop,
left: this.grid.nativeElement.offsetLeft,
height: this.grid.nativeElement.offsetHeight,
width: this.grid.nativeElement.offsetWidth
};
}
rowsSelected() {
return this.api && this.api.getSelectedRows().length > 0;
}
deleteSelectedRows() {
const selectRows = this.api.getSelectedRows();
// create an Observable for each row to delete
const deleteSubscriptions = selectRows.map((rowToDelete) => {
return this.athleteService.delete(rowToDelete);
});
// then subscribe to these and once all done, update the grid
Observable.forkJoin(...deleteSubscriptions).subscribe(
results => {
// only redraw removed rows...
this.api.updateRowData(
{
remove: selectRows
}
);
}
);
}
}
import {Sport} from './sport.model';
export class Result {
id: number;
version: number;
age: number;
year: number;
date: string;
gold: number;
silver: number;
bronze: number;
sport: Sport;
}
import {StaticData} from './static-data.model';
export class Sport implements StaticData {
id: number;
name: string;
}
export interface StaticData {
name: string;
}
import {Injectable} from '@angular/core';
import {Http, Response} from '@angular/http';
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/catch';
import {Observable} from 'rxjs/Observable';
import {Sport} from '../model/sport.model';
import {Country} from '../model/country.model';
import {StaticData} from '../model/static-data.model';
@Injectable()
export class StaticDataService {
private apiRootUrl = 'http://localhost:8080';
private countriesUrl = this.apiRootUrl + '/countries';
private sportsUrl = this.apiRootUrl + '/sports';
static alphabeticalSort() {
return (a: StaticData, b: StaticData) => a.name.localeCompare(b.name);
}
constructor(private http: Http) {
}
countries(): Observable<Country[]> {
return this.http.get(this.countriesUrl)
.map((response: Response) => response.json())
.catch(this.defaultErrorHandler());
}
sports(): Observable<Sport[]> {
return this.http.get(this.sportsUrl)
.map((response: Response) => response.json())
.catch(this.defaultErrorHandler());
}
private defaultErrorHandler() {
return (error: any) => Observable.throw(error.json().error || 'Server error');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment