Merge pull request #264 from 4Science/dynamic_forms

Dynamic forms Components
This commit is contained in:
Art Lowel
2018-07-20 14:40:59 +02:00
committed by GitHub
157 changed files with 13174 additions and 39 deletions

View File

@@ -10,8 +10,8 @@ addons:
language: node_js
node_js:
- "6"
- "8"
- "9"
cache:
yarn: true

View File

@@ -14,7 +14,7 @@ If you're looking for the 2016 Angular 2 DSpace UI prototype, you can find it [h
Quick start
-----------
**Ensure you're running [Node](https://nodejs.org) >= `v6.9.x`, [npm](https://www.npmjs.com/) >= `v3.x` and [yarn](https://yarnpkg.com) >= `v0.20.x`**
**Ensure you're running [Node](https://nodejs.org) >= `v8.0.x`, [npm](https://www.npmjs.com/) >= `v3.x` and [yarn](https://yarnpkg.com) >= `v0.20.x`**
```bash
# clone the repo

View File

@@ -22,6 +22,14 @@ module.exports = {
// msToLive: 1000, // 15 minutes
control: 'max-age=60' // revalidate browser
},
// Form settings
form: {
// NOTE: Map server-side validators to comparative Angular form validators
validatorMap: {
required: 'required',
regex: 'pattern'
}
},
// Notifications
notifications: {
rtl: false,

View File

@@ -8,7 +8,7 @@
},
"license": "BSD-2-Clause",
"engines": {
"node": ">=6.0.0"
"node": ">=8.0.0"
},
"scripts": {
"global": "npm install -g @angular/cli marked node-gyp nodemon node-nightly npm-check-updates npm-run-all rimraf typescript ts-node typedoc webpack webpack-bundle-analyzer pm2 rollup",
@@ -80,13 +80,18 @@
"@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",
"@nguniversal/express-engine": "5.0.0",
"@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",
"angular2-text-mask": "8.0.4",
"angulartics2": "^5.2.0",
"body-parser": "1.18.2",
"bootstrap": "^4.0.0",
@@ -105,10 +110,14 @@
"jwt-decode": "^2.2.0",
"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",

View File

@@ -204,7 +204,28 @@
"recent-submissions": "Error fetching recent submissions",
"item": "Error fetching item",
"objects": "Error fetching objects",
"search-results": "Error fetching search results"
"search-results": "Error fetching search results",
"validation": {
"pattern": "This input is restricted by the current pattern: {{ pattern }}.",
"license": {
"notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission."
}
}
},
"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"
},
"login": {
"title": "Login",

View File

@@ -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.reducer';
import {
SearchSidebarState,
sidebarReducer
@@ -18,6 +19,7 @@ export interface AppState {
router: fromRouter.RouterReducerState;
hostWindow: HostWindowState;
header: HeaderState;
forms: FormState;
notifications: NotificationsState;
searchSidebar: SearchSidebarState;
searchFilter: SearchFiltersState;
@@ -28,6 +30,7 @@ export const appReducers: ActionReducerMap<AppState> = {
router: fromRouter.routerReducer,
hostWindow: hostWindowReducer,
header: headerReducer,
forms: formReducer,
notifications: notificationsReducer,
searchSidebar: sidebarReducer,
searchFilter: filterReducer,

View File

@@ -1,26 +1,25 @@
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
import { RequestError } from '../data/request.models';
import { BrowseEntry } from '../shared/browse-entry.model';
import { PageInfo } from '../shared/page-info.model';
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';
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
import { MetadataSchema } from '../metadata/metadataschema.model';
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
import { AuthTokenInfo } from '../auth/models/auth-token-info.model';
import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model';
import { AuthStatus } from '../auth/models/auth-status.model';
/* tslint:disable:max-classes-per-file */
export class RestResponse {
public toCache = true;
constructor(
public isSuccessful: boolean,
public statusCode: string,
) { }
) {
}
}
export class DSOSuccessResponse extends RestResponse {
@@ -158,6 +157,7 @@ export class ConfigSuccessResponse extends RestResponse {
export class AuthStatusResponse extends RestResponse {
public toCache = false;
constructor(
public response: AuthStatus,
public statusCode: string
@@ -166,4 +166,14 @@ export class AuthStatusResponse extends RestResponse {
}
}
export class IntegrationSuccessResponse extends RestResponse {
constructor(
public dataDefinition: IntegrationModel[],
public statusCode: string,
public pageInfo?: PageInfo
) {
super(true, statusCode);
}
}
/* tslint:enable:max-classes-per-file */

View File

@@ -8,6 +8,7 @@ import { CommonModule } from '@angular/common';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { coreEffects } from './core.effects';
import { coreReducers } from './core.reducers';
@@ -22,6 +23,8 @@ import { DebugResponseParsingService } from './data/debug-response-parsing.servi
import { DSOResponseParsingService } from './data/dso-response-parsing.service';
import { SearchResponseParsingService } from './data/search-response-parsing.service';
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
import { FormBuilderService } from '../shared/form/builder/form-builder.service';
import { FormService } from '../shared/form/form.service';
import { HostWindowService } from '../shared/host-window.service';
import { ItemDataService } from './data/item-data.service';
import { MetadataService } from './metadata/metadata.service';
@@ -40,6 +43,8 @@ import { RouteService } from '../shared/services/route.service';
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
import { AuthorityService } from './integration/authority.service';
import { IntegrationResponseParsingService } from './integration/integration-response-parsing.service';
import { UUIDService } from './shared/uuid.service';
import { AuthenticatedGuard } from './auth/authenticated.guard';
import { AuthRequestService } from './auth/auth-request.service';
@@ -56,6 +61,7 @@ import { MetadataschemaParsingService } from './data/metadataschema-parsing.serv
import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service';
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
import { NotificationsService } from '../shared/notifications/notifications.service';
import { UploaderService } from '../shared/uploader/uploader.service';
const IMPORTS = [
CommonModule,
@@ -80,6 +86,11 @@ const PROVIDERS = [
CollectionDataService,
DSOResponseParsingService,
DSpaceRESTv2Service,
DynamicFormLayoutService,
DynamicFormService,
DynamicFormValidationService,
FormBuilderService,
FormService,
HALEndpointService,
HostWindowService,
ItemDataService,
@@ -109,6 +120,9 @@ const PROVIDERS = [
SubmissionDefinitionsConfigService,
SubmissionFormsConfigService,
SubmissionSectionsConfigService,
AuthorityService,
IntegrationResponseParsingService,
UploaderService,
UUIDService,
// register AuthInterceptor as HttpInterceptor
{

View File

@@ -27,7 +27,7 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && data.statusCode === '200') {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '201' || data.statusCode === '200' || data.statusCode === 'OK')) {
const configDefinition = this.process<ConfigObject,ConfigType>(data.payload, request.href);
return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload));
} else {

View File

@@ -11,6 +11,7 @@ import { ConfigResponseParsingService } from './config-response-parsing.service'
import { AuthResponseParsingService } from '../auth/auth-response-parsing.service';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { HttpHeaders } from '@angular/common/http';
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
/* tslint:disable:max-classes-per-file */
@@ -212,6 +213,15 @@ export class AuthGetRequest 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;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,196 @@
import { ErrorResponse, IntegrationSuccessResponse } from '../cache/response-cache.models';
import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { IntegrationResponseParsingService } from './integration-response-parsing.service';
import { IntegrationRequest } from '../data/request.models';
import { AuthorityValueModel } from './models/authority-value.model';
describe('IntegrationResponseParsingService', () => {
let service: IntegrationResponseParsingService;
const EnvConfig = {} as GlobalConfig;
const store = {} as Store<CoreState>;
const objectCacheService = new ObjectCacheService(store);
const name = 'type';
const metadata = 'dc.type';
const query = '';
const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
const integrationEndpoint = 'https://rest.api/integration/authorities';
const entriesEndpoint = `${integrationEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`;
beforeEach(() => {
service = new IntegrationResponseParsingService(EnvConfig, objectCacheService);
});
describe('parse', () => {
const validRequest = new IntegrationRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', entriesEndpoint);
const validResponse = {
payload: {
page: {
number: 0,
size: 5,
totalElements: 5,
totalPages: 1
},
_embedded: {
authorityEntries: [
{
display: 'One',
id: 'One',
otherInformation: {},
type: 'authority',
value: 'One'
},
{
display: 'Two',
id: 'Two',
otherInformation: {},
type: 'authority',
value: 'Two'
},
{
display: 'Three',
id: 'Three',
otherInformation: {},
type: 'authority',
value: 'Three'
},
{
display: 'Four',
id: 'Four',
otherInformation: {},
type: 'authority',
value: 'Four'
},
{
display: 'Five',
id: 'Five',
otherInformation: {},
type: 'authority',
value: 'Five'
},
],
},
_links: {
self: 'https://rest.api/integration/authorities/type/entries'
}
},
statusCode: '200'
};
const invalidResponse1 = {
payload: {},
statusCode: '200'
};
const invalidResponse2 = {
payload: {
page: {
number: 0,
size: 5,
totalElements: 5,
totalPages: 1
},
_embedded: {
authorityEntries: [
{
display: 'One',
id: 'One',
otherInformation: {},
type: 'authority',
value: 'One'
},
{
display: 'Two',
id: 'Two',
otherInformation: {},
type: 'authority',
value: 'Two'
},
{
display: 'Three',
id: 'Three',
otherInformation: {},
type: 'authority',
value: 'Three'
},
{
display: 'Four',
id: 'Four',
otherInformation: {},
type: 'authority',
value: 'Four'
},
{
display: 'Five',
id: 'Five',
otherInformation: {},
type: 'authority',
value: 'Five'
},
],
},
_links: {}
},
statusCode: '200'
};
const definitions = [
Object.assign(new AuthorityValueModel(), {
display: 'One',
id: 'One',
otherInformation: {},
value: 'One'
}),
Object.assign(new AuthorityValueModel(), {
display: 'Two',
id: 'Two',
otherInformation: {},
value: 'Two'
}),
Object.assign(new AuthorityValueModel(), {
display: 'Three',
id: 'Three',
otherInformation: {},
value: 'Three'
}),
Object.assign(new AuthorityValueModel(), {
display: 'Four',
id: 'Four',
otherInformation: {},
value: 'Four'
}),
Object.assign(new AuthorityValueModel(), {
display: 'Five',
id: 'Five',
otherInformation: {},
value: 'Five'
})
];
it('should return a IntegrationSuccessResponse if data contains a valid endpoint response', () => {
const response = service.parse(validRequest, validResponse);
expect(response.constructor).toBe(IntegrationSuccessResponse);
});
it('should return an ErrorResponse if data contains an invalid config endpoint response', () => {
const response1 = service.parse(validRequest, invalidResponse1);
const response2 = service.parse(validRequest, invalidResponse2);
expect(response1.constructor).toBe(ErrorResponse);
expect(response2.constructor).toBe(ErrorResponse);
});
it('should return a IntegrationSuccessResponse with data definition', () => {
const response = service.parse(validRequest, validResponse);
expect((response as any).dataDefinition).toEqual(definitions);
});
});
});

View File

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

View File

@@ -0,0 +1,83 @@
import { cold, getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/Rx';
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service';
import { IntegrationRequest } from '../data/request.models';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
import { IntegrationService } from './integration.service';
import { IntegrationSearchOptions } from './models/integration-options.model';
const LINK_NAME = 'authorities';
const BROWSE = 'entries';
class TestService extends IntegrationService {
protected linkPath = LINK_NAME;
protected browseEndpoint = BROWSE;
constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected halService: HALEndpointService) {
super();
}
}
describe('IntegrationService', () => {
let scheduler: TestScheduler;
let service: TestService;
let responseCache: ResponseCacheService;
let requestService: RequestService;
let halService: any;
let findOptions: IntegrationSearchOptions;
const name = 'type';
const metadata = 'dc.type';
const query = '';
const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
const integrationEndpoint = 'https://rest.api/integration';
const serviceEndpoint = `${integrationEndpoint}/${LINK_NAME}`;
const entriesEndpoint = `${serviceEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`;
findOptions = new IntegrationSearchOptions(uuid, name, metadata);
function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService {
return jasmine.createSpyObj('responseCache', {
get: cold('c-', {
c: {response: {isSuccessful}}
})
});
}
function initTestService(): TestService {
return new TestService(
responseCache,
requestService,
halService
);
}
beforeEach(() => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService();
scheduler = getTestScheduler();
halService = new HALEndpointServiceStub(integrationEndpoint);
findOptions = new IntegrationSearchOptions(uuid, name, metadata, query);
service = initTestService();
});
describe('getEntriesByName', () => {
it('should configure a new IntegrationRequest', () => {
const expected = new IntegrationRequest(requestService.generateRequestId(), entriesEndpoint);
scheduler.schedule(() => service.getEntriesByName(findOptions).subscribe());
scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected);
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import { SubmissionFormsModel } from './config-submission-forms.model';
import { SubmissionDefinitionsModel } from './config-submission-definitions.model';
import { 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;
}

View File

@@ -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[];
}

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
<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 class="nav-item mr-2 mb-1"
(dragstart)="onDragStart(i)"
(dragend)="onDragEnd(i)">
<a class="flex-sm-fill text-sm-center nav-link active"
href="#"
[ngClass]="{'chip-selected disabled': (editable && c.editMode) || dragged == i}"
(click)="chipsSelected($event, i);">
<span>
<ng-container *ngIf="c.hasIcons()">
<i *ngFor="let icon of c.icons; let l = last"
[ngbTooltip]="tipContent"
triggers="manual"
#t="ngbTooltip"
class="fa {{icon.style}}"
[class.mr-1]="!l"
[class.mr-2]="l"
aria-hidden="true"
(dragstart)="tooltip.close();"
(mouseover)="showTooltip(t, i, icon.metadata)"
(mouseout)="t.close()"></i>
</ng-container>
<p class="chip-label text-truncate d-table-cell">{{c.display}}</p><i class="fa fa-times ml-2" (click)="removeChips($event, i)"></i>
</span>
</a>
</li>
</ng-container>
<ng-content></ng-content>
</ul>
</div>

View File

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

View File

@@ -0,0 +1,227 @@
// Load the implementations that should be tested
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing';
import 'rxjs/add/observable/of';
import { Chips } from './models/chips.model';
import { UploaderService } from '../uploader/uploader.service';
import { ChipsComponent } from './chips.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { SortablejsModule } from 'angular-sortablejs';
import { By } from '@angular/platform-browser';
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';
import { createTestComponent, hasClass } from '../testing/utils';
describe('ChipsComponent test suite', () => {
let testComp: TestComponent;
let chipsComp: ChipsComponent;
let testFixture: ComponentFixture<TestComponent>;
let chipsFixture: ComponentFixture<ChipsComponent>;
let html;
let chips: Chips;
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
NgbModule.forRoot(),
SortablejsModule.forRoot({animation: 150}),
],
declarations: [
ChipsComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
ChipsComponent,
UploaderService
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-chips
*ngIf="chips.hasItems()"
[chips]="chips"
[editable]="editable"
(selected)="onChipSelected($event)"></ds-chips>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create Chips Component', inject([ChipsComponent], (app: ChipsComponent) => {
expect(app).toBeDefined();
}));
});
describe('when has items as string', () => {
beforeEach(() => {
chips = new Chips(['a', 'b', 'c']);
chipsFixture = TestBed.createComponent(ChipsComponent);
chipsComp = chipsFixture.componentInstance; // TruncatableComponent test instance
chipsComp.editable = true;
chipsComp.chips = chips;
chipsFixture.detectChanges();
});
afterEach(() => {
chipsFixture.destroy();
chipsComp = null;
});
it('should set edit mode when a chip item is selected', fakeAsync(() => {
spyOn(chipsComp.selected, 'emit');
chipsComp.chipsSelected(new Event('click'), 1);
chipsFixture.detectChanges();
tick();
const item = chipsComp.chips.getChipByIndex(1);
expect(item.editMode).toBe(true);
expect(chipsComp.selected.emit).toHaveBeenCalledWith(1);
}));
it('should not set edit mode when a chip item is selected and editable is false', fakeAsync(() => {
chipsComp.editable = false;
spyOn(chipsComp.selected, 'emit');
chipsComp.chipsSelected(new Event('click'), 1);
chipsFixture.detectChanges();
tick();
const item = chipsComp.chips.getChipByIndex(1);
expect(item.editMode).toBe(false);
expect(chipsComp.selected.emit).not.toHaveBeenCalledWith(1);
}));
it('should emit when a chip item is removed and editable is true', fakeAsync(() => {
spyOn(chipsComp.chips, 'remove');
const item = chipsComp.chips.getChipByIndex(1);
chipsComp.removeChips(new Event('click'), 1);
chipsFixture.detectChanges();
tick();
expect(chipsComp.chips.remove).toHaveBeenCalledWith(item);
}));
it('should save chips item index when drag and drop start', fakeAsync(() => {
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
de.triggerEventHandler('dragstart', null);
expect(chipsComp.dragged).toBe(0);
}));
it('should update chips item order when drag and drop end', fakeAsync(() => {
spyOn(chipsComp.chips, 'updateOrder');
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
de.triggerEventHandler('dragend', null);
expect(chipsComp.dragged).toBe(-1);
expect(chipsComp.chips.updateOrder).toHaveBeenCalled();
}));
});
describe('when has items as object', () => {
beforeEach(() => {
const item = {
mainField: new FormFieldMetadataValueObject('main test', null, 'test001'),
relatedField: new FormFieldMetadataValueObject('related test', null, 'test002'),
otherRelatedField: new FormFieldMetadataValueObject('other related test')
};
const iconsConfig = [
{
name: 'mainField',
config: {
withAuthority:{
style: 'fa-user'
}
}
},
{
name: 'relatedField',
config: {
withAuthority:{
style: 'fa-user-alt'
},
withoutAuthority:{
style: 'fa-user-alt text-muted'
}
}
},
{
name: 'otherRelatedField',
config: {
withAuthority:{
style: 'fa-user-alt'
},
withoutAuthority:{
style: 'fa-user-alt text-muted'
}
}
},
{
name: 'default',
config: {}
}
];
chips = new Chips([item], 'display', 'mainField', iconsConfig);
chipsFixture = TestBed.createComponent(ChipsComponent);
chipsComp = chipsFixture.componentInstance; // TruncatableComponent test instance
chipsComp.editable = true;
chipsComp.chips = chips;
chipsFixture.detectChanges();
});
it('should show icon for every field that has a configured icon', () => {
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
const icons = de.queryAll(By.css('i.fa'));
expect(icons.length).toBe(4);
});
it('should has text-muted on icon style when field value had not authority', () => {
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
const icons = de.queryAll(By.css('i.fa'));
expect(hasClass(icons[2].nativeElement, 'text-muted')).toBeTruthy();
});
it('should show tooltip on mouse over an icon', () => {
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
const icons = de.queryAll(By.css('i.fa'));
icons[0].triggerEventHandler('mouseover', null);
expect(chipsComp.tipText).toBe('main test')
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
public chips = new Chips(['a', 'b', 'c']);
public editable = true;
}

View File

@@ -0,0 +1,95 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, } from '@angular/core';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { SortablejsOptions } from 'angular-sortablejs';
import { isObject } from 'lodash';
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 = true;
@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'
};
}
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(index) {
this.uploaderService.overrideDragOverPage();
this.dragged = index;
}
onDragEnd(event) {
this.uploaderService.allowDragOverPage();
this.dragged = -1;
this.chips.updateOrder();
}
showTooltip(tooltip: NgbTooltip, index, field?) {
tooltip.close();
const item = this.chips.getChipByIndex(index);
if (!item.editMode && this.dragged === -1) {
if (field) {
this.tipText = (isObject(item.item[field])) ? item.item[field].display : item.item[field];
} else {
this.tipText = item.display;
}
this.cdr.detectChanges();
tooltip.open();
}
}
}

View File

@@ -0,0 +1,78 @@
import { ChipsItem, ChipsItemIcon } from './chips-item.model';
import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
describe('ChipsItem model test suite', () => {
let item: ChipsItem;
beforeEach(() => {
item = new ChipsItem('a');
});
it('should init ChipsItem object properly', () => {
expect(item.item).toBe('a');
expect(item.display).toBe('a');
expect(item.editMode).toBe(false);
expect(item.icons).toEqual([]);
});
it('should update item', () => {
item.updateItem('b');
expect(item.item).toBe('b');
});
it('should set editMode', () => {
item.setEditMode();
expect(item.editMode).toBe(true);
});
it('should unset editMode', () => {
item.unsetEditMode();
expect(item.editMode).toBe(false);
});
it('should update icons', () => {
const icons: ChipsItemIcon[] = [{metadata: 'test', hasAuthority: false, style: 'fa fa-plus'}];
item.updateIcons(icons);
expect(item.icons).toEqual(icons);
});
it('should return true if has icons', () => {
const icons: ChipsItemIcon[] = [{metadata: 'test', hasAuthority: false, style: 'fa fa-plus'}];
item.updateIcons(icons);
const hasIcons = item.hasIcons();
expect(hasIcons).toBe(true);
});
it('should return false if has not icons', () => {
const hasIcons = item.hasIcons();
expect(hasIcons).toBe(false);
});
it('should set display property with a different fieldToDisplay', () => {
item = new ChipsItem(
{
label: 'A',
value: 'a'
},
'label');
expect(item.display).toBe('A');
});
it('should set display property with a different objToDisplay', () => {
item = new ChipsItem(
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'other'
},
'value', 'toDisplay');
expect(item.display).toBe('a');
});
});

View File

@@ -0,0 +1,72 @@
import { uniqueId, isObject } from 'lodash';
import { isNotEmpty } from '../../empty.util';
export interface ChipsItemIcon {
metadata: string;
hasAuthority: boolean;
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 = 'display',
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 (isObject(this.item)) {
// Check If displayField is in an internal object
const obj = this.objToDisplay ? this.item[this.objToDisplay] : this.item;
if (isObject(obj) && obj) {
value = obj[this.fieldToDisplay] || obj.value;
} else {
value = obj;
}
}
this.display = value;
}
}

View File

@@ -0,0 +1,126 @@
import { Chips } from './chips.model';
import { ChipsItem } from './chips-item.model';
import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
describe('Chips model test suite', () => {
let items: any[];
let item: ChipsItem;
let chips: Chips;
beforeEach(() => {
items = ['a', 'b', 'c'];
chips = new Chips(items);
});
it('should init Chips object properly', () => {
expect(chips.getChipsItems()).toEqual(items);
expect(chips.displayField).toBe('display');
expect(chips.displayObj).toBe(undefined);
expect(chips.iconsConfig).toEqual([]);
});
it('should add an element to items', () => {
items = ['a', 'b', 'c', 'd'];
chips.add('d');
expect(chips.getChipsItems()).toEqual(items);
});
it('should remove an element from items', () => {
items = ['a', 'c'];
item = chips.getChipByIndex(1);
chips.remove(item);
expect(chips.getChipsItems()).toEqual(items);
});
it('should update an item', () => {
items = ['a', 'd', 'c'];
const id = chips.getChipByIndex(1).id;
chips.update(id, 'd');
expect(chips.getChipsItems()).toEqual(items);
});
it('should update items order', () => {
items = ['a', 'c', 'b'];
const chipsItems = chips.getChips();
const b = chipsItems[1];
chipsItems[1] = chipsItems[2];
chipsItems[2] = b;
chips.updateOrder();
expect(chips.getChipsItems()).toEqual(items);
});
it('should set a different displayField', () => {
items = [
{
label: 'A',
value: 'a'
},
{
label: 'B',
value: 'b'
},
{
label: 'C',
value: 'c'
},
];
chips = new Chips(items, 'label');
expect(chips.displayField).toBe('label');
expect(chips.getChipsItems()).toEqual(items);
});
it('should set a different displayObj', () => {
items = [
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'a'
},
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'a'
},
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'a'
},
];
chips = new Chips(items, 'value', 'toDisplay');
expect(chips.displayField).toBe('value');
expect(chips.displayObj).toBe('toDisplay');
expect(chips.getChipsItems()).toEqual(items);
});
it('should set iconsConfig', () => {
items = [
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'a'
},
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'a'
},
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'a'
},
];
const iconsConfig = [{
name: 'toDisplay',
config: {
withAuthority:{
style: 'fa-user'
},
withoutAuthority:{
style: 'fa-user text-muted'
}
}
}];
chips = new Chips(items, 'value', 'toDisplay', iconsConfig);
expect(chips.displayField).toBe('value');
expect(chips.displayObj).toBe('toDisplay');
expect(chips.iconsConfig).toEqual(iconsConfig);
expect(chips.getChipsItems()).toEqual(items);
});
});

View File

@@ -0,0 +1,159 @@
import { findIndex, isEqual, isObject } from 'lodash';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { ChipsItem, ChipsItemIcon } from './chips-item.model';
import { hasValue, isNotEmpty } from '../../empty.util';
export interface IconsConfig {
withAuthority?: {
style: string;
};
withoutAuthority?: {
style: string;
};
}
export interface MetadataIconsConfig {
name: string;
config: IconsConfig;
}
export class Chips {
chipsItems: BehaviorSubject<ChipsItem[]>;
displayField: string;
displayObj: string;
iconsConfig: MetadataIconsConfig[];
private _items: ChipsItem[];
constructor(items: any[] = [],
displayField: string = 'display',
displayObj?: string,
iconsConfig?: MetadataIconsConfig[]) {
this.displayField = displayField;
this.displayObj = displayObj;
this.iconsConfig = iconsConfig || [];
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 = [];
const defaultConfigIndex: number = findIndex(this.iconsConfig, {name: 'default'});
const defaultConfig: IconsConfig = (defaultConfigIndex !== -1) ? this.iconsConfig[defaultConfigIndex].config : undefined;
let config: IconsConfig;
let configIndex: number;
let value: any;
Object.keys(item)
.forEach((metadata) => {
value = item[metadata];
configIndex = findIndex(this.iconsConfig, {name: metadata});
config = (configIndex !== -1) ? this.iconsConfig[configIndex].config : defaultConfig;
if (hasValue(value) && isNotEmpty(config)) {
let icon: ChipsItemIcon;
const hasAuthority: boolean = !!(isObject(value) && ((value.hasOwnProperty('authority') && value.authority) || (value.hasOwnProperty('id') && value.id)));
// Set icons
if ((this.displayObj && this.displayObj === metadata && hasAuthority)
|| (this.displayObj && this.displayObj !== metadata)) {
icon = {
metadata,
hasAuthority: hasAuthority,
style: (hasAuthority) ? config.withAuthority.style : config.withoutAuthority.style
};
}
if (icon) {
icons.push(icon);
}
}
});
return icons;
}
/**
* Sets initial items, used in edit mode
*/
private setInitialItems(items: any[]): void {
this._items = [];
items.forEach((item) => {
const icons = this.getChipsIcons(item);
const chipsItem = new ChipsItem(item, this.displayField, this.displayObj, icons);
this._items.push(chipsItem);
});
this.chipsItems = new BehaviorSubject<ChipsItem[]>(this._items);
}
}

View File

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

View File

@@ -0,0 +1,461 @@
<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"
[id]="bindId && checkboxModel.id"
[dynamicId]="bindId && checkboxModel.id"
[formControlName]="checkboxModel.id"
[indeterminate]="checkboxModel.indeterminate"
[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"
(dateSelect)="onValueChange($event)"
(blur)="onBlur($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-prepend">
<span class="input-group-text" [innerHTML]="model.prefix"></span>
</div>
<ng-container *ngTemplateOutlet="inputTemplate;
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
</ng-container>
<div *ngIf="model.suffix" class="input-group-append">
<span class="input-group-text" [innerHTML]="model.suffix"></span>
</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 btn-group-toggle" 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>
<!-- SELECT ----------------------------------------------------------------------------------------------->
<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"
[showErrorMessages]="showErrorMessages"
(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>
<ng-container *ngSwitchCase="19">
<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="model.hint" 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 | translate:model.validators }}</small>
</div>
</ng-container>
<ng-template #inputTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<input [attr.accept]="model.accept"
[attr.list]="model.listId"
[attr.max]="model.max"
[attr.min]="model.min"
[attr.multiple]="model.multiple"
[attr.step]="model.step"
[autocomplete]="model.autoComplete"
[autofocus]="model.autoFocus"
[class.form-control]="model.inputType !== 'file'"
[class.form-control-file]="model.inputType === 'file'"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[formControlName]="model.id"
[maxlength]="model.maxLength"
[minlength]="model.minLength"
[name]="model.name"
[ngClass]="getClass('element', 'control')"
[pattern]="model.pattern"
[placeholder]="(model.placeholder | translate)"
[readonly]="model.readOnly"
[required]="model.required"
[spellcheck]="model.spellCheck"
[tabindex]="model.tabIndex"
[textMask]="{mask: (model.mask || false), showMask: model.mask && !(model.placeholder)}"
[type]="model.inputType"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"/>
</ng-template>
<ng-template #selectTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<select class="form-control"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[formControlName]="model.id"
[name]="model.name"
[ngClass]="getClass('element', 'control')"
[required]="model.required"
[tabindex]="model.tabIndex"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)">
<option *ngFor="let option of model.options$ | async"
[disabled]="option.disabled"
[ngValue]="option.value">{{ option.label }}</option>
</select>
</ng-template>
<ng-template #typeaheadTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<ds-dynamic-typeahead [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-typeahead>
</ng-template>
<ng-template #scrollableDropdownTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<ds-dynamic-scrollable-dropdown [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-scrollable-dropdown>
</ng-template>
<ng-template #tagTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<ds-dynamic-tag [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-tag>
</ng-template>
<ng-template #listTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<ds-dynamic-list [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-list>
</ng-template>
</div>
<div *ngIf="model.languageCodes && model.languageCodes.length > 0" class="col-xs-2">
<select
#language="ngModel"
[disabled]="model.readOnly"
[(ngModel)]="model.language"
class="form-control"
(blur)="onBlur($event)"
(change)="onChangeLanguage($event)"
[ngModelOptions]="{standalone: true}"
required>
<!--<option [value]="null" disabled>Language</option>-->
<option *ngFor="let lang of model.languageCodes" [value]="lang.code">{{lang.display}}</option>
</select>
</div>
</div>
<ng-container *ngTemplateOutlet="templates[1]?.templateRef; context: model"></ng-container>
<ng-content></ng-content>
</div>

View File

@@ -0,0 +1,281 @@
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement, SimpleChange } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TextMaskModule } from 'angular2-text-mask';
import {
DynamicCheckboxGroupModel,
DynamicCheckboxModel,
DynamicColorPickerModel,
DynamicDatePickerModel,
DynamicEditorModel,
DynamicFileUploadModel,
DynamicFormArrayModel,
DynamicFormControlModel,
DynamicFormGroupModel,
DynamicFormsCoreModule,
DynamicFormService,
DynamicInputModel,
DynamicRadioGroupModel,
DynamicRatingModel,
DynamicSelectModel,
DynamicSliderModel,
DynamicSwitchModel,
DynamicTextAreaModel,
DynamicTimePickerModel
} from '@ng-dynamic-forms/core';
import { DsDynamicFormControlComponent, NGBootstrapFormControlType } from './ds-dynamic-form-control.component';
import { TranslateModule } from '@ngx-translate/core';
import { SharedModule } from '../../../shared.module';
import { DynamicDsDatePickerModel } from './models/date-picker/date-picker.model';
import { DynamicGroupModel } from './models/dynamic-group/dynamic-group.model';
import { DynamicListCheckboxGroupModel } from './models/list/dynamic-list-checkbox-group.model';
import { AuthorityOptions } from '../../../../core/integration/models/authority-options.model';
import { DynamicListRadioGroupModel } from './models/list/dynamic-list-radio-group.model';
import { DynamicLookupModel } from './models/lookup/dynamic-lookup.model';
import { DynamicScrollableDropdownModel } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
import { DynamicTagModel } from './models/tag/dynamic-tag.model';
import { DynamicTypeaheadModel } from './models/typeahead/dynamic-typeahead.model';
import { DynamicQualdropModel } from './models/ds-dynamic-qualdrop.model';
import { DynamicLookupNameModel } from './models/lookup/dynamic-lookup-name.model';
describe('DsDynamicFormControlComponent test suite', () => {
const authorityOptions: AuthorityOptions = {
closed: false,
metadata: 'list',
name: 'type_programme',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
};
const formModel = [
new DynamicCheckboxModel({id: 'checkbox'}),
new DynamicCheckboxGroupModel({id: 'checkboxGroup', group: []}),
new DynamicColorPickerModel({id: 'colorpicker'}),
new DynamicDatePickerModel({id: 'datepicker'}),
new DynamicEditorModel({id: 'editor'}),
new DynamicFileUploadModel({id: 'upload', url: ''}),
new DynamicFormArrayModel({id: 'formArray', groupFactory: () => []}),
new DynamicFormGroupModel({id: 'formGroup', group: []}),
new DynamicInputModel({id: 'input', maxLength: 51}),
new DynamicRadioGroupModel({id: 'radioGroup'}),
new DynamicRatingModel({id: 'rating'}),
new DynamicSelectModel({id: 'select', options: [{value: 'One'}, {value: 'Two'}], value: 'One'}),
new DynamicSliderModel({id: 'slider'}),
new DynamicSwitchModel({id: 'switch'}),
new DynamicTextAreaModel({id: 'textarea'}),
new DynamicTimePickerModel({id: 'timepicker'}),
new DynamicTypeaheadModel({id: 'typeahead'}),
new DynamicScrollableDropdownModel({id: 'scrollableDropdown', authorityOptions: authorityOptions}),
new DynamicTagModel({id: 'tag'}),
new DynamicListCheckboxGroupModel({id: 'checkboxList', authorityOptions: authorityOptions, repeatable: true}),
new DynamicListRadioGroupModel({id: 'radioList', authorityOptions: authorityOptions, repeatable: false}),
new DynamicGroupModel({
id: 'relationGroup',
formConfiguration: [],
mandatoryField: '',
name: 'relationGroup',
relationFields: [],
scopeUUID: '',
submissionScope: ''
}),
new DynamicDsDatePickerModel({id: 'datepicker'}),
new DynamicLookupModel({id: 'lookup'}),
new DynamicLookupNameModel({id: 'lookupName'}),
new DynamicQualdropModel({id: 'combobox', readOnly: false})
];
const testModel = formModel[8];
let formGroup: FormGroup;
let fixture: ComponentFixture<DsDynamicFormControlComponent>;
let component: DsDynamicFormControlComponent;
let debugElement: DebugElement;
let testElement: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
FormsModule,
ReactiveFormsModule,
NgbModule.forRoot(),
DynamicFormsCoreModule.forRoot(),
SharedModule,
TranslateModule.forRoot(),
TextMaskModule,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(DsDynamicFormControlComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
});
}));
beforeEach(inject([DynamicFormService], (service: DynamicFormService) => {
formGroup = service.createFormGroup(formModel);
component.group = formGroup;
component.model = testModel;
component.ngOnChanges({
group: new SimpleChange(null, component.group, true),
model: new SimpleChange(null, component.model, true)
});
fixture.detectChanges();
testElement = debugElement.query(By.css(`input[id='${testModel.id}']`));
}));
it('should initialize correctly', () => {
expect(component.context).toBeNull();
expect(component.control instanceof FormControl).toBe(true);
expect(component.group instanceof FormGroup).toBe(true);
expect(component.model instanceof DynamicFormControlModel).toBe(true);
expect(component.hasErrorMessaging).toBe(false);
expect(component.asBootstrapFormGroup).toBe(true);
expect(component.onControlValueChanges).toBeDefined();
expect(component.onModelDisabledUpdates).toBeDefined();
expect(component.onModelValueUpdates).toBeDefined();
expect(component.blur).toBeDefined();
expect(component.change).toBeDefined();
expect(component.focus).toBeDefined();
expect(component.onValueChange).toBeDefined();
expect(component.onBlur).toBeDefined();
expect(component.onFocus).toBeDefined();
expect(component.isValid).toBe(true);
expect(component.isInvalid).toBe(false);
expect(component.showErrorMessages).toBe(false);
expect(component.type).toBe(NGBootstrapFormControlType.Input);
});
it('should have an input element', () => {
expect(testElement instanceof DebugElement).toBe(true);
});
it('should listen to native blur events', () => {
spyOn(component, 'onBlur');
testElement.triggerEventHandler('blur', null);
expect(component.onBlur).toHaveBeenCalled();
});
it('should listen to native focus events', () => {
spyOn(component, 'onFocus');
testElement.triggerEventHandler('focus', null);
expect(component.onFocus).toHaveBeenCalled();
});
it('should listen to native change event', () => {
spyOn(component, 'onValueChange');
testElement.triggerEventHandler('change', null);
expect(component.onValueChange).toHaveBeenCalled();
});
it('should update model value when control value changes', () => {
spyOn(component, 'onControlValueChanges');
component.control.setValue('test');
expect(component.onControlValueChanges).toHaveBeenCalled();
});
it('should update control value when model value changes', () => {
spyOn(component, 'onModelValueUpdates');
(testModel as DynamicInputModel).valueUpdates.next('test');
expect(component.onModelValueUpdates).toHaveBeenCalled();
});
it('should update control activation when model disabled property changes', () => {
spyOn(component, 'onModelDisabledUpdates');
testModel.disabledUpdates.next(true);
expect(component.onModelDisabledUpdates).toHaveBeenCalled();
});
it('should determine correct form control type', () => {
const testFn = DsDynamicFormControlComponent.getFormControlType;
expect(testFn(formModel[0])).toEqual(NGBootstrapFormControlType.Checkbox);
expect(testFn(formModel[1])).toEqual(NGBootstrapFormControlType.CheckboxGroup);
expect(testFn(formModel[2])).toBeNull();
expect(testFn(formModel[3])).toEqual(NGBootstrapFormControlType.DatePicker);
(formModel[3] as DynamicDatePickerModel).inline = true;
expect(testFn(formModel[3])).toEqual(NGBootstrapFormControlType.Calendar);
expect(testFn(formModel[4])).toBeNull();
expect(testFn(formModel[5])).toBeNull();
expect(testFn(formModel[6])).toEqual(NGBootstrapFormControlType.Array);
expect(testFn(formModel[7])).toEqual(NGBootstrapFormControlType.Group);
expect(testFn(formModel[8])).toEqual(NGBootstrapFormControlType.Input);
expect(testFn(formModel[9])).toEqual(NGBootstrapFormControlType.RadioGroup);
expect(testFn(formModel[10])).toBeNull();
expect(testFn(formModel[11])).toEqual(NGBootstrapFormControlType.Select);
expect(testFn(formModel[12])).toBeNull();
expect(testFn(formModel[13])).toBeNull();
expect(testFn(formModel[14])).toEqual(NGBootstrapFormControlType.TextArea);
expect(testFn(formModel[15])).toEqual(NGBootstrapFormControlType.TimePicker);
expect(testFn(formModel[16])).toEqual(NGBootstrapFormControlType.TypeAhead);
expect(testFn(formModel[17])).toEqual(NGBootstrapFormControlType.ScrollableDropdown);
expect(testFn(formModel[18])).toEqual(NGBootstrapFormControlType.Tag);
expect(testFn(formModel[19])).toEqual(NGBootstrapFormControlType.List);
expect(testFn(formModel[20])).toEqual(NGBootstrapFormControlType.List);
expect(testFn(formModel[21])).toEqual(NGBootstrapFormControlType.Relation);
expect(testFn(formModel[22])).toEqual(NGBootstrapFormControlType.Date);
expect(testFn(formModel[23])).toEqual(NGBootstrapFormControlType.Lookup);
expect(testFn(formModel[24])).toEqual(NGBootstrapFormControlType.LookupName);
expect(testFn(formModel[25])).toEqual(NGBootstrapFormControlType.Group);
});
});

View File

@@ -0,0 +1,178 @@
import {
ChangeDetectorRef,
Component,
ContentChildren,
EventEmitter,
Input,
OnChanges,
Output,
QueryList,
SimpleChanges
} from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
DynamicDatePickerModel,
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_GROUP } from './models/dynamic-group/dynamic-group.model';
import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/date-picker/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';
import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME } from './models/lookup/dynamic-lookup-name.model';
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'
Tag = 14, // 'TAG'
List = 15, // 'TYPELIST'
Relation = 16, // 'RELATION'
Date = 17, // 'DATE'
Lookup = 18, // LOOKUP
LookupName = 19, // LOOKUP_NAME
}
@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 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.Tag;
case DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP:
return NGBootstrapFormControlType.Relation;
case DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER:
return NGBootstrapFormControlType.Date;
case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP:
return NGBootstrapFormControlType.Lookup;
case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME:
return NGBootstrapFormControlType.LookupName;
default:
return null;
}
}
constructor(protected changeDetectorRef: ChangeDetectorRef, protected layoutService: DynamicFormLayoutService,
protected validationService: DynamicFormValidationService) {
super(changeDetectorRef, layoutService, validationService);
}
ngOnChanges(changes: SimpleChanges) {
if (changes) {
super.ngOnChanges(changes);
}
if (changes.model) {
this.type = DsDynamicFormControlComponent.getFormControlType(this.model);
}
}
onChangeLanguage(event) {
if (isNotEmpty((this.model as any).value)) {
this.onValueChange(event);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,255 @@
// Load the implementations that should be tested
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, inject, TestBed, } from '@angular/core/testing';
import { FormControl, FormGroup } from '@angular/forms';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { DsDatePickerComponent } from './date-picker.component';
import { DynamicDsDatePickerModel } from './date-picker.model';
import { FormBuilderService } from '../../../form-builder.service';
import { FormComponent } from '../../../../form.component';
import { FormService } from '../../../../form.service';
import { createTestComponent } from '../../../../../testing/utils';
export const DATE_TEST_GROUP = new FormGroup({
date: new FormControl()
});
export const DATE_TEST_MODEL_CONFIG = {
disabled: false,
errorMessages: {required: 'You must enter at least the year.'},
id: 'date',
label: 'Date',
name: 'date',
placeholder: 'Date',
readOnly: false,
required: true,
toggleIcon: 'fa fa-calendar'
};
describe('DsDatePickerComponent test suite', () => {
let testComp: TestComponent;
let dateComp: DsDatePickerComponent;
let testFixture: ComponentFixture<TestComponent>;
let dateFixture: ComponentFixture<DsDatePickerComponent>;
let html;
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
NgbModule.forRoot()
],
declarations: [
DsDatePickerComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
DsDatePickerComponent,
DynamicFormValidationService,
FormBuilderService,
FormComponent,
FormService
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-date-picker
[bindId]='bindId'
[group]='group'
[model]='model'
[showErrorMessages]='showErrorMessages'
(blur)='onBlur($event)'
(change)='onValueChange($event)'
(focus)='onFocus($event)'></ds-date-picker>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create DsDatePickerComponent', inject([DsDatePickerComponent], (app: DsDatePickerComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
describe('when init model value is empty', () => {
beforeEach(() => {
dateFixture = TestBed.createComponent(DsDatePickerComponent);
dateComp = dateFixture.componentInstance; // FormComponent test instance
dateComp.group = DATE_TEST_GROUP;
dateComp.model = new DynamicDsDatePickerModel(DATE_TEST_MODEL_CONFIG);
dateFixture.detectChanges();
});
it('should init component properly', () => {
expect(dateComp.initialYear).toBeDefined();
expect(dateComp.initialMonth).toBeDefined();
expect(dateComp.initialDay).toBeDefined();
expect(dateComp.maxYear).toBeDefined();
expect(dateComp.disabledMonth).toBeTruthy();
expect(dateComp.disabledDay).toBeTruthy();
});
it('should set year and enable month field when year field is entered', () => {
const event = {
field: 'year',
value: '1983'
};
dateComp.onChange(event);
expect(dateComp.year).toEqual('1983');
expect(dateComp.disabledMonth).toBeFalsy();
expect(dateComp.disabledDay).toBeTruthy();
});
it('should set month and enable day field when month field is entered', () => {
const event = {
field: 'month',
value: '11'
};
dateComp.year = '1983';
dateComp.disabledMonth = false;
dateFixture.detectChanges();
dateComp.onChange(event);
expect(dateComp.year).toEqual('1983');
expect(dateComp.month).toEqual('11');
expect(dateComp.disabledMonth).toBeFalsy();
expect(dateComp.disabledDay).toBeFalsy();
});
it('should set day when day field is entered', () => {
const event = {
field: 'day',
value: '18'
};
dateComp.year = '1983';
dateComp.month = '11';
dateComp.disabledMonth = false;
dateComp.disabledDay = false;
dateFixture.detectChanges();
dateComp.onChange(event);
expect(dateComp.year).toEqual('1983');
expect(dateComp.month).toEqual('11');
expect(dateComp.day).toEqual('18');
expect(dateComp.disabledMonth).toBeFalsy();
expect(dateComp.disabledDay).toBeFalsy();
});
it('should emit blur Event onBlur', () => {
spyOn(dateComp.blur, 'emit');
dateComp.onBlur(new Event('blur'));
expect(dateComp.blur.emit).toHaveBeenCalled();
});
it('should emit focus Event onFocus', () => {
spyOn(dateComp.focus, 'emit');
dateComp.onFocus(new Event('focus'));
expect(dateComp.focus.emit).toHaveBeenCalled();
});
});
describe('when init model value is not empty', () => {
beforeEach(() => {
dateFixture = TestBed.createComponent(DsDatePickerComponent);
dateComp = dateFixture.componentInstance; // FormComponent test instance
dateComp.group = DATE_TEST_GROUP;
dateComp.model = new DynamicDsDatePickerModel(DATE_TEST_MODEL_CONFIG);
dateComp.model.value = '1983-11-18';
dateFixture.detectChanges();
});
it('should init component properly', () => {
expect(dateComp.initialYear).toBeDefined();
expect(dateComp.initialMonth).toBeDefined();
expect(dateComp.initialDay).toBeDefined();
expect(dateComp.maxYear).toBeDefined();
expect(dateComp.year).toBe(1983);
expect(dateComp.month).toBe(11);
expect(dateComp.day).toBe(18);
expect(dateComp.disabledMonth).toBeFalsy();
expect(dateComp.disabledDay).toBeFalsy();
});
it('should disable month and day fields when year field is canceled', () => {
const event = {
field: 'year',
value: null
};
dateComp.onChange(event);
expect(dateComp.year).not.toBeDefined();
expect(dateComp.month).not.toBeDefined();
expect(dateComp.day).not.toBeDefined();
expect(dateComp.disabledMonth).toBeTruthy();
expect(dateComp.disabledDay).toBeTruthy();
});
it('should disable day field when month field is canceled', () => {
const event = {
field: 'month',
value: null
};
dateComp.onChange(event);
expect(dateComp.year).toBe(1983);
expect(dateComp.month).not.toBeDefined();
expect(dateComp.day).not.toBeDefined();
expect(dateComp.disabledMonth).toBeFalsy();
expect(dateComp.disabledDay).toBeTruthy();
});
it('should not disable day field when day field is canceled', () => {
const event = {
field: 'day',
value: null
};
dateComp.onChange(event);
expect(dateComp.year).toBe(1983);
expect(dateComp.month).toBe(11);
expect(dateComp.day).not.toBeDefined();
expect(dateComp.disabledMonth).toBeFalsy();
expect(dateComp.disabledDay).toBeFalsy();
});
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group = DATE_TEST_GROUP;
model = new DynamicDsDatePickerModel(DATE_TEST_MODEL_CONFIG);
showErrorMessages = false;
}

View File

@@ -0,0 +1,172 @@
import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { DynamicDsDatePickerModel } from './date-picker.model';
import { hasNoValue, hasValue, isNotEmpty } from '../../../../../empty.util';
export const DS_DATE_PICKER_SEPARATOR = '-';
@Component({
selector: 'ds-date-picker',
styleUrls: ['./date-picker.component.scss'],
templateUrl: './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() blur = new EventEmitter<any>();
@Output() change = new EventEmitter<any>();
@Output() focus = new EventEmitter<any>();
initialYear: number;
initialMonth: number;
initialDay: number;
year: any;
month: any;
day: any;
minYear: 0;
maxYear: number;
minMonth = 1;
maxMonth = 12;
minDay = 1;
maxDay = 31;
yearPlaceholder = 'year';
monthPlaceholder = 'month';
dayPlaceholder = 'day';
disabledMonth = true;
disabledDay = true;
ngOnInit() {
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;
}
onBlur(event) {
this.blur.emit();
}
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;
this.disabledMonth = true;
this.disabledDay = true;
}
break;
}
case 'month': {
if (event.value !== null) {
this.month = event.value;
} else {
this.month = undefined;
this.day = undefined;
this.disabledDay = true;
}
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 (hasValue(this.year) && event.field === 'year') {
this.disabledMonth = false;
} else if (hasValue(this.month) && event.field === 'month') {
this.disabledDay = false;
}
// update value
let value = null;
if (hasValue(this.year)) {
let yyyy = this.year.toString();
while (yyyy.length < 4) {
yyyy = '0' + yyyy;
}
value = yyyy;
}
if (hasValue(this.month)) {
const mm = this.month.toString().length === 1
? '0' + this.month.toString()
: this.month.toString();
value += DS_DATE_PICKER_SEPARATOR + mm;
}
if (hasValue(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(value);
}
onFocus(event) {
this.focus.emit(event);
}
getLastDay(date: Date) {
// Last Day of the same month (+1 month, -1 day)
date.setMonth(date.getMonth() + 1, 0);
return date.getDate();
}
}

View File

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

View File

@@ -0,0 +1,52 @@
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';
import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-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;
isCustomGroup = true;
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 new FormFieldMetadataValueObject(firstValue + this.separator + secondValue);
} else {
return null;
}
}
set value(value: string | FormFieldMetadataValueObject) {
let values;
if (typeof value === 'string') {
values = value ? value.split(this.separator) : [null, null];
} else {
values = value ? value.value.split(this.separator) : [null, null];
}
if (values.length > 1) {
(this.get(0) as DsDynamicInputModel).valueUpdates.next(values[0]);
(this.get(1) as DsDynamicInputModel).valueUpdates.next(values[1]);
}
}
}

View File

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

View File

@@ -0,0 +1,65 @@
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 QUALDROP_GROUP_SUFFIX = '_QUALDROP_GROUP';
export const QUALDROP_METADATA_SUFFIX = '_QUALDROP_METADATA';
export const QUALDROP_VALUE_SUFFIX = '_QUALDROP_VALUE';
export interface DsDynamicQualdropModelConfig extends DynamicFormGroupModelConfig {
languageCodes?: LanguageCode[];
language?: string;
readOnly: boolean;
}
export class DynamicQualdropModel extends DynamicFormGroupModel {
@serializable() private _language: string;
@serializable() private _languageCodes: LanguageCode[];
@serializable() languageUpdates: Subject<string>;
@serializable() hasLanguages = false;
@serializable() readOnly: boolean;
isCustomGroup = true;
constructor(config: DsDynamicQualdropModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.readOnly = config.readOnly;
this.language = config.language;
this.languageCodes = config.languageCodes;
this.languageUpdates = new Subject<string>();
this.languageUpdates.subscribe((lang: string) => {
this.language = lang;
});
}
get value() {
return (this.get(1) as DsDynamicInputModel).value;
}
get qualdropId(): string {
return (this.get(0) as DsDynamicInputModel).value.toString();
}
get language(): string {
return this._language;
}
set language(language: string) {
this._language = language;
}
get languageCodes(): LanguageCode[] {
return this._languageCodes;
}
set languageCodes(languageCodes: LanguageCode[]) {
this._languageCodes = languageCodes;
if (!this.language || this.language === null || this.language === '') {
this.language = this.languageCodes ? this.languageCodes[0].code : null;
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,72 @@
<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': !showErrorMessages, 'border border-danger': showErrorMessages}">
<div *ngIf="!(formCollapsed | async)" class="pl-2 row" @shrinkInOut>
<ds-form #formRef="formComponent"
class="col-sm-12 col-md-8 col-lg-9 col-xl-10 pl-0"
[formId]="formId"
[formModel]="formModel"
[displaySubmit]="false"
[emitChange]="false"
(dfBlur)="onBlur($event)"
(dfFocus)="onFocus($event)"></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"
class="border-0 form-control-plaintext tag-input mt-1 mb-1 pl-2 text-muted"
readonly
tabindex="-1"
value="{{'form.no-value' | translate}}">
</div>
<ds-chips
*ngIf="chips.hasItems()"
[chips]="chips"
[editable]="true"
(selected)="onChipSelected($event)"></ds-chips>
</div>
</div>

View File

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

View File

@@ -0,0 +1,315 @@
// Load the implementations that should be tested
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, inject, TestBed, } from '@angular/core/testing';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { DsDynamicGroupComponent } from './dynamic-group.components';
import { DynamicGroupModel, DynamicGroupModelConfig } from './dynamic-group.model';
import { FormRowModel, SubmissionFormsModel } from '../../../../../../core/shared/config/config-submission-forms.model';
import { FormFieldModel } from '../../../models/form-field.model';
import { FormBuilderService } from '../../../form-builder.service';
import { FormService } from '../../../../form.service';
import { GLOBAL_CONFIG } from '../../../../../../../config';
import { FormComponent } from '../../../../form.component';
import { AppState } from '../../../../../../app.reducer';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Chips } from '../../../../../chips/models/chips.model';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { DsDynamicInputModel } from '../ds-dynamic-input.model';
import { createTestComponent } from '../../../../../testing/utils';
export const FORM_GROUP_TEST_MODEL_CONFIG = {
disabled: false,
errorMessages: {required: 'You must specify at least one author.'},
formConfiguration: [{
fields: [{
hints: 'Enter the name of the author.',
input: {type: 'onebox'},
label: 'Author',
languageCodes: [],
mandatory: 'true',
mandatoryMessage: 'Required field!',
repeatable: false,
selectableMetadata: [{
authority: 'RPAuthority',
closed: false,
metadata: 'dc.contributor.author'
}],
} as FormFieldModel]
} as FormRowModel, {
fields: [{
hints: 'Enter the affiliation of the author.',
input: {type: 'onebox'},
label: 'Affiliation',
languageCodes: [],
mandatory: 'false',
repeatable: false,
selectableMetadata: [{
authority: 'OUAuthority',
closed: false,
metadata: 'local.contributor.affiliation'
}]
} as FormFieldModel]
} as FormRowModel],
id: 'dc_contributor_author',
label: 'Authors',
mandatoryField: 'dc.contributor.author',
name: 'dc.contributor.author',
placeholder: 'Authors',
readOnly: false,
relationFields: ['local.contributor.affiliation'],
required: true,
scopeUUID: '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f',
submissionScope: undefined,
validators: {required: null}
} as DynamicGroupModelConfig;
export const FORM_GROUP_TEST_GROUP = new FormGroup({
dc_contributor_author: new FormControl(),
});
describe('DsDynamicGroupComponent test suite', () => {
const config = {
form: {
validatorMap: {
required: 'required',
regex: 'pattern'
}
}
} as any;
let testComp: TestComponent;
let groupComp: DsDynamicGroupComponent;
let testFixture: ComponentFixture<TestComponent>;
let groupFixture: ComponentFixture<DsDynamicGroupComponent>;
let modelValue: any;
let html;
let control1: FormControl;
let model1: DsDynamicInputModel;
let control2: FormControl;
let model2: DsDynamicInputModel;
const store: Store<AppState> = jasmine.createSpyObj('store', {
dispatch: {},
select: Observable.of(true)
});
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
FormsModule,
ReactiveFormsModule,
NgbModule.forRoot(),
TranslateModule.forRoot()
],
declarations: [
FormComponent,
DsDynamicGroupComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
DsDynamicGroupComponent,
DynamicFormValidationService,
FormBuilderService,
FormComponent,
FormService,
{provide: GLOBAL_CONFIG, useValue: config},
{provide: Store, useValue: store},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-dynamic-group [model]="model"
[formId]="formId"
[group]="group"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-group>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create DsDynamicGroupComponent', inject([DsDynamicGroupComponent], (app: DsDynamicGroupComponent) => {
expect(app).toBeDefined();
}));
});
describe('when init model value is empty', () => {
beforeEach(inject([FormBuilderService], (service: FormBuilderService) => {
groupFixture = TestBed.createComponent(DsDynamicGroupComponent);
groupComp = groupFixture.componentInstance; // FormComponent test instance
groupComp.formId = 'testForm';
groupComp.group = FORM_GROUP_TEST_GROUP;
groupComp.model = new DynamicGroupModel(FORM_GROUP_TEST_MODEL_CONFIG);
groupComp.showErrorMessages = false;
groupFixture.detectChanges();
control1 = service.getFormControlById('dc_contributor_author', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl;
model1 = service.findById('dc_contributor_author', groupComp.formModel) as DsDynamicInputModel;
control2 = service.getFormControlById('local_contributor_affiliation', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl;
model2 = service.findById('local_contributor_affiliation', groupComp.formModel) as DsDynamicInputModel;
// spyOn(store, 'dispatch');
}));
afterEach(() => {
groupFixture.destroy();
groupComp = null;
});
it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => {
const formConfig = {rows: groupComp.model.formConfiguration} as SubmissionFormsModel;
const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
const chips = new Chips([], 'value', 'dc.contributor.author');
expect(groupComp.formCollapsed).toEqual(Observable.of(false));
expect(groupComp.formModel.length).toEqual(formModel.length);
expect(groupComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
}));
it('should save a new chips item', () => {
control1.setValue('test author');
(model1 as any).value = new FormFieldMetadataValueObject('test author');
control2.setValue('test affiliation');
(model2 as any).value = new FormFieldMetadataValueObject('test affiliation');
modelValue = [{
'dc.contributor.author': new FormFieldMetadataValueObject('test author'),
'local.contributor.affiliation': new FormFieldMetadataValueObject('test affiliation')
}];
groupFixture.detectChanges();
const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button');
const btnEl = buttons[0];
btnEl.click();
expect(groupComp.chips.getChipsItems()).toEqual(modelValue);
expect(groupComp.formCollapsed).toEqual(Observable.of(true));
});
it('should clear form inputs', () => {
control1.setValue('test author');
(model1 as any).value = new FormFieldMetadataValueObject('test author');
control2.setValue('test affiliation');
(model2 as any).value = new FormFieldMetadataValueObject('test affiliation');
groupFixture.detectChanges();
const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button');
const btnEl = buttons[2];
btnEl.click();
expect(control1.value).toBeNull();
expect(control2.value).toBeNull();
expect(groupComp.formCollapsed).toEqual(Observable.of(false));
});
});
describe('when init model value is not empty', () => {
beforeEach(() => {
groupFixture = TestBed.createComponent(DsDynamicGroupComponent);
groupComp = groupFixture.componentInstance; // FormComponent test instance
groupComp.formId = 'testForm';
groupComp.group = FORM_GROUP_TEST_GROUP;
groupComp.model = new DynamicGroupModel(FORM_GROUP_TEST_MODEL_CONFIG);
modelValue = [{
'dc.contributor.author': new FormFieldMetadataValueObject('test author'),
'local.contributor.affiliation': new FormFieldMetadataValueObject('test affiliation')
}];
groupComp.model.value = modelValue;
groupComp.showErrorMessages = false;
groupFixture.detectChanges();
});
afterEach(() => {
groupFixture.destroy();
groupComp = null;
});
it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => {
const formConfig = {rows: groupComp.model.formConfiguration} as SubmissionFormsModel;
const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
const chips = new Chips(modelValue, 'value', 'dc.contributor.author');
expect(groupComp.formCollapsed).toEqual(Observable.of(true));
expect(groupComp.formModel.length).toEqual(formModel.length);
expect(groupComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
}));
it('should modify existing chips item', inject([FormBuilderService], (service: FormBuilderService) => {
groupComp.onChipSelected(0);
groupFixture.detectChanges();
control1 = service.getFormControlById('dc_contributor_author', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl;
model1 = service.findById('dc_contributor_author', groupComp.formModel) as DsDynamicInputModel;
control1.setValue('test author modify');
(model1 as any).value = new FormFieldMetadataValueObject('test author modify');
modelValue = [{
'dc.contributor.author': new FormFieldMetadataValueObject('test author modify'),
'local.contributor.affiliation': new FormFieldMetadataValueObject('test affiliation')
}];
groupFixture.detectChanges();
const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button');
const btnEl = buttons[0];
btnEl.click();
groupFixture.detectChanges();
expect(groupComp.chips.getChipsItems()).toEqual(modelValue);
expect(groupComp.formCollapsed).toEqual(Observable.of(true));
}));
it('should delete existing chips item', () => {
groupComp.onChipSelected(0);
groupFixture.detectChanges();
const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button');
const btnEl = buttons[1];
btnEl.click();
expect(groupComp.chips.getChipsItems()).toEqual([]);
expect(groupComp.formCollapsed).toEqual(Observable.of(false));
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group = FORM_GROUP_TEST_GROUP;
groupModelConfig = FORM_GROUP_TEST_MODEL_CONFIG;
model = new DynamicGroupModel(this.groupModelConfig);
showErrorMessages = false;
}

View File

@@ -0,0 +1,240 @@
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 { 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';
import { hasOnlyEmptyProperties } from '../../../../../object.util';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
@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;
@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 chips: Chips;
public formCollapsed = Observable.of(false);
public formModel: DynamicFormControlModel[];
public editMode = 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() {
const config = {rows: this.model.formConfiguration} as SubmissionFormsModel;
if (!this.model.isEmpty()) {
this.formCollapsed = Observable.of(true);
}
this.model.valueUpdates.subscribe((value: any[]) => {
if ((isNotEmpty(value) && !(value.length === 1 && hasOnlyEmptyProperties(value[0])))) {
this.collapseForm();
} else {
this.expandForm();
}
// this.formCollapsed = (isNotEmpty(value) && !(value.length === 1 && hasOnlyEmptyProperties(value[0]))) ? Observable.of(true) : Observable.of(false);
});
this.formId = this.formService.getUniqueId(this.model.id);
this.formModel = this.formBuilderService.modelFromConfiguration(
config,
this.model.scopeUUID,
{},
this.model.submissionScope,
this.model.readOnly);
const initChipsValue = this.model.isEmpty() ? [] : this.model.value;
this.chips = new Chips(
initChipsValue,
'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 ((isNotEmpty(items) && !this.model.isEmpty()) || (isEmpty(items) && !this.model.isEmpty())) {
if (!(isEmpty(items) && this.model.isEmpty())) {
this.model.valueUpdates.next(items);
this.change.emit();
}
}
}),
)
}
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;
}
onBlur(event) {
this.blur.emit();
}
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
|| this.selectedChipItem.item[model.name].value === PLACEHOLDER_PARENT_METADATA)
? null
: this.selectedChipItem.item[model.name];
// if (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValueModel) {
// model.valueUpdates.next(value.display);
// } else {
// model.valueUpdates.next(value);
// }
model.valueUpdates.next(value);
});
});
this.editMode = true;
}
onFocus(event) {
this.focus.emit(event);
}
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();
if (!this.model.isEmpty()) {
this.formCollapsed = Observable.of(true);
}
}
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() {
if (this.formRef) {
this.formService.resetForm(this.formRef.formGroup, this.formModel, this.formId);
}
}
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -0,0 +1,73 @@
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';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { isEmpty, isNull } from '../../../../../empty.util';
export const DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP = 'RELATION';
export const PLACEHOLDER_PARENT_METADATA = '#PLACEHOLDER_PARENT_METADATA_VALUE#';
/**
* Dynamic Group Model configuration interface
*/
export interface DynamicGroupModelConfig extends DsDynamicInputModelConfig {
formConfiguration: FormRowModel[],
mandatoryField: string,
relationFields: string[],
scopeUUID: string,
submissionScope: string;
}
/**
* Dynamic Group Model class
*/
export class DynamicGroupModel extends DsDynamicInputModel {
@serializable() formConfiguration: FormRowModel[];
@serializable() mandatoryField: string;
@serializable() relationFields: string[];
@serializable() scopeUUID: string;
@serializable() submissionScope: string;
@serializable() _value: any[];
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP;
constructor(config: DynamicGroupModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.formConfiguration = config.formConfiguration;
this.mandatoryField = config.mandatoryField;
this.relationFields = config.relationFields;
this.scopeUUID = config.scopeUUID;
this.submissionScope = config.submissionScope;
const value = config.value || [];
this.valueUpdates.next(value);
}
get value() {
return this._value
}
set value(value) {
this._value = (isEmpty(value)) ? null : value;
}
isEmpty() {
const value = this.getGroupValue();
return (value.length === 1 && isNull(value[0][this.mandatoryField]));
}
getGroupValue(): any[] {
if (isEmpty(this._value)) {
// 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({});
emptyItem[this.mandatoryField] = null;
this.relationFields
.forEach((field) => {
emptyItem[field] = null;
});
return [emptyItem];
}
return this._value
}
}

View File

@@ -0,0 +1,60 @@
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[];
isListGroup = true;
valueUpdates: Subject<any>;
constructor(config: DynamicListCheckboxGroupModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.authorityOptions = config.authorityOptions;
this.groupLength = config.groupLength || 5;
this._value = [];
this.repeatable = config.repeatable;
this.valueUpdates = new Subject<any>();
this.valueUpdates.subscribe((value: AuthorityValueModel | AuthorityValueModel[]) => this.value = value);
this.valueUpdates.next(config.value);
}
get hasAuthority(): boolean {
return this.authorityOptions && hasValue(this.authorityOptions.name);
}
get value() {
return this._value;
}
set value(value: AuthorityValueModel | AuthorityValueModel[]) {
if (value) {
if (Array.isArray(value)) {
this._value = value;
} else {
// _value is non extendible so assign it a new array
const newValue = (this.value as AuthorityValueModel[]).concat([value]);
this._value = newValue
}
}
}
}

View File

@@ -0,0 +1,36 @@
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;
isListGroup = true;
constructor(config: DynamicListModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.authorityOptions = config.authorityOptions;
this.groupLength = config.groupLength || 5;
this.repeatable = config.repeatable;
this.valueUpdates.next(config.value);
}
get hasAuthority(): boolean {
return this.authorityOptions && hasValue(this.authorityOptions.name);
}
}

View File

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

View File

@@ -0,0 +1,299 @@
// Load the implementations that should be tested
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { async, ComponentFixture, inject, TestBed, } from '@angular/core/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { DsDynamicListComponent } from './dynamic-list.component';
import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
import { FormBuilderService } from '../../../form-builder.service';
import { DynamicFormControlLayout, DynamicFormsCoreModule, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub';
import { DynamicListRadioGroupModel } from './dynamic-list-radio-group.model';
import { By } from '@angular/platform-browser';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { createTestComponent } from '../../../../../testing/utils';
export const LAYOUT_TEST = {
element: {
group: ''
}
} as DynamicFormControlLayout;
export const LIST_TEST_GROUP = new FormGroup({
listCheckbox: new FormGroup({}),
listRadio: new FormGroup({})
});
export const LIST_CHECKBOX_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'listCheckbox',
name: 'type_programme',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
id: 'listCheckbox',
label: 'Programme',
name: 'listCheckbox',
placeholder: 'Programme',
readOnly: false,
required: false,
repeatable: true
};
export const LIST_RADIO_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'listRadio',
name: 'type_programme',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
id: 'listRadio',
label: 'Programme',
name: 'listRadio',
placeholder: 'Programme',
readOnly: false,
required: false,
repeatable: false
};
describe('DsDynamicListComponent test suite', () => {
let testComp: TestComponent;
let listComp: DsDynamicListComponent;
let testFixture: ComponentFixture<TestComponent>;
let listFixture: ComponentFixture<DsDynamicListComponent>;
let html;
let modelValue;
const authorityServiceStub = new AuthorityServiceStub();
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DynamicFormsCoreModule,
DynamicFormsNGBootstrapUIModule,
FormsModule,
ReactiveFormsModule,
NgbModule.forRoot()
],
declarations: [
DsDynamicListComponent,
TestComponent,
], // declare the test component
providers: [
AuthorityService,
ChangeDetectorRef,
DsDynamicListComponent,
DynamicFormValidationService,
FormBuilderService,
{provide: AuthorityService, useValue: authorityServiceStub},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-dynamic-list
[bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-list>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create DsDynamicListComponent', inject([DsDynamicListComponent], (app: DsDynamicListComponent) => {
expect(app).toBeDefined();
}));
});
describe('when model is a DynamicListCheckboxGroupModel', () => {
describe('and init model value is empty', () => {
beforeEach(() => {
listFixture = TestBed.createComponent(DsDynamicListComponent);
listComp = listFixture.componentInstance; // FormComponent test instance
listComp.group = LIST_TEST_GROUP;
listComp.model = new DynamicListCheckboxGroupModel(LIST_CHECKBOX_TEST_MODEL_CONFIG, LAYOUT_TEST);
listFixture.detectChanges();
});
afterEach(() => {
listFixture.destroy();
listComp = null;
});
it('should init component properly', () => {
const results$ = authorityServiceStub.getEntriesByName({} as any);
results$.subscribe((results) => {
expect((listComp as any).optionsList).toEqual(results.payload);
expect(listComp.items.length).toBe(1);
expect(listComp.items[0].length).toBe(2);
})
});
it('should set model value properly when a checkbox option is selected', () => {
const de = listFixture.debugElement.queryAll(By.css('div.custom-checkbox'));
const items = de[0].queryAll(By.css('input.custom-control-input'));
const item = items[0];
modelValue = [Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1})];
item.nativeElement.click();
expect(listComp.model.value).toEqual(modelValue)
});
it('should emit blur Event onBlur', () => {
spyOn(listComp.blur, 'emit');
listComp.onBlur(new Event('blur'));
expect(listComp.blur.emit).toHaveBeenCalled();
});
it('should emit focus Event onFocus', () => {
spyOn(listComp.focus, 'emit');
listComp.onFocus(new Event('focus'));
expect(listComp.focus.emit).toHaveBeenCalled();
});
});
describe('and init model value is not empty', () => {
beforeEach(() => {
listFixture = TestBed.createComponent(DsDynamicListComponent);
listComp = listFixture.componentInstance; // FormComponent test instance
listComp.group = LIST_TEST_GROUP;
listComp.model = new DynamicListCheckboxGroupModel(LIST_CHECKBOX_TEST_MODEL_CONFIG, LAYOUT_TEST);
modelValue = [Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1})];
listComp.model.value = modelValue;
listFixture.detectChanges();
});
afterEach(() => {
listFixture.destroy();
listComp = null;
});
it('should init component properly', () => {
const results$ = authorityServiceStub.getEntriesByName({} as any);
results$.subscribe((results) => {
expect((listComp as any).optionsList).toEqual(results.payload);
expect(listComp.model.value).toEqual(modelValue);
expect((listComp.model as DynamicListCheckboxGroupModel).group[0].value).toBeTruthy();
})
});
it('should set model value properly when a checkbox option is deselected', () => {
const de = listFixture.debugElement.queryAll(By.css('div.custom-checkbox'));
const items = de[0].queryAll(By.css('input.custom-control-input'));
const item = items[0];
modelValue = [];
item.nativeElement.click();
expect(listComp.model.value).toEqual(modelValue)
});
});
});
describe('when model is a DynamicListRadioGroupModel', () => {
describe('and init model value is empty', () => {
beforeEach(() => {
listFixture = TestBed.createComponent(DsDynamicListComponent);
listComp = listFixture.componentInstance; // FormComponent test instance
listComp.group = LIST_TEST_GROUP;
listComp.model = new DynamicListRadioGroupModel(LIST_RADIO_TEST_MODEL_CONFIG, LAYOUT_TEST);
listFixture.detectChanges();
});
afterEach(() => {
listFixture.destroy();
listComp = null;
});
it('should init component properly', () => {
const results$ = authorityServiceStub.getEntriesByName({} as any);
results$.subscribe((results) => {
expect((listComp as any).optionsList).toEqual(results.payload);
expect(listComp.items.length).toBe(1);
expect(listComp.items[0].length).toBe(2);
})
});
it('should set model value when a radio option is selected', () => {
const de = listFixture.debugElement.queryAll(By.css('div.custom-radio'));
const items = de[0].queryAll(By.css('input.custom-control-input'));
const item = items[0];
modelValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1});
item.nativeElement.click();
expect(listComp.model.value).toEqual(modelValue)
});
});
describe('and init model value is not empty', () => {
beforeEach(() => {
listFixture = TestBed.createComponent(DsDynamicListComponent);
listComp = listFixture.componentInstance; // FormComponent test instance
listComp.group = LIST_TEST_GROUP;
listComp.model = new DynamicListRadioGroupModel(LIST_RADIO_TEST_MODEL_CONFIG, LAYOUT_TEST);
modelValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1});
listComp.model.value = modelValue;
listFixture.detectChanges();
});
afterEach(() => {
listFixture.destroy();
listComp = null;
});
it('should init component properly', () => {
const results$ = authorityServiceStub.getEntriesByName({} as any);
results$.subscribe((results) => {
expect((listComp as any).optionsList).toEqual(results.payload);
expect(listComp.model.value).toEqual(modelValue);
expect((listComp.model as DynamicListRadioGroupModel).options[0].value).toBeTruthy();
})
});
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group: FormGroup = LIST_TEST_GROUP;
model = new DynamicListCheckboxGroupModel(LIST_CHECKBOX_TEST_MODEL_CONFIG, LAYOUT_TEST);
showErrorMessages = false;
}

View File

@@ -0,0 +1,135 @@
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 { 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';
import { IntegrationData } from '../../../../../../core/integration/integration-data';
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'
})
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 optionsList: 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();
}
}
onBlur(event: Event) {
this.blur.emit(event);
}
onFocus(event: Event) {
this.focus.emit(event);
}
onChange(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.optionsList[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.optionsList[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: IntegrationData) => {
let groupCounter = 0;
let itemsPerGroup = 0;
let tempList: ListItem[] = [];
this.optionsList = authorities.payload as AuthorityValueModel[];
// Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength'
(authorities.payload as AuthorityValueModel[]).forEach((option, key) => {
const value = option.id || option.value;
const checked: boolean = isNotEmpty(findKey(
this.model.value,
{value: option.value}));
const item: ListItem = {
id: value,
label: option.display,
value: checked,
index: key
};
if (this.model.repeatable) {
this.formBuilderService.addFormGroupControl(listGroup, (this.model as DynamicListCheckboxGroupModel), new DynamicCheckboxModel(item));
} else {
(this.model as DynamicListRadioGroupModel).options.push({label: item.label, value: option});
}
tempList.push(item);
itemsPerGroup++;
this.items[groupCounter] = tempList;
if (itemsPerGroup === this.model.groupLength) {
groupCounter++;
itemsPerGroup = 0;
tempList = [];
}
});
this.cdr.detectChanges();
});
}
}
protected hasAuthorityOptions() {
return (hasValue(this.model.authorityOptions.scope)
&& hasValue(this.model.authorityOptions.name)
&& hasValue(this.model.authorityOptions.metadata));
}
}

View File

@@ -0,0 +1,26 @@
import { DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { DynamicLookupModel, DynamicLookupModelConfig } from './dynamic-lookup.model';
export const DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME = 'LOOKUP_NAME';
export interface DynamicLookupNameModelConfig extends DynamicLookupModelConfig {
separator?: string;
firstPlaceholder?: string;
secondPlaceholder?: string;
}
export class DynamicLookupNameModel extends DynamicLookupModel {
@serializable() separator: string;
@serializable() secondPlaceholder: string;
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME;
constructor(config: DynamicLookupNameModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.separator = config.separator || ',';
this.placeholder = config.firstPlaceholder || 'form.last-name';
this.secondPlaceholder = config.secondPlaceholder || 'form.first-name';
}
}

View File

@@ -0,0 +1,127 @@
<div ngbDropdown #sdRef="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); $event.stopPropagation(); sdRef.close();"
(focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();"
(click)="$event.stopPropagation(); $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 ngbDropdownAnchor
*ngIf="!isInputDisabled()" class="btn btn-secondary"
type="button"
[disabled]="model.readOnly || isSearchDisabled()"
(click)="sdRef.open(); search(); $event.stopPropagation();">{{'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); $event.stopPropagation(); sdRef.close();"
(focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();"
(click)="$event.stopPropagation(); $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]="model.name + '_2'"
[type]="model.inputType"
[(ngModel)]="secondInputValue"
[disabled]="firstInputValue.length === 0 || isInputDisabled()"
[placeholder]="model.secondPlaceholder | translate"
[readonly]="model.readOnly"
(change)="$event.preventDefault()"
(blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();"
(focus)="onFocusEvent($event); $event.stopPropagation(); 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 ngbDropdownAnchor
*ngIf="!isInputDisabled()" class="btn btn-secondary"
type="button"
[disabled]="isSearchDisabled()"
(click)="sdRef.open(); search(); $event.stopPropagation();">{{'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)="$event.stopPropagation(); clearFields(); sdRef.close();">{{'form.no-results' | translate}}
</button>
<button class="dropdown-item collection-item"
*ngFor="let listEntry of optionsList"
(click)="$event.stopPropagation(); onSelect(listEntry); sdRef.close();"
title="{{ listEntry.display }}">
{{listEntry.value}}
</button>
<div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>{{'form.loading' | translate}}</p></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,65 @@
@import "../../../../../../../styles/variables";
.dropdown-toggle::after {
display:none
}
/* 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: $spacer * 2;
}
.right-addon input {
padding-right: $spacer * 2;
}
:host /deep/ .dropdown-menu {
width: 100% !important;
max-height: $dropdown-menu-max-height;
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: -($spacer * 0.625);
//}
div {
overflow: visible;
//padding-right: 0 !important;
}
//
//button {
// margin-right: $spacer * 0.625;
//}

View File

@@ -0,0 +1,343 @@
// Load the implementations that should be tested
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
import { DynamicFormsCoreModule, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub';
import { DsDynamicLookupComponent } from './dynamic-lookup.component';
import { DynamicLookupModel } from './dynamic-lookup.model';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { TranslateModule } from '@ngx-translate/core';
import { FormBuilderService } from '../../../form-builder.service';
import { FormService } from '../../../../form.service';
import { FormComponent } from '../../../../form.component';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { By } from '@angular/platform-browser';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { DynamicLookupNameModel } from './dynamic-lookup-name.model';
import { createTestComponent } from '../../../../../testing/utils';
export const LOOKUP_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'lookup',
name: 'RPAuthority',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
errorMessages: {required: 'Required field.'},
id: 'lookup',
label: 'Author',
maxOptions: 10,
name: 'lookup',
placeholder: 'Author',
readOnly: false,
required: true,
repeatable: true,
separator: ',',
validators: {required: null},
value: undefined
};
export const LOOKUP_NAME_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'lookup-name',
name: 'RPAuthority',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
errorMessages: {required: 'Required field.'},
id: 'lookupName',
label: 'Author',
maxOptions: 10,
name: 'lookupName',
placeholder: 'Author',
readOnly: false,
required: true,
repeatable: true,
separator: ',',
validators: {required: null},
value: undefined
};
export const LOOKUP_TEST_GROUP = new FormGroup({
lookup: new FormControl(),
lookupName: new FormControl()
});
describe('Dynamic Lookup component', () => {
let testComp: TestComponent;
let lookupComp: DsDynamicLookupComponent;
let testFixture: ComponentFixture<TestComponent>;
let lookupFixture: ComponentFixture<DsDynamicLookupComponent>;
let html;
const authorityServiceStub = new AuthorityServiceStub();
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DynamicFormsCoreModule,
DynamicFormsNGBootstrapUIModule,
FormsModule,
InfiniteScrollModule,
ReactiveFormsModule,
NgbModule.forRoot(),
TranslateModule.forRoot()
],
declarations: [
DsDynamicLookupComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
DsDynamicLookupComponent,
DynamicFormValidationService,
FormBuilderService,
FormComponent,
FormService,
{provide: AuthorityService, useValue: authorityServiceStub},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-dynamic-lookup
[bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-lookup>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create DsDynamicLookupComponent', inject([DsDynamicLookupComponent], (app: DsDynamicLookupComponent) => {
expect(app).toBeDefined();
}));
});
describe('when model is DynamicLookupModel', () => {
describe('', () => {
beforeEach(() => {
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
lookupComp.group = LOOKUP_TEST_GROUP;
lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG);
lookupFixture.detectChanges();
});
it('should render only an input element', () => {
const de = lookupFixture.debugElement.queryAll(By.css('input.form-control'));
expect(de.length).toBe(1);
});
});
describe('and init model value is empty', () => {
beforeEach(() => {
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
lookupComp.group = LOOKUP_TEST_GROUP;
lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG);
lookupFixture.detectChanges();
});
it('should init component properly', () => {
expect(lookupComp.firstInputValue).toBe('');
});
it('should return search results', fakeAsync(() => {
const de = lookupFixture.debugElement.queryAll(By.css('button'));
const btnEl = de[0].nativeElement;
const results$ = authorityServiceStub.getEntriesByName({} as any);
lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges();
btnEl.click();
tick();
lookupFixture.detectChanges();
results$.subscribe((results) => {
expect(lookupComp.optionsList).toEqual(results.payload);
})
}));
it('should select a results entry properly', fakeAsync(() => {
let de = lookupFixture.debugElement.queryAll(By.css('button'));
const btnEl = de[0].nativeElement;
const selectedValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1});
spyOn(lookupComp.change, 'emit');
lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges();
btnEl.click();
tick();
lookupFixture.detectChanges();
de = lookupFixture.debugElement.queryAll(By.css('button.dropdown-item'));
const entryEl = de[0].nativeElement;
entryEl.click();
expect(lookupComp.firstInputValue).toEqual('one');
expect(lookupComp.model.value).toEqual(selectedValue);
expect(lookupComp.change.emit).toHaveBeenCalled();
}));
it('should set model.value on input type when AuthorityOptions.closed is false', fakeAsync(() => {
lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges();
lookupComp.onInput(new Event('input'));
expect(lookupComp.model.value).toEqual(new FormFieldMetadataValueObject('test'))
}));
it('should not set model.value on input type when AuthorityOptions.closed is true', () => {
lookupComp.model.authorityOptions.closed = true;
lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges();
lookupComp.onInput(new Event('input'));
expect(lookupComp.model.value).not.toBeDefined();
});
});
describe('and init model value is not empty', () => {
beforeEach(() => {
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
lookupComp.group = LOOKUP_TEST_GROUP;
lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG);
lookupComp.model.value = new FormFieldMetadataValueObject('test', null, 'test001');
lookupFixture.detectChanges();
// spyOn(store, 'dispatch');
});
it('should init component properly', () => {
expect(lookupComp.firstInputValue).toBe('test')
});
});
});
describe('when model is DynamicLookupNameModel', () => {
describe('', () => {
beforeEach(() => {
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
lookupComp.group = LOOKUP_TEST_GROUP;
lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG);
lookupFixture.detectChanges();
// spyOn(store, 'dispatch');
});
it('should render two input element', () => {
const de = lookupFixture.debugElement.queryAll(By.css('input.form-control'));
expect(de.length).toBe(2);
});
});
describe('and init model value is empty', () => {
beforeEach(() => {
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
lookupComp.group = LOOKUP_TEST_GROUP;
lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG);
lookupFixture.detectChanges();
});
it('should select a results entry properly', fakeAsync(() => {
const payload = [
Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1}),
Object.assign(new AuthorityValueModel(), {id: 2, display: 'NameTwo, LastnameTwo', value: 2}),
];
let de = lookupFixture.debugElement.queryAll(By.css('button'));
const btnEl = de[0].nativeElement;
const selectedValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1});
spyOn(lookupComp.change, 'emit');
authorityServiceStub.setNewPayload(payload);
lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges();
btnEl.click();
tick();
lookupFixture.detectChanges();
de = lookupFixture.debugElement.queryAll(By.css('button.dropdown-item'));
const entryEl = de[0].nativeElement;
entryEl.click();
expect(lookupComp.firstInputValue).toEqual('Name');
expect(lookupComp.secondInputValue).toEqual('Lastname');
expect(lookupComp.model.value).toEqual(selectedValue);
expect(lookupComp.change.emit).toHaveBeenCalled();
}));
});
describe('and init model value is not empty', () => {
beforeEach(() => {
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
lookupComp.group = LOOKUP_TEST_GROUP;
lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG);
lookupComp.model.value = new FormFieldMetadataValueObject('Name, Lastname', null, 'test001');
lookupFixture.detectChanges();
});
it('should init component properly', () => {
expect(lookupComp.firstInputValue).toBe('Name');
expect(lookupComp.secondInputValue).toBe('Lastname');
});
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group: FormGroup = LOOKUP_TEST_GROUP;
inputLookupModelConfig = LOOKUP_TEST_MODEL_CONFIG;
model = new DynamicLookupModel(this.inputLookupModelConfig);
showErrorMessages = false;
}

View File

@@ -0,0 +1,217 @@
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';
import { DynamicLookupNameModel } from './dynamic-lookup-name.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 | DynamicLookupNameModel;
@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;
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);
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 as DynamicLookupNameModel).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 as DynamicLookupNameModel).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);
}
isLookupName() {
return (this.model instanceof DynamicLookupNameModel);
}
isSearchDisabled() {
// if (this.firstInputValue === ''
// && (this.isLookupName ? this.secondInputValue === '' : true)) {
// return true;
// }
// return false;
return isEmpty(this.firstInputValue);
}
remove() {
this.group.markAsPristine();
this.model.valueUpdates.next(null);
this.change.emit(null);
}
openChange(isOpened: boolean) {
if (!isOpened) {
if (this.model.authorityOptions.closed) {
this.setInputsValue('');
}
}
}
onBlurEvent(event: Event) {
this.blur.emit(event);
}
onFocusEvent(event) {
this.focus.emit(event);
}
ngOnDestroy() {
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
}
}

View File

@@ -0,0 +1,26 @@
import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
export const DYNAMIC_FORM_CONTROL_TYPE_LOOKUP = 'LOOKUP';
export interface DynamicLookupModelConfig extends DsDynamicInputModelConfig {
maxOptions?: number;
value?: any;
}
export class DynamicLookupModel extends DsDynamicInputModel {
@serializable() maxOptions: number;
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_LOOKUP;
@serializable() value: any;
constructor(config: DynamicLookupModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.autoComplete = AUTOCOMPLETE_OFF;
this.maxOptions = config.maxOptions || 10;
this.valueUpdates.next(config.value);
}
}

View File

@@ -0,0 +1,43 @@
<div #sdRef="ngbDropdown" ngbDropdown class="input-group w-100">
<input class="form-control"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[name]="model.name"
[readonly]="model.readOnly"
[type]="model.inputType"
[value]="formatItemForInput(model.value)"
(blur)="onBlur($event)"
(click)="$event.stopPropagation(); openDropdown(sdRef);"
(focus)="onFocus($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">{{'form.no-results' | translate}}</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>{{'form.loading' | translate}}</p></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,26 @@
@import '../../../../form.component';
.scrollable-menu {
height: auto;
max-height: $dropdown-menu-max-height;
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: $spacer * 2 !important;
line-height: $spacer * 2;
position: sticky;
bottom: 0;
}
.scrollable-dropdown-menu {
left: 0 !important;
margin-bottom: $spacer;
z-index: 1000;
}

View File

@@ -0,0 +1,235 @@
// Load the implementations that should be tested
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { TranslateModule } from '@ngx-translate/core';
import { DsDynamicScrollableDropdownComponent } from './dynamic-scrollable-dropdown.component';
import { DynamicScrollableDropdownModel } from './dynamic-scrollable-dropdown.model';
import { DsDynamicTypeaheadComponent } from '../typeahead/dynamic-typeahead.component';
import { DynamicTypeaheadModel } from '../typeahead/dynamic-typeahead.model';
import { TYPEAHEAD_TEST_GROUP, TYPEAHEAD_TEST_MODEL_CONFIG } from '../typeahead/dynamic-typeahead.component.spec';
import { By } from '@angular/platform-browser';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { hasClass, createTestComponent } from '../../../../../testing/utils';
export const SD_TEST_GROUP = new FormGroup({
dropdown: new FormControl(),
});
export const SD_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'dropdown',
name: 'common_iso_languages',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
errorMessages: {required: 'Required field.'},
id: 'dropdown',
label: 'Language',
maxOptions: 10,
name: 'dropdown',
placeholder: 'Language',
readOnly: false,
required: false,
repeatable: false,
value: undefined
};
describe('Dynamic Dynamic Scrollable Dropdown component', () => {
let testComp: TestComponent;
let scrollableDropdownComp: DsDynamicScrollableDropdownComponent;
let testFixture: ComponentFixture<TestComponent>;
let scrollableDropdownFixture: ComponentFixture<DsDynamicScrollableDropdownComponent>;
let html;
let modelValue;
const authorityServiceStub = new AuthorityServiceStub();
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DynamicFormsCoreModule,
DynamicFormsNGBootstrapUIModule,
FormsModule,
InfiniteScrollModule,
ReactiveFormsModule,
NgbModule.forRoot(),
TranslateModule.forRoot()
],
declarations: [
DsDynamicScrollableDropdownComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
DsDynamicScrollableDropdownComponent,
{provide: AuthorityService, useValue: authorityServiceStub},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<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>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create DsDynamicScrollableDropdownComponent', inject([DsDynamicScrollableDropdownComponent], (app: DsDynamicScrollableDropdownComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
describe('when init model value is empty', () => {
beforeEach(() => {
scrollableDropdownFixture = TestBed.createComponent(DsDynamicScrollableDropdownComponent);
scrollableDropdownComp = scrollableDropdownFixture.componentInstance; // FormComponent test instance
scrollableDropdownComp.group = SD_TEST_GROUP;
scrollableDropdownComp.model = new DynamicScrollableDropdownModel(SD_TEST_MODEL_CONFIG);
scrollableDropdownFixture.detectChanges();
});
afterEach(() => {
scrollableDropdownFixture.destroy();
scrollableDropdownComp = null;
});
it('should init component properly', () => {
const results$ = authorityServiceStub.getEntriesByName({} as any);
expect(scrollableDropdownComp.optionsList).toBeDefined();
results$.subscribe((results) => {
expect(scrollableDropdownComp.optionsList).toEqual(results.payload);
})
});
it('should display dropdown menu entries', () => {
const de = scrollableDropdownFixture.debugElement.query(By.css('button.ds-form-input-btn'));
const btnEl = de.nativeElement;
const deMenu = scrollableDropdownFixture.debugElement.query(By.css('div.scrollable-dropdown-menu'));
const menuEl = deMenu.nativeElement;
btnEl.click();
scrollableDropdownFixture.detectChanges();
expect(hasClass(menuEl, 'show')).toBeTruthy();
});
it('should fetch the next set of results when the user scroll to the end of the list', fakeAsync(() => {
scrollableDropdownComp.pageInfo.currentPage = 1;
scrollableDropdownComp.pageInfo.totalPages = 2;
scrollableDropdownFixture.detectChanges();
scrollableDropdownComp.onScroll();
tick();
expect(scrollableDropdownComp.optionsList.length).toBe(4);
}));
it('should select a results entry properly', fakeAsync(() => {
const selectedValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1});
let de: any = scrollableDropdownFixture.debugElement.query(By.css('button.ds-form-input-btn'));
let btnEl = de.nativeElement;
de = scrollableDropdownFixture.debugElement.query(By.css('div.scrollable-dropdown-menu'));
const menuEl = de.nativeElement;
btnEl.click();
scrollableDropdownFixture.detectChanges();
de = scrollableDropdownFixture.debugElement.queryAll(By.css('button.dropdown-item'));
btnEl = de[0].nativeElement;
btnEl.click();
scrollableDropdownFixture.detectChanges();
expect((scrollableDropdownComp.model as any).value).toEqual(selectedValue);
}));
it('should emit blur Event onBlur', () => {
spyOn(scrollableDropdownComp.blur, 'emit');
scrollableDropdownComp.onBlur(new Event('blur'));
expect(scrollableDropdownComp.blur.emit).toHaveBeenCalled();
});
it('should emit focus Event onFocus', () => {
spyOn(scrollableDropdownComp.focus, 'emit');
scrollableDropdownComp.onFocus(new Event('focus'));
expect(scrollableDropdownComp.focus.emit).toHaveBeenCalled();
});
});
describe('when init model value is not empty', () => {
beforeEach(() => {
scrollableDropdownFixture = TestBed.createComponent(DsDynamicScrollableDropdownComponent);
scrollableDropdownComp = scrollableDropdownFixture.componentInstance; // FormComponent test instance
scrollableDropdownComp.group = SD_TEST_GROUP;
modelValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1});
scrollableDropdownComp.model = new DynamicScrollableDropdownModel(SD_TEST_MODEL_CONFIG);
scrollableDropdownComp.model.value = modelValue;
scrollableDropdownFixture.detectChanges();
});
afterEach(() => {
scrollableDropdownFixture.destroy();
scrollableDropdownComp = null;
});
it('should init component properly', () => {
const results$ = authorityServiceStub.getEntriesByName({} as any);
expect(scrollableDropdownComp.optionsList).toBeDefined();
results$.subscribe((results) => {
expect(scrollableDropdownComp.optionsList).toEqual(results.payload);
expect(scrollableDropdownComp.model.value).toEqual(modelValue);
})
});
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group: FormGroup = SD_TEST_GROUP;
model = new DynamicScrollableDropdownModel(SD_TEST_MODEL_CONFIG);
showErrorMessages = false;
}

View File

@@ -0,0 +1,92 @@
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();
})
}
}
onBlur(event: Event) {
this.blur.emit(event);
}
onFocus(event) {
this.focus.emit(event);
}
onSelect(event) {
this.group.markAsDirty();
this.model.valueUpdates.next(event);
this.change.emit(event);
}
}

View File

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

View File

@@ -0,0 +1,46 @@
<ng-template #rt let-r="result" let-t="term">
{{ r.display }}
</ng-template>
<ds-chips [chips]="chips" [wrapperClass]="'border-bottom border-light'">
<input *ngIf="!searchOptions"
class="border-0 form-control-plaintext tag-input flex-grow-1 mt-1 mb-1 chips-sort-ignore"
type="text"
[class.pl-3]="chips.hasItems()"
[placeholder]="model.placeholder"
[readonly]="model.readOnly"
[(ngModel)]="currentValue"
(blur)="onBlur($event)"
(keypress)="preventEventsPropagation($event)"
(keydown)="preventEventsPropagation($event)"
(keyup)="onKeyUp($event)" />
<input *ngIf="searchOptions"
class="border-0 form-control-plaintext tag-input flex-grow-1 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.placeholder"
[readonly]="model.readOnly"
[resultTemplate]="rt"
[type]="model.inputType"
(blur)="onBlur($event)"
(focus)="onFocus($event)"
(change)="$event.stopPropagation()"
(input)="onInput($event)"
(selectItem)="onSelectItem($event)"
(keypress)="preventEventsPropagation($event)"
(keydown)="preventEventsPropagation($event)"
(keyup)="onKeyUp($event)"/>
<i *ngIf="searching" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
</ds-chips>

View File

@@ -0,0 +1,33 @@
@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: $dropdown-menu-max-height;
overflow-y: scroll;
overflow-x: hidden;
left: 0 !important;
margin-top: $spacer !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 {
outline: none;
width: auto !important;
}

View File

@@ -0,0 +1,293 @@
// Load the implementations that should be tested
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, } from '@angular/core/testing';
import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
import { NgbModule, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of'
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub';
import { DsDynamicTagComponent } from './dynamic-tag.component';
import { DynamicTagModel } from './dynamic-tag.model';
import { GlobalConfig } from '../../../../../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../../../../../config';
import { Chips } from '../../../../../chips/models/chips.model';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { createTestComponent } from '../../../../../testing/utils';
function createKeyUpEvent(key: number) {
/* tslint:disable:no-empty */
const event = {
keyCode: key, preventDefault: () => {
}, stopPropagation: () => {
}
};
/* tslint:enable:no-empty */
spyOn(event, 'preventDefault');
spyOn(event, 'stopPropagation');
return event;
}
export const TAG_TEST_GROUP = new FormGroup({
tag: new FormControl(),
});
export const TAG_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'tag',
name: 'common_iso_languages',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
id: 'tag',
label: 'Keywords',
minChars: 3,
name: 'tag',
placeholder: 'Keywords',
readOnly: false,
required: false,
repeatable: false
};
describe('DsDynamicTagComponent test suite', () => {
let testComp: TestComponent;
let tagComp: DsDynamicTagComponent;
let testFixture: ComponentFixture<TestComponent>;
let tagFixture: ComponentFixture<DsDynamicTagComponent>;
let html;
let chips: Chips;
let modelValue: any;
// async beforeEach
beforeEach(async(() => {
const authorityServiceStub = new AuthorityServiceStub();
TestBed.configureTestingModule({
imports: [
DynamicFormsCoreModule,
DynamicFormsNGBootstrapUIModule,
FormsModule,
NgbModule.forRoot(),
ReactiveFormsModule,
],
declarations: [
DsDynamicTagComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
DsDynamicTagComponent,
{provide: AuthorityService, useValue: authorityServiceStub},
{provide: GLOBAL_CONFIG, useValue: {} as GlobalConfig},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-dynamic-tag [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-tag>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create DsDynamicTagComponent', inject([DsDynamicTagComponent], (app: DsDynamicTagComponent) => {
expect(app).toBeDefined();
}));
});
describe('when authorityOptions are setted', () => {
describe('and init model value is empty', () => {
beforeEach(() => {
tagFixture = TestBed.createComponent(DsDynamicTagComponent);
tagComp = tagFixture.componentInstance; // FormComponent test instance
tagComp.group = TAG_TEST_GROUP;
tagComp.model = new DynamicTagModel(TAG_TEST_MODEL_CONFIG);
tagFixture.detectChanges();
});
afterEach(() => {
tagFixture.destroy();
tagComp = null;
});
it('should init component properly', () => {
chips = new Chips([], 'display');
expect(tagComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
expect(tagComp.searchOptions).toBeDefined();
});
it('should search when 3+ characters typed', fakeAsync(() => {
spyOn((tagComp as any).authorityService, 'getEntriesByName').and.callThrough();
tagComp.search(Observable.of('test')).subscribe(() => {
expect((tagComp as any).authorityService.getEntriesByName).toHaveBeenCalled();
});
}));
it('should select a results entry properly', fakeAsync(() => {
modelValue = [
Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1})
];
const event: NgbTypeaheadSelectItemEvent = {
item: Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1}),
preventDefault: () => {
return;
}
};
spyOn(tagComp.change, 'emit');
tagComp.onSelectItem(event);
tagFixture.detectChanges();
flush();
expect(tagComp.chips.getChipsItems()).toEqual(modelValue);
expect(tagComp.model.value).toEqual(modelValue);
expect(tagComp.currentValue).toBeNull();
expect(tagComp.change.emit).toHaveBeenCalled();
}));
it('should emit blur Event onBlur', () => {
spyOn(tagComp.blur, 'emit');
tagComp.onBlur(new Event('blur'));
expect(tagComp.blur.emit).toHaveBeenCalled();
});
it('should emit focus Event onFocus', () => {
spyOn(tagComp.focus, 'emit');
tagComp.onFocus(new Event('focus'));
expect(tagComp.focus.emit).toHaveBeenCalled();
});
it('should emit change Event onBlur when currentValue is not empty', fakeAsync(() => {
tagComp.currentValue = 'test value';
tagFixture.detectChanges();
spyOn(tagComp.blur, 'emit');
spyOn(tagComp.change, 'emit');
tagComp.onBlur(new Event('blur'));
tagFixture.detectChanges();
flush();
expect(tagComp.change.emit).toHaveBeenCalled();
expect(tagComp.blur.emit).toHaveBeenCalled();
}));
});
describe('and init model value is not empty', () => {
beforeEach(() => {
tagFixture = TestBed.createComponent(DsDynamicTagComponent);
tagComp = tagFixture.componentInstance; // FormComponent test instance
tagComp.group = TAG_TEST_GROUP;
tagComp.model = new DynamicTagModel(TAG_TEST_MODEL_CONFIG);
modelValue = [
new FormFieldMetadataValueObject('a', null, 'test001'),
new FormFieldMetadataValueObject('b', null, 'test002'),
new FormFieldMetadataValueObject('c', null, 'test003'),
];
tagComp.model.value = modelValue;
tagFixture.detectChanges();
});
afterEach(() => {
tagFixture.destroy();
tagComp = null;
});
it('should init component properly', () => {
chips = new Chips(modelValue, 'display');
expect(tagComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
expect(tagComp.searchOptions).toBeDefined();
});
});
});
describe('when authorityOptions are not setted', () => {
describe('and init model value is empty', () => {
beforeEach(() => {
tagFixture = TestBed.createComponent(DsDynamicTagComponent);
tagComp = tagFixture.componentInstance; // FormComponent test instance
tagComp.group = TAG_TEST_GROUP;
const config = TAG_TEST_MODEL_CONFIG;
config.authorityOptions = null;
tagComp.model = new DynamicTagModel(config);
tagFixture.detectChanges();
});
afterEach(() => {
tagFixture.destroy();
tagComp = null;
});
it('should init component properly', () => {
chips = new Chips([], 'display');
expect(tagComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
expect(tagComp.searchOptions).not.toBeDefined();
});
it('should add an item on ENTER or key press is \',\' or \';\'', fakeAsync(() => {
let event = createKeyUpEvent(13);
tagComp.currentValue = 'test value';
tagFixture.detectChanges();
tagComp.onKeyUp(event);
flush();
expect(tagComp.model.value).toEqual(['test value']);
expect(tagComp.currentValue).toBeNull();
event = createKeyUpEvent(188);
tagComp.currentValue = 'test value';
tagFixture.detectChanges();
tagComp.onKeyUp(event);
flush();
expect(tagComp.model.value).toEqual(['test value']);
expect(tagComp.currentValue).toBeNull();
}));
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group: FormGroup = TAG_TEST_GROUP;
model = new DynamicTagModel(TAG_TEST_MODEL_CONFIG);
showErrorMessages = false;
}

View File

@@ -0,0 +1,182 @@
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, 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();
}
onBlur(event: Event) {
if (isNotEmpty(this.currentValue)) {
this.addTagsToChips();
}
this.blur.emit(event);
}
onFocus(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) {
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);
}
}
}

View File

@@ -0,0 +1,28 @@
import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
export const DYNAMIC_FORM_CONTROL_TYPE_TAG = 'TAG';
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 || 3;
const value = config.value || [];
this.valueUpdates.next(value)
}
}

View File

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

View File

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

View File

@@ -0,0 +1,224 @@
// Load the implementations that should be tested
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { async, ComponentFixture, fakeAsync, inject, TestBed, } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub';
import { GlobalConfig } from '../../../../../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../../../../../config';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { DsDynamicTypeaheadComponent } from './dynamic-typeahead.component';
import { DynamicTypeaheadModel } from './dynamic-typeahead.model';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { createTestComponent } from '../../../../../testing/utils';
export const TYPEAHEAD_TEST_GROUP = new FormGroup({
typeahead: new FormControl(),
});
export const TYPEAHEAD_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'typeahead',
name: 'EVENTAuthority',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
id: 'typeahead',
label: 'Conference',
minChars: 3,
name: 'typeahead',
placeholder: 'Conference',
readOnly: false,
required: false,
repeatable: false,
value: undefined
};
describe('DsDynamicTypeaheadComponent test suite', () => {
let testComp: TestComponent;
let typeaheadComp: DsDynamicTypeaheadComponent;
let testFixture: ComponentFixture<TestComponent>;
let typeaheadFixture: ComponentFixture<DsDynamicTypeaheadComponent>;
let html;
// async beforeEach
beforeEach(async(() => {
const authorityServiceStub = new AuthorityServiceStub();
TestBed.configureTestingModule({
imports: [
DynamicFormsCoreModule,
DynamicFormsNGBootstrapUIModule,
FormsModule,
NgbModule.forRoot(),
ReactiveFormsModule,
],
declarations: [
DsDynamicTypeaheadComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
DsDynamicTypeaheadComponent,
{provide: AuthorityService, useValue: authorityServiceStub},
{provide: GLOBAL_CONFIG, useValue: {} as GlobalConfig},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-dynamic-typeahead [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-typeahead>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create DsDynamicTypeaheadComponent', inject([DsDynamicTypeaheadComponent], (app: DsDynamicTypeaheadComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
describe('when init model value is empty', () => {
beforeEach(() => {
typeaheadFixture = TestBed.createComponent(DsDynamicTypeaheadComponent);
typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance
typeaheadComp.group = TYPEAHEAD_TEST_GROUP;
typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG);
typeaheadFixture.detectChanges();
});
afterEach(() => {
typeaheadFixture.destroy();
typeaheadComp = null;
});
it('should init component properly', () => {
expect(typeaheadComp.currentValue).not.toBeDefined();
});
it('should search when 3+ characters typed', fakeAsync(() => {
spyOn((typeaheadComp as any).authorityService, 'getEntriesByName').and.callThrough();
typeaheadComp.search(Observable.of('test')).subscribe(() => {
expect((typeaheadComp as any).authorityService.getEntriesByName).toHaveBeenCalled();
});
}));
it('should set model.value on input type when AuthorityOptions.closed is false', () => {
const inputDe = typeaheadFixture.debugElement.query(By.css('input.form-control'));
const inputElement = inputDe.nativeElement;
inputElement.value = 'test value';
inputElement.dispatchEvent(new Event('input'));
expect((typeaheadComp.model as any).value).toEqual(new FormFieldMetadataValueObject('test value'))
});
it('should not set model.value on input type when AuthorityOptions.closed is true', () => {
typeaheadComp.model.authorityOptions.closed = true;
typeaheadFixture.detectChanges();
const inputDe = typeaheadFixture.debugElement.query(By.css('input.form-control'));
const inputElement = inputDe.nativeElement;
inputElement.value = 'test value';
inputElement.dispatchEvent(new Event('input'));
expect(typeaheadComp.model.value).not.toBeDefined();
});
it('should emit blur Event onBlur', () => {
spyOn(typeaheadComp.blur, 'emit');
typeaheadComp.onBlur(new Event('blur'));
expect(typeaheadComp.blur.emit).toHaveBeenCalled();
});
it('should emit change Event onBlur when AuthorityOptions.closed is false', () => {
typeaheadComp.inputValue = 'test value';
typeaheadFixture.detectChanges();
spyOn(typeaheadComp.blur, 'emit');
spyOn(typeaheadComp.change, 'emit');
typeaheadComp.onBlur(new Event('blur'));
// expect(typeaheadComp.change.emit).toHaveBeenCalled();
expect(typeaheadComp.blur.emit).toHaveBeenCalled();
});
it('should emit focus Event onFocus', () => {
spyOn(typeaheadComp.focus, 'emit');
typeaheadComp.onFocus(new Event('focus'));
expect(typeaheadComp.focus.emit).toHaveBeenCalled();
});
});
describe('and init model value is not empty', () => {
beforeEach(() => {
typeaheadFixture = TestBed.createComponent(DsDynamicTypeaheadComponent);
typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance
typeaheadComp.group = TYPEAHEAD_TEST_GROUP;
typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG);
(typeaheadComp.model as any).value = new FormFieldMetadataValueObject('test', null, 'test001');
typeaheadFixture.detectChanges();
});
afterEach(() => {
typeaheadFixture.destroy();
typeaheadComp = null;
});
it('should init component properly', () => {
expect(typeaheadComp.currentValue).toEqual(new FormFieldMetadataValueObject('test', null, 'test001'));
});
it('should emit change Event onChange and currentValue is empty', () => {
typeaheadComp.currentValue = null;
spyOn(typeaheadComp.change, 'emit');
typeaheadComp.onChange(new Event('change'));
expect(typeaheadComp.change.emit).toHaveBeenCalled();
expect(typeaheadComp.model.value).toBeNull();
});
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group: FormGroup = TYPEAHEAD_TEST_GROUP;
model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG);
showErrorMessages = false;
}

View File

@@ -0,0 +1,123 @@
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;
inputValue: 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.inputValue = valueObj;
this.model.valueUpdates.next(this.inputValue);
}
}
onBlur(event: Event) {
if (!this.model.authorityOptions.closed && isNotEmpty(this.inputValue)) {
this.change.emit(this.inputValue);
this.inputValue = null;
}
this.blur.emit(event);
}
onChange(event: Event) {
event.stopPropagation();
if (isEmpty(this.currentValue)) {
this.model.valueUpdates.next(null);
this.change.emit(null);
}
}
onFocus(event) {
this.focus.emit(event);
}
onSelectItem(event: NgbTypeaheadSelectItemEvent) {
this.inputValue = null;
this.currentValue = event.item;
this.model.valueUpdates.next(event.item);
this.change.emit(event.item);
}
}

View File

@@ -0,0 +1,24 @@
import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.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 || 3;
}
}

View File

@@ -0,0 +1,830 @@
import { inject, TestBed } from '@angular/core/testing';
import {
FormArray,
FormControl,
FormGroup,
NG_ASYNC_VALIDATORS,
NG_VALIDATORS,
ReactiveFormsModule
} from '@angular/forms';
import {
DynamicCheckboxGroupModel,
DynamicCheckboxModel,
DynamicColorPickerModel,
DynamicDatePickerModel,
DynamicEditorModel,
DynamicFileUploadModel, DynamicFormArrayGroupModel,
DynamicFormArrayModel,
DynamicFormControlModel,
DynamicFormControlValue,
DynamicFormGroupModel,
DynamicFormService,
DynamicFormValidationService,
DynamicFormValueControlModel,
DynamicInputModel,
DynamicRadioGroupModel,
DynamicRatingModel,
DynamicSelectModel,
DynamicSliderModel,
DynamicSwitchModel,
DynamicTextAreaModel,
DynamicTimePickerModel
} from '@ng-dynamic-forms/core';
import { DynamicTagModel } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model';
import { DynamicListCheckboxGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model';
import { DynamicQualdropModel } from './ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model';
import { DynamicScrollableDropdownModel } from './ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
import { DynamicGroupModel } from './ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model';
import { DynamicLookupModel } from './ds-dynamic-form-ui/models/lookup/dynamic-lookup.model';
import { DynamicDsDatePickerModel } from './ds-dynamic-form-ui/models/date-picker/date-picker.model';
import { DynamicTypeaheadModel } from './ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model';
import { DynamicListRadioGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model';
import { AuthorityOptions } from '../../../core/integration/models/authority-options.model';
import { FormFieldModel } from './models/form-field.model';
import { FormRowModel, SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model';
import { FormBuilderService } from './form-builder.service';
import { DynamicRowGroupModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-group-model';
import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model';
import { DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model';
import { DynamicLookupNameModel } from './ds-dynamic-form-ui/models/lookup/dynamic-lookup-name.model';
import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
describe('FormBuilderService test suite', () => {
let testModel: DynamicFormControlModel[];
let testFormConfiguration: SubmissionFormsModel;
let service: FormBuilderService;
function testValidator() {
return {testValidator: {valid: true}};
}
function testAsyncValidator() {
return new Promise<boolean>((resolve) => setTimeout(() => resolve(true), 0));
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ReactiveFormsModule],
providers: [
FormBuilderService,
DynamicFormService,
DynamicFormValidationService,
{provide: NG_VALIDATORS, useValue: testValidator, multi: true},
{provide: NG_ASYNC_VALIDATORS, useValue: testAsyncValidator, multi: true}
]
});
const authorityOptions: AuthorityOptions = {
closed: false,
metadata: 'list',
name: 'type_programme',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
};
testModel = [
new DynamicSelectModel<string>(
{
id: 'testSelect',
options: [
{
label: 'Option 1',
value: 'option-1'
},
{
label: 'Option 2',
value: 'option-2'
}
],
value: 'option-3'
}
),
new DynamicInputModel(
{
id: 'testInput',
mask: ['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/],
}
),
new DynamicCheckboxGroupModel(
{
id: 'testCheckboxGroup',
group: [
new DynamicCheckboxModel(
{
id: 'testCheckboxGroup1',
value: true
}
),
new DynamicCheckboxModel(
{
id: 'testCheckboxGroup2',
value: true
}
)
]
}
),
new DynamicRadioGroupModel<string>(
{
id: 'testRadioGroup',
options: [
{
label: 'Option 1',
value: 'option-1'
},
{
label: 'Option 2',
value: 'option-2'
}
],
value: 'option-3'
}
),
new DynamicTextAreaModel({id: 'testTextArea'}),
new DynamicCheckboxModel({id: 'testCheckbox'}),
new DynamicFormArrayModel(
{
id: 'testFormArray',
initialCount: 5,
groupFactory: () => {
return [
new DynamicInputModel({id: 'testFormArrayGroupInput'}),
new DynamicFormArrayModel({
id: 'testNestedFormArray', groupFactory: () => [
new DynamicInputModel({id: 'testNestedFormArrayGroupInput'})
]
})
];
}
}
),
new DynamicFormGroupModel(
{
id: 'testFormGroup',
group: [
new DynamicInputModel({id: 'nestedTestInput'}),
new DynamicTextAreaModel({id: 'nestedTestTextArea'})
]
}
),
new DynamicSliderModel({id: 'testSlider'}),
new DynamicSwitchModel({id: 'testSwitch'}),
new DynamicDatePickerModel({id: 'testDatepicker', value: new Date()}),
new DynamicFileUploadModel({id: 'testFileUpload'}),
new DynamicEditorModel({id: 'testEditor'}),
new DynamicTimePickerModel({id: 'testTimePicker'}),
new DynamicRatingModel({id: 'testRating'}),
new DynamicColorPickerModel({id: 'testColorPicker'}),
new DynamicTypeaheadModel({id: 'testTypeahead'}),
new DynamicScrollableDropdownModel({id: 'testScrollableDropdown', authorityOptions: authorityOptions}),
new DynamicTagModel({id: 'testTag'}),
new DynamicListCheckboxGroupModel({id: 'testCheckboxList', authorityOptions: authorityOptions, repeatable: true}),
new DynamicListRadioGroupModel({id: 'testRadioList', authorityOptions: authorityOptions, repeatable: false}),
new DynamicGroupModel({
id: 'testRelationGroup',
formConfiguration: [{
fields: [{
hints: 'Enter the name of the author.',
input: {type: 'onebox'},
label: 'Authors',
languageCodes: [],
mandatory: 'true',
mandatoryMessage: 'Required field!',
repeatable: false,
selectableMetadata: [{
authority: 'RPAuthority',
closed: false,
metadata: 'dc.contributor.author'
}],
} as FormFieldModel]
} as FormRowModel, {
fields: [{
hints: 'Enter the affiliation of the author.',
input: {type: 'onebox'},
label: 'Affiliation',
languageCodes: [],
mandatory: 'false',
repeatable: false,
selectableMetadata: [{
authority: 'OUAuthority',
closed: false,
metadata: 'local.contributor.affiliation'
}]
} as FormFieldModel]
} as FormRowModel],
mandatoryField: '',
name: 'testRelationGroup',
relationFields: [],
scopeUUID: '',
submissionScope: ''
}),
new DynamicDsDatePickerModel({id: 'testDate'}),
new DynamicLookupModel({id: 'testLookup'}),
new DynamicLookupNameModel({id: 'testLookupName'}),
new DynamicQualdropModel({id: 'testCombobox', readOnly: false}),
new DynamicRowArrayModel(
{
id: 'testFormRowArray',
initialCount: 5,
notRepeteable: false,
groupFactory: () => {
return [
new DynamicInputModel({id: 'testFormRowArrayGroupInput'})
];
},
}
),
];
testFormConfiguration = {
name: 'testFormConfiguration',
rows: [
{
fields: [
{
input: {type: 'lookup'},
label: 'Journal',
mandatory: 'false',
repeatable: false,
hints: 'Enter the name of the journal where the item has been\n\t\t\t\t\tpublished, if any.',
selectableMetadata: [
{
metadata: 'journal',
authority: 'JOURNALAuthority',
closed: false
}
],
languageCodes: []
} as FormFieldModel,
{
input: {type: 'onebox'},
label: 'Issue',
mandatory: 'false',
repeatable: false,
hints: ' Enter issue number.',
selectableMetadata: [
{
metadata: 'issue'
}
],
languageCodes: []
} as FormFieldModel,
{
input: {type: 'name'},
label: 'Name',
mandatory: 'false',
repeatable: false,
hints: 'Enter full name.',
selectableMetadata: [
{
metadata: 'name'
}
],
languageCodes: []
} as FormFieldModel
]
} as FormRowModel,
{
fields: [
{
hints: 'If the item has any identification numbers or codes associated with↵ it, please enter the types and the actual numbers or codes.',
input: {type: 'onebox'},
label: 'Identifiers',
languageCodes: [],
mandatory: 'false',
repeatable: false,
selectableMetadata: [
{metadata: 'dc.identifier.issn', label: 'ISSN'},
{metadata: 'dc.identifier.other', label: 'Other'},
{metadata: 'dc.identifier.ismn', label: 'ISMN'},
{metadata: 'dc.identifier.govdoc', label: 'Gov\'t Doc #'},
{metadata: 'dc.identifier.uri', label: 'URI'},
{metadata: 'dc.identifier.isbn', label: 'ISBN'},
{metadata: 'dc.identifier.doi', label: 'DOI'},
{metadata: 'dc.identifier.pmid', label: 'PubMed ID'},
{metadata: 'dc.identifier.arxiv', label: 'arXiv'}
]
}, {
input: {type: 'onebox'},
label: 'Publisher',
mandatory: 'false',
repeatable: false,
hints: 'Enter the name of the publisher of the previously issued instance of this item.',
selectableMetadata: [
{
metadata: 'publisher'
}
],
languageCodes: []
}
]
} as FormRowModel,
{
fields: [
{
input: {type: 'onebox'},
label: 'Conference',
mandatory: 'false',
repeatable: false,
hints: 'Enter the name of the events, if any.',
selectableMetadata: [
{
metadata: 'conference',
authority: 'EVENTAuthority',
closed: false
}
],
languageCodes: []
}
]
} as FormRowModel
],
self: 'testFormConfiguration.url',
type: 'submissionform',
_links: {
self: 'testFormConfiguration.url'
}
}
});
beforeEach(inject([FormBuilderService], (formService: FormBuilderService) => service = formService));
it('should find a dynamic form control model by id', () => {
expect(service.findById('testCheckbox', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testCheckboxGroup', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testDatepicker', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testFormArray', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testInput', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testRadioGroup', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testSelect', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testSlider', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testSwitch', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testTextArea', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testFileUpload', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testEditor', testModel) instanceof DynamicEditorModel).toBe(true);
expect(service.findById('testTimePicker', testModel) instanceof DynamicTimePickerModel).toBe(true);
expect(service.findById('testRating', testModel) instanceof DynamicRatingModel).toBe(true);
expect(service.findById('testColorPicker', testModel) instanceof DynamicColorPickerModel).toBe(true);
});
it('should find a nested dynamic form control model by id', () => {
expect(service.findById('testCheckboxGroup1', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testCheckboxGroup2', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('nestedTestInput', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testFormRowArrayGroupInput', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testFormRowArrayGroupInput', testModel, 2) instanceof DynamicFormControlModel).toBe(true);
});
it('should create an array of form models', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
expect(formModel[0] instanceof DynamicRowGroupModel).toBe(true);
expect((formModel[0] as DynamicRowGroupModel).group.length).toBe(3);
expect((formModel[0] as DynamicRowGroupModel).get(0) instanceof DynamicLookupModel).toBe(true);
expect((formModel[0] as DynamicRowGroupModel).get(1) instanceof DsDynamicInputModel).toBe(true);
expect((formModel[0] as DynamicRowGroupModel).get(2) instanceof DynamicConcatModel).toBe(true);
expect(formModel[1] instanceof DynamicRowGroupModel).toBe(true);
expect((formModel[1] as DynamicRowGroupModel).group.length).toBe(2);
expect((formModel[1] as DynamicRowGroupModel).get(0) instanceof DynamicQualdropModel).toBe(true);
expect(((formModel[1] as DynamicRowGroupModel).get(0) as DynamicQualdropModel).get(0) instanceof DynamicSelectModel).toBe(true);
expect(((formModel[1] as DynamicRowGroupModel).get(0) as DynamicQualdropModel).get(1) instanceof DsDynamicInputModel).toBe(true);
expect((formModel[1] as DynamicRowGroupModel).get(1) instanceof DsDynamicInputModel).toBe(true);
expect(formModel[2] instanceof DynamicRowGroupModel).toBe(true);
expect((formModel[2] as DynamicRowGroupModel).group.length).toBe(1);
expect((formModel[2] as DynamicRowGroupModel).get(0) instanceof DynamicTypeaheadModel).toBe(true);
});
it('should return form\'s fields value from form model', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
let value = {} as any;
expect(service.getValueFromModel(formModel)).toEqual(value);
((formModel[0] as DynamicRowGroupModel).get(1) as DsDynamicInputModel).valueUpdates.next('test');
value = {
issue: [new FormFieldMetadataValueObject('test')]
};
expect(service.getValueFromModel(formModel)).toEqual(value);
((formModel[2] as DynamicRowGroupModel).get(0) as DynamicTypeaheadModel).valueUpdates.next('test one');
value = {
issue: [new FormFieldMetadataValueObject('test')],
conference: [new FormFieldMetadataValueObject('test one')]
};
expect(service.getValueFromModel(formModel)).toEqual(value);
});
it('should clear all form\'s fields value', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
const value = {} as any;
((formModel[0] as DynamicRowGroupModel).get(1) as DsDynamicInputModel).valueUpdates.next('test');
((formModel[2] as DynamicRowGroupModel).get(0) as DynamicTypeaheadModel).valueUpdates.next('test one');
service.clearAllModelsValue(formModel);
expect(((formModel[0] as DynamicRowGroupModel).get(1) as DynamicTypeaheadModel).value).toEqual(undefined)
expect(((formModel[2] as DynamicRowGroupModel).get(0) as DynamicTypeaheadModel).value).toEqual(undefined)
});
it('should return true when model has a custom group model as parent', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
let model = service.findById('dc_identifier_QUALDROP_VALUE', formModel);
let modelParent = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
model.parent = modelParent;
expect(service.isModelInCustomGroup(model)).toBe(true);
model = service.findById('name_CONCAT_FIRST_INPUT', formModel);
modelParent = service.findById('name_CONCAT_GROUP', formModel);
model.parent = modelParent;
expect(service.isModelInCustomGroup(model)).toBe(true);
});
it('should return true when model value is an array', () => {
let model = service.findById('testCheckboxList', testModel) as DynamicFormArrayModel;
expect(service.hasArrayGroupValue(model)).toBe(true);
model = service.findById('testRadioList', testModel) as DynamicFormArrayModel;
expect(service.hasArrayGroupValue(model)).toBe(true);
model = service.findById('testTag', testModel) as DynamicFormArrayModel;
expect(service.hasArrayGroupValue(model)).toBe(true);
});
it('should return true when model value is a map', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
const model = service.findById('dc_identifier_QUALDROP_VALUE', formModel);
const modelParent = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
model.parent = modelParent;
expect(service.hasMappedGroupValue(model)).toBe(true);
});
it('should return true when model is a Qualdrop Group', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
expect(service.isQualdropGroup(model)).toBe(true);
model = service.findById('name_CONCAT_GROUP', formModel);
expect(service.isQualdropGroup(model)).toBe(false);
});
it('should return true when model is a Custom or List Group', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
expect(service.isCustomOrListGroup(model)).toBe(true);
model = service.findById('name_CONCAT_GROUP', formModel);
expect(service.isCustomOrListGroup(model)).toBe(true);
model = service.findById('testCheckboxList', testModel);
expect(service.isCustomOrListGroup(model)).toBe(true);
model = service.findById('testRadioList', testModel);
expect(service.isCustomOrListGroup(model)).toBe(true);
});
it('should return true when model is a Custom Group', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
expect(service.isCustomGroup(model)).toBe(true);
model = service.findById('name_CONCAT_GROUP', formModel);
expect(service.isCustomGroup(model)).toBe(true);
model = service.findById('testCheckboxList', testModel);
expect(service.isCustomGroup(model)).toBe(false);
model = service.findById('testRadioList', testModel);
expect(service.isCustomGroup(model)).toBe(false);
});
it('should return true when model is a List Group', () => {
let model = service.findById('testCheckboxList', testModel);
expect(service.isListGroup(model)).toBe(true);
model = service.findById('testRadioList', testModel);
expect(service.isListGroup(model)).toBe(true);
});
it('should return true when model is a Relation Group', () => {
const model = service.findById('testRelationGroup', testModel);
expect(service.isRelationGroup(model)).toBe(true);
});
it('should return true when model is a Array Row Group', () => {
let model = service.findById('testFormRowArray', testModel, null);
expect(service.isRowArrayGroup(model)).toBe(true);
model = service.findById('testFormArray', testModel);
expect(service.isRowArrayGroup(model)).toBe(false);
});
it('should return true when model is a Array Group', () => {
let model = service.findById('testFormRowArray', testModel) as DynamicFormArrayModel;
expect(service.isArrayGroup(model)).toBe(true);
model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
expect(service.isArrayGroup(model)).toBe(true);
});
it('should return properly form control by field id', () => {
const group = service.createFormGroup(testModel);
const control = group.controls.testLookup;
expect(service.getFormControlById('testLookup', group, testModel)).toEqual(control);
});
it('should return field id from model', () => {
const model = service.findById('testRadioList', testModel);
expect(service.getId(model)).toEqual('testRadioList');
});
it('should create a form group', () => {
const formGroup = service.createFormGroup(testModel);
expect(formGroup instanceof FormGroup).toBe(true);
expect(formGroup.get('testCheckbox') instanceof FormControl).toBe(true);
expect(formGroup.get('testCheckboxGroup') instanceof FormGroup).toBe(true);
expect(formGroup.get('testDatepicker') instanceof FormControl).toBe(true);
expect(formGroup.get('testFormArray') instanceof FormArray).toBe(true);
expect(formGroup.get('testInput') instanceof FormControl).toBe(true);
expect(formGroup.get('testRadioGroup') instanceof FormControl).toBe(true);
expect(formGroup.get('testSelect') instanceof FormControl).toBe(true);
expect(formGroup.get('testTextArea') instanceof FormControl).toBe(true);
expect(formGroup.get('testFileUpload') instanceof FormControl).toBe(true);
expect(formGroup.get('testEditor') instanceof FormControl).toBe(true);
expect(formGroup.get('testTimePicker') instanceof FormControl).toBe(true);
expect(formGroup.get('testRating') instanceof FormControl).toBe(true);
expect(formGroup.get('testColorPicker') instanceof FormControl).toBe(true);
});
it('should throw when unknown DynamicFormControlModel id is specified in JSON', () => {
expect(() => service.fromJSON([{id: 'test'}]))
.toThrow(new Error(`unknown form control model type defined on JSON object with id "test"`));
});
it('should resolve array group path', () => {
service.createFormGroup(testModel);
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const nestedModel = (model.get(0).get(1) as DynamicFormArrayModel).get(0);
expect(service.getPath(model)).toEqual(['testFormArray']);
expect(service.getPath(nestedModel)).toEqual(['testFormArray', '0', 'testNestedFormArray', '0']);
});
it('should add a form control to an existing form group', () => {
const formGroup = service.createFormGroup(testModel);
const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup;
const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel;
const newModel1 = new DynamicInputModel({id: 'newInput1'});
const newModel2 = new DynamicInputModel({id: 'newInput2'});
service.addFormGroupControl(formGroup, testModel, newModel1);
service.addFormGroupControl(nestedFormGroup, nestedFormGroupModel, newModel2);
expect(formGroup.controls[newModel1.id]).toBeTruthy();
expect(testModel[testModel.length - 1] === newModel1).toBe(true);
expect((formGroup.controls.testFormGroup as FormGroup).controls[newModel2.id]).toBeTruthy();
expect(nestedFormGroupModel.get(nestedFormGroupModel.group.length - 1) === newModel2).toBe(true);
});
it('should insert a form control to an existing form group', () => {
const formGroup = service.createFormGroup(testModel);
const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup;
const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel;
const newModel1 = new DynamicInputModel({id: 'newInput1'});
const newModel2 = new DynamicInputModel({id: 'newInput2'});
service.insertFormGroupControl(4, formGroup, testModel, newModel1);
service.insertFormGroupControl(0, nestedFormGroup, nestedFormGroupModel, newModel2);
expect(formGroup.controls[newModel1.id]).toBeTruthy();
expect(testModel[4] === newModel1).toBe(true);
expect(service.getPath(testModel[4])).toEqual(['newInput1']);
expect((formGroup.controls.testFormGroup as FormGroup).controls[newModel2.id]).toBeTruthy();
expect(nestedFormGroupModel.get(0) === newModel2).toBe(true);
expect(service.getPath(nestedFormGroupModel.get(0))).toEqual(['testFormGroup', 'newInput2']);
});
it('should move an existing form control to a different group position', () => {
const formGroup = service.createFormGroup(testModel);
const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel;
const model1 = testModel[0];
const model2 = nestedFormGroupModel.get(0);
service.moveFormGroupControl(0, 2, testModel);
expect(formGroup.controls[model1.id]).toBeTruthy();
expect(testModel[2] === model1).toBe(true);
service.moveFormGroupControl(0, 1, nestedFormGroupModel);
expect((formGroup.controls.testFormGroup as FormGroup).controls[model2.id]).toBeTruthy();
expect(nestedFormGroupModel.get(1) === model2).toBe(true);
});
it('should remove a form control from an existing form group', () => {
const formGroup = service.createFormGroup(testModel);
const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup;
const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel;
const length = testModel.length;
const size = nestedFormGroupModel.size();
const index = 1;
const id1 = testModel[index].id;
const id2 = nestedFormGroupModel.get(index).id;
service.removeFormGroupControl(index, formGroup, testModel);
expect(Object.keys(formGroup.controls).length).toBe(length - 1);
expect(formGroup.controls[id1]).toBeUndefined();
expect(testModel.length).toBe(length - 1);
expect(service.findById(id1, testModel)).toBeNull();
service.removeFormGroupControl(index, nestedFormGroup, nestedFormGroupModel);
expect(Object.keys(nestedFormGroup.controls).length).toBe(size - 1);
expect(nestedFormGroup.controls[id2]).toBeUndefined();
expect(nestedFormGroupModel.size()).toBe(size - 1);
expect(service.findById(id2, testModel)).toBeNull();
});
it('should create a form array', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
let formArray;
expect(service.createFormArray).toBeTruthy();
formArray = service.createFormArray(model);
expect(formArray instanceof FormArray).toBe(true);
expect(formArray.length).toBe(model.initialCount);
});
it('should add a form array group', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
service.addFormArrayGroup(formArray, model);
expect(formArray.length).toBe(model.initialCount + 1);
});
it('should insert a form array group', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
service.insertFormArrayGroup(0, formArray, model);
expect(formArray.length).toBe(model.initialCount + 1);
});
it('should move up a form array group', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
const index = 3;
const step = 1;
(formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 1');
(formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 2');
(model.get(index).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).valueUpdates.next('next test value 1');
(model.get(index + step).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).valueUpdates.next('next test value 2');
service.moveFormArrayGroup(index, step, formArray, model);
expect(formArray.length).toBe(model.initialCount);
expect((formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 2');
expect((formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 1');
expect((model.get(index).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).value).toEqual('next test value 2');
expect((model.get(index + step).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).value).toEqual('next test value 1');
});
it('should move down a form array group', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
const index = 3;
const step = -1;
(formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 1');
(formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 2');
(model.get(index).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).valueUpdates.next('next test value 1');
(model.get(index + step).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).valueUpdates.next('next test value 2');
service.moveFormArrayGroup(index, step, formArray, model);
expect(formArray.length).toBe(model.initialCount);
expect((formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 2');
expect((formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 1');
expect((model.get(index).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).value).toEqual('next test value 2');
expect((model.get(index + step).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).value).toEqual('next test value 1');
});
it('should throw when form array group is to be moved out of bounds', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
expect(() => service.moveFormArrayGroup(2, -5, formArray, model))
.toThrow(new Error(`form array group cannot be moved due to index or new index being out of bounds`));
});
it('should remove a form array group', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
service.removeFormArrayGroup(0, formArray, model);
expect(formArray.length).toBe(model.initialCount - 1);
});
it('should clear a form array', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
service.clearFormArray(formArray, model);
expect(formArray.length === 0).toBe(true);
});
});

View File

@@ -0,0 +1,284 @@
import { Injectable } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms';
import {
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP,
DYNAMIC_FORM_CONTROL_TYPE_GROUP,
DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP,
DynamicFormArrayModel,
DynamicFormControlModel,
DynamicFormGroupModel,
DynamicFormService,
DynamicPathable,
JSONUtils,
} from '@ng-dynamic-forms/core';
import { isObject, isString, mergeWith } from 'lodash';
import { hasValue, isEmpty, isNotEmpty, isNotNull, isNotUndefined, isNull } from '../../empty.util';
import { DynamicQualdropModel } from './ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model';
import { SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model';
import {
DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP,
DynamicGroupModel
} from './ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model';
import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model';
import { RowParser } from './parsers/row-parser';
import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.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[], findArrayIndex): void => {
for (const controlModel of findGroupModel) {
if (controlModel.id === findId) {
if (this.isArrayGroup(controlModel) && isNotNull(findArrayIndex)) {
result = (controlModel as DynamicFormArrayModel).get(findArrayIndex);
} else {
result = controlModel;
}
break;
}
if (this.isGroup(controlModel)) {
findByIdFn(findId, (controlModel as DynamicFormGroupModel).group, findArrayIndex);
}
if (this.isArrayGroup(controlModel)
&& (isNull(findArrayIndex) || (controlModel as DynamicFormArrayModel).size > (findArrayIndex))) {
const index = (isNull(findArrayIndex)) ? 0 : findArrayIndex;
findByIdFn(findId, (controlModel as DynamicFormArrayModel).get(index).group, index);
}
}
};
findByIdFn(id, groupModel, arrayIndex);
return result;
}
clearAllModelsValue(groupModel: DynamicFormControlModel[]): void {
const iterateControlModels = (findGroupModel: DynamicFormControlModel[]): void => {
for (const controlModel of findGroupModel) {
if (this.isGroup(controlModel)) {
iterateControlModels((controlModel as DynamicFormGroupModel).group);
continue;
}
if (this.isArrayGroup(controlModel)) {
iterateControlModels((controlModel as DynamicFormArrayModel).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 normalizeValue = (controlModel, controlValue, controlModelIndex) => {
const controlLanguage = (controlModel as DsDynamicInputModel).hasLanguages ? (controlModel as DsDynamicInputModel).language : null;
if (isString(controlValue)) {
return new FormFieldMetadataValueObject(controlValue, controlLanguage, null, null, controlModelIndex);
} else if (isObject(controlValue)) {
const authority = controlValue.authority || controlValue.id || null;
const place = controlModelIndex || controlValue.place;
return new FormFieldMetadataValueObject(controlValue.value, controlLanguage, authority, controlValue.display, place);
}
};
const iterateControlModels = (findGroupModel: DynamicFormControlModel[], controlModelIndex: number = 0): void => {
let iterateResult = Object.create({});
// Iterate over all group's controls
for (const controlModel of findGroupModel) {
if (this.isRowGroup(controlModel) && !this.isCustomOrListGroup(controlModel)) {
iterateResult = mergeWith(iterateResult, iterateControlModels((controlModel as DynamicFormGroupModel).group), customizer);
continue;
}
if (this.isGroup(controlModel) && !this.isCustomOrListGroup(controlModel)) {
iterateResult[controlModel.name] = iterateControlModels((controlModel as DynamicFormGroupModel).group);
continue;
}
if (this.isRowArrayGroup(controlModel)) {
for (const arrayItemModel of (controlModel as DynamicRowArrayModel).groups) {
iterateResult = mergeWith(iterateResult, iterateControlModels(arrayItemModel.group, arrayItemModel.index), customizer);
}
continue;
}
if (this.isArrayGroup(controlModel)) {
iterateResult[controlModel.name] = [];
for (const arrayItemModel of (controlModel as DynamicFormArrayModel).groups) {
iterateResult[controlModel.name].push(iterateControlModels(arrayItemModel.group, arrayItemModel.index));
}
continue;
}
let controlId;
// Get the field's name
if (this.isQualdropGroup(controlModel)) {
// If is instance of DynamicQualdropModel take the qualdrop id as field's name
controlId = (controlModel as DynamicQualdropModel).qualdropId;
} else {
controlId = controlModel.name;
}
if (this.isRelationGroup(controlModel)) {
const values = (controlModel as DynamicGroupModel).getGroupValue();
values.forEach((groupValue, groupIndex) => {
const newGroupValue = Object.create({});
Object.keys(groupValue)
.forEach((key) => {
const normValue = normalizeValue(controlModel, groupValue[key], groupIndex);
if (isNotEmpty(normValue) && normValue.hasValue()) {
if (iterateResult.hasOwnProperty(key)) {
iterateResult[key].push(normValue);
} else {
iterateResult[key] = [normValue];
}
}
});
})
} else if (isNotUndefined((controlModel as any).value) && isNotEmpty((controlModel as any).value)) {
const controlArrayValue = [];
// Normalize control value as an array of FormFieldMetadataValueObject
const values = Array.isArray((controlModel as any).value) ? (controlModel as any).value : [(controlModel as any).value];
values.forEach((controlValue) => {
controlArrayValue.push(normalizeValue(controlModel, controlValue, controlModelIndex))
});
if (controlId && iterateResult.hasOwnProperty(controlId) && isNotNull(iterateResult[controlId])) {
iterateResult[controlId] = iterateResult[controlId].concat(controlArrayValue);
} else {
iterateResult[controlId] = isNotEmpty(controlArrayValue) ? controlArrayValue : 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): boolean {
return this.isCustomGroup((model as any).parent);
}
hasArrayGroupValue(model: DynamicFormControlModel): boolean {
return model && (this.isListGroup(model) || model.type === DYNAMIC_FORM_CONTROL_TYPE_TAG);
}
hasMappedGroupValue(model: DynamicFormControlModel): boolean {
return (this.isQualdropGroup((model as any).parent)
|| this.isRelationGroup((model as any).parent));
}
isGroup(model: DynamicFormControlModel): boolean {
return model && (model.type === DYNAMIC_FORM_CONTROL_TYPE_GROUP || model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP);
}
isQualdropGroup(model: DynamicFormControlModel): boolean {
return (model && model.type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && hasValue((model as any).qualdropId));
}
isCustomGroup(model: DynamicFormControlModel): boolean {
return model && ((model as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && (model as any).isCustomGroup === true);
}
isRowGroup(model: DynamicFormControlModel): boolean {
return model && ((model as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && (model as any).isRowGroup === true);
}
isCustomOrListGroup(model: DynamicFormControlModel): boolean {
return model &&
(this.isCustomGroup(model)
|| this.isListGroup(model));
}
isListGroup(model: DynamicFormControlModel): boolean {
return model &&
((model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP && (model as any).isListGroup === true)
|| (model.type === DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP && (model as any).isListGroup === true));
}
isRelationGroup(model: DynamicFormControlModel): boolean {
return model && model.type === DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP;
}
isRowArrayGroup(model: DynamicFormControlModel): boolean {
return model.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY && (model as any).isRowArray === true;
}
isArrayGroup(model: DynamicFormControlModel): boolean {
return model.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY;
}
getFormControlById(id: string, formGroup: FormGroup, groupModel: DynamicFormControlModel[], index = 0): AbstractControl {
const fieldModel = this.findById(id, groupModel, index);
return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null;
}
getId(model: DynamicPathable): string {
if (this.isArrayGroup(model as DynamicFormControlModel)) {
return model.index.toString();
} else {
return ((model as DynamicFormControlModel).id !== (model as DynamicFormControlModel).name) ?
(model as DynamicFormControlModel).name :
(model as DynamicFormControlModel).id;
}
}
}

View File

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

View File

@@ -0,0 +1,46 @@
import { isNotEmpty, isNotNull } from '../../../empty.util';
export class FormFieldMetadataValueObject {
metadata?: string;
value: any;
display: string;
language: any;
authority: string;
confidence: number;
place: number;
closed: boolean;
label: string;
constructor(value: any = null,
language: any = null,
authority: string = null,
display: string = null,
place: number = 0,
confidence: number = -1,
metadata: string = null) {
this.value = isNotNull(value) ? ((typeof value === 'string') ? value.trim() : value) : null;
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);
}
hasValue(): boolean {
return isNotEmpty(this.value);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,43 @@
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;
regex?: string;
};
@autoserialize
selectableMetadata: FormFieldMetadataValueObject[];
@autoserialize
rows: FormRowModel[];
@autoserialize
scope: string;
@autoserialize
value: any;
}

View File

@@ -0,0 +1,105 @@
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';
import { ParserOptions } from './parser-options';
export class ConcatFieldParser extends FieldParser {
constructor(protected configData: FormFieldModel,
protected initFormValues,
protected parserOptions: ParserOptions,
protected separator: string,
protected firstPlaceholder: string = null,
protected secondPlaceholder: string = null) {
super(configData, initFormValues, parserOptions);
this.separator = separator;
this.firstPlaceholder = firstPlaceholder;
this.secondPlaceholder = secondPlaceholder;
}
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
let clsGroup: DynamicFormControlLayout;
let clsInput: DynamicFormControlLayout;
let newId: string;
if (this.configData.selectableMetadata[0].metadata.includes('.')) {
newId = this.configData.selectableMetadata[0].metadata
.split('.')
.slice(0, this.configData.selectableMetadata[0].metadata.split('.').length - 1)
.join('.');
} else {
newId = this.configData.selectableMetadata[0].metadata
}
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, label, false, false);
const input2ModelConfig: DynamicInputModelConfig = this.initModel(newId + CONCAT_SECOND_INPUT_SUFFIX, label, 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.value.split(this.separator);
if (values.length > 1) {
input1ModelConfig.value = values[0].trim();
input2ModelConfig.value = values[1].trim();
}
}
// 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();
return concatModel;
}
}

View File

@@ -0,0 +1,65 @@
import { FormFieldModel } from '../models/form-field.model';
import { DynamicConcatModel } from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model';
import { SeriesFieldParser } from './series-field-parser';
import { DateFieldParser } from './date-field-parser';
import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { ParserOptions } from './parser-options';
describe('DateFieldParser test suite', () => {
let field: FormFieldModel;
let initFormValues: any = {};
const parserOptions: ParserOptions = {
readOnly: false,
submissionScope: null,
authorityUuid: null
};
beforeEach(() => {
field = {
input: {
type: 'date'
},
label: 'Date of Issue.',
mandatory: 'true',
repeatable: false,
hints: 'Please give the date of previous publication or public distribution. You can leave out the day and/or month if they aren\'t applicable.',
mandatoryMessage: 'You must enter at least the year.',
selectableMetadata: [
{
metadata: 'date',
}
],
languageCodes: []
} as FormFieldModel;
});
it('should init parser properly', () => {
const parser = new DateFieldParser(field, initFormValues, parserOptions);
expect(parser instanceof DateFieldParser).toBe(true);
});
it('should return a DynamicDsDatePickerModel object when repeatable option is false', () => {
const parser = new DateFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
expect(fieldModel instanceof DynamicDsDatePickerModel).toBe(true);
});
it('should set init value properly', () => {
initFormValues = {
date: [new FormFieldMetadataValueObject('1983-11-18')],
};
const expectedValue = '1983-11-18';
const parser = new DateFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
expect(fieldModel.value).toEqual(expectedValue);
});
});

View File

@@ -0,0 +1,36 @@
import { FieldParser } from './field-parser';
import { DynamicDatePickerModelConfig } from '@ng-dynamic-forms/core';
import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model';
import { isNotEmpty } from '../../../empty.util';
import { DS_DATE_PICKER_SEPARATOR } from '../ds-dynamic-form-ui/models/date-picker/date-picker.component';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
export class DateFieldParser extends FieldParser {
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
let malformedDate = false;
const inputDateModelConfig: DynamicDatePickerModelConfig = this.initModel(null, label);
inputDateModelConfig.toggleIcon = 'fa fa-calendar';
this.setValues(inputDateModelConfig as any, fieldValue);
// Init Data and validity check
if (isNotEmpty(inputDateModelConfig.value)) {
const value = inputDateModelConfig.value.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) {
malformedDate = true;
}
}
}
}
}
const dateModel = new DynamicDsDatePickerModel(inputDateModelConfig);
dateModel.malformedDate = malformedDate;
return dateModel;
}
}

View File

@@ -0,0 +1,59 @@
import { FormFieldModel } from '../models/form-field.model';
import { DropdownFieldParser } from './dropdown-field-parser';
import { DynamicScrollableDropdownModel } from '../ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
import { ParserOptions } from './parser-options';
describe('DropdownFieldParser test suite', () => {
let field: FormFieldModel;
const initFormValues = {};
const parserOptions: ParserOptions = {
readOnly: false,
submissionScope: 'testScopeUUID',
authorityUuid: null
};
beforeEach(() => {
field = {
input: {
type: 'dropdown'
},
label: 'Type',
mandatory: 'false',
repeatable: false,
hints: 'Select the tyupe.',
selectableMetadata: [
{
metadata: 'type',
authority: 'common_types_dataset',
closed: false
}
],
languageCodes: []
} as FormFieldModel;
});
it('should init parser properly', () => {
const parser = new DropdownFieldParser(field, initFormValues, parserOptions);
expect(parser instanceof DropdownFieldParser).toBe(true);
});
it('should return a DynamicScrollableDropdownModel object when repeatable option is false', () => {
const parser = new DropdownFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
expect(fieldModel instanceof DynamicScrollableDropdownModel).toBe(true);
});
it('should throw when authority is not passed', () => {
field.selectableMetadata[0].authority = null;
const parser = new DropdownFieldParser(field, initFormValues, parserOptions);
expect(() => parser.parse())
.toThrow();
});
});

View File

@@ -0,0 +1,35 @@
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 { isNotEmpty } from '../../../empty.util';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
export class DropdownFieldParser extends FieldParser {
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
const dropdownModelConfig: DynamicScrollableDropdownModelConfig = this.initModel(null, label);
let layout: DynamicFormControlLayout;
if (isNotEmpty(this.configData.selectableMetadata[0].authority)) {
this.setAuthorityOptions(dropdownModelConfig, this.parserOptions.authorityUuid);
if (isNotEmpty(fieldValue)) {
dropdownModelConfig.value = fieldValue;
}
layout = {
element: {
control: 'col'
},
grid: {
host: 'col'
}
};
const dropdownModel = new DynamicScrollableDropdownModel(dropdownModelConfig, layout);
return dropdownModel;
} else {
throw Error(`Authority name is not available. Please checks form configuration file.`);
}
}
}

View File

@@ -0,0 +1,307 @@
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';
import { ParserOptions } from './parser-options';
export abstract class FieldParser {
protected fieldId: string;
constructor(protected configData: FormFieldModel, protected initFormValues, protected parserOptions: ParserOptions) {
}
public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): 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',
label: this.configData.label,
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, false);
}
setLayout(model, 'element', 'host', 'col');
if (model.hasLanguages) {
setLayout(model, 'grid', 'control', 'col');
}
return [model];
}
} as DynamicRowArrayModelConfig;
const layout: DynamicFormControlLayout = {
grid: {
group: '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.getAllFieldIds();
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.getAllFieldIds();
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.getAllFieldIds();
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.assign(new FormFieldMetadataValueObject(), this.initFormValues[id][innerIndex]);
valueObj.metadata = id;
// valueObj.value = this.initFormValues[id][innerIndex];
values.push(valueObj);
}
});
return values[outerIndex];
} else {
return null;
}
}
protected getInitArrayIndex() {
const fieldIds: any = this.getAllFieldIds();
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 {
const ids = this.getAllFieldIds();
return isNotNull(ids) ? ids[0] : null;
}
protected getAllFieldIds(): 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();
// 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.parserOptions.readOnly;
controlModel.disabled = this.parserOptions.readOnly;
// Set label
this.setLabel(controlModel, label, labelEmpty);
controlModel.placeholder = this.configData.label;
if (this.configData.mandatory && setErrors) {
this.markAsRequired(controlModel);
}
if (this.hasRegex()) {
this.addPatternValidator(controlModel);
}
// Available Languages
if (this.configData.languageCodes && this.configData.languageCodes.length > 0) {
(controlModel as DsDynamicInputModel).languageCodes = this.configData.languageCodes;
}
return controlModel;
}
protected hasRegex() {
return hasValue(this.configData.input.regex);
}
protected addPatternValidator(controlModel) {
const regex = new RegExp(this.configData.input.regex);
controlModel.validators = Object.assign({}, controlModel.validators, {pattern: regex});
controlModel.errorMessages = Object.assign(
{},
controlModel.errorMessages,
{pattern: 'error.validation.pattern'});
}
protected markAsRequired(controlModel) {
controlModel.required = true;
controlModel.validators = Object.assign({}, controlModel.validators, {required: null});
controlModel.errorMessages = Object.assign(
{},
controlModel.errorMessages,
{required: this.configData.mandatoryMessage});
}
protected setLabel(controlModel, label = true, labelEmpty = false) {
if (label) {
controlModel.label = (labelEmpty) ? '&nbsp;' : this.configData.label;
}
}
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 (forceValueAsObj) {
modelConfig.value = fieldValue;
} else {
modelConfig.value = fieldValue.value;
}
// if (hasValue(fieldValue.language)) {
// // Instance of FormFieldLanguageValueObject
// modelConfig.value = fieldValue.value;
// } else if (hasValue(fieldValue.metadata)) {
// // Is a combobox field's value
// modelConfig.value = fieldValue.value;
// } else {
// // Instance of FormFieldMetadataValueObject
// modelConfig.value = fieldValue;
// }
} else {
if (forceValueAsObj) {
// If value isn't an instance of FormFieldMetadataValueObject instantiate it
modelConfig.value = new FormFieldMetadataValueObject(fieldValue);
} else {
if (typeof fieldValue === 'string') {
// Case only string
modelConfig.value = fieldValue;
}
}
}
}
return modelConfig;
}
}

View File

@@ -0,0 +1,111 @@
import { FormFieldModel } from '../models/form-field.model';
import { GroupFieldParser } from './group-field-parser';
import { DynamicGroupModel } from '../ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { ParserOptions } from './parser-options';
describe('GroupFieldParser test suite', () => {
let field: FormFieldModel;
let initFormValues = {};
const parserOptions: ParserOptions = {
readOnly: false,
submissionScope: 'testScopeUUID',
authorityUuid: 'WORKSPACE'
};
beforeEach(() => {
field = {
input: {
type: 'group'
},
rows: [
{
fields: [
{
input: {
type: 'onebox'
},
label: 'Author',
mandatory: 'false',
repeatable: false,
hints: 'Enter the name of the author.',
selectableMetadata: [
{
metadata: 'author'
}
],
languageCodes: []
},
{
input: {
type: 'onebox'
},
label: 'Affiliation',
mandatory: false,
repeatable: true,
hints: 'Enter the affiliation of the author.',
selectableMetadata: [
{
metadata: 'affiliation'
}
],
languageCodes: []
}
]
}
],
label: 'Authors',
mandatory: 'true',
repeatable: false,
mandatoryMessage: 'Entering at least the first author is mandatory.',
hints: 'Enter the names of the authors of this item.',
selectableMetadata: [
{
metadata: 'author'
}
],
languageCodes: []
} as FormFieldModel;
});
it('should init parser properly', () => {
const parser = new GroupFieldParser(field, initFormValues, parserOptions);
expect(parser instanceof GroupFieldParser).toBe(true);
});
it('should return a DynamicGroupModel object', () => {
const parser = new GroupFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
expect(fieldModel instanceof DynamicGroupModel).toBe(true);
});
it('should throw when rows configuration is empty', () => {
field.rows = null;
const parser = new GroupFieldParser(field, initFormValues, parserOptions);
expect(() => parser.parse())
.toThrow();
});
it('should set group init value properly', () => {
initFormValues = {
author: [new FormFieldMetadataValueObject('test author')],
affiliation: [new FormFieldMetadataValueObject('test affiliation')]
};
const parser = new GroupFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
const expectedValue = [{
author: new FormFieldMetadataValueObject('test author'),
affiliation: new FormFieldMetadataValueObject('test affiliation')
}];
expect(fieldModel.value).toEqual(expectedValue);
});
});

View File

@@ -0,0 +1,62 @@
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/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 {
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean) {
const modelConfiguration: DynamicGroupModelConfig = this.initModel(null, label);
modelConfiguration.scopeUUID = this.parserOptions.authorityUuid;
modelConfiguration.submissionScope = this.parserOptions.submissionScope;
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.mandatoryField].concat(modelConfiguration.relationFields);
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();
return model;
}
}

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