mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-16 06:23:03 +00:00
Merged dynamic form module
This commit is contained in:
@@ -80,14 +80,19 @@
|
||||
"@angular/router": "^5.2.5",
|
||||
"@angularclass/bootloader": "1.0.1",
|
||||
"@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/router-store": "^5.0.1",
|
||||
"@ngrx/store": "^5.1.0",
|
||||
"@nguniversal/express-engine": "5.0.0-beta.5",
|
||||
"@ngx-translate/core": "9.1.1",
|
||||
"@ngx-translate/http-loader": "2.0.1",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^0.6.0",
|
||||
"angular-idle-preload": "2.0.4",
|
||||
"angular-sortablejs": "^2.5.0",
|
||||
"angulartics2": "^5.2.0",
|
||||
"angular2-text-mask": "8.0.4",
|
||||
"body-parser": "1.18.2",
|
||||
"bootstrap": "^4.0.0",
|
||||
"cerialize": "0.1.18",
|
||||
@@ -103,10 +108,14 @@
|
||||
"jsonschema": "1.2.2",
|
||||
"methods": "1.1.2",
|
||||
"morgan": "1.9.0",
|
||||
"ng2-file-upload": "1.2.1",
|
||||
"ngx-infinite-scroll": "0.8.2",
|
||||
"ngx-pagination": "3.0.3",
|
||||
"pem": "1.12.3",
|
||||
"reflect-metadata": "0.1.12",
|
||||
"rxjs": "5.5.6",
|
||||
"sortablejs": "1.7.0",
|
||||
"text-mask-core": "5.0.1",
|
||||
"ts-md5": "^1.2.4",
|
||||
"uuid": "^3.2.1",
|
||||
"webfontloader": "1.6.28",
|
||||
|
@@ -154,5 +154,20 @@
|
||||
"item": "Error fetching item",
|
||||
"objects": "Error fetching objects",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import * as fromRouter from '@ngrx/router-store';
|
||||
|
||||
import { headerReducer, HeaderState } from './header/header.reducer';
|
||||
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
|
||||
import { formReducer, FormState } from './shared/form/form.reducers';
|
||||
import {
|
||||
SearchSidebarState,
|
||||
sidebarReducer
|
||||
@@ -17,6 +18,7 @@ export interface AppState {
|
||||
router: fromRouter.RouterReducerState;
|
||||
hostWindow: HostWindowState;
|
||||
header: HeaderState;
|
||||
forms: FormState;
|
||||
searchSidebar: SearchSidebarState;
|
||||
searchFilter: SearchFiltersState;
|
||||
truncatable: TruncatablesState;
|
||||
@@ -26,6 +28,7 @@ export const appReducers: ActionReducerMap<AppState> = {
|
||||
router: fromRouter.routerReducer,
|
||||
hostWindow: hostWindowReducer,
|
||||
header: headerReducer,
|
||||
forms: formReducer,
|
||||
searchSidebar: sidebarReducer,
|
||||
searchFilter: filterReducer,
|
||||
truncatable: truncatableReducer
|
||||
|
11
src/app/core/cache/response-cache.models.ts
vendored
11
src/app/core/cache/response-cache.models.ts
vendored
@@ -5,6 +5,7 @@ import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||
import { ConfigObject } from '../shared/config/config.model';
|
||||
import { FacetValue } from '../../+search-page/search-service/facet-value.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 */
|
||||
export class RestResponse {
|
||||
@@ -106,4 +107,14 @@ export class ConfigSuccessResponse extends RestResponse {
|
||||
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 */
|
||||
|
@@ -44,6 +44,7 @@ import { HALEndpointService } from './shared/hal-endpoint.service';
|
||||
import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service';
|
||||
import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service';
|
||||
import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service';
|
||||
import { UploaderService } from '../shared/uploader/uploader.service';
|
||||
|
||||
const IMPORTS = [
|
||||
CommonModule,
|
||||
@@ -88,6 +89,7 @@ const PROVIDERS = [
|
||||
SubmissionDefinitionsConfigService,
|
||||
SubmissionFormsConfigService,
|
||||
SubmissionSectionsConfigService,
|
||||
UploaderService,
|
||||
UUIDService,
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
||||
];
|
||||
|
@@ -7,6 +7,7 @@ import { ResponseParsingService } from './parsing.service';
|
||||
import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service';
|
||||
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
||||
import { ConfigResponseParsingService } from './config-response-parsing.service';
|
||||
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
|
||||
|
||||
/* 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 {
|
||||
statusText: string;
|
||||
}
|
||||
|
19
src/app/core/integration/authority.service.ts
Normal file
19
src/app/core/integration/authority.service.ts
Normal 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();
|
||||
}
|
||||
}
|
12
src/app/core/integration/integration-data.ts
Normal file
12
src/app/core/integration/integration-data.ts
Normal 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[]
|
||||
) { }
|
||||
}
|
17
src/app/core/integration/integration-object-factory.ts
Normal file
17
src/app/core/integration/integration-object-factory.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
85
src/app/core/integration/integration.service.ts
Normal file
85
src/app/core/integration/integration.service.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
4
src/app/core/integration/intergration-type.ts
Normal file
4
src/app/core/integration/intergration-type.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
export enum IntegrationType {
|
||||
Authority = 'authority'
|
||||
}
|
16
src/app/core/integration/models/authority-options.model.ts
Normal file
16
src/app/core/integration/models/authority-options.model.ts
Normal 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;
|
||||
}
|
||||
}
|
20
src/app/core/integration/models/authority-value.model.ts
Normal file
20
src/app/core/integration/models/authority-value.model.ts
Normal 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;
|
||||
}
|
14
src/app/core/integration/models/integration-options.model.ts
Normal file
14
src/app/core/integration/models/integration-options.model.ts
Normal 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) {
|
||||
|
||||
}
|
||||
}
|
12
src/app/core/integration/models/integration.model.ts
Normal file
12
src/app/core/integration/models/integration.model.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { autoserialize } from 'cerialize';
|
||||
|
||||
export abstract class IntegrationModel {
|
||||
|
||||
@autoserialize
|
||||
public type: string;
|
||||
|
||||
@autoserialize
|
||||
public _links: {
|
||||
[name: string]: string
|
||||
}
|
||||
}
|
23
src/app/core/shared/config/config-authority.model.ts
Normal file
23
src/app/core/shared/config/config-authority.model.ts
Normal 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;
|
||||
|
||||
}
|
@@ -6,6 +6,7 @@ import { SubmissionFormsModel } from './config-submission-forms.model';
|
||||
import { SubmissionDefinitionsModel } from './config-submission-definitions.model';
|
||||
import { ConfigType } from './config-type';
|
||||
import { ConfigObject } from './config.model';
|
||||
import { ConfigAuthorityModel } from './config-authority.model';
|
||||
|
||||
export class ConfigObjectFactory {
|
||||
public static getConstructor(type): GenericConstructor<ConfigObject> {
|
||||
@@ -22,6 +23,9 @@ export class ConfigObjectFactory {
|
||||
case ConfigType.SubmissionSections: {
|
||||
return SubmissionSectionModel
|
||||
}
|
||||
case ConfigType.Authority: {
|
||||
return ConfigAuthorityModel
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -1,10 +1,14 @@
|
||||
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||
import { autoserialize, inheritSerialization } from 'cerialize';
|
||||
import { ConfigObject } from './config.model';
|
||||
import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model';
|
||||
|
||||
export interface FormRowModel {
|
||||
fields: FormFieldModel[];
|
||||
}
|
||||
|
||||
@inheritSerialization(ConfigObject)
|
||||
export class SubmissionFormsModel extends ConfigObject {
|
||||
|
||||
@autoserialize
|
||||
fields: any[];
|
||||
|
||||
rows: FormRowModel[];
|
||||
}
|
||||
|
@@ -10,5 +10,6 @@ export enum ConfigType {
|
||||
SubmissionForm = 'submissionform',
|
||||
SubmissionForms = 'submissionforms',
|
||||
SubmissionSections = 'submissionsections',
|
||||
SubmissionSection = 'submissionsection'
|
||||
SubmissionSection = 'submissionsection',
|
||||
Authority = 'authority'
|
||||
}
|
||||
|
13
src/app/shared/animations/shrink.ts
Normal file
13
src/app/shared/animations/shrink.ts
Normal 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}))
|
||||
])
|
||||
]);
|
33
src/app/shared/chips/chips.component.html
Normal file
33
src/app/shared/chips/chips.component.html
Normal 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>
|
9
src/app/shared/chips/chips.component.scss
Normal file
9
src/app/shared/chips/chips.component.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@import "../../../styles/variables";
|
||||
|
||||
.chip-selected {
|
||||
background-color: map-get($theme-colors, info) !important;
|
||||
}
|
||||
|
||||
.chip-label {
|
||||
max-width: 10rem;
|
||||
}
|
95
src/app/shared/chips/chips.component.ts
Normal file
95
src/app/shared/chips/chips.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
73
src/app/shared/chips/models/chips-item.model.ts
Normal file
73
src/app/shared/chips/models/chips-item.model.ts
Normal 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;
|
||||
}
|
||||
}
|
130
src/app/shared/chips/models/chips.model.ts
Normal file
130
src/app/shared/chips/models/chips.model.ts
Normal 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);
|
||||
}
|
||||
}
|
19
src/app/shared/date.util.ts
Normal file
19
src/app/shared/date.util.ts
Normal 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`;
|
||||
|
||||
}
|
@@ -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>
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,5 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,3 @@
|
||||
.col-lg-1 {
|
||||
width: auto;
|
||||
}
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,3 @@
|
||||
.close {
|
||||
top: -2.5rem;
|
||||
}
|
@@ -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());
|
||||
}
|
||||
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
import { DynamicFormGroupModel } from '@ng-dynamic-forms/core';
|
||||
|
||||
export class DynamicRowGroupModel extends DynamicFormGroupModel {
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -0,0 +1 @@
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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;
|
||||
// }
|
||||
//}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
// });
|
||||
}
|
||||
}
|
@@ -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>
|
||||
|
||||
|
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
||||
|
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
// });
|
||||
// });
|
@@ -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)
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
212
src/app/shared/form/builder/form-builder.service.ts
Normal file
212
src/app/shared/form/builder/form-builder.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
export interface FormFieldChangedObject {
|
||||
string: any
|
||||
}
|
42
src/app/shared/form/builder/models/form-field.model.ts
Normal file
42
src/app/shared/form/builder/models/form-field.model.ts
Normal 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;
|
||||
}
|
99
src/app/shared/form/builder/parsers/concat-field-parser.ts
Normal file
99
src/app/shared/form/builder/parsers/concat-field-parser.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
49
src/app/shared/form/builder/parsers/date-field-parser.ts
Normal file
49
src/app/shared/form/builder/parsers/date-field-parser.ts
Normal 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;
|
||||
}
|
||||
}
|
44
src/app/shared/form/builder/parsers/dropdown-field-parser.ts
Normal file
44
src/app/shared/form/builder/parsers/dropdown-field-parser.ts
Normal 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.`);
|
||||
}
|
||||
}
|
||||
}
|
272
src/app/shared/form/builder/parsers/field-parser.ts
Normal file
272
src/app/shared/form/builder/parsers/field-parser.ts
Normal 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) ? ' ' : 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;
|
||||
}
|
||||
|
||||
}
|
67
src/app/shared/form/builder/parsers/group-field-parser.ts
Normal file
67
src/app/shared/form/builder/parsers/group-field-parser.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
52
src/app/shared/form/builder/parsers/list-field-parser.ts
Normal file
52
src/app/shared/form/builder/parsers/list-field-parser.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
30
src/app/shared/form/builder/parsers/lookup-field-parser.ts
Normal file
30
src/app/shared/form/builder/parsers/lookup-field-parser.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
||||
}
|
10
src/app/shared/form/builder/parsers/name-field-parser.ts
Normal file
10
src/app/shared/form/builder/parsers/name-field-parser.ts
Normal 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');
|
||||
}
|
||||
}
|
95
src/app/shared/form/builder/parsers/onebox-field-parser.ts
Normal file
95
src/app/shared/form/builder/parsers/onebox-field-parser.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
17
src/app/shared/form/builder/parsers/parser.utils.ts
Normal file
17
src/app/shared/form/builder/parsers/parser.utils.ts
Normal 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}`);
|
||||
}
|
||||
}
|
162
src/app/shared/form/builder/parsers/row-parser.ts
Normal file
162
src/app/shared/form/builder/parsers/row-parser.ts
Normal 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;
|
||||
}
|
||||
}
|
10
src/app/shared/form/builder/parsers/series-field-parser.ts
Normal file
10
src/app/shared/form/builder/parsers/series-field-parser.ts
Normal 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, ';');
|
||||
}
|
||||
}
|
31
src/app/shared/form/builder/parsers/tag-field-parser.ts
Normal file
31
src/app/shared/form/builder/parsers/tag-field-parser.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
32
src/app/shared/form/builder/parsers/textarea-field-parser.ts
Normal file
32
src/app/shared/form/builder/parsers/textarea-field-parser.ts
Normal 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;
|
||||
}
|
||||
}
|
10
src/app/shared/form/builder/parsers/twobox-field-parser.ts
Normal file
10
src/app/shared/form/builder/parsers/twobox-field-parser.ts
Normal 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;
|
||||
}
|
||||
}
|
126
src/app/shared/form/form.actions.ts
Normal file
126
src/app/shared/form/form.actions.ts
Normal 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
|
61
src/app/shared/form/form.component.html
Normal file
61
src/app/shared/form/form.component.html
Normal 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>
|
23
src/app/shared/form/form.component.scss
Normal file
23
src/app/shared/form/form.component.scss
Normal 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;
|
||||
}
|
175
src/app/shared/form/form.component.spec.ts
Normal file
175
src/app/shared/form/form.component.spec.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
274
src/app/shared/form/form.component.ts
Normal file
274
src/app/shared/form/form.component.ts
Normal 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};
|
||||
}
|
||||
}
|
11
src/app/shared/form/form.effects.ts
Normal file
11
src/app/shared/form/form.effects.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Actions, Effect } from '@ngrx/effects';
|
||||
|
||||
@Injectable()
|
||||
export class FormEffects {
|
||||
|
||||
constructor(private actions$: Actions) {
|
||||
|
||||
}
|
||||
|
||||
}
|
179
src/app/shared/form/form.reducers.ts
Normal file
179
src/app/shared/form/form.reducers.ts
Normal 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;
|
||||
}
|
||||
}
|
118
src/app/shared/form/form.service.ts
Normal file
118
src/app/shared/form/form.service.ts
Normal 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));
|
||||
}
|
||||
}
|
10
src/app/shared/form/selectors.ts
Normal file
10
src/app/shared/form/selectors.ts
Normal 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
Reference in New Issue
Block a user