Merged dynamic form module

This commit is contained in:
Giuseppe Digilio
2018-05-09 12:14:18 +02:00
parent 99f8d4f24d
commit 7a32d18b1b
111 changed files with 6778 additions and 45 deletions

View File

@@ -80,14 +80,19 @@
"@angular/router": "^5.2.5", "@angular/router": "^5.2.5",
"@angularclass/bootloader": "1.0.1", "@angularclass/bootloader": "1.0.1",
"@ng-bootstrap/ng-bootstrap": "^1.0.0", "@ng-bootstrap/ng-bootstrap": "^1.0.0",
"@ng-dynamic-forms/core": "5.4.7",
"@ng-dynamic-forms/ui-ng-bootstrap": "5.4.7",
"@ngrx/effects": "^5.1.0", "@ngrx/effects": "^5.1.0",
"@ngrx/router-store": "^5.0.1", "@ngrx/router-store": "^5.0.1",
"@ngrx/store": "^5.1.0", "@ngrx/store": "^5.1.0",
"@nguniversal/express-engine": "5.0.0-beta.5", "@nguniversal/express-engine": "5.0.0-beta.5",
"@ngx-translate/core": "9.1.1", "@ngx-translate/core": "9.1.1",
"@ngx-translate/http-loader": "2.0.1", "@ngx-translate/http-loader": "2.0.1",
"@nicky-lenaers/ngx-scroll-to": "^0.6.0",
"angular-idle-preload": "2.0.4", "angular-idle-preload": "2.0.4",
"angular-sortablejs": "^2.5.0",
"angulartics2": "^5.2.0", "angulartics2": "^5.2.0",
"angular2-text-mask": "8.0.4",
"body-parser": "1.18.2", "body-parser": "1.18.2",
"bootstrap": "^4.0.0", "bootstrap": "^4.0.0",
"cerialize": "0.1.18", "cerialize": "0.1.18",
@@ -103,10 +108,14 @@
"jsonschema": "1.2.2", "jsonschema": "1.2.2",
"methods": "1.1.2", "methods": "1.1.2",
"morgan": "1.9.0", "morgan": "1.9.0",
"ng2-file-upload": "1.2.1",
"ngx-infinite-scroll": "0.8.2",
"ngx-pagination": "3.0.3", "ngx-pagination": "3.0.3",
"pem": "1.12.3", "pem": "1.12.3",
"reflect-metadata": "0.1.12", "reflect-metadata": "0.1.12",
"rxjs": "5.5.6", "rxjs": "5.5.6",
"sortablejs": "1.7.0",
"text-mask-core": "5.0.1",
"ts-md5": "^1.2.4", "ts-md5": "^1.2.4",
"uuid": "^3.2.1", "uuid": "^3.2.1",
"webfontloader": "1.6.28", "webfontloader": "1.6.28",

View File

@@ -154,5 +154,20 @@
"item": "Error fetching item", "item": "Error fetching item",
"objects": "Error fetching objects", "objects": "Error fetching objects",
"search-results": "Error fetching search results" "search-results": "Error fetching search results"
},
"form": {
"submit": "Submit",
"cancel": "Cancel",
"search": "Search",
"remove": "Remove",
"first-name": "First name",
"last-name": "Last name",
"loading": "Loading...",
"no-results": "No results found",
"no-value": "No value entered",
"group-collapse": "Collapse",
"group-expand": "Expand",
"group-collapse-help": "Click here to collapse",
"group-expand-help": "Click here to expand and add more element"
} }
} }

View File

@@ -1,32 +1,35 @@
import { ActionReducerMap } from '@ngrx/store'; import { ActionReducerMap } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store'; import * as fromRouter from '@ngrx/router-store';
import { headerReducer, HeaderState } from './header/header.reducer'; import { headerReducer, HeaderState } from './header/header.reducer';
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer'; import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
import { import { formReducer, FormState } from './shared/form/form.reducers';
SearchSidebarState, import {
sidebarReducer SearchSidebarState,
} from './+search-page/search-sidebar/search-sidebar.reducer'; sidebarReducer
import { } from './+search-page/search-sidebar/search-sidebar.reducer';
filterReducer, import {
SearchFiltersState filterReducer,
} from './+search-page/search-filters/search-filter/search-filter.reducer'; SearchFiltersState
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; } from './+search-page/search-filters/search-filter/search-filter.reducer';
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
export interface AppState {
router: fromRouter.RouterReducerState; export interface AppState {
hostWindow: HostWindowState; router: fromRouter.RouterReducerState;
header: HeaderState; hostWindow: HostWindowState;
searchSidebar: SearchSidebarState; header: HeaderState;
searchFilter: SearchFiltersState; forms: FormState;
truncatable: TruncatablesState; searchSidebar: SearchSidebarState;
} searchFilter: SearchFiltersState;
truncatable: TruncatablesState;
export const appReducers: ActionReducerMap<AppState> = { }
router: fromRouter.routerReducer,
hostWindow: hostWindowReducer, export const appReducers: ActionReducerMap<AppState> = {
header: headerReducer, router: fromRouter.routerReducer,
searchSidebar: sidebarReducer, hostWindow: hostWindowReducer,
searchFilter: filterReducer, header: headerReducer,
truncatable: truncatableReducer forms: formReducer,
}; searchSidebar: sidebarReducer,
searchFilter: filterReducer,
truncatable: truncatableReducer
};

View File

@@ -5,6 +5,7 @@ import { BrowseDefinition } from '../shared/browse-definition.model';
import { ConfigObject } from '../shared/config/config.model'; import { ConfigObject } from '../shared/config/config.model';
import { FacetValue } from '../../+search-page/search-service/facet-value.model'; import { FacetValue } from '../../+search-page/search-service/facet-value.model';
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model'; import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
import { IntegrationModel } from '../integration/models/integration.model';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
export class RestResponse { export class RestResponse {
@@ -106,4 +107,14 @@ export class ConfigSuccessResponse extends RestResponse {
super(true, statusCode); super(true, statusCode);
} }
} }
export class IntegrationSuccessResponse extends RestResponse {
constructor(
public dataDefinition: IntegrationModel[],
public statusCode: string,
public pageInfo?: PageInfo
) {
super(true, statusCode);
}
}
/* tslint:enable:max-classes-per-file */ /* tslint:enable:max-classes-per-file */

View File

@@ -44,6 +44,7 @@ import { HALEndpointService } from './shared/hal-endpoint.service';
import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service'; import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service';
import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service'; import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service';
import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service'; import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service';
import { UploaderService } from '../shared/uploader/uploader.service';
const IMPORTS = [ const IMPORTS = [
CommonModule, CommonModule,
@@ -88,6 +89,7 @@ const PROVIDERS = [
SubmissionDefinitionsConfigService, SubmissionDefinitionsConfigService,
SubmissionFormsConfigService, SubmissionFormsConfigService,
SubmissionSectionsConfigService, SubmissionSectionsConfigService,
UploaderService,
UUIDService, UUIDService,
{ provide: NativeWindowService, useFactory: NativeWindowFactory } { provide: NativeWindowService, useFactory: NativeWindowFactory }
]; ];

View File

@@ -7,6 +7,7 @@ import { ResponseParsingService } from './parsing.service';
import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service'; import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service';
import { BrowseResponseParsingService } from './browse-response-parsing.service'; import { BrowseResponseParsingService } from './browse-response-parsing.service';
import { ConfigResponseParsingService } from './config-response-parsing.service'; import { ConfigResponseParsingService } from './config-response-parsing.service';
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
@@ -174,6 +175,15 @@ export class ConfigRequest extends GetRequest {
} }
} }
export class IntegrationRequest extends GetRequest {
constructor(uuid: string, href: string) {
super(uuid, href);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
return IntegrationResponseParsingService;
}
}
export class RequestError extends Error { export class RequestError extends Error {
statusText: string; statusText: string;
} }

View File

@@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service';
import { IntegrationService } from './integration.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
@Injectable()
export class AuthorityService extends IntegrationService {
protected linkPath = 'authorities';
protected browseEndpoint = 'entries';
constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected halService: HALEndpointService) {
super();
}
}

View File

@@ -0,0 +1,12 @@
import { PageInfo } from '../shared/page-info.model';
import { IntegrationModel } from './models/integration.model';
/**
* A class to represent the data retrieved by a Eperson service
*/
export class IntegrationData {
constructor(
public pageInfo: PageInfo,
public payload: IntegrationModel[]
) { }
}

View File

@@ -0,0 +1,17 @@
import { GenericConstructor } from '../shared/generic-constructor';
import { IntegrationType } from './intergration-type';
import { AuthorityValueModel } from './models/authority-value.model';
import { IntegrationModel } from './models/integration.model';
export class IntegrationObjectFactory {
public static getConstructor(type): GenericConstructor<IntegrationModel> {
switch (type) {
case IntegrationType.Authority: {
return AuthorityValueModel;
}
default: {
return undefined;
}
}
}
}

View File

@@ -0,0 +1,47 @@
import { Inject, Injectable } from '@angular/core';
import { RestRequest } from '../data/request.models';
import { ResponseParsingService } from '../data/parsing.service';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import {
ErrorResponse,
IntegrationSuccessResponse,
RestResponse
} from '../cache/response-cache.models';
import { isNotEmpty } from '../../shared/empty.util';
import { IntegrationObjectFactory } from './integration-object-factory';
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
import { ObjectCacheService } from '../cache/object-cache.service';
import { IntegrationModel } from './models/integration.model';
import { IntegrationType } from './intergration-type';
@Injectable()
export class IntegrationResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
protected objectFactory = IntegrationObjectFactory;
protected toCache = false;
constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,
) {
super();
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
const dataDefinition = this.process<IntegrationModel,IntegrationType>(data.payload, request.href);
return new IntegrationSuccessResponse(dataDefinition[Object.keys(dataDefinition)[0]], data.statusCode, this.processPageInfo(data.payload.page));
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from Integration endpoint'),
{statusText: data.statusCode}
)
);
}
}
}

View File

@@ -0,0 +1,85 @@
import { Observable } from 'rxjs/Observable';
import { RequestService } from '../data/request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { ErrorResponse, IntegrationSuccessResponse, RestResponse } from '../cache/response-cache.models';
import { GetRequest, IntegrationRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { IntegrationData } from './integration-data';
import { IntegrationSearchOptions } from './models/integration-options.model';
export abstract class IntegrationService {
protected request: IntegrationRequest;
protected abstract responseCache: ResponseCacheService;
protected abstract requestService: RequestService;
protected abstract linkPath: string;
protected abstract browseEndpoint: string;
protected abstract halService: HALEndpointService;
protected getData(request: GetRequest): Observable<IntegrationData> {
const [successResponse, errorResponse] = this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response)
.partition((response: RestResponse) => response.isSuccessful);
return Observable.merge(
errorResponse.flatMap((response: ErrorResponse) =>
Observable.throw(new Error(`Couldn't retrieve the integration data`))),
successResponse
.filter((response: IntegrationSuccessResponse) => isNotEmpty(response))
.map((response: IntegrationSuccessResponse) => new IntegrationData(response.pageInfo, response.dataDefinition))
.distinctUntilChanged());
}
protected getIntegrationHref(endpoint, options: IntegrationSearchOptions = new IntegrationSearchOptions()): string {
let result;
const args = [];
if (hasValue(options.name)) {
result = `${endpoint}/${options.name}/${this.browseEndpoint}`;
} else {
result = endpoint;
}
if (hasValue(options.query)) {
args.push(`query=${options.query}`);
}
if (hasValue(options.metadata)) {
args.push(`metadata=${options.metadata}`);
}
if (hasValue(options.uuid)) {
args.push(`uuid=${options.uuid}`);
}
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
args.push(`page=${options.currentPage - 1}`);
}
if (hasValue(options.elementsPerPage)) {
args.push(`size=${options.elementsPerPage}`);
}
if (hasValue(options.sort)) {
args.push(`sort=${options.sort.field},${options.sort.direction}`);
}
if (isNotEmpty(args)) {
result = `${result}?${args.join('&')}`;
}
return result;
}
public getEntriesByName(options: IntegrationSearchOptions): Observable<IntegrationData> {
return this.halService.getEndpoint(this.linkPath)
.map((endpoint: string) => this.getIntegrationHref(endpoint, options))
.filter((href: string) => isNotEmpty(href))
.distinctUntilChanged()
.map((endpointURL: string) => new IntegrationRequest(this.requestService.generateRequestId(), endpointURL))
.do((request: GetRequest) => this.requestService.configure(request))
.flatMap((request: GetRequest) => this.getData(request))
.distinctUntilChanged();
}
}

View File

@@ -0,0 +1,4 @@
export enum IntegrationType {
Authority = 'authority'
}

View File

@@ -0,0 +1,16 @@
export class AuthorityOptions {
name: string;
metadata: string;
scope: string;
closed: boolean;
constructor(name: string,
metadata: string,
scope: string,
closed: boolean = false) {
this.name = name;
this.metadata = metadata;
this.scope = scope;
this.closed = closed;
}
}

View File

@@ -0,0 +1,20 @@
import { IntegrationModel } from './integration.model';
import { autoserialize } from 'cerialize';
export class AuthorityValueModel extends IntegrationModel {
@autoserialize
id: string;
@autoserialize
display: string;
@autoserialize
value: string;
@autoserialize
otherInformation: any;
@autoserialize
language: string;
}

View File

@@ -0,0 +1,14 @@
import { SortOptions } from '../../cache/models/sort-options.model';
export class IntegrationSearchOptions {
constructor(public uuid: string = '',
public name: string = '',
public metadata: string = '',
public query: string = '',
public elementsPerPage?: number,
public currentPage?: number,
public sort?: SortOptions) {
}
}

View File

@@ -0,0 +1,12 @@
import { autoserialize } from 'cerialize';
export abstract class IntegrationModel {
@autoserialize
public type: string;
@autoserialize
public _links: {
[name: string]: string
}
}

View File

@@ -0,0 +1,23 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { ConfigObject } from './config.model';
import { SubmissionSectionModel } from './config-submission-section.model';
@inheritSerialization(ConfigObject)
export class ConfigAuthorityModel extends ConfigObject {
@autoserialize
id: string;
@autoserialize
display: string;
@autoserialize
value: string;
@autoserialize
otherInformation: any;
@autoserialize
language: string;
}

View File

@@ -6,6 +6,7 @@ import { SubmissionFormsModel } from './config-submission-forms.model';
import { SubmissionDefinitionsModel } from './config-submission-definitions.model'; import { SubmissionDefinitionsModel } from './config-submission-definitions.model';
import { ConfigType } from './config-type'; import { ConfigType } from './config-type';
import { ConfigObject } from './config.model'; import { ConfigObject } from './config.model';
import { ConfigAuthorityModel } from './config-authority.model';
export class ConfigObjectFactory { export class ConfigObjectFactory {
public static getConstructor(type): GenericConstructor<ConfigObject> { public static getConstructor(type): GenericConstructor<ConfigObject> {
@@ -22,6 +23,9 @@ export class ConfigObjectFactory {
case ConfigType.SubmissionSections: { case ConfigType.SubmissionSections: {
return SubmissionSectionModel return SubmissionSectionModel
} }
case ConfigType.Authority: {
return ConfigAuthorityModel
}
default: { default: {
return undefined; return undefined;
} }

View File

@@ -1,10 +1,14 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; import { autoserialize, inheritSerialization } from 'cerialize';
import { ConfigObject } from './config.model'; import { ConfigObject } from './config.model';
import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model';
export interface FormRowModel {
fields: FormFieldModel[];
}
@inheritSerialization(ConfigObject) @inheritSerialization(ConfigObject)
export class SubmissionFormsModel extends ConfigObject { export class SubmissionFormsModel extends ConfigObject {
@autoserialize @autoserialize
fields: any[]; rows: FormRowModel[];
} }

View File

@@ -10,5 +10,6 @@ export enum ConfigType {
SubmissionForm = 'submissionform', SubmissionForm = 'submissionform',
SubmissionForms = 'submissionforms', SubmissionForms = 'submissionforms',
SubmissionSections = 'submissionsections', SubmissionSections = 'submissionsections',
SubmissionSection = 'submissionsection' SubmissionSection = 'submissionsection',
Authority = 'authority'
} }

View File

@@ -0,0 +1,13 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
export const shrinkInOut = trigger('shrinkInOut', [
state('in', style({height: '100%', opacity: 1})),
transition('* => void', [
style({height: '!', opacity: 1}),
animate(200, style({height: 0, opacity: 0}))
]),
transition('void => *', [
style({height: 0, opacity: 0}),
animate(200, style({height: '*', opacity: 1}))
])
]);

View File

@@ -0,0 +1,33 @@
<div [className]="'float-left w-100 ' + wrapperClass">
<ul class="nav nav-pills d-flex flex-column flex-sm-row" [sortablejs]="chips.getChips()" [sortablejsOptions]="options">
<ng-container *ngFor="let c of chips.getChips(); let i = index">
<ng-template #tipContent>{{tipText}}</ng-template>
<li [ngbTooltip]="tipContent"
triggers="manual"
#t="ngbTooltip"
class="nav-item mr-2 mb-1"
(click)="t.close()"
(dragstart)="onDragStart(t, i)"
(dragend)="onDragEnd(i)"
(mouseover)="showTooltip(t, i, c.display)"
(mouseout)="t.close()">
<a class="flex-sm-fill text-sm-center nav-link active"
href="#"
[ngClass]="{'chip-selected disabled': (editable && c.editMode) || dragged == i}"
(click)="t.close();chipsSelected($event, i);">
<span>
<ng-container *ngIf="c.hasIcons()">
<i *ngFor="let icon of c.icons; let i = index; let l = last"
class="fa {{icon.style}}"
[class.mr-2]="l"
aria-hidden="true"></i>
</ng-container>
<p class="chip-label text-truncate d-table-cell">{{c.display}}</p><i class="fa fa-times ml-2" (click)="removeChips($event, i)"></i>
</span>
</a>
</li>
</ng-container>
<ng-content></ng-content>
</ul>
</div>

View File

@@ -0,0 +1,9 @@
@import "../../../styles/variables";
.chip-selected {
background-color: map-get($theme-colors, info) !important;
}
.chip-label {
max-width: 10rem;
}

View File

@@ -0,0 +1,95 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, } from '@angular/core';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { SortablejsOptions } from 'angular-sortablejs';
import { Chips } from './models/chips.model';
import { ChipsItem } from './models/chips-item.model';
import { UploaderService } from '../uploader/uploader.service';
@Component({
selector: 'ds-chips',
styleUrls: ['./chips.component.scss'],
templateUrl: './chips.component.html',
})
export class ChipsComponent implements OnChanges {
@Input() chips: Chips;
@Input() wrapperClass: string;
@Input() editable: boolean;
@Output() selected: EventEmitter<number> = new EventEmitter<number>();
@Output() remove: EventEmitter<number> = new EventEmitter<number>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
options: SortablejsOptions;
dragged = -1;
tipText: string;
constructor(private cdr: ChangeDetectorRef, private uploaderService: UploaderService) {
this.options = {
animation: 300,
chosenClass: 'm-0',
dragClass: 'm-0',
filter: '.chips-sort-ignore',
ghostClass: 'm-0'
};
}
ngOnInit() {
if (!this.editable) {
this.editable = false;
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes.chips && !changes.chips.isFirstChange()) {
this.chips = changes.chips.currentValue;
}
}
chipsSelected(event: Event, index: number) {
event.preventDefault();
if (this.editable) {
this.chips.getChips().forEach((item: ChipsItem, i: number) => {
if (i === index) {
item.setEditMode();
} else {
item.unsetEditMode();
}
});
this.selected.emit(index);
}
}
removeChips(event: Event, index: number) {
event.preventDefault();
event.stopPropagation();
// Can't remove if this element is in editMode
if (!this.chips.getChipByIndex(index).editMode) {
this.chips.remove(this.chips.getChipByIndex(index));
}
}
onDragStart(tooltip: NgbTooltip, index) {
tooltip.close();
this.uploaderService.overrideDragOverPage();
this.dragged = index;
}
onDragEnd(event) {
this.uploaderService.allowDragOverPage();
this.dragged = -1;
this.chips.updateOrder();
}
showTooltip(tooltip: NgbTooltip, index, content) {
tooltip.close();
if (!this.chips.getChipByIndex(index).editMode && this.dragged === -1) {
this.tipText = content;
this.cdr.detectChanges();
tooltip.open();
}
}
}

View File

@@ -0,0 +1,73 @@
import { uniqueId } from 'lodash';
import { isNotEmpty } from '../../empty.util';
export interface ChipsItemIcon {
style: string;
tooltip?: any;
}
export class ChipsItem {
public id: string;
public display: string;
public item: any;
public editMode?: boolean;
public icons?: ChipsItemIcon[];
private fieldToDisplay: string;
private objToDisplay: string;
constructor(item: any,
fieldToDisplay: string,
objToDisplay: string,
icons?: ChipsItemIcon[],
editMode?: boolean) {
this.id = uniqueId();
this.item = item;
this.fieldToDisplay = fieldToDisplay;
this.objToDisplay = objToDisplay;
this.setDisplayText();
this.editMode = editMode || false;
this.icons = icons || [];
}
hasIcons(): boolean {
return isNotEmpty(this.icons);
}
setEditMode(): void {
this.editMode = true;
}
updateIcons(icons: ChipsItemIcon[]): void {
this.icons = icons;
}
updateItem(item: any): void {
this.item = item;
this.setDisplayText();
}
unsetEditMode(): void {
this.editMode = false;
}
private setDisplayText(): void {
let value = this.item;
if ( typeof this.item === 'object') {
// Check If displayField is in an internal object
const obj = this.objToDisplay ? this.item[this.objToDisplay] : this.item;
const displayFieldBkp = 'value';
if (obj instanceof Object && obj && obj[this.fieldToDisplay]) {
value = obj[this.fieldToDisplay];
} else if (obj instanceof Object && obj && obj[displayFieldBkp]) {
value = obj[displayFieldBkp];
} else {
value = obj;
}
}
this.display = value;
}
}

View File

@@ -0,0 +1,130 @@
import { findIndex, isEqual } from 'lodash';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { ChipsItem, ChipsItemIcon } from './chips-item.model';
import { hasValue } from '../../empty.util';
import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
import { AuthorityValueModel } from '../../../core/integration/models/authority-value.model';
export interface ChipsIconsConfig {
[metadata: string]: string;
}
export class Chips {
chipsItems: BehaviorSubject<ChipsItem[]>;
displayField: string;
displayObj: string;
iconsConfig: ChipsIconsConfig;
private _items: ChipsItem[];
constructor(items: any[] = [],
displayField: string = 'display',
displayObj?: string,
iconsConfig?: ChipsIconsConfig) {
this.displayField = displayField;
this.displayObj = displayObj;
this.iconsConfig = iconsConfig || Object.create({});
if (Array.isArray(items)) {
this.setInitialItems(items);
}
}
public add(item: any): void {
const icons = this.getChipsIcons(item);
const chipsItem = new ChipsItem(item, this.displayField, this.displayObj, icons);
const duplicatedIndex = findIndex(this._items, {display: chipsItem.display.trim()});
if (duplicatedIndex === -1 || !isEqual(item, this.getChipByIndex(duplicatedIndex).item)) {
this._items.push(chipsItem);
this.chipsItems.next(this._items);
}
}
public getChipById(id): ChipsItem {
const index = findIndex(this._items, {id: id});
return this.getChipByIndex(index);
}
public getChipByIndex(index): ChipsItem {
if (this._items.length > 0 && this._items[index]) {
return this._items[index];
} else {
return null;
}
}
public getChips(): ChipsItem[] {
return this._items;
}
/**
* To use to get items before to store it
* @returns {any[]}
*/
public getChipsItems(): any[] {
const out = [];
this._items.forEach((item) => {
out.push(item.item);
});
return out;
}
public hasItems(): boolean {
return this._items.length > 0;
}
public remove(chipsItem: ChipsItem): void {
const index = findIndex(this._items, {id: chipsItem.id});
this._items.splice(index, 1);
this.chipsItems.next(this._items);
}
public update(id: string, item: any): void {
const chipsItemTarget = this.getChipById(id);
const icons = this.getChipsIcons(item);
chipsItemTarget.updateItem(item);
chipsItemTarget.updateIcons(icons);
chipsItemTarget.unsetEditMode();
this.chipsItems.next(this._items);
}
public updateOrder(): void {
this.chipsItems.next(this._items);
}
private getChipsIcons(item) {
const icons = [];
Object.keys(item)
.forEach((metadata) => {
const value = item[metadata];
if (hasValue(value)
&& (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValueModel)
&& ((value as FormFieldMetadataValueObject).authority || (value as AuthorityValueModel).id)
&& this.iconsConfig.hasOwnProperty(metadata)) {
const icon: ChipsItemIcon = {
style: this.iconsConfig[metadata]
};
icons.push(icon);
}
});
return icons;
}
/**
* Sets initial items, used in edit mode
*/
private setInitialItems(items: any[]): void {
this._items = [];
items.forEach((item) => {
const icons = this.getChipsIcons(item);
const chipsItem = new ChipsItem(item, this.displayField, this.displayObj, icons);
this._items.push(chipsItem);
});
this.chipsItems = new BehaviorSubject<ChipsItem[]>(this._items);
}
}

View File

@@ -0,0 +1,19 @@
import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
export function dateToGMTString(date: Date | NgbDateStruct) {
let year = ((date instanceof Date) ? date.getFullYear() : date.year).toString();
let month = ((date instanceof Date) ? date.getMonth() + 1 : date.month).toString();
let day = ((date instanceof Date) ? date.getDate() : date.day).toString();
let hour = ((date instanceof Date) ? date.getHours() : 0).toString();
let min = ((date instanceof Date) ? date.getMinutes() : 0).toString();
let sec = ((date instanceof Date) ? date.getSeconds() : 0).toString();
year = (year.length === 1) ? '0' + year : year;
month = (month.length === 1) ? '0' + month : month;
day = (day.length === 1) ? '0' + day : day;
hour = (hour.length === 1) ? '0' + hour : hour;
min = (min.length === 1) ? '0' + min : min;
sec = (sec.length === 1) ? '0' + sec : sec;
return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
}

View File

@@ -0,0 +1,442 @@
<div [class.form-group]="(type !== 6 && asBootstrapFormGroup) || getClass('element', 'container').includes('form-group')"
[formGroup]="group"
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
<label *ngIf="type !== 3 && model.label"
[for]="model.id"
[innerHTML]="(model.required && model.label) ? (model.label | translate) + ' *' : (model.label | translate)"
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
<ng-container *ngTemplateOutlet="templates[0]?.templateRef; context: model"></ng-container>
<div [ngClass]="{'form-row': model.hasLanguages }">
<div [ngClass]="getClass('grid', 'control')">
<ng-container [ngSwitch]="type">
<!-- FORM ARRAY ------------------------------------------------------------------------------------------->
<div *ngSwitchCase="1"
[dynamicId]="bindId && model.id"
[formArrayName]="model.id"
[ngClass]="getClass('element', 'control')">
<div *ngFor="let groupModel of model.groups; let idx = index" role="group"
[formGroupName]="idx" [ngClass]="[getClass('element', 'group'), getClass('grid', 'group')]">
<ds-dynamic-form-control *ngFor="let _model of groupModel.group"
[bindId]="false"
[formId]="formId"
[context]="groupModel"
[group]="control.at(idx)"
[hasErrorMessaging]="_model.hasErrorMessages"
[hidden]="_model.hidden"
[layout]="layout"
[model]="_model"
[templates]="templateList"
[ngClass]="[getClass('element', 'host', _model), getClass('grid', 'host', _model)]"
(dfBlur)="onBlur($event)"
(dfChange)="onValueChange($event)"
(dfFocus)="onFocus($event)"></ds-dynamic-form-control>
<ng-container *ngTemplateOutlet="templates[2]?.templateRef; context: groupModel"></ng-container>
</div>
</div>
<!-- CALENDAR --------------------------------------------------------------------------------------------->
<ngb-datepicker *ngSwitchCase="2"
[displayMonths]="getAdditional('displayMonths', 1)"
[dynamicId]="bindId && model.id"
[firstDayOfWeek]="getAdditional('firstDayOfWeek', 1)"
[formControlName]="model.id"
[maxDate]="model.max"
[minDate]="model.min"
[navigation]="getAdditional('navigation', 'select')"
[ngClass]="getClass('element', 'control')"
[outsideDays]="getAdditional('outsideDays', 'visible')"
[showWeekdays]="getAdditional('showWeekdays', true)"
[showWeekNumbers]="getAdditional('showWeekNumbers', false)"
[startDate]="model.focusedDate"
(select)="onValueChange($event)"></ngb-datepicker>
<!-- CHECKBOX --------------------------------------------------------------------------------------------->
<div *ngSwitchCase="3" class="custom-control custom-checkbox" [class.disabled]="model.disabled">
<input type="checkbox" class="custom-control-input"
[checked]="model.checked"
[class.is-invalid]="showErrorMessages"
[id]="bindId && model.id"
[dynamicId]="bindId && model.id"
[formControlName]="model.id"
[indeterminate]="model.indeterminate"
[name]="model.name"
[ngClass]="getClass('element', 'control')"
[required]="model.required"
[tabindex]="model.tabIndex"
[value]="model.value"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)" >
<label class="custom-control-label" [for]="bindId && model.id">
<span [innerHTML]="model.label"
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]">
</span>
</label>
</div>
<!-- CHECKBOX GROUP --------------------------------------------------------------------------------------->
<div *ngSwitchCase="4" class="btn-group btn-group-toggle" data-toggle="buttons"
[dynamicId]="bindId && model.id"
[formGroupName]="model.id"
[ngClass]="getClass('element', 'control')">
<label *ngFor="let checkboxModel of model.group" ngbButtonLabel
[hidden]="checkboxModel.hidden"
[ngClass]="getClass('element', 'control', checkboxModel)">
<input type="checkbox" ngbButton
[checked]="checkboxModel.checked"
[dynamicId]="bindId && checkboxModel.id"
[formControlName]="checkboxModel.id"
[name]="checkboxModel.name"
[required]="checkboxModel.required"
[tabindex]="checkboxModel.tabIndex"
[value]="checkboxModel.value"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"/>
<span [ngClass]="getClass('element', 'label', checkboxModel)"
[innerHTML]="checkboxModel.label"></span></label>
</div>
<!-- DATEPICKER ------------------------------------------------------------------------------------------->
<div *ngSwitchCase="5" class="input-group">
<input ngbDatepicker class="form-control" #datepicker="ngbDatepicker"
[class.is-invalid]="showErrorMessages"
[displayMonths]="getAdditional('displayMonths', 1)"
[dynamicId]="bindId && model.id"
[firstDayOfWeek]="getAdditional('firstDayOfWeek', 1)"
[formControlName]="model.id"
[maxDate]="model.max"
[minDate]="model.min"
[name]="model.name"
[navigation]="getAdditional('navigation', 'select')"
[ngClass]="getClass('element', 'control')"
[outsideDays]="getAdditional('outsideDays', 'visible')"
[placeholder]="(model.placeholder | translate)"
[placement]="getAdditional('placement', 'bottom-left')"
[showWeekdays]="getAdditional('showWeekdays', true)"
[showWeekNumbers]="getAdditional('showWeekNumbers', false)"
[startDate]="model.focusedDate"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)">
<div class="input-group-append">
<button class="btn btn-outline-secondary"
type="button"
[class.disabled]="model.disabled"
[disabled]="model.disabled"
(click)="datepicker.toggle()">
<i *ngIf="model.toggleIcon" class="{{model.toggleIcon}}" aria-hidden="true"></i>
<span *ngIf="model.toggleLabel">{{ model.toggleLabel }}</span>
</button>
</div>
</div>
<!-- FORM GROUP ------------------------------------------------------------------------------------------->
<div *ngSwitchCase="6" role="group"
[dynamicId]="bindId && model.id"
[formGroupName]="model.id"
[ngClass]="getClass('element','control')">
<ds-dynamic-form-control *ngFor="let _model of model.group"
[asBootstrapFormGroup]="true"
[formId]="formId"
[group]="control"
[hasErrorMessaging]="_model.hasErrorMessages"
[hidden]="_model.hidden"
[layout]="layout"
[model]="_model"
[templates]="templateList"
[ngClass]="[getClass('element', 'host', _model), getClass('grid', 'host', _model)]"
(dfBlur)="onBlur($event)"
(dfChange)="onValueChange($event)"
(dfFocus)="onFocus($event)"></ds-dynamic-form-control>
</div>
<!-- INPUT ------------------------------------------------------------------------------------------------>
<div *ngSwitchCase="7" [class.input-group]="model.prefix || model.suffix">
<div *ngIf="model.prefix" class="input-group-addon" [innerHTML]="model.prefix"></div>
<ng-container *ngTemplateOutlet="inputTemplate;
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
</ng-container>
<div *ngIf="model.suffix" class="input-group-addon" [innerHTML]="model.suffix"></div>
<datalist *ngIf="model.list" [id]="model.listId">
<option *ngFor="let option of model.list" [value]="option">
</datalist>
</div>
<!-- RADIO GROUP ------------------------------------------------------------------------------------------>
<div *ngSwitchCase="8" ngbRadioGroup class="btn-group" role="radiogroup"
[dynamicId]="bindId && model.id"
[formControlName]="model.id"
[ngClass]="getClass('element', 'control')"
[tabindex]="model.tabIndex"
(change)="onValueChange($event)">
<legend *ngIf="model.legend" [innerHTML]="model.legend"></legend>
<label *ngFor="let option of model.options$ | async" ngbButtonLabel
[ngClass]="[getClass('element', 'option'), getClass('grid', 'option')]">
<input type="radio" ngbButton
[disabled]="option.disabled"
[name]="model.name"
[value]="option.value"
(blur)="onBlur($event)"
(focus)="onFocus($event)"/><span [innerHTML]="option.label"></span>
</label>
</div>
<ng-container *ngSwitchCase="9">
<ng-container *ngTemplateOutlet="selectTemplate;
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
</ng-container>
</ng-container>
<!-- TEXTAREA --------------------------------------------------------------------------------------------->
<textarea *ngSwitchCase="10" class="form-control"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[cols]="model.cols"
[formControlName]="model.id"
[maxlength]="model.maxLength"
[minlength]="model.minLength"
[name]="model.name"
[ngClass]="getClass('element', 'control')"
[placeholder]="(model.placeholder | translate)"
[readonly]="model.readOnly"
[required]="model.required"
[rows]="model.rows"
[spellcheck]="model.spellCheck"
[tabindex]="model.tabIndex"
[wrap]="model.wrap"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></textarea>
<!-- TIMEPICKER ------------------------------------------------------------------------------------------->
<ngb-timepicker *ngSwitchCase="11"
[dynamicId]="bindId && model.id"
[formControlName]="model.id"
[hourStep]="getAdditional('hourStep', 1)"
[meridian]="model.meridian"
[minuteStep]="getAdditional('minuteStep', 1)"
[ngClass]="getClass('element', 'control')"
[seconds]="model.showSeconds"
[secondStep]="getAdditional('secondStep', 1)"
[size]="getAdditional('size', 'medium')"
[spinners]="getAdditional('spinners', true)"></ngb-timepicker>
<ng-container *ngSwitchCase="12">
<ng-container *ngTemplateOutlet="typeaheadTemplate;
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="13">
<ng-container *ngTemplateOutlet="scrollableDropdownTemplate;
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="14">
<ng-container *ngTemplateOutlet="tagTemplate;
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="15">
<ng-container *ngTemplateOutlet="listTemplate;
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="16">
<ds-dynamic-group [model]="model"
[formId]="formId"
[group]="group"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-group>
</ng-container>
<ng-container *ngSwitchCase="17">
<ds-date-picker
[bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-date-picker>
</ng-container>
<ng-container *ngSwitchCase="18">
<ds-dynamic-lookup
[bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"
></ds-dynamic-lookup>
</ng-container>
<small *ngIf="showHint" class="text-muted" [innerHTML]="model.hint"
[ngClass]="getClass('element', 'hint')"></small>
<div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message }}</small>
</div>
</ng-container>
<ng-template #inputTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<input [attr.accept]="model.accept"
[attr.list]="model.listId"
[attr.max]="model.max"
[attr.min]="model.min"
[attr.multiple]="model.multiple"
[attr.step]="model.step"
[autocomplete]="model.autoComplete"
[autofocus]="model.autoFocus"
[class.form-control]="model.inputType !== 'file'"
[class.form-control-file]="model.inputType === 'file'"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[formControlName]="model.id"
[maxlength]="model.maxLength"
[minlength]="model.minLength"
[name]="model.name"
[ngClass]="getClass('element', 'control')"
[pattern]="model.pattern"
[placeholder]="(model.placeholder | translate)"
[readonly]="model.readOnly"
[required]="model.required"
[spellcheck]="model.spellCheck"
[tabindex]="model.tabIndex"
[textMask]="{mask: (model.mask || false), showMask: model.mask && !(model.placeholder)}"
[type]="model.inputType"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"/>
</ng-template>
<ng-template #selectTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<select class="form-control"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[formControlName]="model.id"
[name]="model.name"
[ngClass]="getClass('element', 'control')"
[required]="model.required"
[tabindex]="model.tabIndex"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)">
<option *ngFor="let option of model.options$ | async"
[disabled]="option.disabled"
[ngValue]="option.value">{{ option.label }}</option>
</select>
</ng-template>
<ng-template #typeaheadTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<ds-dynamic-typeahead [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-typeahead>
</ng-template>
<ng-template #scrollableDropdownTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<ds-dynamic-scrollable-dropdown [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-scrollable-dropdown>
</ng-template>
<ng-template #tagTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<ds-dynamic-tag [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-tag>
</ng-template>
<ng-template #listTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<ds-dynamic-list [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-list>
</ng-template>
</div>
<div *ngIf="model.languageCodes && model.languageCodes.length > 0" class="col-xs-2">
<select
#language="ngModel"
[disabled]="model.readOnly"
[(ngModel)]="model.language"
class="form-control"
(blur)="onBlur($event)"
(change)="onChangeLanguage($event)"
[ngModelOptions]="{standalone: true}"
required>
<!--<option [value]="null" disabled>Language</option>-->
<option *ngFor="let lang of model.languageCodes" [value]="lang.code">{{lang.display}}</option>
</select>
</div>
</div>
<ng-container *ngTemplateOutlet="templates[1]?.templateRef; context: model"></ng-container>
<ng-content></ng-content>
</div>

View File

@@ -0,0 +1,176 @@
import {
ChangeDetectorRef,
Component,
ContentChildren,
EventEmitter,
Input,
OnChanges,
Output,
QueryList,
SimpleChanges
} from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
DynamicDatePickerModel,
DynamicFormArrayGroupModel,
DynamicFormControlComponent,
DynamicFormControlEvent,
DynamicFormControlModel,
DynamicFormLayout,
DynamicFormLayoutService,
DynamicFormValidationService,
DynamicTemplateDirective,
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX,
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP,
DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER,
DYNAMIC_FORM_CONTROL_TYPE_GROUP,
DYNAMIC_FORM_CONTROL_TYPE_INPUT,
DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP,
DYNAMIC_FORM_CONTROL_TYPE_SELECT,
DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA,
DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER,
} from '@ng-dynamic-forms/core';
import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model';
import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './models/tag/dynamic-tag.model';
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION } from './models/ds-dynamic-group/dynamic-group.model';
import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/ds-date-picker/ds-date-picker.model';
import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP } from './models/lookup/dynamic-lookup.model';
import { DynamicListCheckboxGroupModel } from './models/list/dynamic-list-checkbox-group.model';
import { DynamicListRadioGroupModel } from './models/list/dynamic-list-radio-group.model';
import { isNotEmpty } from '../../../empty.util';
export const enum NGBootstrapFormControlType {
Array = 1, // 'ARRAY',
Calendar = 2, // 'CALENDAR',
Checkbox = 3, // 'CHECKBOX',
CheckboxGroup = 4, // 'CHECKBOX_GROUP',
DatePicker = 5, // 'DATEPICKER',
Group = 6, // 'GROUP',
Input = 7, // 'INPUT',
RadioGroup = 8, // 'RADIO_GROUP',
Select = 9, // 'SELECT',
TextArea = 10, // 'TEXTAREA',
TimePicker = 11, // 'TIMEPICKER'
TypeAhead = 12, // 'TYPEAHEAD'
ScrollableDropdown = 13, // 'SCROLLABLE_DROPDOWN'
TypeTag = 14, // 'TYPETAG'
List = 15, // 'TYPELIST'
Relation = 16, // Dynamic Group
DsDatePicker = 17, // Ds Date Picker
Lookup = 18, // LOOKUP
}
@Component({
selector: 'ds-dynamic-form-control',
styleUrls: ['../../form.component.scss', './ds-dynamic-form.component.scss'],
templateUrl: './ds-dynamic-form-control.component.html'
})
export class DsDynamicFormControlComponent extends DynamicFormControlComponent implements OnChanges {
@ContentChildren(DynamicTemplateDirective) contentTemplateList: QueryList<DynamicTemplateDirective>;
// tslint:disable-next-line:no-input-rename
@Input('templates') inputTemplateList: QueryList<DynamicTemplateDirective>;
@Input() formId: string;
@Input() asBootstrapFormGroup = true;
@Input() bindId = true;
@Input() context: any | null = null;
@Input() group: FormGroup;
@Input() hasErrorMessaging = false;
@Input() layout: DynamicFormLayout;
@Input() model: any;
/* tslint:disable:no-output-rename */
@Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output('dfFocus') focus: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
/* tslint:enable:no-output-rename */
type: NGBootstrapFormControlType | null;
static getFormControlType(model: DynamicFormControlModel): NGBootstrapFormControlType | null {
switch (model.type) {
case DYNAMIC_FORM_CONTROL_TYPE_ARRAY:
return NGBootstrapFormControlType.Array;
case DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX:
return NGBootstrapFormControlType.Checkbox;
case DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP:
return (model instanceof DynamicListCheckboxGroupModel) ? NGBootstrapFormControlType.List : NGBootstrapFormControlType.CheckboxGroup;
case DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER:
const datepickerModel = model as DynamicDatePickerModel;
return datepickerModel.inline ? NGBootstrapFormControlType.Calendar : NGBootstrapFormControlType.DatePicker;
case DYNAMIC_FORM_CONTROL_TYPE_GROUP:
// return (model instanceof DynamicGroupModel) ? NGBootstrapFormControlType.DynamicGroup : NGBootstrapFormControlType.Group;
return NGBootstrapFormControlType.Group;
case DYNAMIC_FORM_CONTROL_TYPE_INPUT:
return NGBootstrapFormControlType.Input;
case DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP:
return (model instanceof DynamicListRadioGroupModel) ? NGBootstrapFormControlType.List : NGBootstrapFormControlType.RadioGroup;
case DYNAMIC_FORM_CONTROL_TYPE_SELECT:
return NGBootstrapFormControlType.Select;
case DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA:
return NGBootstrapFormControlType.TextArea;
case DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER:
return NGBootstrapFormControlType.TimePicker;
case DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD:
return NGBootstrapFormControlType.TypeAhead;
case DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN:
return NGBootstrapFormControlType.ScrollableDropdown;
case DYNAMIC_FORM_CONTROL_TYPE_TAG:
return NGBootstrapFormControlType.TypeTag;
case DYNAMIC_FORM_CONTROL_TYPE_RELATION:
return NGBootstrapFormControlType.Relation;
case DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER:
return NGBootstrapFormControlType.DsDatePicker;
case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP:
return NGBootstrapFormControlType.Lookup;
default:
return null;
}
}
constructor(protected changeDetectorRef: ChangeDetectorRef, protected layoutService: DynamicFormLayoutService,
protected validationService: DynamicFormValidationService) {
super(changeDetectorRef, layoutService, validationService);
}
ngOnChanges(changes: SimpleChanges) {
if (changes) {
super.ngOnChanges(changes);
}
if (changes.model) {
this.type = DsDynamicFormControlComponent.getFormControlType(this.model);
}
}
onChangeLanguage(event) {
if (isNotEmpty((this.model as any).value)) {
this.onValueChange(event);
}
}
}

View File

@@ -0,0 +1,12 @@
<ds-dynamic-form-control *ngFor="let model of formModel; trackBy: trackByFn"
[formId]="formId"
[group]="formGroup"
[hasErrorMessaging]="model.hasErrorMessages"
[hidden]="model.hidden"
[layout]="formLayout"
[model]="model"
[ngClass]="[getClass(model, 'element', 'host'), getClass(model, 'grid', 'host')]"
[templates]="templates"
(dfBlur)="onEvent($event, 'blur')"
(dfChange)="onEvent($event, 'change')"
(dfFocus)="onEvent($event, 'focus')"></ds-dynamic-form-control>

View File

@@ -0,0 +1,5 @@
:host {
display: flex;
flex-direction: column;
justify-content: center;
}

View File

@@ -0,0 +1,47 @@
import {
Component,
ContentChildren,
EventEmitter,
Input,
Output,
QueryList,
ViewChildren
} from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
DynamicFormComponent,
DynamicFormControlEvent,
DynamicFormControlModel,
DynamicFormLayout,
DynamicFormLayoutService,
DynamicFormService,
DynamicTemplateDirective,
} from '@ng-dynamic-forms/core';
import { DsDynamicFormControlComponent } from './ds-dynamic-form-control.component';
import { FormBuilderService } from '../form-builder.service';
@Component({
selector: 'ds-dynamic-form',
templateUrl: './ds-dynamic-form.component.html'
})
export class DsDynamicFormComponent extends DynamicFormComponent {
@Input() formId: string;
@Input() formGroup: FormGroup;
@Input() formModel: DynamicFormControlModel[];
@Input() formLayout: DynamicFormLayout = null;
/* tslint:disable:no-output-rename */
@Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output('dfFocus') focus: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
/* tslint:enable:no-output-rename */
@ContentChildren(DynamicTemplateDirective) templates: QueryList<DynamicTemplateDirective>;
@ViewChildren(DsDynamicFormControlComponent) components: QueryList<DsDynamicFormControlComponent>;
constructor(protected formService: FormBuilderService, protected layoutService: DynamicFormLayoutService) {
super(formService, layoutService);
}
}

View File

@@ -0,0 +1,47 @@
<div class="d-flex">
<div class="mr-3">
<ds-number-picker
[disabled]="model.disabled"
[min]="minYear"
[max]="maxYear"
[name]="'year'"
[size]="4"
[(ngModel)]="initialYear"
[value]="year"
[invalid]="invalid"
[placeholder]='yearPlaceholder'
(change)="onChange($event)"
></ds-number-picker>
</div>
<div class="mr-3">
<ds-number-picker
[min]="minMonth"
[max]="maxMonth"
[name]="'month'"
[size]="6"
[(ngModel)]="initialMonth"
[value]="month"
[placeholder]="monthPlaceholder"
[disabled]="!year || model.disabled"
(change)="onChange($event)"
></ds-number-picker>
</div>
<div class="mr-3">
<ds-number-picker
[min]="minDay"
[max]="maxDay"
[name]="'day'"
[size]="2"
[(ngModel)]="initialDay"
[value]="day"
[placeholder]="dayPlaceholder"
[disabled]="!month || model.disabled"
(change)="onChange($event)"
></ds-number-picker>
</div>
</div>
<div class="clearfix"></div>

View File

@@ -0,0 +1,3 @@
.col-lg-1 {
width: auto;
}

View File

@@ -0,0 +1,170 @@
import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { DynamicDsDatePickerModel } from './ds-date-picker.model';
export const DS_DATE_PICKER_SEPARATOR = '-';
@Component({
selector: 'ds-date-picker',
styleUrls: ['./ds-date-picker.component.scss'],
templateUrl: './ds-date-picker.component.html',
})
export class DsDatePickerComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicDsDatePickerModel;
@Input() showErrorMessages = false;
// @Input()
// minDate;
// @Input()
// maxDate;
@Output()
selected = new EventEmitter<number>();
@Output()
remove = new EventEmitter<number>();
@Output()
change = new EventEmitter<any>();
initialYear: number;
initialMonth: number;
initialDay: number;
year: number;
month: number;
day: number;
minYear: 0;
maxYear: number;
minMonth = 1;
maxMonth = 12;
minDay = 1;
maxDay = 31;
yearPlaceholder = 'year';
monthPlaceholder = 'month';
dayPlaceholder = 'day';
disabledMonth = true;
disabledDay = true;
invalid = false;
ngOnInit() {// TODO Manage fields when not setted
const now = new Date();
this.initialYear = now.getFullYear();
this.initialMonth = now.getMonth() + 1;
this.initialDay = now.getDate();
if (this.model.value && this.model.value !== null) {
const values = this.model.value.toString().split(DS_DATE_PICKER_SEPARATOR);
if (values.length > 0) {
this.initialYear = parseInt(values[0], 10);
this.year = this.initialYear;
this.disabledMonth = false;
}
if (values.length > 1) {
this.initialMonth = parseInt(values[1], 10);
this.month = this.initialMonth;
this.disabledDay = false;
}
if (values.length > 2) {
this.initialDay = parseInt(values[2], 10);
this.day = this.initialDay;
}
}
this.maxYear = this.initialYear + 100;
// Invalid state for year
this.group.get(this.model.id).statusChanges.subscribe((state) => {
if (state === 'INVALID' || this.model.malformedDate) {
this.invalid = true;
} else {
this.invalid = false;
this.model.malformedDate = false;
}
});
}
onChange(event) {
// update year-month-day
switch (event.field) {
case 'year': {
if (event.value !== null) {
this.year = event.value;
} else {
this.year = undefined;
this.month = undefined;
this.day = undefined;
}
break;
}
case 'month': {
if (event.value !== null) {
this.month = event.value;
} else {
this.month = undefined;
this.day = undefined;
}
break;
}
case 'day': {
if (event.value !== null) {
this.day = event.value;
} else {
this.day = undefined;
}
break;
}
}
// set max for days by month/year
if (!this.disabledDay) {
const month = this.month ? this.month - 1 : 0;
const date = new Date(this.year, month, 1);
this.maxDay = this.getLastDay(date);
if (this.day > this.maxDay) {
this.day = this.maxDay;
}
}
// Manage disable
if (!this.model.value && event.field === 'year') {
this.disabledMonth = false;
} else if (this.disabledDay && event.field === 'month') {
this.disabledDay = false;
}
// update value
let value = null;
if (this.year) {
let yyyy = this.year.toString();
while (yyyy.length < 4) {
yyyy = '0' + yyyy;
}
value = yyyy;
}
if (this.month) {
const mm = this.month.toString().length === 1
? '0' + this.month.toString()
: this.month.toString();
value += DS_DATE_PICKER_SEPARATOR + mm;
}
if (this.day) {
const dd = this.day.toString().length === 1
? '0' + this.day.toString()
: this.day.toString();
value += DS_DATE_PICKER_SEPARATOR + dd;
}
this.model.valueUpdates.next(value);
this.change.emit(event);
}
getLastDay(date: Date) {
// Last Day of the same month (+1 month, -1 day)
date.setMonth(date.getMonth() + 1, 0);
return date.getDate();
}
}

View File

@@ -0,0 +1,21 @@
import { DynamicDateControlModel, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { DynamicDateControlModelConfig } from '@ng-dynamic-forms/core/src/model/dynamic-date-control.model';
import { Subject } from 'rxjs/Subject';
export const DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER = 'DSDATEPICKER';
/**
* Dynamic Date Picker Model class
*/
export class DynamicDsDatePickerModel extends DynamicDateControlModel {
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER;
valueUpdates: Subject<any>;
malformedDate: boolean;
hasLanguages = false;
constructor(config: DynamicDateControlModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.malformedDate = false;
}
}

View File

@@ -0,0 +1,64 @@
import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicInputModelConfig, serializable } from '@ng-dynamic-forms/core';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from './ds-dynamic-input.model';
import { Subject } from 'rxjs/Subject';
import { DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core/src/model/form-group/dynamic-form-group.model';
import { LanguageCode } from '../../models/form-field-language-value.model';
export const COMBOBOX_GROUP_SUFFIX = '_COMBO_GROUP';
export const COMBOBOX_METADATA_SUFFIX = '_COMBO_METADATA';
export const COMBOBOX_VALUE_SUFFIX = '_COMBO_VALUE';
export interface DsDynamicComboboxModelConfig extends DynamicFormGroupModelConfig {
languageCodes: LanguageCode[];
language: string;
readOnly: boolean;
}
export class DynamicComboboxModel extends DynamicFormGroupModel {
@serializable() private _language: string;
@serializable() private _languageCodes: LanguageCode[];
@serializable() languageUpdates: Subject<string>;
@serializable() hasLanguages = false;
@serializable() readOnly: boolean;
constructor(config: DsDynamicComboboxModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.readOnly = config.readOnly;
this.language = config.language;
this.languageCodes = config.languageCodes;
this.languageUpdates = new Subject<string>();
this.languageUpdates.subscribe((lang: string) => {
this.language = lang;
});
}
get value() {
return (this.get(1) as DsDynamicInputModel).value;
}
get qualdropId(): string {
return (this.get(0) as DsDynamicInputModel).value.toString();
}
get language(): string {
return this._language;
}
set language(language: string) {
this._language = language;
}
get languageCodes(): LanguageCode[] {
return this._languageCodes;
}
set languageCodes(languageCodes: LanguageCode[]) {
this._languageCodes = languageCodes;
if (!this.language || this.language === null || this.language === '') {
this.language = this.languageCodes ? this.languageCodes[0].code : null;
}
}
}

View File

@@ -0,0 +1,50 @@
import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core';
import { isNotEmpty } from '../../../../empty.util';
import { DsDynamicInputModel } from './ds-dynamic-input.model';
import { AuthorityValueModel } from '../../../../../core/integration/models/authority-value.model';
export const CONCAT_GROUP_SUFFIX = '_CONCAT_GROUP';
export const CONCAT_FIRST_INPUT_SUFFIX = '_CONCAT_FIRST_INPUT';
export const CONCAT_SECOND_INPUT_SUFFIX = '_CONCAT_SECOND_INPUT';
export interface DynamicConcatModelConfig extends DynamicFormGroupModelConfig {
separator: string;
}
export class DynamicConcatModel extends DynamicFormGroupModel {
@serializable() separator: string;
@serializable() hasLanguages = false;
constructor(config: DynamicConcatModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.separator = config.separator + ' ';
}
get value() {
const firstValue = (this.get(0) as DsDynamicInputModel).value;
const secondValue = (this.get(1) as DsDynamicInputModel).value;
if (isNotEmpty(firstValue) && isNotEmpty(secondValue)) {
return firstValue + this.separator + secondValue;
} else {
return null
}
}
set value(value: string | AuthorityValueModel) {
let values;
if (typeof value === 'string') {
values = value ? value.split(this.separator) : [null, null];
} else {
values = value ? value.value.split(this.separator) : [null, null];
}
if (values.length > 1) {
(this.get(0) as DsDynamicInputModel).valueUpdates.next(values[0]);
(this.get(1) as DsDynamicInputModel).valueUpdates.next(values[1]);
}
}
}

View File

@@ -0,0 +1,65 @@
<a *ngIf="!(formCollapsed | async)"
class="close position-relative"
ngbTooltip="{{'form.group-collapse-help' | translate}}"
placement="left">
<span class="fa fa-angle-up fa-fw fa-2x"
aria-hidden="true"
(click)="collapseForm()"></span>
</a>
<a *ngIf="(formCollapsed | async)"
class="close position-relative"
ngbTooltip="{{'form.group-expand-help' | translate}}"
placement="left">
<span class="fa fa-angle-down fa-fw fa-2x"
aria-hidden="true"
(click)="expandForm()"></span>
</a>
<div class="pt-2" [ngClass]="{'border-top': !invalid, 'border border-danger': invalid}">
<div *ngIf="!(formCollapsed | async)" class="pl-2 row" @shrinkInOut>
<ds-form #formRef="formComponent"
class="col-sm-9 pl-0"
[formId]="formId"
[formModel]="formModel"
[displaySubmit]="false"></ds-form>
<div *ngIf="!(formCollapsed | async)" class="col p-0 m-0 d-flex justify-content-center align-items-center">
<button type="button"
class="btn btn-link"
[disabled]="isMandatoryFieldEmpty()"
(click)="save()">
<i class="fa fa-save text-primary fa-2x"
aria-hidden="true"></i>
</button>
<button type="button"
class="btn btn-link"
[disabled]="!editMode"
(click)="delete()">
<i class="fa fa-trash text-danger fa-2x"
aria-hidden="true"></i>
</button>
<button type="button"
class="btn btn-link"
[disabled]="isMandatoryFieldEmpty()"
(click)="clear()">
<i class="fa fa-undo fa-2x"
aria-hidden="true"></i>
</button>
</div>
<div class="clearfix"></div>
</div>
<div class="d-flex">
<div *ngIf="!chips.hasItems()">
<input type="text" readonly class="border-0 form-control-plaintext tag-input mt-1 mb-1 pl-2 text-muted" value="{{'form.no-value' | translate}}">
</div>
<ds-chips
*ngIf="chips.hasItems()"
[chips]="chips"
[editable]="true"
(selected)="onChipSelected($event)"></ds-chips>
</div>
</div>

View File

@@ -0,0 +1,3 @@
.close {
top: -2.5rem;
}

View File

@@ -0,0 +1,224 @@
import {
ChangeDetectorRef,
Component,
EventEmitter,
Inject,
Input,
OnDestroy,
OnInit,
Output,
ViewChild
} from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { DynamicFormControlModel, DynamicFormGroupModel, DynamicInputModel } from '@ng-dynamic-forms/core';
import { isEqual } from 'lodash';
import { DynamicGroupModel, PLACEHOLDER_PARENT_METADATA } from './dynamic-group.model';
import { FormBuilderService } from '../../../form-builder.service';
import { SubmissionFormsModel } from '../../../../../../core/shared/config/config-submission-forms.model';
import { FormService } from '../../../../form.service';
import { FormComponent } from '../../../../form.component';
import { Chips } from '../../../../../chips/models/chips.model';
import { DynamicLookupModel } from '../lookup/dynamic-lookup.model';
import { hasValue, isEmpty, isNotEmpty } from '../../../../../empty.util';
import { shrinkInOut } from '../../../../../animations/shrink';
import { ChipsItem } from '../../../../../chips/models/chips-item.model';
import { GlobalConfig } from '../../../../../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../../../../../config';
import { FormGroup } from '@angular/forms';
import { Subscription } from 'rxjs/Subscription';
@Component({
selector: 'ds-dynamic-group',
styleUrls: ['./dynamic-group.component.scss'],
templateUrl: './dynamic-group.component.html',
animations: [shrinkInOut]
})
export class DsDynamicGroupComponent implements OnDestroy, OnInit {
@Input() formId: string;
@Input() group: FormGroup;
@Input() model: DynamicGroupModel;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
public chips: Chips;
public formCollapsed = Observable.of(false);
public formModel: DynamicFormControlModel[];
public editMode = false;
public invalid = false;
private selectedChipItem: ChipsItem;
private subs: Subscription[] = [];
@ViewChild('formRef') private formRef: FormComponent;
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
private formBuilderService: FormBuilderService,
private formService: FormService,
private cdr: ChangeDetectorRef) {
}
ngOnInit() {
console.log(this.model.hasErrorMessages);
const config = {rows: this.model.formConfiguration} as SubmissionFormsModel;
if (isNotEmpty(this.model.value)) {
this.formCollapsed = Observable.of(true);
}
this.formId = this.formService.getUniqueId(this.model.id);
this.formModel = this.formBuilderService.modelFromConfiguration(config, this.model.scopeUUID, {});
this.chips = new Chips(this.model.value, 'value', this.model.mandatoryField);
this.subs.push(
this.chips.chipsItems
.subscribe((subItems: any[]) => {
const items = this.chips.getChipsItems();
// Does not emit change if model value is equal to the current value
if (!isEqual(items, this.model.value)) {
if (isEmpty(items)) {
// If items is empty, last element has been removed
// so emit an empty value that allows to dispatch
// a remove JSON PATCH operation
const emptyItem = Object.create({});
Object.keys(this.model.value[0])
.forEach((key) => {
emptyItem[key] = null;
});
items.push(emptyItem);
}
this.model.valueUpdates.next(items);
this.change.emit();
}
}),
// Invalid state for year
this.group.get(this.model.id).statusChanges.subscribe((state) => {
if (state === 'INVALID') {
this.invalid = true;
} else {
this.invalid = false;
}
})
)
}
isMandatoryFieldEmpty() {
// formModel[0].group[0].value == null
let res = true;
this.formModel.forEach((row) => {
const modelRow = row as DynamicFormGroupModel;
modelRow.group.forEach((model: DynamicInputModel) => {
if (model.name === this.model.mandatoryField) {
res = model.value == null;
return;
}
});
});
return res;
}
onChipSelected(event) {
this.expandForm();
this.selectedChipItem = this.chips.getChipByIndex(event);
this.formModel.forEach((row) => {
const modelRow = row as DynamicFormGroupModel;
modelRow.group.forEach((model: DynamicInputModel) => {
const value = this.selectedChipItem.item[model.name] === PLACEHOLDER_PARENT_METADATA ? null : this.selectedChipItem.item[model.name];
if (model instanceof DynamicLookupModel) {
(model as DynamicLookupModel).valueUpdates.next(value);
} else if (model instanceof DynamicInputModel) {
model.valueUpdates.next(value);
} else {
(model as any).value = value;
}
});
});
this.editMode = true;
}
collapseForm() {
this.formCollapsed = Observable.of(true);
this.clear();
}
expandForm() {
this.formCollapsed = Observable.of(false);
}
clear() {
if (this.editMode) {
this.selectedChipItem.editMode = false;
this.selectedChipItem = null;
this.editMode = false;
}
this.resetForm();
// this.change.emit(event);
}
save() {
if (this.editMode) {
this.modifyChip();
} else {
this.addToChips();
}
}
delete() {
this.chips.remove(this.selectedChipItem);
this.clear();
}
private addToChips() {
if (!this.formRef.formGroup.valid) {
this.formService.validateAllFormFields(this.formRef.formGroup);
return;
}
// Item to add
if (!this.isMandatoryFieldEmpty()) {
const item = this.buildChipItem();
this.chips.add(item);
this.resetForm();
}
}
private modifyChip() {
if (!this.formRef.formGroup.valid) {
this.formService.validateAllFormFields(this.formRef.formGroup);
return;
}
if (!this.isMandatoryFieldEmpty()) {
const item = this.buildChipItem();
this.chips.update(this.selectedChipItem.id, item);
this.resetForm();
this.cdr.detectChanges();
}
}
private buildChipItem() {
const item = Object.create({});
this.formModel.forEach((row) => {
const modelRow = row as DynamicFormGroupModel;
modelRow.group.forEach((control: DynamicInputModel) => {
item[control.name] = control.value || PLACEHOLDER_PARENT_METADATA;
});
});
return item;
}
private resetForm() {
this.formService.resetForm(this.formRef.formGroup, this.formModel, this.formId);
}
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -0,0 +1,41 @@
import { DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { FormRowModel } from '../../../../../../core/shared/config/config-submission-forms.model';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
export const DYNAMIC_FORM_CONTROL_TYPE_RELATION = 'RELATION';
export const PLACEHOLDER_PARENT_METADATA = '#PLACEHOLDER_PARENT_METADATA_VALUE#';
/**
* Dynamic Group Model configuration interface
*/
export interface DynamicGroupModelConfig extends DsDynamicInputModelConfig {
formConfiguration: FormRowModel[],
mandatoryField: string,
name: string,
relationFields: string[],
scopeUUID: string,
value?: any;
}
/**
* Dynamic Group Model class
*/
export class DynamicGroupModel extends DsDynamicInputModel {
@serializable() formConfiguration: FormRowModel[];
@serializable() mandatoryField: string;
@serializable() relationFields: string[];
@serializable() scopeUUID: string;
@serializable() value: any[];
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_RELATION;
constructor(config: DynamicGroupModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.formConfiguration = config.formConfiguration;
this.mandatoryField = config.mandatoryField;
this.relationFields = config.relationFields;
this.scopeUUID = config.scopeUUID;
const value = config.value || [];
this.valueUpdates.next(value)
}
}

View File

@@ -0,0 +1,86 @@
import {
DynamicFormControlLayout,
DynamicInputModel,
DynamicInputModelConfig,
serializable
} from '@ng-dynamic-forms/core';
import { Subject } from 'rxjs/Subject';
import { LanguageCode } from '../../models/form-field-language-value.model';
import { AuthorityOptions } from '../../../../../core/integration/models/authority-options.model';
import { hasValue } from '../../../../empty.util';
import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model';
export interface DsDynamicInputModelConfig extends DynamicInputModelConfig {
authorityOptions: AuthorityOptions;
languageCodes: LanguageCode[];
language?: string;
value?: any;
}
export class DsDynamicInputModel extends DynamicInputModel {
@serializable() authorityOptions: AuthorityOptions;
@serializable() private _languageCodes: LanguageCode[];
@serializable() private _language: string;
@serializable() languageUpdates: Subject<string>;
constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.readOnly = config.readOnly;
this.value = config.value;
this.language = config.language;
if (!this.language) {
// TypeAhead
if (config.value instanceof FormFieldMetadataValueObject) {
this.language = config.value.language;
} else if (Array.isArray(config.value)) {
// Tag of Authority
if (config.value[0].language) {
this.language = config.value[0].language;
}
}
}
this.languageCodes = config.languageCodes;
this.languageUpdates = new Subject<string>();
this.languageUpdates.subscribe((lang: string) => {
this.language = lang;
});
this.authorityOptions = config.authorityOptions;
}
get hasAuthority(): boolean {
return this.authorityOptions && hasValue(this.authorityOptions.name);
}
get hasLanguages(): boolean {
if (this.languageCodes && this.languageCodes.length > 1) {
return true;
} else {
return false;
}
}
get language(): string {
return this._language;
}
set language(language: string) {
this._language = language;
}
get languageCodes(): LanguageCode[] {
return this._languageCodes;
}
set languageCodes(languageCodes: LanguageCode[]) {
this._languageCodes = languageCodes;
if (!this.language || this.language === null || this.language === '') {
this.language = this.languageCodes ? this.languageCodes[0].code : null;
}
}
}

View File

@@ -0,0 +1,18 @@
import {
DynamicFormArrayModel, DynamicFormArrayModelConfig, DynamicFormControlLayout,
serializable
} from '@ng-dynamic-forms/core';
export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig {
notRepeteable: boolean;
}
export class DynamicRowArrayModel extends DynamicFormArrayModel {
@serializable() notRepeteable = false;
constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.notRepeteable = config.notRepeteable;
}
}

View File

@@ -0,0 +1,5 @@
import { DynamicFormGroupModel } from '@ng-dynamic-forms/core';
export class DynamicRowGroupModel extends DynamicFormGroupModel {
}

View File

@@ -0,0 +1,30 @@
import {
DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA,
DynamicFormControlLayout, DynamicTextAreaModel, DynamicTextAreaModelConfig,
serializable
} from '@ng-dynamic-forms/core';
import { Subject } from 'rxjs/Subject';
import { LanguageCode } from '../../models/form-field-language-value.model';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from './ds-dynamic-input.model';
export interface DsDynamicTextAreaModelConfig extends DsDynamicInputModelConfig {
cols?: number;
rows?: number;
wrap?: string;
}
export class DsDynamicTextAreaModel extends DsDynamicInputModel {
@serializable() cols: number;
@serializable() rows: number;
@serializable() wrap: string;
@serializable() type = DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA;
constructor(config: DsDynamicTextAreaModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.cols = config.cols;
this.rows = config.rows;
this.wrap = config.wrap;
}
}

View File

@@ -0,0 +1,59 @@
import { Subject } from 'rxjs/Subject';
import {
DynamicCheckboxGroupModel, DynamicFormControlLayout,
DynamicFormGroupModelConfig,
serializable
} from '@ng-dynamic-forms/core';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
import { hasValue } from '../../../../../empty.util';
export interface DynamicListCheckboxGroupModelConfig extends DynamicFormGroupModelConfig {
authorityOptions: AuthorityOptions;
groupLength: number;
repeatable: boolean;
value?: any;
}
export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel {
@serializable() authorityOptions: AuthorityOptions;
@serializable() repeatable: boolean;
@serializable() groupLength: number;
@serializable() _value: AuthorityValueModel[];
valueUpdates: Subject<any>;
constructor(config: DynamicListCheckboxGroupModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.authorityOptions = config.authorityOptions;
this.groupLength = config.groupLength || 5;
this._value = [];
this.repeatable = config.repeatable;
this.valueUpdates = new Subject<any>();
this.valueUpdates.subscribe((value: AuthorityValueModel | AuthorityValueModel[]) => this.value = value);
this.valueUpdates.next(config.value);
}
get hasAuthority(): boolean {
return this.authorityOptions && hasValue(this.authorityOptions.name);
}
get value() {
return this._value;
}
set value(value: AuthorityValueModel | AuthorityValueModel[]) {
if (value) {
if (Array.isArray(value)) {
this._value = value;
} else {
// _value is non extendible so assign it a new array
const newValue = (this.value as AuthorityValueModel[]).concat([value]);
this._value = newValue
}
}
}
}

View File

@@ -0,0 +1,35 @@
import {
DynamicFormControlLayout,
DynamicRadioGroupModel,
DynamicRadioGroupModelConfig,
serializable
} from '@ng-dynamic-forms/core';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
import { hasValue } from '../../../../../empty.util';
export interface DynamicListModelConfig extends DynamicRadioGroupModelConfig<any> {
authorityOptions: AuthorityOptions;
groupLength: number;
repeatable: boolean;
value?: any;
}
export class DynamicListRadioGroupModel extends DynamicRadioGroupModel<any> {
@serializable() authorityOptions: AuthorityOptions;
@serializable() repeatable: boolean;
@serializable() groupLength: number;
constructor(config: DynamicListModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.authorityOptions = config.authorityOptions;
this.groupLength = config.groupLength || 5;
this.repeatable = config.repeatable;
this.valueUpdates.next(config.value);
}
get hasAuthority(): boolean {
return this.authorityOptions && hasValue(this.authorityOptions.name);
}
}

View File

@@ -0,0 +1,66 @@
<div [formGroup]="group">
<div *ngIf="model.repeatable"
class="form-row"
[attr.tabindex]="model.tabIndex"
[dynamicId]="bindId && model.id"
[formGroupName]="model.id"
[ngClass]="model.layout.element?.control">
<div *ngFor="let columnItems of items" class="col-sm ml-3">
<div *ngFor="let item of columnItems" class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input"
[attr.tabindex]="item.index"
[checked]="item.value"
[id]="item.id"
[dynamicId]="item.id"
[formControlName]="item.id"
[name]="model.name"
[required]="model.required"
[value]="item.value"
(blur)="onBlurEvent($event)"
(change)="onChangeEvent($event)"
(focus)="onFocusEvent($event)"/>
<label class="custom-control-label"
[class.disabled]="model.disabled"
[ngClass]="model.layout.element?.control"
[for]="item.id">
<span [ngClass]="model.layout.element?.label" [innerHTML]="item.label"></span>
</label>
</div>
<br>
</div>
</div>
<div *ngIf="!model.repeatable"
class="form-row"
ngbRadioGroup
[attr.tabindex]="model.tabIndex"
[dynamicId]="bindId && model.id"
[ngClass]="model.layout.element?.control"
(change)="onChangeEvent($event)">
<div *ngFor="let columnItems of items" class="col-sm ml-3">
<div *ngFor="let item of columnItems" class="custom-control custom-radio">
<label class="custom-control-label"
[class.disabled]="model.disabled"
[ngClass]="model.layout.element?.control">
<input type="radio" class="custom-control-input"
[checked]="item.value"
[dynamicId]="item.id"
[name]="model.id"
[required]="model.required"
[value]="item.index"
(blur)="onBlurEvent($event)"
(focus)="onFocusEvent($event)"/>
<span [ngClass]="model.layout.element?.label" [innerHTML]="item.label"></span>
</label>
</div>
<br>
</div>
</div>
</div>

View File

@@ -0,0 +1,137 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { findKey } from 'lodash';
import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
import { hasValue, isNotEmpty } from '../../../../../empty.util';
import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model';
import { ConfigData } from '../../../../../../core/config/config-data';
import { ConfigAuthorityModel } from '../../../../../../core/shared/config/config-authority.model';
import { FormBuilderService } from '../../../form-builder.service';
import { DynamicCheckboxModel } from '@ng-dynamic-forms/core';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { DynamicListRadioGroupModel } from './dynamic-list-radio-group.model';
export interface ListItem {
id: string,
label: string,
value: boolean,
index: number
}
@Component({
selector: 'ds-dynamic-list',
styleUrls: ['./dynamic-list.component.scss'],
templateUrl: './dynamic-list.component.html'
})
// TODO Fare questo componente da zero
export class DsDynamicListComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicListCheckboxGroupModel | DynamicListRadioGroupModel;
@Input() showErrorMessages = false;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
public items: ListItem[][] = [];
protected authorityList: AuthorityValueModel[];
protected searchOptions: IntegrationSearchOptions;
constructor(private authorityService: AuthorityService,
private cdr: ChangeDetectorRef,
private formBuilderService: FormBuilderService) {
}
ngOnInit() {
if (this.hasAuthorityOptions()) {
// TODO Replace max elements 1000 with a paginated request when pagination bug is resolved
this.searchOptions = new IntegrationSearchOptions(
this.model.authorityOptions.scope,
this.model.authorityOptions.name,
this.model.authorityOptions.metadata,
'',
1000, // Max elements
1);// Current Page
this.setOptionsFromAuthority();
}
}
onBlurEvent(event: Event) {
this.blur.emit(event);
}
onFocusEvent(event: Event) {
this.focus.emit(event);
}
onChangeEvent(event: Event) {
const target = event.target as any;
if (this.model.repeatable) {
// Target tabindex coincide with the array index of the value into the authority list
const authorityValue: AuthorityValueModel = this.authorityList[target.tabIndex];
if (target.checked) {
this.model.valueUpdates.next(authorityValue);
} else {
const newValue = [];
this.model.value
.filter((item) => item.value !== authorityValue.value)
.forEach((item) => newValue.push(item));
this.model.valueUpdates.next(newValue);
}
} else {
(this.model as DynamicListRadioGroupModel).valueUpdates.next(this.authorityList[target.value]);
}
this.change.emit(event);
}
protected setOptionsFromAuthority() {
if (this.model.authorityOptions.name && this.model.authorityOptions.name.length > 0) {
const listGroup = this.group.controls[this.model.id] as FormGroup;
this.authorityService.getEntriesByName(this.searchOptions).subscribe((authorities: ConfigData) => {
let groupCounter = 0;
let itemsPerGroup = 0;
let tempList: ListItem[] = [];
this.authorityList = authorities.payload as ConfigAuthorityModel[];
// Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength'
(authorities.payload as ConfigAuthorityModel[]).forEach((option, key) => {
const value = option.id || option.value;
const checked: boolean = isNotEmpty(findKey(
this.model.value,
{value: option.value}));
const item: ListItem = {
id: value,
label: option.display,
value: checked,
index: key
};
if (this.model.repeatable) {
this.formBuilderService.addFormGroupControl(listGroup, (this.model as DynamicListCheckboxGroupModel), new DynamicCheckboxModel(item));
} else {
(this.model as DynamicListRadioGroupModel).options.push({label: item.label, value: option});
}
tempList.push(item);
itemsPerGroup++;
this.items[groupCounter] = tempList;
if (itemsPerGroup === this.model.groupLength) {
groupCounter++;
itemsPerGroup = 0;
tempList = [];
}
});
this.cdr.detectChanges();
});
}
}
protected hasAuthorityOptions() {
return (hasValue(this.model.authorityOptions.scope)
&& hasValue(this.model.authorityOptions.name)
&& hasValue(this.model.authorityOptions.metadata));
}
}

View File

@@ -0,0 +1,130 @@
<div #sdRef="ngbDropdown"
ngbDropdown
(click)="$event.stopPropagation();"
(openChange)="openChange($event)">
<!--Simple lookup, only 1 field -->
<div class="form-row" *ngIf="!isLookupName">
<div class="col-xs-12 col-sm-8 col-md-9 col-lg-10">
<div class="row">
<div class="col">
<input class="form-control"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[name]="model.name"
[type]="model.inputType"
[(ngModel)]="firstInputValue"
[disabled]="isInputDisabled()"
[placeholder]="model.placeholder"
[readonly]="model.readOnly"
(change)="$event.preventDefault()"
(blur)="onBlurEvent($event); sdRef.close();"
(focus)="onFocusEvent($event); sdRef.close();"
(click)="$event.stopPropagation(); sdRef.close();"
(input)="onInput($event)">
</div>
</div>
</div>
<div class="col-xs-12 col-sm-4 col-md-2 col-lg-1 text-center">
<button
*ngIf="!isInputDisabled()" class="btn btn-secondary"
type="button"
[disabled]="model.readOnly || isSearchDisabled()"
(click)="search($event); sdRef.open();">{{'form.search' | translate}}
</button>
<button
*ngIf="isInputDisabled()" class="btn btn-secondary"
type="button"
[disabled]="model.readOnly"
(click)="remove($event)">{{'form.remove' | translate}}
</button>
</div>
</div>
<!--Lookup-name, 2 fields-->
<div class="form-row" *ngIf="isLookupName">
<div class="col-xs-12 col-md-8 col-lg-9">
<div class="row">
<div class="col-xs-12 col-md-6">
<input class="form-control"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[name]="model.name"
[type]="model.inputType"
[(ngModel)]="firstInputValue"
[disabled]="isInputDisabled()"
[placeholder]="model.placeholder | translate"
[readonly]="model.readOnly"
(change)="$event.preventDefault()"
(blur)="onBlurEvent($event); sdRef.close();"
(focus)="onFocusEvent($event); sdRef.close();"
(click)="$event.stopPropagation(); sdRef.close();"
(input)="onInput($event)">
</div>
<div *ngIf="isLookupName" class="col-xs-12 col-md-6 pl-md-0" >
<input class="form-control"
[ngClass]="{}"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[name]="name2"
[type]="model.inputType"
[(ngModel)]="secondInputValue"
[disabled]="firstInputValue.length === 0 || isInputDisabled()"
[placeholder]="model.placeholder2 | translate"
[readonly]="model.readOnly"
(change)="$event.preventDefault()"
(blur)="onBlurEvent($event); sdRef.close();"
(focus)="onFocusEvent($event); sdRef.close();"
(click)="$event.stopPropagation(); sdRef.close();"
(input)="onInput($event)">
</div>
</div>
</div>
<div class="col-xs-12 col-md-3 col-lg-2 text-center">
<button
*ngIf="!isInputDisabled()" class="btn btn-secondary"
type="button"
[disabled]="isSearchDisabled()"
(click)="search($event); sdRef.open();">{{'form.search' | translate}}
</button>
<button
*ngIf="isInputDisabled()" class="btn btn-secondary"
type="button"
(click)="remove($event)">{{'form.remove' | translate}}
</button>
</div>
</div>
<div ngbDropdownMenu
class="mt-0 dropdown-menu scrollable-dropdown-menu w-100"
aria-haspopup="true"
aria-expanded="false"
aria-labelledby="scrollableDropdownMenuButton">
<div class="scrollable-menu"
aria-labelledby="scrollableDropdownMenuButton"
infiniteScroll
[infiniteScrollDistance]="2"
[infiniteScrollThrottle]="50"
(scrolled)="onScroll()"
[scrollWindow]="false">
<button class="dropdown-item disabled"
*ngIf="optionsList && optionsList.length == 0"
(click)="clearFields(); sdRef.close();">{{'form.no-results' | translate}}
</button>
<button class="dropdown-item collection-item"
*ngFor="let listEntry of optionsList"
(click)="onSelect(listEntry); sdRef.close();"
title="{{ listEntry.display }}">
{{listEntry.value}}
</button>
<div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>{{'form.loading' | translate}}</p></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,68 @@
@import "../../../../../../../styles/variables";
/* enable absolute positioning */
.spinner-addon {
position: relative;
}
/* style fa-spin */
.spinner-addon .fa-spin {
color: map-get($theme-colors, primary);
position: absolute;
margin-top: 3px;
padding: 0;
pointer-events: none;
}
/* align fa-spin */
.left-addon .fa-spin {
left: 0px;
}
.right-addon .fa-spin {
right: 0px;
}
/* add padding */
.left-addon input {
padding-left: 30px;
}
.right-addon input {
padding-right: 30px;
}
:host /deep/ .dropdown-menu {
width: 100% !important;
max-height: 200px;
overflow-y: auto !important;
overflow-x: hidden;
}
:host /deep/ .dropdown-item.active,
:host /deep/ .dropdown-item:active,
:host /deep/ .dropdown-item:focus,
:host /deep/ .dropdown-item:hover {
color: $dropdown-link-hover-color !important;
background-color: $dropdown-link-hover-bg !important;
}
.dropdown-menu {
margin-top: -10px;
}
div {
overflow: visible;
//padding-right: 0 !important;
}
button {
margin-right: 10px;
}
//@media (min-width: $screen-sm-min) {
// .firstName {
// padding-left: 0 !important;
// margin-left: -5px !important;
// }
//}

View File

@@ -0,0 +1,220 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { DynamicLookupModel } from './dynamic-lookup.model';
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
import { hasValue, isEmpty, isNotEmpty, isNull, isUndefined } from '../../../../../empty.util';
import { IntegrationData } from '../../../../../../core/integration/integration-data';
import { PageInfo } from '../../../../../../core/shared/page-info.model';
import { Subscription } from 'rxjs/Subscription';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
@Component({
selector: 'ds-dynamic-lookup',
styleUrls: ['./dynamic-lookup.component.scss'],
templateUrl: './dynamic-lookup.component.html'
})
export class DsDynamicLookupComponent implements OnDestroy, OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicLookupModel;
@Input() showErrorMessages = false;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
public firstInputValue = '';
public secondInputValue = '';
public loading = false;
public pageInfo: PageInfo;
public optionsList: any;
public isLookupName: boolean;
public name2: string;
protected searchOptions: IntegrationSearchOptions;
protected sub: Subscription;
constructor(private authorityService: AuthorityService,
private cdr: ChangeDetectorRef) {
}
ngOnInit() {
this.searchOptions = new IntegrationSearchOptions(
this.model.authorityOptions.scope,
this.model.authorityOptions.name,
this.model.authorityOptions.metadata,
'',
this.model.maxOptions,
1);
// Switch Lookup/LookupName
if (this.model.separator) {
this.isLookupName = true;
this.name2 = this.model.name + '2';
}
this.setInputsValue(this.model.value);
this.model.valueUpdates
.subscribe((value) => {
if (isEmpty(value)) {
this.resetFields();
} else {
this.setInputsValue(this.model.value);
}
});
}
public formatItemForInput(item: any, field: number): string {
if (isUndefined(item) || isNull(item)) {
return '';
}
return (typeof item === 'string') ? item : this.inputFormatter(item, field);
}
// inputFormatter = (x: { display: string }) => x.display;
inputFormatter = (x: { display: string }, y: number) => {
// this.splitValues();
return y === 1 ? this.firstInputValue : this.secondInputValue;
};
onInput(event) {
if (!this.model.authorityOptions.closed) {
if (isNotEmpty(this.getCurrentValue())) {
const currentValue = new FormFieldMetadataValueObject(this.getCurrentValue());
this.onSelect(currentValue);
} else {
this.remove();
}
}
}
onScroll() {
if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) {
this.searchOptions.currentPage++;
this.search();
}
}
protected setInputsValue(value) {
if (hasValue(value)) {
let displayValue = value;
if (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValueModel) {
displayValue = value.display;
}
if (hasValue(displayValue)) {
if (this.isLookupName) {
const values = displayValue.split(this.model.separator);
this.firstInputValue = (values[0] || '').trim();
this.secondInputValue = (values[1] || '').trim();
} else {
this.firstInputValue = displayValue || '';
}
}
}
}
protected getCurrentValue(): string {
let result = '';
if (!this.isLookupName) {
result = this.firstInputValue;
} else {
if (isNotEmpty(this.firstInputValue)) {
result = this.firstInputValue;
}
if (isNotEmpty(this.secondInputValue)) {
result = isEmpty(result)
? this.secondInputValue
: this.firstInputValue + this.model.separator + ' ' + this.secondInputValue;
}
}
return result;
}
search() {
this.optionsList = null;
this.pageInfo = null;
// Query
this.searchOptions.query = this.getCurrentValue();
this.loading = true;
this.authorityService.getEntriesByName(this.searchOptions)
.distinctUntilChanged()
.subscribe((object: IntegrationData) => {
this.optionsList = object.payload;
this.pageInfo = object.pageInfo;
this.loading = false;
this.cdr.detectChanges();
});
}
clearFields() {
// Clear inputs whether there is no results and authority is closed
if (this.model.authorityOptions.closed) {
this.resetFields();
}
}
protected resetFields() {
this.firstInputValue = '';
if (this.isLookupName) {
this.secondInputValue = '';
}
}
onSelect(event) {
this.group.markAsDirty();
this.model.valueUpdates.next(event);
this.setInputsValue(event);
this.change.emit(event);
this.optionsList = null;
this.pageInfo = null;
}
isInputDisabled() {
return this.model.authorityOptions.closed && hasValue(this.model.value);
}
isSearchDisabled() {
// if (this.firstInputValue === ''
// && (this.isLookupName ? this.secondInputValue === '' : true)) {
// return true;
// }
// return false;
return isEmpty(this.firstInputValue);
}
remove() {
this.group.markAsPristine();
this.model.valueUpdates.next(null);
this.change.emit(null);
}
openChange(isOpened: boolean) {
if (!isOpened) {
if (this.model.authorityOptions.closed) {
this.setInputsValue('');
}
}
}
onBlurEvent(event: Event) {
this.blur.emit(event);
}
onFocusEvent(event) {
this.focus.emit(event);
}
ngOnDestroy() {
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
}
}

View File

@@ -0,0 +1,37 @@
import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
export const DYNAMIC_FORM_CONTROL_TYPE_LOOKUP = 'LOOKUP';
export interface DynamicLookupModelConfig extends DsDynamicInputModelConfig {
maxOptions: number;
value: any;
separator: string;
}
export class DynamicLookupModel extends DsDynamicInputModel {
@serializable() maxOptions: number;
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_LOOKUP;
@serializable() value: any;
@serializable() separator: string; // Defined only for lookup-name
@serializable() placeholder: string;
@serializable() placeholder2: string;
constructor(config: DynamicLookupModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.autoComplete = AUTOCOMPLETE_OFF;
this.maxOptions = config.maxOptions;
this.separator = config.separator; // Defined only for lookup-name
this.valueUpdates.next(config.value);
// this.valueUpdates.subscribe(() => {
// this.setInputsValue();
// });
}
}

View File

@@ -0,0 +1,43 @@
<div #sdRef="ngbDropdown" ngbDropdown class="input-group w-100">
<input class="form-control"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[name]="model.name"
[readonly]="model.readOnly"
[type]="model.inputType"
[value]="formatItemForInput(model.value)"
(blur)="onBlurEvent($event)"
(click)="$event.stopPropagation(); openDropdown(sdRef);"
(focus)="onFocusEvent($event)"
(keypress)="$event.preventDefault()">
<button aria-describedby="collectionControlsMenuLabel"
class="ds-form-input-btn btn btn-outline-primary"
id="scrollableDropdownMenuButton_{{model.id}}"
ngbDropdownToggle
[disabled]="model.readOnly"></button>
<div ngbDropdownMenu
class="dropdown-menu scrollable-dropdown-menu w-100"
aria-haspopup="true"
aria-expanded="false"
aria-labelledby="scrollableDropdownMenuButton">
<div class="scrollable-menu"
aria-labelledby="scrollableDropdownMenuButton"
infiniteScroll
[infiniteScrollDistance]="2"
[infiniteScrollThrottle]="50"
(scrolled)="onScroll()"
[scrollWindow]="false">
<button class="dropdown-item disabled" *ngIf="optionsList && optionsList.length == 0">No results found</button>
<button class="dropdown-item collection-item" *ngFor="let listEntry of optionsList" (click)="onSelect(listEntry)" title="{{ listEntry.display }}">
{{inputFormatter(listEntry)}}
</button>
<div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>Loading...</p></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,26 @@
@import '../../../../form.component';
.scrollable-menu {
height: auto;
max-height: 200px;
overflow-x: hidden;
}
.collection-item {
border-bottom: $dropdown-border-width solid $dropdown-border-color;
}
.scrollable-dropdown-loading {
background-color: map-get($theme-colors, primary);
color: white;
height: 2rem !important;
line-height: 2rem;
position: sticky;
bottom: 0;
}
.scrollable-dropdown-menu {
left: 0 !important;
margin-bottom: 1rem;
z-index: 1000;
}

View File

@@ -0,0 +1,94 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { DynamicScrollableDropdownModel } from './dynamic-scrollable-dropdown.model';
import { PageInfo } from '../../../../../../core/shared/page-info.model';
import { isNull, isUndefined } from '../../../../../empty.util';
import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
import { IntegrationData } from '../../../../../../core/integration/integration-data';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'ds-dynamic-scrollable-dropdown',
styleUrls: ['./dynamic-scrollable-dropdown.component.scss'],
templateUrl: './dynamic-scrollable-dropdown.component.html'
})
export class DsDynamicScrollableDropdownComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicScrollableDropdownModel;
@Input() showErrorMessages = false;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
public loading = false;
public pageInfo: PageInfo;
public optionsList: any;
protected searchOptions: IntegrationSearchOptions;
constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef) {}
ngOnInit() {
this.searchOptions = new IntegrationSearchOptions(
this.model.authorityOptions.scope,
this.model.authorityOptions.name,
this.model.authorityOptions.metadata,
'',
this.model.maxOptions,
1);
this.authorityService.getEntriesByName(this.searchOptions)
.subscribe((object: IntegrationData) => {
this.optionsList = object.payload;
this.pageInfo = object.pageInfo;
this.cdr.detectChanges();
})
}
public formatItemForInput(item: any): string {
if (isUndefined(item) || isNull(item)) { return '' }
return (typeof item === 'string') ? item : this.inputFormatter(item);
}
inputFormatter = (x: AuthorityValueModel) => x.display || x.value;
openDropdown(sdRef: NgbDropdown) {
if (!this.model.readOnly) {
sdRef.open();
}
}
onScroll() {
if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) {
this.loading = true;
this.searchOptions.currentPage++;
this.authorityService.getEntriesByName(this.searchOptions)
.do(() => this.loading = false)
.subscribe((object: IntegrationData) => {
this.optionsList = this.optionsList.concat(object.payload);
this.pageInfo = object.pageInfo;
this.cdr.detectChanges();
})
}
}
onBlurEvent(event: Event) {
this.blur.emit(event);
}
onFocusEvent(event) {
this.focus.emit(event);
}
onSelect(event) {
this.group.markAsDirty();
// (this.model as DynamicScrollableDropdownModel).parent as
// this.group.get(this.model.id).setValue(event);
this.model.valueUpdates.next(event)
this.change.emit(event);
}
}

View File

@@ -0,0 +1,27 @@
import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
export const DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN = 'SCROLLABLE_DROPDOWN';
export interface DynamicScrollableDropdownModelConfig extends DsDynamicInputModelConfig {
authorityOptions: AuthorityOptions;
maxOptions: number;
value: any;
}
export class DynamicScrollableDropdownModel extends DsDynamicInputModel {
@serializable() maxOptions: number;
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN;
constructor(config: DynamicScrollableDropdownModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.autoComplete = AUTOCOMPLETE_OFF;
this.authorityOptions = config.authorityOptions;
this.maxOptions = config.maxOptions;
}
}

View File

@@ -0,0 +1,46 @@
<ng-template #rt let-r="result" let-t="term">
{{ r.display }}
</ng-template>
<ds-chips [chips]="chips" [wrapperClass]="'border-bottom border-light'">
<input *ngIf="!searchOptions"
class="border-0 form-control-plaintext tag-input mt-1 mb-1 chips-sort-ignore"
type="text"
[class.pl-3]="chips.hasItems()"
[placeholder]="model.label"
[readonly]="model.readOnly"
[(ngModel)]="currentValue"
(blur)="onBlurEvent($event)"
(keypress)="preventEventsPropagation($event)"
(keydown)="preventEventsPropagation($event)"
(keyup)="onKeyUp($event)" />
<input *ngIf="searchOptions"
class="border-0 form-control-plaintext tag-input mt-1 mb-1 chips-sort-ignore"
type="text"
[(ngModel)]="currentValue"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[class.pl-3]="chips.hasItems()"
[dynamicId]="bindId && model.id"
[inputFormatter]="formatter"
[name]="model.name"
[ngbTypeahead]="search"
[placeholder]="model.label"
[readonly]="model.readOnly"
[resultTemplate]="rt"
[type]="model.inputType"
(blur)="onBlurEvent($event)"
(focus)="onFocusEvent($event)"
(change)="$event.stopPropagation()"
(input)="onInput($event)"
(selectItem)="onSelectItem($event)"
(keypress)="preventEventsPropagation($event)"
(keydown)="preventEventsPropagation($event)"
(keyup)="onKeyUp($event)"/>
<i *ngIf="searching" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
</ds-chips>

View File

@@ -0,0 +1,34 @@
@import "../../../../../../../styles/variables";
/* style fa-spin */
.fa-spin {
pointer-events: none;
right: 0;
}
.chips-left {
left: 0;
padding-right: 100%;
}
:host /deep/ .dropdown-menu {
width: 100% !important;
max-height: 200px;
overflow-y: scroll;
overflow-x: hidden;
left: 0 !important;
margin-top: 15px !important;
}
:host /deep/ .dropdown-item.active,
:host /deep/ .dropdown-item:active,
:host /deep/ .dropdown-item:focus,
:host /deep/ .dropdown-item:hover {
color: $dropdown-link-hover-color !important;
background-color: $dropdown-link-hover-bg !important;
}
.tag-input {
flex-grow: 1;
outline: none;
width: auto !important;
}

View File

@@ -0,0 +1,194 @@
import { ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { DynamicTagModel } from './dynamic-tag.model';
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
import { Chips } from '../../../../../chips/models/chips.model';
import { hasValue, isEmpty, isNotEmpty } from '../../../../../empty.util';
import { isEqual } from 'lodash';
import { GlobalConfig } from '../../../../../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../../../../../config';
@Component({
selector: 'ds-dynamic-tag',
styleUrls: ['./dynamic-tag.component.scss'],
templateUrl: './dynamic-tag.component.html'
})
export class DsDynamicTagComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicTagModel;
@Input() showErrorMessages = false;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
chips: Chips;
hasAuthority: boolean;
searching = false;
searchOptions: IntegrationSearchOptions;
searchFailed = false;
hideSearchingWhenUnsubscribed = new Observable(() => () => this.changeSearchingStatus(false));
currentValue: any;
formatter = (x: { display: string }) => x.display;
search = (text$: Observable<string>) =>
text$
.debounceTime(300)
.distinctUntilChanged()
.do(() => this.changeSearchingStatus(true))
.switchMap((term) => {
if (term === '' || term.length < this.model.minChars) {
return Observable.of({list: []});
} else {
this.searchOptions.query = term;
return this.authorityService.getEntriesByName(this.searchOptions)
.map((authorities) => {
// @TODO Pagination for authority is not working, to refactor when it will be fixed
return {
list: authorities.payload,
pageInfo: authorities.pageInfo
};
})
.do(() => this.searchFailed = false)
.catch(() => {
this.searchFailed = true;
return Observable.of({list: []});
});
}
})
.map((results) => results.list)
.do(() => this.changeSearchingStatus(false))
.merge(this.hideSearchingWhenUnsubscribed);
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
private authorityService: AuthorityService,
private cdr: ChangeDetectorRef) {
}
ngOnInit() {
this.hasAuthority = this.model.authorityOptions && hasValue(this.model.authorityOptions.name);
if (this.hasAuthority) {
this.searchOptions = new IntegrationSearchOptions(
this.model.authorityOptions.scope,
this.model.authorityOptions.name,
this.model.authorityOptions.metadata);
}
this.chips = new Chips(this.model.value, 'display');
this.chips.chipsItems
.subscribe((subItems: any[]) => {
const items = this.chips.getChipsItems();
// Does not emit change if model value is equal to the current value
if (!isEqual(items, this.model.value)) {
this.model.valueUpdates.next(items);
this.change.emit(event);
}
})
}
changeSearchingStatus(status: boolean) {
this.searching = status;
this.cdr.detectChanges();
}
onInput(event) {
if (event.data) {
this.group.markAsDirty();
}
this.cdr.detectChanges();
}
onBlurEvent(event: Event) {
if (isNotEmpty(this.currentValue)) {
this.addTagsToChips();
}
this.blur.emit(event);
}
onFocusEvent($event) {
this.focus.emit(event);
}
onSelectItem(event: NgbTypeaheadSelectItemEvent) {
this.chips.add(event.item);
// this.group.controls[this.model.id].setValue(this.model.value);
this.updateModel(event);
setTimeout(() => {
// Reset the input text after x ms, mandatory or the formatter overwrite it
this.currentValue = null;
this.cdr.detectChanges();
}, 50);
}
updateModel(event) {
this.model.valueUpdates.next(this.chips.getChipsItems());
this.change.emit(event);
}
onKeyUp(event) {
if (event.keyCode === 13 || event.keyCode === 188) {
event.preventDefault();
// Key: Enter or , or ;
this.addTagsToChips();
event.stopPropagation();
}
}
preventEventsPropagation(event) {
event.stopPropagation();
if (event.keyCode === 13) {
// Key: Enter or , or ;
event.preventDefault();
}
}
private addTagsToChips() {
if (!this.hasAuthority || !this.model.authorityOptions.closed) {
let res: string[] = [];
res = this.currentValue.split(',');
const res1 = [];
res.forEach((item) => {
item.split(';').forEach((i) => {
res1.push(i);
});
});
res1.forEach((c) => {
c = c.trim();
if (c.length > 0) {
this.chips.add(c);
}
});
// this.currentValue = '';
setTimeout(() => {
// Reset the input text after x ms, mandatory or the formatter overwrite it
this.currentValue = null;
this.cdr.detectChanges();
}, 50);
this.updateModel(event);
}
}
removeChips(event) {
// console.log("Removed chips index: "+event);
this.model.valueUpdates.next(this.chips.getChipsItems());
this.change.emit(event);
}
changeChips(event) {
this.model.valueUpdates.next(this.chips.getChipsItems());
this.change.emit(event);
}
}

View File

@@ -0,0 +1,143 @@
// import { AUTOCOMPLETE_OFF, DYNAMIC_FORM_CONTROL_INPUT_TYPE_TEXT } from '@ng-dynamic-forms/core';
// import { Observable } from 'rxjs/Observable';
//
// import {
// DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD, DynamicTypeaheadModel,
// DynamicTypeaheadResponseModel
// } from './dynamic-tag.model';
// import { PageInfo } from '../../../../../../core/shared/page-info.model';
//
// describe('DynamicTypeaheadModel test suite', () => {
//
// let model: any;
// const search = (text: string): Observable<DynamicTypeaheadResponseModel> =>
// Observable.of({
// list: ['One', 'Two', 'Three'],
// pageInfo: new PageInfo()
// });
// const config = {
// id: 'input',
// minChars: 3,
// search: search
// };
//
// beforeEach(() => model = new DynamicTypeaheadModel(config));
//
// it('tests if correct default type property is set', () => {
//
// expect(model.type).toEqual(DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD);
// });
//
// it('tests if correct default input type property is set', () => {
//
// expect(model.inputType).toEqual(DYNAMIC_FORM_CONTROL_INPUT_TYPE_TEXT);
// });
//
// it('tests if correct default autoComplete property is set', () => {
//
// expect(model.autoComplete).toEqual(AUTOCOMPLETE_OFF);
// });
//
// it('tests if correct default autoFocus property is set', () => {
//
// expect(model.autoFocus).toBe(false);
// });
//
// it('tests if correct default cls properties aree set', () => {
//
// expect(model.cls).toBeDefined();
// expect(model.cls.element.container).toEqual('');
// expect(model.cls.element.control).toEqual('');
// expect(model.cls.element.errors).toEqual('');
// expect(model.cls.element.label).toEqual('');
// expect(model.cls.grid.container).toEqual('');
// expect(model.cls.grid.control).toEqual('');
// expect(model.cls.grid.errors).toEqual('');
// expect(model.cls.grid.label).toEqual('');
// });
//
// it('tests if correct default hint property is set', () => {
//
// expect(model.hint).toBeNull();
// });
//
// it('tests if correct default label property is set', () => {
//
// expect(model.label).toBeNull();
// });
//
// it('tests if correct default max property is set', () => {
//
// expect(model.max).toBeNull();
// });
//
// it('tests if correct default maxLength property is set', () => {
//
// expect(model.maxLength).toBeNull();
// });
//
// it('tests if correct default minLength property is set', () => {
//
// expect(model.minLength).toBeNull();
// });
//
// it('tests if correct minChars property is set', () => {
//
// expect(model.minChars).toEqual(3);
// });
//
// it('tests if correct default min property is set', () => {
//
// expect(model.min).toBeNull();
// });
//
// it('tests if correct default placeholder property is set', () => {
//
// expect(model.placeholder).toEqual('');
// });
//
// it('tests if correct default readonly property is set', () => {
//
// expect(model.readOnly).toBe(false);
// });
//
// it('tests if correct default required property is set', () => {
//
// expect(model.required).toBe(false);
// });
//
// it('tests if correct default spellcheck property is set', () => {
//
// expect(model.spellCheck).toBe(false);
// });
//
// it('tests if correct default step property is set', () => {
//
// expect(model.step).toBeNull();
// });
//
// it('tests if correct default prefix property is set', () => {
//
// expect(model.prefix).toBeNull();
// });
//
// it('tests if correct default suffix property is set', () => {
//
// expect(model.suffix).toBeNull();
// });
//
// it('tests if correct search function is set', () => {
//
// expect(model.search).toBe(search);
// });
//
// it('should serialize correctly', () => {
//
// const json = JSON.parse(JSON.stringify(model));
//
// expect(json.id).toEqual(model.id);
// expect(json.disabled).toEqual(model.disabled);
// expect(json.value).toBe(model.value);
// expect(json.type).toEqual(DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD);
// });
// });

View File

@@ -0,0 +1,28 @@
import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
export const DYNAMIC_FORM_CONTROL_TYPE_TAG = 'TYPETAG';
export interface DynamicTagModelConfig extends DsDynamicInputModelConfig {
minChars: number;
value?: any;
}
export class DynamicTagModel extends DsDynamicInputModel {
@serializable() minChars: number;
@serializable() value: any[];
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_TAG;
constructor(config: DynamicTagModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.autoComplete = AUTOCOMPLETE_OFF;
this.minChars = config.minChars;
const value = config.value || [];
this.valueUpdates.next(value)
}
}

View File

@@ -0,0 +1,25 @@
<ng-template #rt let-r="result" let-t="term">
{{ r.display}}
</ng-template>
<div class="position-relative right-addon">
<i *ngIf="searching" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
<input class="form-control"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[inputFormatter]="formatter"
[name]="model.name"
[ngbTypeahead]="search"
[placeholder]="model.label"
[readonly]="model.readOnly"
[resultTemplate]="rt"
[type]="model.inputType"
[(ngModel)]="currentValue"
(blur)="onBlurEvent($event)"
(focus)="onFocusEvent($event)"
(change)="onChangeEvent($event)"
(input)="onInput($event)"
(selectItem)="onSelectItem($event)">
<div class="invalid-feedback" *ngIf="searchFailed">Sorry, suggestions could not be loaded.</div>
</div>

View File

@@ -0,0 +1,25 @@
@import "../../../../../../../styles/variables";
/* style fa-spin */
.fa-spin {
pointer-events: none;
}
/* align fa-spin */
.left-addon .fa-spin { left: 0;}
.right-addon .fa-spin { right: 0;}
:host /deep/ .dropdown-menu {
width: 100% !important;
max-height: 200px;
overflow-y: auto !important;
overflow-x: hidden;
}
:host /deep/ .dropdown-item.active,
:host /deep/ .dropdown-item:active,
:host /deep/ .dropdown-item:focus,
:host /deep/ .dropdown-item:hover {
color: $dropdown-link-hover-color !important;
background-color: $dropdown-link-hover-bg !important;
}

View File

@@ -0,0 +1,121 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { DynamicTypeaheadModel } from './dynamic-typeahead.model';
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
import { isEmpty, isNotEmpty } from '../../../../../empty.util';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
@Component({
selector: 'ds-dynamic-typeahead',
styleUrls: ['./dynamic-typeahead.component.scss'],
templateUrl: './dynamic-typeahead.component.html'
})
export class DsDynamicTypeaheadComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicTypeaheadModel;
@Input() showErrorMessages = false;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
searching = false;
searchOptions: IntegrationSearchOptions;
searchFailed = false;
hideSearchingWhenUnsubscribed = new Observable(() => () => this.changeSearchingStatus(false));
currentValue: any;
formatter = (x: { display: string }) => {
return (typeof x === 'object') ? x.display : x
};
search = (text$: Observable<string>) =>
text$
.debounceTime(300)
.distinctUntilChanged()
.do(() => this.changeSearchingStatus(true))
.switchMap((term) => {
if (term === '' || term.length < this.model.minChars) {
return Observable.of({list: []});
} else {
this.searchOptions.query = term;
return this.authorityService.getEntriesByName(this.searchOptions)
.map((authorities) => {
// @TODO Pagination for authority is not working, to refactor when it will be fixed
return {
list: authorities.payload,
pageInfo: authorities.pageInfo
};
})
.do(() => this.searchFailed = false)
.catch(() => {
this.searchFailed = true;
return Observable.of({list: []});
});
}
})
.map((results) => results.list)
.do(() => this.changeSearchingStatus(false))
.merge(this.hideSearchingWhenUnsubscribed);
constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef) {
}
ngOnInit() {
this.currentValue = this.model.value;
this.searchOptions = new IntegrationSearchOptions(
this.model.authorityOptions.scope,
this.model.authorityOptions.name,
this.model.authorityOptions.metadata);
this.group.get(this.model.id).valueChanges
.filter((value) => this.currentValue !== value)
.subscribe((value) => {
this.currentValue = value;
});
}
changeSearchingStatus(status: boolean) {
this.searching = status;
this.cdr.detectChanges();
}
onInput(event) {
if (!this.model.authorityOptions.closed && isNotEmpty(event.target.value)) {
const valueObj = new FormFieldMetadataValueObject(event.target.value);
this.currentValue = valueObj;
this.model.valueUpdates.next(valueObj as any);
this.change.emit(valueObj);
}
if (event.data) {
// this.group.markAsDirty();
}
}
onBlurEvent(event: Event) {
this.blur.emit(event);
}
onChangeEvent(event: Event) {
event.stopPropagation();
if (isEmpty(this.currentValue)) {
this.model.valueUpdates.next(null);
this.change.emit(null);
}
}
onFocusEvent(event) {
this.focus.emit(event);
}
onSelectItem(event: NgbTypeaheadSelectItemEvent) {
this.currentValue = event.item;
this.model.valueUpdates.next(event.item);
this.change.emit(event.item);
}
}

View File

@@ -0,0 +1,25 @@
import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
export const DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD = 'TYPEAHEAD';
export interface DsDynamicTypeaheadModelConfig extends DsDynamicInputModelConfig {
minChars: number;
value: any;
}
export class DynamicTypeaheadModel extends DsDynamicInputModel {
@serializable() minChars: number;
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD;
constructor(config: DsDynamicTypeaheadModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.autoComplete = AUTOCOMPLETE_OFF;
this.minChars = config.minChars;
}
}

View File

@@ -0,0 +1,212 @@
import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
DynamicFormArrayGroupModel,
DynamicFormArrayModel,
DynamicFormControlModel,
DynamicFormGroupModel,
DynamicFormService,
DynamicPathable,
JSONUtils,
} from '@ng-dynamic-forms/core';
import { mergeWith } from 'lodash';
import { isEmpty, isNotEmpty, isNotNull, isNull } from '../../empty.util';
import { DynamicComboboxModel } from './ds-dynamic-form-ui/models/ds-dynamic-combobox.model';
import { SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model';
import { DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model';
import { DynamicListCheckboxGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model';
import { DynamicGroupModel } from './ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model';
import { DynamicTagModel } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model';
import { DynamicListRadioGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model';
import { RowParser } from './parsers/row-parser';
import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
import { DynamicRowGroupModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-group-model';
@Injectable()
export class FormBuilderService extends DynamicFormService {
findById(id: string, groupModel: DynamicFormControlModel[], arrayIndex = null): DynamicFormControlModel | null {
let result = null;
const findByIdFn = (findId: string, findGroupModel: DynamicFormControlModel[]): void => {
for (const controlModel of findGroupModel) {
if (controlModel.id === findId) {
if (controlModel instanceof DynamicFormArrayModel && isNotNull(arrayIndex)) {
result = controlModel.get(arrayIndex);
} else {
result = controlModel;
}
break;
}
if (controlModel instanceof DynamicFormGroupModel) {
findByIdFn(findId, (controlModel as DynamicFormGroupModel).group);
}
if (controlModel instanceof DynamicFormArrayModel && (isNull(arrayIndex) || controlModel.size > (arrayIndex))) {
arrayIndex = (isNull(arrayIndex)) ? 0 : arrayIndex;
findByIdFn(findId, controlModel.get(arrayIndex).group);
}
}
};
findByIdFn(id, groupModel);
return result;
}
clearAllModelsValue(groupModel: DynamicFormControlModel[]): void {
const iterateControlModels = (findGroupModel: DynamicFormControlModel[]): void => {
for (const controlModel of findGroupModel) {
if (controlModel instanceof DynamicFormGroupModel) {
iterateControlModels((controlModel as DynamicFormGroupModel).group);
continue;
}
if (controlModel instanceof DynamicFormArrayModel) {
iterateControlModels(controlModel.groupFactory());
continue;
}
if (controlModel.hasOwnProperty('valueUpdates')) {
(controlModel as any).valueUpdates.next(undefined);
}
}
};
iterateControlModels(groupModel);
}
getValueFromModel(groupModel: DynamicFormControlModel[]): void {
let result = Object.create({});
const customizer = (objValue, srcValue) => {
if (Array.isArray(objValue)) {
return objValue.concat(srcValue);
}
};
const iterateControlModels = (findGroupModel: DynamicFormControlModel[]): void => {
let iterateResult = Object.create({});
// Iterate over all group's controls
for (const controlModel of findGroupModel) {
if (controlModel instanceof DynamicRowGroupModel && !this.isCustomGroup(controlModel)) {
iterateResult = mergeWith(iterateResult, iterateControlModels((controlModel as DynamicFormGroupModel).group), customizer);
continue;
}
if (controlModel instanceof DynamicFormGroupModel && !this.isCustomGroup(controlModel)) {
iterateResult[controlModel.name] = iterateControlModels((controlModel as DynamicFormGroupModel).group);
continue;
}
if (controlModel instanceof DynamicRowArrayModel) {
for (const arrayItemModel of controlModel.groups) {
iterateResult = mergeWith(iterateResult, iterateControlModels(arrayItemModel.group), customizer);
}
continue;
}
if (controlModel instanceof DynamicFormArrayModel) {
iterateResult[controlModel.name] = [];
for (const arrayItemModel of controlModel.groups) {
iterateResult[controlModel.name].push(iterateControlModels(arrayItemModel.group));
}
continue;
}
let controlId;
// Get the field's name
if (controlModel instanceof DynamicComboboxModel) {
// If is instance of DynamicComboboxModel take the qualdrop id as field's name
controlId = controlModel.qualdropId;
} else {
controlId = controlModel.name;
}
const controlValue = (controlModel as any).value || null;
if (controlId && iterateResult.hasOwnProperty(controlId) && isNotNull(iterateResult[controlId])) {
iterateResult[controlId].push(controlValue);
} else {
iterateResult[controlId] = isNotEmpty(controlValue) ? (Array.isArray(controlValue) ? controlValue : [controlValue]) : null;
}
}
return iterateResult;
};
result = iterateControlModels(groupModel);
return result;
}
modelFromConfiguration(json: string | SubmissionFormsModel, scopeUUID: string, initFormValues: any, submissionScope?: string, readOnly = false): DynamicFormControlModel[] | never {
let rows: DynamicFormControlModel[] = [];
const rawData = typeof json === 'string' ? JSON.parse(json, JSONUtils.parseReviver) : json;
if (rawData.rows && !isEmpty(rawData.rows)) {
rawData.rows.forEach((currentRow) => {
const rowParsed = new RowParser(currentRow, scopeUUID, initFormValues, submissionScope, readOnly).parse();
if (isNotNull(rowParsed)) {
if (Array.isArray(rowParsed)) {
rows = rows.concat(rowParsed);
} else {
rows.push(rowParsed);
}
}
});
}
return rows;
}
isModelInCustomGroup(model: DynamicFormControlModel) {
return model.parent &&
(model.parent instanceof DynamicConcatModel
|| model.parent instanceof DynamicComboboxModel);
}
hasMappedGroupValue(model: DynamicFormControlModel) {
return ((model.parent && model.parent instanceof DynamicComboboxModel)
|| model.parent instanceof DynamicGroupModel);
}
isCustomGroup(model: DynamicFormControlModel) {
return model &&
(model instanceof DynamicConcatModel
|| model instanceof DynamicComboboxModel
|| model instanceof DynamicListCheckboxGroupModel
|| model instanceof DynamicListRadioGroupModel);
}
isModelInAuthorityGroup(model: DynamicFormControlModel) {
return (model instanceof DynamicListCheckboxGroupModel || model instanceof DynamicTagModel);
}
getFormControlById(id: string, formGroup: FormGroup, groupModel: DynamicFormControlModel[], index = 0) {
const fieldModel = this.findById(id, groupModel, index);
return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null;
}
getId(model: DynamicPathable) {
if (model instanceof DynamicFormArrayGroupModel) {
return model.index.toString();
} else {
return ((model as DynamicFormControlModel).id !== (model as DynamicFormControlModel).name) ?
(model as DynamicFormControlModel).name :
(model as DynamicFormControlModel).id;
}
}
}

View File

@@ -0,0 +1,14 @@
export class FormFieldLanguageValueObject {
value: string;
language: string;
constructor(value: string, language: string) {
this.value = value;
this.language = language;
}
}
export interface LanguageCode {
display: string;
code: string;
}

View File

@@ -0,0 +1,42 @@
import { isNotEmpty } from '../../../empty.util';
export class FormFieldMetadataValueObject {
metadata?: string;
value: string;
display: string;
language: any;
authority: string;
confidence: number;
place: number;
closed: boolean;
label: string;
constructor(value: string,
language: any = null,
authority: string = null,
display: string = null,
confidence: number = -1,
place: number = -1,
metadata: string = null) {
this.value = value;
this.language = language;
this.authority = authority;
this.display = display || value;
this.confidence = confidence;
if (authority != null) {
this.confidence = 600;
} else if (isNotEmpty(confidence)) {
this.confidence = confidence;
}
this.place = place;
if (isNotEmpty(metadata)) {
this.metadata = metadata;
}
}
hasAuthority(): boolean {
return isNotEmpty(this.authority);
}
}

View File

@@ -0,0 +1,37 @@
import { isEqual } from 'lodash';
export class FormFieldPreviousValueObject {
private _path;
private _value;
constructor(path: any[] = null, value: any = null) {
this._path = path;
this._value = value;
}
get path() {
return this._path;
}
set path(path: any[]) {
this._path = path;
}
get value() {
return this._value;
}
set value(value: any) {
this._value = value;
}
public delete() {
this._value = null;
this._path = null;
}
public isPathEqual(path) {
return this._path && isEqual(this._path, path);
}
}

View File

@@ -0,0 +1,3 @@
export interface FormFieldChangedObject {
string: any
}

View File

@@ -0,0 +1,42 @@
import { autoserialize } from 'cerialize';
import { FormRowModel } from '../../../../core/shared/config/config-submission-forms.model';
import { LanguageCode } from './form-field-language-value.model';
import { FormFieldMetadataValueObject } from './form-field-metadata-value.model';
export class FormFieldModel {
@autoserialize
hints: string;
@autoserialize
label: string;
@autoserialize
languageCodes: LanguageCode[];
@autoserialize
mandatoryMessage: string;
@autoserialize
mandatory: string;
@autoserialize
repeatable: boolean;
@autoserialize
input: {
type: string;
};
@autoserialize
selectableMetadata: FormFieldMetadataValueObject[];
@autoserialize
rows: FormRowModel[];
@autoserialize
scope: string;
@autoserialize
value: any;
}

View File

@@ -0,0 +1,99 @@
import { FieldParser } from './field-parser';
import { FormFieldModel } from '../models/form-field.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import {
DynamicFormControlLayout, DynamicInputModel,
DynamicInputModelConfig
} from '@ng-dynamic-forms/core';
import {
CONCAT_FIRST_INPUT_SUFFIX,
CONCAT_GROUP_SUFFIX, CONCAT_SECOND_INPUT_SUFFIX,
DynamicConcatModel, DynamicConcatModelConfig
} from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model';
import { isNotEmpty } from '../../../empty.util';
export class ConcatFieldParser extends FieldParser {
constructor(protected configData: FormFieldModel,
protected initFormValues,
protected readOnly: boolean,
private separator: string,
protected firstPlaceholder: string = null,
protected secondPlaceholder: string = null) {
super(configData, initFormValues, readOnly);
this.separator = separator;
this.firstPlaceholder = firstPlaceholder;
this.secondPlaceholder = secondPlaceholder;
}
public modelFactory(fieldValue: FormFieldMetadataValueObject | any): any {
let clsGroup: DynamicFormControlLayout;
let clsInput: DynamicFormControlLayout;
const newId = this.configData.selectableMetadata[0].metadata
.split('.')
.slice(0, this.configData.selectableMetadata[0].metadata.split('.').length - 1)
.join('.');
clsInput = {
grid: {
host: 'col-sm-6'
}
};
const groupId = newId.replace(/\./g, '_') + CONCAT_GROUP_SUFFIX;
const concatGroup: DynamicConcatModelConfig = this.initModel(groupId, false, false);
concatGroup.group = [];
concatGroup.separator = this.separator;
const input1ModelConfig: DynamicInputModelConfig = this.initModel(newId + CONCAT_FIRST_INPUT_SUFFIX, true, false, false);
const input2ModelConfig: DynamicInputModelConfig = this.initModel(newId + CONCAT_SECOND_INPUT_SUFFIX, true, true, false);
if (this.configData.mandatory) {
input1ModelConfig.required = true;
}
if (isNotEmpty(this.firstPlaceholder)) {
input1ModelConfig.placeholder = this.firstPlaceholder;
}
if (isNotEmpty(this.secondPlaceholder)) {
input2ModelConfig.placeholder = this.secondPlaceholder;
}
// Init values
if (isNotEmpty(fieldValue)) {
const values = fieldValue.split(this.separator);
if (values.length > 1) {
input1ModelConfig.value = values[0];
input2ModelConfig.value = values[1];
}
}
// Split placeholder if is like 'placeholder1/placeholder2'
const placeholder = this.configData.label.split('/');
if (placeholder.length === 2) {
input1ModelConfig.placeholder = placeholder[0];
input2ModelConfig.placeholder = placeholder[1];
}
const model1 = new DynamicInputModel(input1ModelConfig, clsInput);
const model2 = new DynamicInputModel(input2ModelConfig, clsInput);
concatGroup.group.push(model1);
concatGroup.group.push(model2);
clsGroup = {
element: {
control: 'form-row',
}
};
const concatModel = new DynamicConcatModel(concatGroup, clsGroup);
concatModel.name = this.getFieldId()[0];
return concatModel;
}
}

View File

@@ -0,0 +1,49 @@
import { FieldParser } from './field-parser';
import { DynamicDatePickerModelConfig } from '@ng-dynamic-forms/core';
import { FormFieldModel } from '../models/form-field.model';
import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.model';
import { isNotEmpty } from '../../../empty.util';
import { DS_DATE_PICKER_SEPARATOR } from '../ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component';
export class DateFieldParser extends FieldParser {
public modelFactory(): any {
const inputDateModelConfig: DynamicDatePickerModelConfig = this.initModel();
inputDateModelConfig.toggleIcon = 'fa fa-calendar';
const dateModel = new DynamicDsDatePickerModel(inputDateModelConfig);
// Init Data and validity check
if (isNotEmpty(this.getInitFieldValue())) {
let malformedData = false;
const value = this.getInitFieldValue().toString();
if (value.length >= 4) {
const valuesArray = value.split(DS_DATE_PICKER_SEPARATOR);
if (valuesArray.length < 4) {
for (let i = 0; i < valuesArray.length; i++) {
const len = i === 0 ? 4 : 2;
if (valuesArray[i].length !== len) {
malformedData = true;
}
}
}
if (!malformedData) {
dateModel.valueUpdates.next(this.getInitFieldValue());
} else {
// TODO Set error message
dateModel.malformedDate = true;
// TODO
// const errorMessage = 'The stored date is not compliant';
// dateModel.validators = Object.assign({}, dateModel.validators, {malformedDate: null});
// dateModel.errorMessages = Object.assign({}, dateModel.errorMessages, {malformedDate: errorMessage});
// this.formService.addErrorToField(this.group.get(this.model.id), this.model, errorMessage)
}
}
}
return dateModel;
}
}

View File

@@ -0,0 +1,44 @@
import { FieldParser } from './field-parser';
import { DynamicFormControlLayout, } from '@ng-dynamic-forms/core';
import {
DynamicScrollableDropdownModel,
DynamicScrollableDropdownModelConfig
} from '../ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
import { FormFieldModel } from '../models/form-field.model';
import { isNotEmpty } from '../../../empty.util';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
export class DropdownFieldParser extends FieldParser {
constructor(protected configData: FormFieldModel,
protected initFormValues,
protected readOnly: boolean,
protected authorityUuid: string) {
super(configData, initFormValues, readOnly);
}
public modelFactory(fieldValue: FormFieldMetadataValueObject): any {
const dropdownModelConfig: DynamicScrollableDropdownModelConfig = this.initModel();
let layout: DynamicFormControlLayout;
if (isNotEmpty(this.configData.selectableMetadata[0].authority)) {
this.setAuthorityOptions(dropdownModelConfig, this.authorityUuid);
dropdownModelConfig.maxOptions = 10;
if (isNotEmpty(fieldValue)) {
dropdownModelConfig.value = fieldValue;
}
layout = {
element: {
control: 'col'
},
grid: {
host: 'col'
}
};
const dropdownModel = new DynamicScrollableDropdownModel(dropdownModelConfig, layout);
return dropdownModel;
} else {
throw Error(`Authority name is not available. Please checks form configuration file.`);
}
}
}

View File

@@ -0,0 +1,272 @@
import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util';
import { FormFieldModel } from '../models/form-field.model';
import { uniqueId } from 'lodash';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import {
DynamicRowArrayModel,
DynamicRowArrayModelConfig
} from '../ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { DynamicFormControlLayout } from '@ng-dynamic-forms/core';
import { setLayout } from './parser.utils';
import { AuthorityOptions } from '../../../../core/integration/models/authority-options.model';
export abstract class FieldParser {
protected fieldId: string;
constructor(protected configData: FormFieldModel, protected initFormValues, protected readOnly: boolean) {
}
public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject): any;
public parse() {
if (((this.getInitValueCount() > 1 && !this.configData.repeatable) || (this.configData.repeatable))
&& (this.configData.input.type !== 'list')
&& (this.configData.input.type !== 'tag')
&& (this.configData.input.type !== 'group')
) {
let arrayCounter = 0;
let fieldArrayCounter = 0;
const config = {
id: uniqueId() + '_array',
initialCount: this.getInitArrayIndex(),
notRepeteable: !this.configData.repeatable,
groupFactory: () => {
let model;
if ((arrayCounter === 0)) {
model = this.modelFactory();
arrayCounter++;
} else {
const fieldArrayOfValueLenght = this.getInitValueCount(arrayCounter - 1);
let fieldValue = null;
if (fieldArrayOfValueLenght > 0) {
fieldValue = this.getInitFieldValue(arrayCounter - 1, fieldArrayCounter++);
if (fieldArrayCounter === fieldArrayOfValueLenght) {
fieldArrayCounter = 0;
arrayCounter++;
}
}
model = this.modelFactory(fieldValue);
}
setLayout(model, 'element', 'host', 'col');
if (model.hasLanguages) {
setLayout(model, 'grid', 'control', 'col');
}
return [model];
}
} as DynamicRowArrayModelConfig;
const layout: DynamicFormControlLayout = {
grid: {
group: 'dsgridgroup form-row'
}
};
return new DynamicRowArrayModel(config, layout);
} else {
const model = this.modelFactory(this.getInitFieldValue());
if (model.hasLanguages) {
setLayout(model, 'grid', 'control', 'col');
}
return model;
}
}
protected getInitValueCount(index = 0, fieldId?): number {
const fieldIds = fieldId || this.getFieldId();
if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds[0])) {
return this.initFormValues[fieldIds[0]].length;
} else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) {
const values = [];
fieldIds.forEach((id) => {
if (this.initFormValues.hasOwnProperty(id)) {
values.push(this.initFormValues[id].length);
}
});
return values[index];
} else {
return 0;
}
}
protected getInitGroupValues(): FormFieldMetadataValueObject[] {
const fieldIds = this.getFieldId();
if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds[0])) {
return this.initFormValues[fieldIds[0]];
}
}
protected getInitFieldValues(fieldId): FormFieldMetadataValueObject[] {
if (isNotEmpty(this.initFormValues) && isNotNull(fieldId) && this.initFormValues.hasOwnProperty(fieldId)) {
return this.initFormValues[fieldId];
}
}
protected getInitFieldValue(outerIndex = 0, innerIndex = 0, fieldId?): FormFieldMetadataValueObject {
const fieldIds = fieldId || this.getFieldId();
if (isNotEmpty(this.initFormValues)
&& isNotNull(fieldIds)
&& fieldIds.length === 1
&& this.initFormValues.hasOwnProperty(fieldIds[outerIndex])
&& this.initFormValues[fieldIds[outerIndex]].length > innerIndex) {
return this.initFormValues[fieldIds[outerIndex]][innerIndex];
} else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) {
const values: FormFieldMetadataValueObject[] = [];
fieldIds.forEach((id) => {
if (this.initFormValues.hasOwnProperty(id)) {
const valueObj: FormFieldMetadataValueObject = Object.create({});
valueObj.metadata = id;
valueObj.value = this.initFormValues[id][innerIndex];
values.push(valueObj);
}
});
return values[outerIndex];
} else {
return null;
}
}
protected getInitArrayIndex() {
const fieldIds: any = this.getFieldId();
if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds)) {
return this.initFormValues[fieldIds].length;
} else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) {
let counter = 0;
fieldIds.forEach((id) => {
if (this.initFormValues.hasOwnProperty(id)) {
counter = counter + this.initFormValues[id].length;
}
});
return (counter === 0) ? 1 : counter;
} else {
return 1;
}
}
protected getFieldId(): string[] {
if (Array.isArray(this.configData.selectableMetadata)) {
if (this.configData.selectableMetadata.length === 1) {
return [this.configData.selectableMetadata[0].metadata];
} else {
const ids = [];
this.configData.selectableMetadata.forEach((entry) => ids.push(entry.metadata));
return ids;
}
} else {
return null;
}
}
protected initModel(id?: string, label = true, labelEmpty = false, setErrors = true) {
const controlModel = Object.create(null);
// Sets input ID
this.fieldId = id ? id : this.getFieldId()[0];
// Sets input name (with the original field's id value)
controlModel.name = this.fieldId;
// input ID doesn't allow dots, so replace them
controlModel.id = (this.fieldId).replace(/\./g, '_');
// Set read only option
controlModel.readOnly = this.readOnly;
controlModel.disabled = this.readOnly;
if (label) {
controlModel.label = (labelEmpty) ? '&nbsp;' : this.configData.label;
}
controlModel.placeholder = this.configData.label;
if (this.configData.mandatory && setErrors) {
this.setErrors(controlModel);
}
// Available Languages
if (this.configData.languageCodes && this.configData.languageCodes.length > 0) {
(controlModel as DsDynamicInputModel).languageCodes = this.configData.languageCodes;
}
return controlModel;
}
protected setErrors(controlModel) {
controlModel.required = true;
controlModel.validators = Object.assign({}, controlModel.validators, {required: null});
controlModel.errorMessages = Object.assign(
{},
controlModel.errorMessages,
{required: this.configData.mandatoryMessage});
}
protected setOptions(controlModel) {
// Checks if field has multiple values and sets options available
if (isNotUndefined(this.configData.selectableMetadata) && this.configData.selectableMetadata.length > 1) {
controlModel.options = [];
this.configData.selectableMetadata.forEach((option, key) => {
if (key === 0) {
controlModel.value = option.metadata;
}
controlModel.options.push({label: option.label, value: option.metadata});
});
}
}
public setAuthorityOptions(controlModel, authorityUuid) {
if (isNotEmpty(this.configData.selectableMetadata[0].authority)) {
controlModel.authorityOptions = new AuthorityOptions(
this.configData.selectableMetadata[0].authority,
this.configData.selectableMetadata[0].metadata,
authorityUuid,
this.configData.selectableMetadata[0].closed
)
}
}
public setValues(modelConfig: DsDynamicInputModelConfig, fieldValue: any, forceValueAsObj: boolean = false, groupModel?: boolean) {
if (isNotEmpty(fieldValue)) {
if (groupModel) {
// Array, values is an array
modelConfig.value = this.getInitGroupValues();
if (Array.isArray(modelConfig.value) && modelConfig.value.length > 0 && modelConfig.value[0].language) {
// Array Item has language, ex. AuthorityModel
modelConfig.language = modelConfig.value[0].language;
}
return;
}
if (typeof fieldValue === 'object') {
modelConfig.language = fieldValue.language;
if (hasValue(fieldValue.language)) {
// Instance of FormFieldLanguageValueObject
modelConfig.value = fieldValue.value;
} else if (hasValue(fieldValue.metadata)) {
// Is a combobox field's value
modelConfig.value = fieldValue.value;
} else {
// Instance of FormFieldMetadataValueObject
modelConfig.value = fieldValue;
}
} else {
if (forceValueAsObj) {
// If value isn't an instance of FormFieldMetadataValueObject instantiate it
modelConfig.value = new FormFieldMetadataValueObject(fieldValue);
} else {
if (typeof fieldValue === 'string') {
// Case only string
modelConfig.value = fieldValue;
}
}
}
}
return modelConfig;
}
}

View File

@@ -0,0 +1,67 @@
import { FieldParser } from './field-parser';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { FormFieldModel } from '../models/form-field.model';
import {
DynamicGroupModel,
DynamicGroupModelConfig, PLACEHOLDER_PARENT_METADATA
} from '../ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model';
import { isNotEmpty } from '../../../empty.util';
import { FormRowModel } from '../../../../core/shared/config/config-submission-forms.model';
export class GroupFieldParser extends FieldParser {
constructor(protected configData: FormFieldModel,
protected initFormValues,
protected readOnly: boolean,
protected authorityUuid: string) {
super(configData, initFormValues, readOnly);
}
public modelFactory(fieldValue: FormFieldMetadataValueObject) {
const modelConfiguration: DynamicGroupModelConfig = this.initModel();
modelConfiguration.scopeUUID = this.authorityUuid;
if (this.configData && this.configData.rows && this.configData.rows.length > 0) {
modelConfiguration.formConfiguration = this.configData.rows;
modelConfiguration.relationFields = [];
this.configData.rows.forEach((row: FormRowModel) => {
row.fields.forEach((field: FormFieldModel) => {
if (field.selectableMetadata[0].metadata === this.configData.selectableMetadata[0].metadata) {
if (!field.mandatory) {
// throw new Error(`Configuration not valid: Main field ${this.configData.selectableMetadata[0].metadata} may be mandatory`);
}
modelConfiguration.mandatoryField = this.configData.selectableMetadata[0].metadata;
} else {
modelConfiguration.relationFields.push(field.selectableMetadata[0].metadata);
}
})
});
} else {
throw new Error(`Configuration not valid: ${modelConfiguration.name}`);
}
if (isNotEmpty(this.getInitGroupValues())) {
modelConfiguration.value = [];
const mandatoryFieldEntries: FormFieldMetadataValueObject[] = this.getInitFieldValues(modelConfiguration.mandatoryField);
mandatoryFieldEntries.forEach((entry, index) => {
const item = Object.create(null);
const listFields = modelConfiguration.relationFields.concat(modelConfiguration.mandatoryField);
listFields.forEach((fieldId) => {
const value = this.getInitFieldValue(0, index, [fieldId]);
item[fieldId] = isNotEmpty(value) ? value : PLACEHOLDER_PARENT_METADATA;
});
modelConfiguration.value.push(item);
})
}
const cls = {
element: {
container: 'mb-3'
}
};
const model = new DynamicGroupModel(modelConfiguration, cls);
model.name = this.getFieldId()[0];
return model;
}
}

View File

@@ -0,0 +1,52 @@
import { FieldParser } from './field-parser';
import { FormFieldModel } from '../models/form-field.model';
import { isNotEmpty } from '../../../empty.util';
import { IntegrationSearchOptions } from '../../../../core/integration/models/integration-options.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { DynamicListCheckboxGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model';
import { DynamicListRadioGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model';
export class ListFieldParser extends FieldParser {
searchOptions: IntegrationSearchOptions;
constructor(protected configData: FormFieldModel,
protected initFormValues,
protected readOnly: boolean,
protected authorityUuid: string) {
super(configData, initFormValues, readOnly);
}
public modelFactory(fieldValue: FormFieldMetadataValueObject): any {
const listModelConfig = this.initModel();
listModelConfig.repeatable = this.configData.repeatable;
if (this.configData.selectableMetadata[0].authority
&& this.configData.selectableMetadata[0].authority.length > 0) {
if (isNotEmpty(this.getInitGroupValues())) {
listModelConfig.value = [];
this.getInitGroupValues().forEach((value: any) => {
if (value instanceof FormFieldMetadataValueObject) {
listModelConfig.value.push(value);
} else {
const valueObj = new FormFieldMetadataValueObject(value);
listModelConfig.value.push(valueObj);
}
});
}
this.setAuthorityOptions(listModelConfig, this.authorityUuid);
}
let listModel;
if (listModelConfig.repeatable) {
listModelConfig.group = [];
listModel = new DynamicListCheckboxGroupModel(listModelConfig);
} else {
listModelConfig.options = [];
listModel = new DynamicListRadioGroupModel(listModelConfig);
}
return listModel;
}
}

View File

@@ -0,0 +1,30 @@
import { FieldParser } from './field-parser';
import { FormFieldModel } from '../models/form-field.model';
import { AuthorityValueModel } from '../../../../core/integration/models/authority-value.model';
import { isNotEmpty } from '../../../empty.util';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { DynamicLookupModel, DynamicLookupModelConfig } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup.model';
export class LookupFieldParser extends FieldParser {
constructor(protected configData: FormFieldModel,
protected initFormValues,
protected readOnly: boolean,
protected authorityUuid: string) {
super(configData, initFormValues, readOnly);
}
public modelFactory(fieldValue: any): any {
if (this.configData.selectableMetadata[0].authority) {
const lookupModelConfig: DynamicLookupModelConfig = this.initModel();
this.setAuthorityOptions(lookupModelConfig, this.authorityUuid);
lookupModelConfig.maxOptions = 10;
this.setValues(lookupModelConfig, fieldValue, true);
const lookupModel = new DynamicLookupModel(lookupModelConfig);
return lookupModel;
}
}
}

View File

@@ -0,0 +1,22 @@
import { FormFieldModel } from '../models/form-field.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { LookupFieldParser } from './lookup-field-parser';
export class LookupNameFieldParser extends LookupFieldParser {
constructor(protected configData: FormFieldModel,
protected initFormValues,
protected readOnly: boolean,
protected authorityUuid: string) {
super(configData, initFormValues, readOnly, authorityUuid);
}
public modelFactory(fieldValue: FormFieldMetadataValueObject | any): any {
const lookupModel = super.modelFactory(fieldValue);
lookupModel.separator = ',';
lookupModel.placeholder = 'form.last-name';
lookupModel.placeholder2 = 'form.first-name';
return lookupModel;
}
}

View File

@@ -0,0 +1,10 @@
import { FormFieldModel } from '../models/form-field.model';
import { ConcatFieldParser } from './concat-field-parser';
export class NameFieldParser extends ConcatFieldParser {
constructor(protected configData: FormFieldModel, protected initFormValues, protected readOnly: boolean) {
super(configData, initFormValues, readOnly, ',', 'form.last-name', 'form.first-name');
}
}

View File

@@ -0,0 +1,95 @@
import { DynamicSelectModel, DynamicSelectModelConfig } from '@ng-dynamic-forms/core';
import { FieldParser } from './field-parser';
import { FormFieldModel } from '../models/form-field.model';
import {
COMBOBOX_GROUP_SUFFIX,
COMBOBOX_METADATA_SUFFIX,
COMBOBOX_VALUE_SUFFIX,
DsDynamicComboboxModelConfig,
DynamicComboboxModel
} from '../ds-dynamic-form-ui/models/ds-dynamic-combobox.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { isNotEmpty } from '../../../empty.util';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model';
import {
DsDynamicTypeaheadModelConfig,
DynamicTypeaheadModel
} from '../ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model';
export class OneboxFieldParser extends FieldParser {
constructor(protected configData: FormFieldModel,
protected initFormValues,
protected readOnly: boolean,
protected authorityUuid: string) {
super(configData, initFormValues, readOnly);
}
public modelFactory(fieldValue: FormFieldMetadataValueObject): any {
if (this.configData.selectableMetadata.length > 1) {
// Case ComboBox
const clsGroup = {
element: {
control: 'form-row',
}
};
const clsSelect = {
element: {
control: 'input-group-addon ds-form-input-addon',
},
grid: {
host: 'col-sm-4 pr-0'
}
};
const clsInput = {
element: {
control: 'ds-form-input-value',
},
grid: {
host: 'col-sm-8 pl-0'
}
};
const newId = this.configData.selectableMetadata[0].metadata
.split('.')
.slice(0, this.configData.selectableMetadata[0].metadata.split('.').length - 1)
.join('.');
const inputSelectGroup: DsDynamicComboboxModelConfig = Object.create(null);
inputSelectGroup.id = newId.replace(/\./g, '_') + COMBOBOX_GROUP_SUFFIX;
inputSelectGroup.group = [];
inputSelectGroup.legend = this.configData.label;
const selectModelConfig: DynamicSelectModelConfig<any> = this.initModel(newId + COMBOBOX_METADATA_SUFFIX);
this.setOptions(selectModelConfig);
if (isNotEmpty(fieldValue)) {
selectModelConfig.value = fieldValue.metadata;
}
selectModelConfig.disabled = true;
inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect));
const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + COMBOBOX_VALUE_SUFFIX, true, true);
this.setValues(inputModelConfig, fieldValue);
inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly;
inputSelectGroup.group.push(new DsDynamicInputModel(inputModelConfig, clsInput));
return new DynamicComboboxModel(inputSelectGroup, clsGroup);
} else if (this.configData.selectableMetadata[0].authority) {
const typeaheadModelConfig: DsDynamicTypeaheadModelConfig = this.initModel();
this.setAuthorityOptions(typeaheadModelConfig, this.authorityUuid);
this.setValues(typeaheadModelConfig, fieldValue, true);
typeaheadModelConfig.minChars = 3;
const typeaheadModel = new DynamicTypeaheadModel(typeaheadModelConfig);
return typeaheadModel;
} else {
const inputModelConfig: DsDynamicInputModelConfig = this.initModel();
this.setValues(inputModelConfig, fieldValue);
const inputModel = new DsDynamicInputModel(inputModelConfig);
return inputModel;
}
}
}

View File

@@ -0,0 +1,17 @@
import { isNull, isUndefined } from '../../../empty.util';
import { DynamicFormControlLayout, DynamicFormControlLayoutConfig } from '@ng-dynamic-forms/core';
export function setLayout(model: any, controlLayout: string, controlLayoutConfig: string, style: string) {
if (isNull(model.layout)) {
model.layout = {} as DynamicFormControlLayout;
model.layout[controlLayout] = {} as DynamicFormControlLayoutConfig;
model.layout[controlLayout][controlLayoutConfig] = style;
} else if (isUndefined(model.layout[controlLayout])) {
model.layout[controlLayout] = {} as DynamicFormControlLayoutConfig;
model.layout[controlLayout][controlLayoutConfig] = style;
} else if (isUndefined(model.layout[controlLayout][controlLayoutConfig])) {
model.layout[controlLayout][controlLayoutConfig] = style;
} else {
model.layout[controlLayout][controlLayoutConfig] = model.layout[controlLayout][controlLayoutConfig].concat(` ${style}`);
}
}

View File

@@ -0,0 +1,162 @@
import { DynamicFormArrayModel, DynamicFormControlModel, DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core';
import { uniqueId } from 'lodash';
import { DateFieldParser } from './date-field-parser';
import { DropdownFieldParser } from './dropdown-field-parser';
import { ListFieldParser } from './list-field-parser';
import { OneboxFieldParser } from './onebox-field-parser';
import { NameFieldParser } from './name-field-parser';
import { SeriesFieldParser } from './series-field-parser';
import { TagFieldParser } from './tag-field-parser';
import { TextareaFieldParser } from './textarea-field-parser';
import { GroupFieldParser } from './group-field-parser';
import { IntegrationSearchOptions } from '../../../../core/integration/models/integration-options.model';
import { DynamicGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model';
import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model';
import { isEmpty } from '../../../empty.util';
import { LookupFieldParser } from './lookup-field-parser';
import { LookupNameFieldParser } from './lookup-name-field-parser';
import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { setLayout } from './parser.utils';
import { FormFieldModel } from '../models/form-field.model';
export const ROW_ID_PREFIX = 'df-row-group-config-';
export class RowParser {
protected authorityOptions: IntegrationSearchOptions;
constructor(protected rowData,
protected scopeUUID,
protected initFormValues: any,
protected submissionScope,
protected readOnly: boolean) {
this.authorityOptions = new IntegrationSearchOptions(scopeUUID);
}
public parse(): DynamicRowGroupModel {
let fieldModel: any = null;
let parsedResult = null;
const config: DynamicFormGroupModelConfig = {
id: uniqueId(ROW_ID_PREFIX),
group: [],
};
const scopedFields: FormFieldModel[] = this.filterScopedFields(this.rowData.fields);
const layoutGridClass = ' col-sm-' + Math.trunc(12 / scopedFields.length) + ' d-flex flex-column justify-content-start';
// Iterate over row's fields
scopedFields.forEach((fieldData: FormFieldModel) => {
switch (fieldData.input.type) {
case 'date':
fieldModel = (new DateFieldParser(fieldData, this.initFormValues, this.readOnly).parse());
break;
case 'dropdown':
fieldModel = (new DropdownFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse());
break;
case 'list':
fieldModel = (new ListFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse());
break;
case 'lookup':
fieldModel = (new LookupFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse());
break;
case 'onebox':
fieldModel = (new OneboxFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse());
break;
case 'lookup-name':
fieldModel = (new LookupNameFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse());
break;
case 'name':
fieldModel = (new NameFieldParser(fieldData, this.initFormValues, this.readOnly).parse());
break;
case 'series':
fieldModel = (new SeriesFieldParser(fieldData, this.initFormValues, this.readOnly).parse());
break;
case 'tag':
fieldModel = (new TagFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse());
break;
case 'textarea':
fieldModel = (new TextareaFieldParser(fieldData, this.initFormValues, this.readOnly).parse());
break;
case 'group':
fieldModel = new GroupFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse();
break;
case 'twobox':
// group.push(new TwoboxFieldParser(fieldData).parse());
break;
default:
throw new Error(`unknown form control model type defined on JSON object with label "${fieldData.label}"`);
}
if (fieldModel) {
if (fieldModel instanceof DynamicFormArrayModel || fieldModel instanceof DynamicGroupModel) {
if (this.rowData.fields.length > 1) {
setLayout(fieldModel, 'grid', 'host', layoutGridClass);
config.group.push(fieldModel);
// if (isEmpty(parsedResult)) {
// parsedResult = [];
// }
// parsedResult.push(fieldModel);
} else {
parsedResult = fieldModel;
}
return;
} else {
if (fieldModel instanceof Array) {
fieldModel.forEach((model) => {
parsedResult = model;
return;
})
} else {
setLayout(fieldModel, 'grid', 'host', layoutGridClass);
config.group.push(fieldModel);
}
}
fieldModel = null;
}
});
if (config && !isEmpty(config.group)) {
const clsGroup = {
element: {
control: 'form-row',
}
};
const groupModel = new DynamicRowGroupModel(config, clsGroup);
if (Array.isArray(parsedResult)) {
parsedResult.push(groupModel)
} else {
parsedResult = groupModel;
}
}
return parsedResult;
}
checksFieldScope(fieldScope) {
return (isEmpty(fieldScope) || isEmpty(this.submissionScope) || fieldScope === this.submissionScope);
}
filterScopedFields(fields: FormFieldModel[]): FormFieldModel[] {
const filteredFields: FormFieldModel[] = [];
fields.forEach((field: FormFieldModel) => {
// Whether field scope doesn't match the submission scope, skip it
if (this.checksFieldScope(field.scope)) {
filteredFields.push(field);
}
});
return filteredFields;
}
}

View File

@@ -0,0 +1,10 @@
import { FormFieldModel } from '../models/form-field.model';
import { ConcatFieldParser } from './concat-field-parser';
export class SeriesFieldParser extends ConcatFieldParser {
constructor(protected configData: FormFieldModel, protected initFormValues, protected readOnly: boolean) {
super(configData, initFormValues, readOnly, ';');
}
}

View File

@@ -0,0 +1,31 @@
import { FieldParser } from './field-parser';
import { FormFieldModel } from '../models/form-field.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { isNotEmpty } from '../../../empty.util';
import { DynamicTagModel, DynamicTagModelConfig } from '../ds-dynamic-form-ui/models/tag/dynamic-tag.model';
export class TagFieldParser extends FieldParser {
constructor(protected configData: FormFieldModel,
protected initFormValues,
protected readOnly: boolean,
protected authorityUuid: string) {
super(configData, initFormValues, readOnly);
}
public modelFactory(fieldValue: FormFieldMetadataValueObject): any {
const tagModelConfig: DynamicTagModelConfig = this.initModel();
if (this.configData.selectableMetadata[0].authority
&& this.configData.selectableMetadata[0].authority.length > 0) {
this.setAuthorityOptions(tagModelConfig, this.authorityUuid);
}
tagModelConfig.minChars = 3;
this.setValues(tagModelConfig, fieldValue, null, true);
const tagModel = new DynamicTagModel(tagModelConfig);
return tagModel;
}
}

View File

@@ -0,0 +1,32 @@
import { FieldParser } from './field-parser';
import {
DynamicFormControlLayout, DynamicTextAreaModel, DynamicTextAreaModelConfig
} from '@ng-dynamic-forms/core';
import { FormFieldModel } from '../models/form-field.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { isNotEmpty } from '../../../empty.util';
import {
DsDynamicTextAreaModel,
DsDynamicTextAreaModelConfig
} from '../ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
export class TextareaFieldParser extends FieldParser {
public modelFactory(fieldValue: FormFieldMetadataValueObject | any): any {
const textAreaModelConfig: DsDynamicTextAreaModelConfig = this.initModel();
let layout: DynamicFormControlLayout;
layout = {
element: {
label: 'col-form-label'
}
};
textAreaModelConfig.rows = 10;
this.setValues(textAreaModelConfig, fieldValue);
const textAreaModel = new DsDynamicTextAreaModel(textAreaModelConfig, layout);
return textAreaModel;
}
}

View File

@@ -0,0 +1,10 @@
import { FieldParser } from './field-parser';
import { FormFieldModel } from '../models/form-field.model';
// @TODO to be implemented
export class TwoboxFieldParser extends FieldParser {
public modelFactory(): any {
return null;
}
}

View File

@@ -0,0 +1,126 @@
import { Action } from '@ngrx/store';
import { type } from '../ngrx/type';
/**
* For each action type in an action group, make a simple
* enum object for all of this group's action types.
*
* The 'type' utility function coerces strings into string
* literal types and runs a simple check to guarantee all
* action types in the application are unique.
*/
export const FormActionTypes = {
FORM_INIT: type('dspace/form/FORM_INIT'),
FORM_CHANGE: type('dspace/form/FORM_CHANGE'),
FORM_REMOVE: type('dspace/form/FORM_REMOVE'),
FORM_STATUS_CHANGE: type('dspace/form/FORM_STATUS_CHANGE'),
FORM_ADD_ERROR: type('dspace/form/ADD_ERROR'),
FORM_REMOVE_ERROR: type('dspace/form/REMOVE_ERROR'),
CLEAR_ERRORS: type('dspace/form/CLEAR_ERRORS'),
};
/* tslint:disable:max-classes-per-file */
export class FormInitAction implements Action {
type = FormActionTypes.FORM_INIT;
payload: {
formId: string;
formData: any;
valid: boolean;
};
/**
* Create a new FormInitAction
*
* @param formId
* the Form's ID
* @param formData
* the FormGroup Object
* @param valid
* the Form validation status
*/
constructor(formId: string, formData: any, valid: boolean) {
this.payload = {formId, formData, valid};
}
}
export class FormChangeAction implements Action {
type = FormActionTypes.FORM_CHANGE;
payload: {
formId: string;
formData: any;
};
/**
* Create a new FormInitAction
*
* @param formId
* the Form's ID
* @param formData
* the FormGroup Object
*/
constructor(formId: string, formData: any) {
this.payload = {formId, formData};
}
}
export class FormRemoveAction implements Action {
type = FormActionTypes.FORM_REMOVE;
payload: {
formId: string;
};
/**
* Create a new FormRemoveAction
*
* @param formId
* the Form's ID
*/
constructor(formId: string) {
this.payload = {formId};
}
}
export class FormStatusChangeAction implements Action {
type = FormActionTypes.FORM_STATUS_CHANGE;
payload: {
formId: string;
valid: boolean;
};
/**
* Create a new FormInitAction
*
* @param formId
* the Form's ID
* @param valid
* the Form validation status
*/
constructor(formId: string, valid: boolean) {
this.payload = {formId, valid};
}
}
export class FormAddError implements Action {
type = FormActionTypes.FORM_ADD_ERROR;
payload: {
formId: string,
fieldId: string,
errorMessage: string,
};
constructor(formId: string, fieldId: string, errorMessage: string) {
this.payload = {formId, fieldId, errorMessage};
}
}
/* tslint:enable:max-classes-per-file */
/**
* Export a type alias of all actions in this action group
* so that reducers can easily compose action types
*/
export type FormAction = FormInitAction
| FormChangeAction
| FormStatusChangeAction
| FormAddError

View File

@@ -0,0 +1,61 @@
<div class="container-fluid">
<form [formGroup]="formGroup">
<ds-dynamic-form
[formId]="formId"
[formGroup]="formGroup"
[formModel]="formModel"
(dfBlur)="onBlur($event)"
(dfChange)="onChange($event)"
(dfFocus)="onFocus($event)">
<ng-template modelType="ARRAY" let-group let-index="index" let-context="context">
<!--Array with repeteable items-->
<div *ngIf="!context.notRepeteable"
class="col-xs-2 d-flex flex-column justify-content-sm-center align-items-end mt-3">
<div class="btn-group" role="group" aria-label="Basic example">
<button type="button" class="btn btn-secondary"
[disabled]="isItemReadOnly(context, index)"
(click)="insertItem($event, group.context, group.index + 1)">
<i class="fa fa-plus" aria-hidden="true"></i>
</button>
<button type="button" class="btn btn-secondary"
(click)="removeItem($event, context, index)"
[disabled]="group.context.groups.length === 1 || isItemReadOnly(context, index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!--Array with non repeteable items - Only delete button-->
<div *ngIf="context.notRepeteable && group.context.groups.length > 1"
class="col-xs-2 d-flex flex-column justify-content-sm-center align-items-end mt-3">
<div class="btn-group" role="group" aria-label="Basic example">
<button type="button" class="btn btn-secondary"
(click)="removeItem($event, context, index)"
[disabled]="group.context.groups.length === 1 || isItemReadOnly(context, index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</ng-template>
</ds-dynamic-form>
<div *ngIf="displaySubmit">
<hr>
<div class="form-group row">
<div class="col text-right">
<button type="reset" class="btn btn-default" (click)="reset()">{{'form.cancel' | translate}}</button>
<button type="submit" class="btn btn-primary" (click)="onSubmit($event)"
[disabled]="!(isValid() | async)">{{'form.submit' | translate}}
</button>
</div>
</div>
</div>
</form>
</div>

View File

@@ -0,0 +1,23 @@
@import "../../../styles/_variables.scss";
.ds-form-input-addon {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 0;
}
.ds-form-input-btn {
border: $input-btn-border-width solid $input-border-color;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 0;
}
.ds-form-input-btn:focus {
box-shadow: none !important;
}
.ds-form-input-value {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

View File

@@ -0,0 +1,175 @@
// Load the implementations that should be tested
import { CommonModule } from '@angular/common';
import {
Component,
CUSTOM_ELEMENTS_SCHEMA,
DebugElement
} from '@angular/core';
import {
async,
ComponentFixture,
inject,
TestBed,
} from '@angular/core/testing';
import { StoreModule } from '@ngrx/store';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import Spy = jasmine.Spy;
import { FormComponent } from './form.component';
import { FormService } from './form.service';
import { DynamicFormControlModel, DynamicFormValidationService, DynamicInputModel } from '@ng-dynamic-forms/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { FormBuilderService } from './builder/form-builder.service';
import { SubmissionFormsConfigService } from '../../core/config/submission-forms-config.service';
import { ResponseCacheService } from '../../core/cache/response-cache.service';
import { RequestService } from '../../core/data/request.service';
import { ObjectCacheService } from '../../core/cache/object-cache.service';
import { Observable } from 'rxjs/Observable';
function createTestComponent<T>(html: string, type: { new(...args: any[]): T }): ComponentFixture<T> {
TestBed.overrideComponent(type, {
set: { template: html }
});
const fixture = TestBed.createComponent(type);
fixture.detectChanges();
return fixture as ComponentFixture<T>;
}
export const TEST_FORM_MODEL = [
new DynamicInputModel(
{
id: 'dc_title',
label: 'Title',
placeholder: 'Title',
validators: {
required: null
},
errorMessages: {
required: 'You must enter a main title for this item.'
}
}
),
new DynamicInputModel(
{
id: 'dc_title_alternative',
label: 'Other Titles',
placeholder: 'Other Titles',
}
),
new DynamicInputModel(
{
id: 'dc_publisher',
label: 'Publisher',
placeholder: 'Publisher',
}
),
new DynamicInputModel(
{
id: 'dc_identifier_citation',
label: 'Citation',
placeholder: 'Citation',
}
),
new DynamicInputModel(
{
id: 'dc_identifier_issn',
label: 'Identifiers',
placeholder: 'Identifiers',
}
),
];
export const TEST_FORM_GROUP = {
dc_title: new FormControl(),
dc_title_alternative: new FormControl(),
dc_publisher: new FormControl(),
dc_identifier_citation: new FormControl(),
dc_identifier_issn: new FormControl()
}
describe('Form component', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
let html;
const formServiceStub = {
getFormData: (formId) => Observable.of([])
}
const formBuilderServiceStub = {
createFormGroup: (formModel) => new FormGroup(TEST_FORM_GROUP)
}
const submissionFormsConfigServiceStub = { }
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
BrowserModule,
CommonModule,
FormsModule,
ReactiveFormsModule,
StoreModule.forRoot({}),
NgbModule.forRoot(),
],
declarations: [
FormComponent,
TestComponent,
], // declare the test component
providers: [
FormComponent,
{ provide: FormService, useValue: formServiceStub },
{ provide: FormBuilderService, useValue: formBuilderServiceStub },
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-form #formRef="formComponent"
[formId]="formId"
[formModel]="formModel"
[displaySubmit]="false"></ds-form>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create Form Component', inject([FormComponent], (app: FormComponent) => {
expect(app).toBeDefined();
}));
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
public formId;
public formModel: DynamicFormControlModel[];
constructor() {
this.formId = 'testForm';
this.formModel = TEST_FORM_MODEL;
}
}

View File

@@ -0,0 +1,274 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
import {
DynamicFormArrayModel,
DynamicFormControlEvent,
DynamicFormControlModel,
DynamicFormGroupModel,
} from '@ng-dynamic-forms/core';
import { Store } from '@ngrx/store';
import { AppState } from '../../app.reducer';
import { FormChangeAction, FormInitAction, FormRemoveAction, FormStatusChangeAction } from './form.actions';
import { FormBuilderService } from './builder/form-builder.service';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { hasValue, isNotNull, isNull } from '../empty.util';
import { FormService } from './form.service';
import { formObjectFromIdSelector } from './selectors';
import { FormEntry, FormError } from './form.reducers';
import { isEmpty } from 'lodash';
/**
* The default form component.
*/
@Component({
exportAs: 'formComponent',
selector: 'ds-form',
styleUrls: ['form.component.scss'],
templateUrl: 'form.component.html',
})
export class FormComponent implements OnDestroy, OnInit {
private formValid: boolean;
/**
* A boolean that indicate if to display form's submit and cancel buttons
*/
@Input() displaySubmit = true;
/**
* The form unique ID
*/
@Input() formId: string;
/**
* An array of DynamicFormControlModel type
*/
@Input() formModel: DynamicFormControlModel[];
@Input() parentFormModel: DynamicFormGroupModel | DynamicFormGroupModel[];
@Input() formGroup: FormGroup;
/* tslint:disable:no-output-rename */
@Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output('dfFocus') focus: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
/* tslint:enable:no-output-rename */
@Output() addArrayItem: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output() removeArrayItem: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
/**
* An event fired when form is valid and submitted .
* Event's payload equals to the form content.
*/
@Output() submit: EventEmitter<Observable<any>> = new EventEmitter<Observable<any>>();
/**
* An object of FormGroup type
*/
// public formGroup: FormGroup;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
private subs: Subscription[] = [];
constructor(private formService: FormService,
protected changeDetectorRef: ChangeDetectorRef,
private formBuilderService: FormBuilderService,
private store: Store<AppState>) {
}
/**
* Method provided by Angular. Invoked after the view has been initialized.
*/
/*ngAfterViewChecked(): void {
this.subs.push(this.formGroup.valueChanges
.filter((formGroup) => this.formGroup.dirty)
.subscribe(() => {
// Dispatch a FormChangeAction if the user has changed the value in the UI
this.store.dispatch(new FormChangeAction(this.formId, this.formGroup.value));
this.formGroup.markAsPristine();
}));
}*/
private getFormGroup(): FormGroup {
if (!!this.parentFormModel) {
return this.formGroup.parent as FormGroup;
}
return this.formGroup;
}
private getFormGroupValue() {
return this.getFormGroup().value;
}
private getFormGroupValidStatus() {
return this.getFormGroup().valid;
}
/**
* Method provided by Angular. Invoked after the constructor
*/
ngOnInit() {
if (!this.formGroup) {
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
} else {
this.formModel.forEach((model) => {
this.formBuilderService.addFormGroupControl(this.formGroup, this.parentFormModel, model);
});
}
this.store.dispatch(new FormInitAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel), this.getFormGroupValidStatus()));
// TODO: take a look to the following method:
// this.keepSync();
this.formValid = this.getFormGroupValidStatus();
this.subs.push(this.formGroup.statusChanges
.filter((currentStatus) => this.formValid !== this.getFormGroupValidStatus())
.subscribe((currentStatus) => {
// Dispatch a FormStatusChangeAction if the form status has changed
this.store.dispatch(new FormStatusChangeAction(this.formId, this.getFormGroupValidStatus()));
this.formValid = this.getFormGroupValidStatus();
}));
this.subs.push(
this.store.select(formObjectFromIdSelector(this.formId))
.filter((formState: FormEntry) => !!formState && !isEmpty(formState.errors))
.map((formState) => formState.errors)
.distinctUntilChanged()
.delay(100) // this terrible delay is here to prevent the detection change error
.subscribe((errors: FormError[]) => {
const {formGroup, formModel} = this;
errors.forEach((error: FormError) => {
const {fieldId} = error;
let field: AbstractControl;
if (!!this.parentFormModel) {
field = this.formBuilderService.getFormControlById(fieldId, formGroup.parent as FormGroup, formModel);
} else {
field = this.formBuilderService.getFormControlById(fieldId, formGroup, formModel);
}
if (field) {
const model: DynamicFormControlModel = this.formBuilderService.findById(fieldId, formModel);
this.formService.addErrorToField(field, model, error.message);
}
});
this.changeDetectorRef.detectChanges();
})
);
}
/**
* Method provided by Angular. Invoked when the instance is destroyed
*/
ngOnDestroy() {
this.store.dispatch(new FormRemoveAction(this.formId));
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
/**
* Method to check if the form status is valid or not
*/
public isValid(): Observable<boolean> {
return this.formService.isValid(this.formId)
}
/**
* Method to keep synchronized form controls values with form state
*/
private keepSync() {
this.subs.push(this.formService.getFormData(this.formId)
.subscribe((stateFormData) => {
if (!Object.is(stateFormData, this.formGroup.value) && this.formGroup) {
this.formGroup.setValue(stateFormData);
}
}));
}
onBlur(event) {
this.blur.emit(event);
}
onFocus(event) {
this.focus.emit(event);
}
onChange(event) {
const action: FormChangeAction = new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel));
this.store.dispatch(action);
this.formGroup.markAsPristine();
this.change.emit(event);
const control: FormControl = event.control;
control.setErrors(null);
}
/**
* Method called on submit.
* Emit a new submit Event whether the form is valid, mark fields with error otherwise
*/
onSubmit() {
if (this.getFormGroupValidStatus()) {
this.submit.emit(this.formService.getFormData(this.formId));
} else {
this.formService.validateAllFormFields(this.formGroup);
}
}
/**
* Method to reset form fields
*/
reset() {
this.formGroup.reset();
}
isItemReadOnly(arrayContext: DynamicFormArrayModel, index: number): boolean {
const context = arrayContext.groups[index];
const model = context.group[0] as any;
return model.readOnly;
}
removeItem($event, arrayContext: DynamicFormArrayModel, index: number) {
const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray;
this.removeArrayItem.emit(this.getEvent($event, arrayContext, index, 'remove'));
this.formBuilderService.removeFormArrayGroup(index, formArrayControl, arrayContext);
this.store.dispatch(new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel)));
}
insertItem($event, arrayContext: DynamicFormArrayModel, index: number) {
const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray;
this.formBuilderService.insertFormArrayGroup(index, formArrayControl, arrayContext);
this.addArrayItem.emit(this.getEvent($event, arrayContext, index, 'add'));
this.store.dispatch(new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel)));
}
protected getEvent($event: any, arrayContext: DynamicFormArrayModel, index: number, type: string): DynamicFormControlEvent {
const context = arrayContext.groups[index];
const itemGroupModel = context.context;
let group = this.formGroup.get(itemGroupModel.id) as FormGroup;
if (isNull(group)) {
for (const key of Object.keys(this.formGroup.controls)) {
group = this.formGroup.controls[key].get(itemGroupModel.id) as FormGroup;
if (isNotNull(group)) {
break;
}
}
}
const model = context.group[0] as DynamicFormControlModel;
const control = group.controls[index] as FormControl;
return {$event, context, control, group, model, type};
}
}

View File

@@ -0,0 +1,11 @@
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
@Injectable()
export class FormEffects {
constructor(private actions$: Actions) {
}
}

View File

@@ -0,0 +1,179 @@
import {
FormAction, FormActionTypes, FormAddError, FormChangeAction, FormInitAction, FormRemoveAction,
FormStatusChangeAction
} from './form.actions';
import { hasValue } from '../empty.util';
import { uniqWith, isEqual } from 'lodash';
export interface FormError {
message: string;
fieldId: string;
}
export interface FormEntry {
data: any;
valid: boolean;
errors: FormError[];
}
export interface FormState {
[formId: string]: FormEntry;
}
const initialState: FormState = Object.create(null);
export function formReducer(state = initialState, action: FormAction): FormState {
switch (action.type) {
case FormActionTypes.FORM_INIT: {
return initForm(state, action as FormInitAction);
}
case FormActionTypes.FORM_CHANGE: {
return changeDataForm(state, action as FormChangeAction);
}
case FormActionTypes.FORM_REMOVE: {
return removeForm(state, action as FormRemoveAction);
}
case FormActionTypes.FORM_STATUS_CHANGE: {
return changeStatusForm(state, action as FormStatusChangeAction);
}
case FormActionTypes.FORM_ADD_ERROR: {
return addFormErrors(state, action as FormAddError)
}
default: {
return state;
}
}
}
function addFormErrors(state: FormState, action: FormAddError) {
const formId = action.payload.formId;
if (hasValue(state[formId])) {
const error: FormError = {
fieldId: action.payload.fieldId,
message: action.payload.errorMessage
};
return Object.assign({}, state, {
[ formId ]: {
data: state[formId].data,
valid: state[formId].valid,
errors: state[formId].errors ? uniqWith(state[formId].errors.concat(error), isEqual) : [].concat(error),
}
});
} else {
return state;
}
}
/**
* Init form state.
*
* @param state
* the current state
* @param action
* an FormInitAction
* @return FormState
* the new state, with the form initialized.
*/
function initForm(state: FormState, action: FormInitAction): FormState {
if (!hasValue(state[ action.payload.formId ])) {
return Object.assign({}, state, {
[ action.payload.formId ]: {
data: action.payload.formData,
valid: action.payload.valid
}
});
} else {
const newState = Object.assign({}, state);
newState[ action.payload.formId ] = Object.assign({}, newState[ action.payload.formId ], {
data: action.payload.formData,
valid: action.payload.valid
}
);
return newState;
}
}
/**
* Set form data.
*
* @param state
* the current state
* @param action
* an FormChangeAction
* @return FormState
* the new state, with the data changed.
*/
function changeDataForm(state: FormState, action: FormChangeAction): FormState {
if (!hasValue(state[ action.payload.formId ])) {
return Object.assign({}, state, {
[ action.payload.formId ]: {
data: action.payload.formData,
valid: state[ action.payload.formId ].valid
}
});
} else {
const newState = Object.assign({}, state);
newState[ action.payload.formId ] = Object.assign({}, newState[ action.payload.formId ], {
data: action.payload.formData,
valid: state[ action.payload.formId ].valid
}
);
return newState;
}
}
/**
* Set form status.
*
* @param state
* the current state
* @param action
* an FormStatusChangeAction
* @return FormState
* the new state, with the status changed.
*/
function changeStatusForm(state: FormState, action: FormStatusChangeAction): FormState {
if (!hasValue(state[ action.payload.formId ])) {
return Object.assign({}, state, {
[ action.payload.formId ]: {
data: state[ action.payload.formId ].data,
valid: action.payload.valid
}
});
} else {
const newState = Object.assign({}, state);
newState[ action.payload.formId ] = Object.assign({}, newState[ action.payload.formId ], {
data: state[ action.payload.formId ].data,
valid: action.payload.valid
}
);
return newState;
}
}
/**
* Remove form state.
*
* @param state
* the current state
* @param action
* an FormRemoveAction
* @return FormState
* the new state, with the form initialized.
*/
function removeForm(state: FormState, action: FormRemoveAction): FormState {
if (hasValue(state[ action.payload.formId ])) {
const newState = Object.assign({}, state);
delete newState[ action.payload.formId ];
return newState;
} else {
return state;
}
}

View File

@@ -0,0 +1,118 @@
import { Injectable } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
import { AppState } from '../../app.reducer';
import { formObjectFromIdSelector } from './selectors';
import { FormBuilderService } from './builder/form-builder.service';
import { DynamicFormControlModel, DynamicFormGroupModel } from '@ng-dynamic-forms/core';
import { isNotEmpty, isNotUndefined } from '../empty.util';
import { find, uniqueId } from 'lodash';
import { FormChangeAction } from './form.actions';
@Injectable()
export class FormService {
constructor(private formBuilderService: FormBuilderService,
private store: Store<AppState>) {
}
/**
* Method to retrieve form's status from state
*/
public isValid(formId: string): Observable<boolean> {
return this.store.select(formObjectFromIdSelector(formId))
.filter((state) => isNotUndefined(state))
.map((state) => state.valid)
.distinctUntilChanged();
}
/**
* Method to retrieve form's data from state
*/
public getFormData(formId: string): Observable<FormControl> {
return this.store.select(formObjectFromIdSelector(formId))
.filter((state) => isNotUndefined(state))
.map((state) => state.data)
.distinctUntilChanged();
}
/**
* Method to retrieve form's data from state
*/
public isFormInitialized(formId: string): Observable<boolean> {
return this.store.select(formObjectFromIdSelector(formId))
.distinctUntilChanged()
.map((state) => isNotUndefined(state));
}
public getUniqueId(formId): string {
return uniqueId() + '_' + formId;
}
/**
* Method to validate form's fields
*/
public validateAllFormFields(formGroup: FormGroup) {
Object.keys(formGroup.controls).forEach((field) => {
const control = formGroup.get(field);
if (control instanceof FormControl) {
control.markAsTouched({onlySelf: true});
} else if (control instanceof FormGroup) {
this.validateAllFormFields(control);
}
});
}
public setValue(formGroup: FormGroup, fieldModel: DynamicFormControlModel, fieldId: string, value: any) {
if (isNotEmpty(fieldModel)) {
const path = this.formBuilderService.getPath(fieldModel);
const fieldControl = formGroup.get(path);
fieldControl.markAsDirty();
fieldControl.setValue(value);
}
}
public addErrorToField(field: AbstractControl, model: DynamicFormControlModel, message: string) {
const errorFound = !!(find(field.errors, (err) => err === message));
// search for the same error in the formControl.errors property
if (!errorFound) {
const errorKey = uniqueId('error-'); // create a single key for the error
const error = {}; // create the error object
error[errorKey] = message; // assign message
// if form control model has errorMessages object, create it
if (!model.errorMessages) {
model.errorMessages = {};
}
// put the error in the form control model
model.errorMessages[errorKey] = message;
// Use correct error messages from the model
const lastArray = message.split('.');
if (lastArray && lastArray.length > 0) {
const last = lastArray[lastArray.length - 1];
const modelMsg = model.errorMessages[last];
if (modelMsg && modelMsg.length > 0) {
model.errorMessages[errorKey] = modelMsg;
}
}
// add the error in the form control
field.setErrors(error);
// formGroup.markAsDirty();
field.markAsTouched();
}
}
public resetForm(formGroup: FormGroup, groupModel: DynamicFormControlModel[], formId: string) {
this.formBuilderService.clearAllModelsValue(groupModel);
formGroup.reset();
this.store.dispatch(new FormChangeAction(formId, formGroup.value));
}
}

View File

@@ -0,0 +1,10 @@
import { createSelector, MemoizedSelector, Selector } from '@ngrx/store';
import { AppState } from '../../app.reducer';
import { FormEntry, FormState } from './form.reducers';
export const formStateSelector = (state: AppState) => state.forms;
export function formObjectFromIdSelector(formId: string): MemoizedSelector<AppState, FormEntry> {
return createSelector(formStateSelector, (forms: FormState) => forms[formId]);
}

Some files were not shown because too many files have changed in this diff Show More