ds-command

This commit is contained in:
David Lebee 2021-08-18 16:38:26 -04:00
parent 11cdfd131a
commit 222e0be33b
20 changed files with 493 additions and 5 deletions

6
package-lock.json generated
View File

@ -1684,9 +1684,9 @@
} }
}, },
"@poweredsoft/ngx-data": { "@poweredsoft/ngx-data": {
"version": "0.0.21", "version": "0.0.22",
"resolved": "https://registry.npmjs.org/@poweredsoft/ngx-data/-/ngx-data-0.0.21.tgz", "resolved": "https://registry.npmjs.org/@poweredsoft/ngx-data/-/ngx-data-0.0.22.tgz",
"integrity": "sha512-+g8plxcrivlnd5VBsHtGiqfXabRlDeYeyQI5lOCZRlfLt+IwIlCUB5kS0Hm3LvWZddyQvhg/stAzaeao0uyt3A==", "integrity": "sha512-U4kMKlshTpu2/BpjQ/ULESFbaxJOYUjB6Qoax5rXY/vXErYBdVeQWhv/v4MxMLn8gPltjOzVKpEnQPVAlWZT5g==",
"requires": { "requires": {
"tslib": "^1.9.0" "tslib": "^1.9.0"
} }

View File

@ -25,7 +25,7 @@
"@angular/router": "~9.1.4", "@angular/router": "~9.1.4",
"@ng-select/ng-select": "^4.0.1", "@ng-select/ng-select": "^4.0.1",
"@poweredsoft/data": "0.0.30", "@poweredsoft/data": "0.0.30",
"@poweredsoft/ngx-data": "0.0.21", "@poweredsoft/ngx-data": "0.0.22",
"@poweredsoft/ngx-data-apollo": "0.0.10", "@poweredsoft/ngx-data-apollo": "0.0.10",
"apollo-angular": "^1.8.0", "apollo-angular": "^1.8.0",
"apollo-angular-link-http": "^1.9.0", "apollo-angular-link-http": "^1.9.0",

View File

@ -0,0 +1,10 @@
import { Directive, TemplateRef } from '@angular/core';
@Directive({
selector: '[psDsCommandContent]'
})
export class DsCommandContentDirective {
constructor(public template: TemplateRef<any>) { }
}

View File

@ -0,0 +1,10 @@
import { Directive, TemplateRef } from '@angular/core';
@Directive({
selector: '[psDsCommandError]'
})
export class DsCommandErrorDirective {
constructor(public template: TemplateRef<any>) { }
}

View File

@ -0,0 +1,10 @@
import { Directive, TemplateRef } from '@angular/core';
@Directive({
selector: '[psDsCommandFooter]'
})
export class DsCommandFooterDirective {
constructor(public template: TemplateRef<any>) { }
}

View File

@ -0,0 +1,10 @@
import { Directive, TemplateRef } from '@angular/core';
@Directive({
selector: '[psDsCommandNoCommand]'
})
export class DsCommandNoCommandDirective {
constructor(public template: TemplateRef<any>) { }
}

View File

@ -0,0 +1,11 @@
import { Directive, ElementRef, HostListener } from '@angular/core';
@Directive({
selector: '[psDsCommandSubmit]'
})
export class DsCommandSubmitDirective {
constructor(public element: ElementRef) { }
}

View File

@ -0,0 +1,10 @@
import { Directive, TemplateRef } from '@angular/core';
@Directive({
selector: '[psDsCommandValidation]'
})
export class DsCommandValidationDirective {
constructor(public template: TemplateRef<any>) { }
}

View File

@ -0,0 +1,30 @@
<ng-container *ngIf="command; else noCommand">
<ng-container *ngIf="hasContent" [ngTemplateOutlet]="contentTemplate"
[ngTemplateOutletContext]="{ $implicit: command, loading: isLoading }">
</ng-container>
</ng-container>
<ng-container *ngIf="lastValidationResult">
<ng-container *ngIf="hasValidationTemplate" [ngTemplateOutlet]="validationTemplate"
[ngTemplateOutletContext]="{ $implicit: lastValidationResult }">
</ng-container>
</ng-container>
<ng-container *ngIf="lastErrorMessage && errorContent && errorContent.template">
<ng-container [ngTemplateOutlet]="errorContent.template"
[ngTemplateOutletContext]="{ $implicit: lastErrorMessage }">
</ng-container>
</ng-container>
<ng-container *ngIf="hasFooterTemplate" [ngTemplateOutlet]="footerTemplate"
[ngTemplateOutletContext]="{ $implicit: command, loading: isLoading }">
</ng-container>
<ng-template #noCommand>
<ng-container *ngIf="noCommandContent && noCommandContent.template">
<ng-container [ngTemplateOutlet]="noCommandContent.template">
</ng-container>
</ng-container>
</ng-template>

View File

@ -0,0 +1,182 @@
import { Component, ContentChild, ContentChildren, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, QueryList } from '@angular/core';
import { IDataSource, IDataSourceValidationError } from '@poweredsoft/data';
import { Subscription } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { DsCommandContentDirective } from './directives/ds-command-content.directive';
import { DsCommandErrorDirective } from './directives/ds-command-error.directive';
import { DsCommandFooterDirective } from './directives/ds-command-footer.directive';
import { DsCommandNoCommandDirective } from './directives/ds-command-no-command.directive';
import { DsCommandSubmitDirective } from './directives/ds-command-submit.directive';
import { DsCommandValidationDirective } from './directives/ds-command-validation.directive';
export interface DsCommandPropertyError
{
name: string,
errors: string[];
}
@Component({
selector: 'ps-ds-command',
templateUrl: './ds-command.component.html',
styleUrls: ['./ds-command.component.scss']
})
export class DsCommandComponent implements OnInit, OnDestroy {
@Input() dataSource: IDataSource<any>;
@Input() name: string;
@Input() model: any;
@Input() refreshOnSuccess: boolean;
@Input() resolveCommand: boolean;
@Output() success = new EventEmitter<any>();
@Output() loading = new EventEmitter<boolean>();
@Output() commandChange = new EventEmitter<any>();
@ContentChild(DsCommandContentDirective) commandContent: DsCommandContentDirective;
@ContentChild(DsCommandFooterDirective) footerContent: DsCommandFooterDirective;
@ContentChild(DsCommandNoCommandDirective) noCommandContent: DsCommandNoCommandDirective;
@ContentChild(DsCommandErrorDirective) errorContent: DsCommandErrorDirective;
@ContentChild(DsCommandValidationDirective) validationDirective: DsCommandValidationDirective;
@ContentChildren(DsCommandSubmitDirective) submitDirectives: QueryList<DsCommandSubmitDirective>;
lastErrorMessage: string;
lastValidationResult: DsCommandPropertyError[];
protected _command: any = null;
private _loading = false;
private _validationErrorSubscription: Subscription;
private _notifyMessageSubscription: Subscription;
@Input() set command(value: any) {
if (this._command != value) {
this._command = value;
this.commandChange.emit(value);
}
}
get noCommand() {
return this._command ? true : false;
}
get command() {
return this._command;
}
get hasContent() {
return this.commandContent != null && this.commandContent.template != null;
}
get footerTemplate() {
return this.footerContent.template;
}
get hasFooterTemplate() {
return this.footerContent?.template != null;
}
get hasValidationTemplate() {
return this.validationDirective != null && this.validationDirective.template != null;
}
get validationTemplate() {
return this.validationDirective.template;
}
get contentTemplate() {
return this.commandContent.template;
}
constructor() {
}
ngOnDestroy() {
this._validationErrorSubscription?.unsubscribe();
this._notifyMessageSubscription?.unsubscribe();
}
ngOnInit(): void {
this._validationErrorSubscription = this.dataSource
.validationError$.subscribe(validationResult => {
this.lastValidationResult = Object.keys(validationResult.errors).reduce<DsCommandPropertyError[]>((prev, attr) => {
prev.push({
name: attr,
errors: validationResult.errors[attr]
})
return prev;
}, []);
});
this._notifyMessageSubscription = this.dataSource.notifyMessage$.subscribe(message => {
if (message.type != 'info')
this.lastErrorMessage = message.message;
});
const shouldResolve = this.resolveCommand === undefined ? true : this.resolveCommand;
if (shouldResolve)
this.resolveModel();
}
resolveModel() {
this.dataSource.resolveCommandModelByName({
model: this.model,
command: this.name
}).subscribe(
commandModel => {
this.command = commandModel;
},
_ => { }
);
}
private changeLoadingState(loading: boolean) {
if (loading != this._loading) {
this._loading = loading;
this.loading.emit(loading);
}
}
@HostListener("click", ["$event"])
childClicked($event: MouseEvent) {
const element = $event.target;
const found = this.submitDirectives
.toArray()
.find(t => t.element.nativeElement == element);
if (found != null)
this.execute()
}
execute() {
// secure from double send.
if (this._loading)
return;
this.changeLoadingState(true);
this.lastValidationResult = null;
this.lastErrorMessage = null;
this.dataSource.executeCommandByName(this.name, this._command)
.pipe(
finalize(() => this.changeLoadingState(false))
)
.subscribe(
commandResult => {
this.success.emit(commandResult);
if (this.refreshOnSuccess)
this.dataSource.refresh();
},
_ => {
// we just catch the error, this way its not uncatched its handled via the other
// subscriptions.
}
);
}
get isLoading() {
return this._loading;
}
}

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DsCommandComponent } from './ds-command.component';
import { DsCommandContentDirective } from './directives/ds-command-content.directive';
import { DsCommandSubmitDirective } from './directives/ds-command-submit.directive';
import { DsCommandValidationDirective } from './directives/ds-command-validation.directive';
import { DsCommandFooterDirective } from './directives/ds-command-footer.directive';
import { DsCommandErrorDirective } from './directives/ds-command-error.directive';
import { DsCommandNoCommandDirective } from './directives/ds-command-no-command.directive';
@NgModule({
declarations: [DsCommandComponent, DsCommandContentDirective, DsCommandSubmitDirective, DsCommandValidationDirective, DsCommandFooterDirective, DsCommandErrorDirective, DsCommandNoCommandDirective],
imports: [
CommonModule
],
exports: [DsCommandComponent, DsCommandContentDirective, DsCommandSubmitDirective, DsCommandValidationDirective, DsCommandFooterDirective, DsCommandErrorDirective, DsCommandNoCommandDirective]
})
export class DsCommandModule { }

View File

@ -36,3 +36,13 @@ export * from './lib/view/view.component';
export * from './lib/view/directives/view-content.directive'; export * from './lib/view/directives/view-content.directive';
export * from './lib/view/directives/view-loading.directive'; export * from './lib/view/directives/view-loading.directive';
export * from './lib/view/directives/view-no-records.directive'; export * from './lib/view/directives/view-no-records.directive';
// ds-command
export * from './lib/ds-command/ds-command.module';
export * from './lib/ds-command/ds-command.component';
export * from './lib/ds-command/directives/ds-command-content.directive';
export * from './lib/ds-command/directives/ds-command-submit.directive';
export * from './lib/ds-command/directives/ds-command-validation.directive';
export * from './lib/ds-command/directives/ds-command-footer.directive';
export * from './lib/ds-command/directives/ds-command-no-command.directive';
export * from './lib/ds-command/directives/ds-command-error.directive';

View File

@ -19,6 +19,9 @@ const routes: Routes = [
{ {
path: 'list-view', path: 'list-view',
loadChildren: () => import('./list-view-demo/list-view-demo.module').then(m => m.ListViewDemoModule) loadChildren: () => import('./list-view-demo/list-view-demo.module').then(m => m.ListViewDemoModule)
},{
path: 'ds-command',
loadChildren: () => import('./ds-command-demo/ds-command-demo.module').then(m => m.DsCommandDemoModule)
}, },
{ {
path: 'command-modal', path: 'command-modal',

View File

@ -15,6 +15,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="view">View</a> <a class="nav-link" routerLink="view">View</a>
</li> </li>
<li class="nav-item">
<a class="nav-link" routerLink="ds-command">Data Source Command</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="command-modal">Command Modal</a> <a class="nav-link" routerLink="command-modal">Command Modal</a>
</li> </li>

View File

@ -0,0 +1,60 @@
<ps-view [dataSource]="dataSource">
<ng-container *psViewContent="let model">
<div class="card">
<div class="card-header">
{{ model.firstName }} {{ model.lastName }}
</div>
<div class="card-body">
{{ model.number }}
<ng-container *ngIf="model.extension">
,{{model.extension}}
</ng-container>
</div>
<div class="card-footer">
<button class="btn btn-primary btn-sm" (click)="formActivated = true">Change Phone Number</button>
</div>
</div>
<div class="card mt-2" *ngIf="formActivated">
<div class="card-body">
<ps-ds-command [dataSource]="dataSource" name="changePhone" (success)="onSuccess($event)"
[model]="model">
<ng-container *psDsCommandContent="let command; let loading=loading">
<div class="form-group">
<label>Number</label>
<input type="text" class="form-control" [(ngModel)]="command.number" />
<ps-ds-validation-error class="text-danger" [dataSource]="dataSource" field="number">
</ps-ds-validation-error>
</div>
<div class="form-group">
<label>Extension</label>
<input type="text" class="form-control" [(ngModel)]="command.extension" />
<ps-ds-validation-error class="text-danger" [dataSource]="dataSource" field="extension">
</ps-ds-validation-error>
</div>
</ng-container>
<div class="alert alert-danger mt-2" *psDsCommandValidation="let validationResult">
<ng-container *ngFor="let propertyErrors of validationResult">
<ng-container *ngFor="let err of propertyErrors.errors">
{{ err }}<br>
</ng-container>
</ng-container>
</div>
<ng-container *psDsCommandFooter="let command; let loading=loading">
<button class="btn btn-info" [disabled]="loading"
(click)="formActivated = false">Cancel</button>
&nbsp;
<button class="btn btn-primary" [disabled]="loading" psDsCommandSubmit>Save</button>
</ng-container>
</ps-ds-command>
</div>
</div>
</ng-container>
</ps-view>

View File

@ -0,0 +1,81 @@
import { Component, OnInit } from '@angular/core';
import { IDataSource, IDataSourceValidationError } from '@poweredsoft/data';
import { HttpDataSourceService } from '@poweredsoft/ngx-data';
import { of, throwError } from 'rxjs';
interface ContactQuery {
contactId: number
}
interface Contact {
id: number;
firstName: string,
lastName: string,
number: string,
extension: string;
}
interface ChangeContactPhone {
contactId: number,
number: string,
extension: string
}
@Component({
selector: 'ps-ds-command-demo-page',
templateUrl: './ds-command-demo-page.component.html',
styleUrls: ['./ds-command-demo-page.component.scss']
})
export class DsCommandDemoPageComponent implements OnInit {
private contact: Contact = {
id: 1,
firstName: "David",
lastName: "Lebee",
extension: null,
number: "514 448 8444"
};
formActivated: boolean = false;
dataSource: IDataSource<Contact>;
constructor(private hdss: HttpDataSourceService) {
this.dataSource = this.hdss.singleBuilder<ContactQuery, Contact, number>()
.keyResolver(m => m.id)
.queryHandler(_ => of(this.contact))
.addCommandByCallback<ChangeContactPhone, ChangeContactPhone>("changePhone", (command) => {
if (!command.number || command.number.length == 0) {
return throwError({
status: 400,
error: {
errors: {
number: ['number is required']
}
}
})
}
return of(command);
}, e => {
return of(<ChangeContactPhone>{
contactId: e.model.id,
number: e.model.number,
extension: e.model.extension
});
})
.createDataSource();
}
ngOnInit(): void {
this.dataSource.refresh();
}
onSuccess(command: ChangeContactPhone) {
this.contact.number = command.number;
this.contact.extension = command.extension;
this.formActivated = false;
}
}

View File

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { DsCommandDemoPageComponent } from './ds-command-demo-page/ds-command-demo-page.component';
const routes: Routes = [
{
path: '',
component: DsCommandDemoPageComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DsCommandDemoRoutingModule { }

View File

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DsCommandDemoRoutingModule } from './ds-command-demo-routing.module';
import { DsCommandDemoPageComponent } from './ds-command-demo-page/ds-command-demo-page.component';
import { DsCommandModule, DsValidationErrorModule, ViewModule } from '@poweredsoft/ngx-cdk-ui';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [DsCommandDemoPageComponent],
imports: [
CommonModule,
DsCommandDemoRoutingModule,
DsCommandModule,
ViewModule,
FormsModule,
DsValidationErrorModule
]
})
export class DsCommandDemoModule { }