Customers and Orders
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Things we Need
import { IEntityState } from '@briebug/ngrx-auto-entity';
import { ActionReducerMap } from '@ngrx/store';
import { Customer, Order } from '../models';
import { customerReducer } from './customer.state';
import { orderReducer } from './order.state';
export interface IAppState {
customer: IEntityState<Customer>;
order: IEntityState<Order>;
}
export type AppState = IAppState;
export function appReducer: ActionReducerMap<AppState> = {
customer: customerReducer,
order: orderReducer
};
import { buildState, IEntityState } from '@briebug/ngrx-auto-entity';
import { createReducer } from '@ngrx/store';
import { Customer } from 'models';
export const { initialState: customerInitialState, facade: CustomerFacadeBase } = buildState(Customer);
export function customerReducer(state = initialState): IEntityState<Customer> {
return state;
}
import { buildState, IEntityState } from '@briebug/ngrx-auto-entity';
import { createReducer } from '@ngrx/store';
import { Order } from 'models';
export const { initialState: orderInitialState, facade: OrderFacadeBase } = buildState(Order);
export function orderReducer(state = initialState): IEntityState<Order> {
return state;
}
import { NgModule } from '@angular/core';
import { NgrxAutoEntityModule } from '@briebug/ngrx-auto-entity';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { appReducer } from './app.state';
@NgModule({
imports: [
StoreModule.forRoot(appReducer, {
runtimeChecks: {
strictStateImmutability: true,
strictActionImmutability: true,
strictStateSerializability: true,
strictActionSerializability: true,
},
}),
EffectsModule.forRoot([]),
NgrxAutoEntityModule.forRoot()
]
})
export class StateModule {}
export * from './app.state';
export * from './customer.state';
export * from './order.state';
export * from './state.module';
Bundles of UI
import { Component } from '@angular/core';
import { CustomerFacade } from 'facades';
import { CustomerEditComponent } from 'components';
@Component({
selector: 'app-customers',
templateUrl: './customers.component.html',
styleUrls: ['./customers.component.css']
})
export class CustomersComponent {
constructor(public customers: CustomerFacade, public ui: CustomerUIFacade) {
customers.loadAll();
}
}
<div class="customers" *ngIf="customers.isLoading$ | async; else loading">
<app-customer-list
[customers]="customers.all$ | async"
(deleted)="customers.delete($event)"
(selected)="customers.select($event)">
</app-customer-list>
<app-customer-detail
*ngIf="(customers.currentOrFirst$ | async) as customer"
[customer]="customer"
(deleted)="customers.delete($event)"
(edited)="ui.edit($event)">
</app-customer-detail>
</div>
<ng-template #loading>
Loading customers...
</ng-template>
import { Component } from '@angular/core';
import { Router } froj '@angular/router';
import { OrderFacade, LineItemFacade } from 'facades';
@Component({
selector: 'app-orders',
templateUrl: './orders.component.html',
styleUrls: ['./orders.component.css']
})
export class CustomersComponent {
constructor(
public orders: OrderFacade,
public lineItems: LineItemFacade,
public router: Router) {
orders.loadAll();
}
}
<div class="orders" *ngIf="orders.isLoading$ | async; else loading">
<app-order-list
[orders]="orders.all$ | async"
(deleted)="orders.delete($event)"
(selected)="orders.select($event)">
</app-order-list>
<app-order-detail
*ngIf="(orders.current$ | async) as order"
[order]="order"
[lineItems]="lineItems.byOrder$(order) | async"
(deleted)="orders.delete($event); lineItems.deleteAllForOrder($event)"
(edited)="router.navigate(['edit', $event.id])">
</app-order-detail>
</div>
<ng-template #loading>
Loading orders...
</ng-template>
Bits of UI
import { EventEmitter, Component, Input, Output } from '@angular/core';
import { Customer } from '../models';
@Component({
selector: 'app-customers-list',
templateUrl: './customers-list.component.html',
styleUrls: ['./customers-list.component.scss']
})
export class CustomersListComponent {
@Input() customers: Customer[];
@Output() selected = new EventEmitter<Customer>();
@Output() deleted = new EventEmitter<Customer>();
}
<div class="customer-list">
<table>
<thead>
<tr>
<th><i class="fa fa-key"></i></th>
<th>Name</th>
<th><i class="fa fa-wrench"></i></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let customer of customers">
<td>{{customer.id}}</td>
<td>{{customer.name}}</td>
<td>
<i class="fa fa-edit" (click)="selected.emit(customer)"></i>
<i class="fa fa-trash" (click)="deleted.emit(customer)"></i>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td class="text-right" colspan="3">
Total Customers: {{customers.length}}
</td>
</tr>
</tfoot>
</table>
</div>
import { EventEmitter, Component, Input, Output } from '@angular/core';
import { Customer } from '../models';
@Component({
selector: 'app-customer-detail',
templateUrl: './customer-detail.component.html',
styleUrls: ['./customer-detail.component.scss']
})
export class CustomersListComponent {
@Input() customer: Customer;
@Output() saved = new EventEmitter<Customer>();
@Output() deleted = new EventEmitter<Customer>();
}
<div class="customer-detail">
<h2>{{customer.name}}</h2>
<h3>{{customer.title}}</h3>
<a href="mailto:{{customer.email}}">
{{customer.email | prettyEmail}}
</a>
<a *ngIf="customer.handles?.twitter"
href="https://www.twitter.com/{{customer.handles?.twitter}}">
{{customer.handles?.twitter}}
</a>
<a *ngIf="customer.handles?.facebook"
href="https://www.facebook.com/{{customer.handles?.facebook}}">
{{customer.handles?.facebook}}
</a>
<button (click)="edited.emit(customer)">Edit</button>
<button (click)="deleted.emit(customer)">Delete</button>
</div>
The Beginning
One Service to Rule them All
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import {
CustomersComponent,
CustomerListComponent,
CustomerDetailComponent,
OrdersComponent,
OrderListComponent,
OrderDetailComponent
} from 'components';
import { Customer, Address, Order, LineItem } from 'models';
import { EntityService } from 'services';
import { StateModule } from 'state';
@NgModule({
bootstrap: [
AppComponent
],
declarations: [
AppComponent,
CustomersComponent,
CustomerListComponent,
CustomerDetailComponent,
OrdersComponent,
OrderListComponent,
OrderDetailComponent
],
imports: [
BrowserModule,
HttpClientModule,
StateModule.forRoot(),
],
providers: [
{ provide: Address, useClass: EntityService },
{ provide: Customer, useClass: EntityService },
{ provide: LineItem, useClass: EntityService },
{ provide: Order, useClass: EntityService }
]
})
export class AppModule {}
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { IAutoEntityService, IEntityInfo } from '@briebug/ngrx-auto-entity';
import { environment } from '../../environments/environment';
@Injectable()
export class EntityService implements IAutoEntityService<any> {
constructor(private http: HttpClient) {
}
load(entityInfo: IEntityInfo, id: any, criteria?: any): Observable<any> {
return this.http.get<any>(
`${environment.rootUrl}/${entityInfo.modelName}/${id}`,
{params: criteria ? criteria.query || {} : {}}
);
}
loadAll(entityInfo: IEntityInfo): Observable<any[]> {
return this.http.get<any[]>(
`${environment.rootUrl}/${entityInfo.modelName}`
);
}
loadMany(entityInfo: IEntityInfo, criteria: any): Observable<any[]> {
return this.http.get<any[]>(
`${environment.rootUrl}/${entityInfo.modelName}`,
{params: criteria ? criteria.query || {} : {}}
);
}
loadPage(entityInfo: IEntityInfo, page: PageInfo, criteria: any): Observable<any[]> {
return this.http.get<any[]>(
`${environment.rootUrl}/${entityInfo.modelName}`,
{params: criteria ? criteria.query || {} : {}}
).pipe(
map(entities => ({ // Must return entities with page info!
entityInfo,
pageInfo,
entities
}))
);
}
create(entityInfo: IEntityInfo, entity: any): Observable<any> {
return this.http.post<any>(
`${environment.rootUrl}/${entityInfo.modelName}`,
entity
);
}
update(entityInfo: IEntityInfo, entity: any): Observable<any> {
return this.http.patch<any>(
`${environment.rootUrl}/${entityInfo.modelName}/${entity.id}`,
entity
);
}
replace(entityInfo: IEntityInfo, entity: any): Observable<any> {
return this.http.put<any>(
`${environment.rootUrl}/${entityInfo.modelName}/${entity.id}`,
entity
);
}
delete(entityInfo: IEntityInfo, entity: any): Observable<any> {
return this.http.delete<any>(
`${environment.rootUrl}/${entityInfo.modelName}/${entity.id}`
).pipe(map(() => entity)); // Must return entity with key
}
deleteMany(entityInfo: IEntityInfo, entities: any[]): Observable<any[]> {
return forkJoin(
entities.map(entity => this.delete(entityInfo, entity))
);
}
}
export * from './entity.service';
import { Key } from '@briebug/ngrx-auto-entity';
import { Address } from './address.model';
export class Customer {
@Key id?: number;
name: string;
title: string;
email: string;
handles?: {
twitter?: string;
facebook?: string;
}
addressId?: number;
}
import { Key } from '@briebug/ngrx-auto-entity';
export class Address {
@Key id?: number;
street1: string;
street2: string;
city: string;
state: string;
zip: string;
}
import { Key } from '@briebug/ngrx-auto-entity';
export type ISODate = string; // YYYY-MM-DDTHH:mm:ss-ZZ:zz
export type Never = 'never';
export enum OrderStatus {
PENDING = 'pending',
ONHOLD = 'on-hold',
PARTIAL = 'partial-fill',
FILLED = 'filled',
PARTSHIP = 'partial-shipped',
SHIPPED = 'shipped',
CLOSED = 'closed'
}
export class Order {
@Key id?: number;
purchaseOrderNo: string;
status: OrderStatus;
dateCreated: ISODate;
dateClosed: ISODate | Never;
history: OrderHistory[];
}
export class OrderHistory {
dateOfAction: ISODate;
action: string;
newStatus: OrderStatus;
}
import { Key } from '@briebug/ngrx-auto-entity';
export class LineItem {
@Key orderId: number;
@Key productId: number;
quantity: number;
isRush: boolean;
}
export * from './address.model';
export * from './customer.model';
export * from './lineItem.model';
export * from './order.model';
Business Central
import { Injectable } from '@angular/core';
import { store } from '@ngrx/store';
import { CustomerEditComponent } from 'components';
import { Customer } from 'models';
import { AppState, CustomerFacadeBase, firstCustomer } from 'state'
@Injectable({ providedIn: 'root' })
export class CustomerFacade extends CustomerFacadeBase {
constructor(store: Store<AppState>, private modal: Modal) {
super(Customer, store);
}
get first$(): Observable<Customer> {
return this.store.pipe(select(firstCustomer));
}
get currentOrFirst$(): Observable<Customer> {
return combineLatest(this.current$, this.first$).pipe(
map(([current, first]) => current || first)
);
}
save(customer: Customer): void {
customer.id ? this.update(customer) : this.create(customer);
}
}
import { Injectable } from '@angular/core';
import { CustomerEditComponent } from '../components';
import { Customer } from '../models';
@Injectable({ providedIn: 'root' })
export class CustomerUIFacade {
constructor(private modal: Modal, private customerFacade: CustomerFacade) {
}
edit(customer: Customer): void {
const reference = this.modal.show(CustomerEditComponent);
reference.dismissed(editedCustomer => {
this.customerFacade.save(editedCustomer);
});
}
}
import { Injectable } from '@angular/core';
import { store } from '@ngrx/store';
import { Order } from 'models';
import { AppState, OrderFacadeBase } from 'state'
@Injectable({ providedIn: 'root' })
export class OrderFacade extends OrderFacadeBase {
constructor(store: Store<AppState>, private modal: Modal) {
super(Order, store);
}
save(order: Order): void {
order.id ? this.update(order) : this.create(order);
}
}
import { Injectable } from '@angular/core';
import { store } from '@ngrx/store';
import { LineItem } from 'models';
import { AppState, LineItemFacadeBase } from 'state'
@Injectable({ providedIn: 'root' })
export class LineItemFacade extends LineItemFacadeBase {
constructor(store: Store<AppState>, private modal: Modal) {
super(LineItem, store);
}
byOrder$(order: Order): Observable<LineItem[]> {
return this.all$.pipe(
map(lineItems =>
lineItems.filter(lineItem => lineItem.orderId === order.id)
)
);
}
deleteAllForOrder(order: Order): void {
this.byOrder$(order).pipe(
tap(lineItems => this.deleteMany(lineItems))
);
}
}
export * from './customer.facade';
export * from './customer-ui.facade';
Rogue UI
import { Component } from '@angular/core';
import { CustomerFacade } from '../facades';
@Component({
selector: 'app-customer-edit',
templateUrl: './customer-edit.component.html',
styleUrls: ['./customer-edit.component.css']
})
export class CustomerEditComponent {
@ViewChild('form') form: CustomerEditFormComponent;
canSave = false;
constructor(public customers: CustomerFacade, public modal: ModalInstance) {
}
}
<div class="customer-exit">
<div>
<h2>Edit Customer</h2>
<app-customer-edit-form #form
[customer]="customers.current$ | async"
(submitted)="modal.dismiss($event)"
(validated)="canSave = $event"
>
</app-customer-edit-form>
</div>
<div>
<button [disabled]="canSave" click="form.submit()">Save</span>
<button (click)="modal.dismiss()">Cancel</span>
</div>
</div>
import { Component, Input, Output } from '@angular/core';
import { Customer } from '../models';
@Component({
selector: 'app-customers-edit-form',
templateUrl: './customers-edit-form.component.html',
styleUrls: ['./customers-edit-form.component.scss']
})
export class CustomersEditFormComponent implements OnChanges, OnInit {
@Input() customer: Customer;
@Output() submitted = new EventEmitter<Customer>();
@Output() validated = new EventEmitter<boolean>();
form: FormGroup;
constructor(private builder: FormBuilder) {}
ngOnInit(): void {
this.form = this.buildForm(this.builder);
this.form.statusChanges.subscribe(() => this.validated.emit(form.valid));
}
ngOnChanges(): void {
if (this.customer) {
form.patchValue(customer);
}
}
buildForm(builder: FormBuilder): FormGroup {
return builder.group({
name: [null, [Validators.required, Validators.maxLength(30)]],
title: [null, [Validators.required, Validators.maxLength(60)]],
email: [null, [Validators.required, Validators.maxLength(35)]],
handles: builder.group({
twitter: [null, Validators.maxLength(50)],
facebook: [null, Validators.maxLenth(50)]
}),
address: builder.group({
city: [null, Validators.maxLength(50)],
state: [null, Validators.maxLength(2)],
zip: [null, [Validators.minLength(5), Validators.maxLength(10)]]
})
});
}
submit() {
if (this.form.valid) {
this.submitted.emit({
...this.customer,
...this.form.value
});
}
}
}
<form [formGroup]="form">
<div>
<label>Customer Name</label>
<input id="name" formControlName="name">
<i class="text-red"
*ngIf="name.invalid && (name.dirty || name.touched)">
Please fill out this field.
</i>
</div>
<div>
<label>title</label>
<input id="title" formControlName="title">
<i class="text-red"
*ngIf="title.invalid && (title.dirty || title.touched)">
Please fill out this field.
</i>
</div>
<div>
<label>Email Address</label>
<input id="email" formControlName="email" type="email">
<i class="text-red"
*ngIf="email.invalid && (email.dirty || email.touched)">
Please fill out this field.
</i>
</div>
</div>
<div>
<div>
<label>City</label>
<input id="city" formControlName="city">
</div>
<div>
<label>State</label>
<select id="state" formControlName="state">
<option value="AZ">Arizona</option>
<option value="CO">Colorado</option>
<option value="NM">New Mexico</option>
<option value="UT">Utah</option>
</select>
</div>
<div>
<label>Zip</label>
<input id="zip" formControlName="zip">
</div>
</div>
</form>