Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/app/core/shared/form/models/form-field.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ export class FormFieldModel {
@autoserialize
typeBind: string[];

/**
* Contains field name to type-bind to
*/
@autoserialize
typeBindToField: string;

/**
* Containing the value for this metadata field
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
listId: string;
searchConfig: string;
value: MetadataValue;

/**
* List of subscriptions to unsubscribe from
*/
Expand Down Expand Up @@ -355,8 +356,20 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
if (this.model && this.model.placeholder) {
this.model.placeholder = this.translateService.instant(this.model.placeholder);
}

// Handle typeBind relations
if (this.model.typeBindRelations && this.model.typeBindRelations.length > 0) {
this.subscriptions.push(...this.typeBindRelationService.subscribeRelations(this.model, this.control));
const fieldKey = this.model.metadataKey ?? this.model.name;
if (!this.typeBindRelationService.activatedSubscriptionsByMetadataKey.has(fieldKey)) {
this.subscriptions.push(...this.typeBindRelationService.subscribeRelations(
this.model,
this.control,
this.formGroup ? this.formGroup : this.group,
this.ref,
));
this.typeBindRelationService.activatedSubscriptionsByMetadataKey.add(fieldKey);
}

}
}
}
Expand Down Expand Up @@ -519,6 +532,16 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());

this.clearListOfTypeBindRelationSubscriptions();
}

private clearListOfTypeBindRelationSubscriptions() {
const fieldKey = this.model.metadataKey ?? this.model.name;

if (this.typeBindRelationService.activatedSubscriptionsByMetadataKey.has(fieldKey)) {
this.typeBindRelationService.activatedSubscriptionsByMetadataKey.delete(fieldKey);
}
}

get hasHint(): boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Injector } from '@angular/core';
import {
ChangeDetectorRef,
Injector,
} from '@angular/core';
import {
inject,
TestBed,
Expand Down Expand Up @@ -78,13 +81,13 @@ describe('DSDynamicTypeBindRelationService test suite', () => {
describe('Test getRelatedFormModel method', () => {
it('Should get 0 related form models for simple type bind mock data', () => {
const testModel = MockRelationModel;
const relatedModels = service.getRelatedFormModel(testModel);
const relatedModels = service.getRelatedFormModel(testModel, undefined);
expect(relatedModels).toHaveSize(0);
});
it('Should get 1 related form models for mock relation model data', () => {
const testModel = mockInputWithTypeBindModel;
testModel.typeBindRelations = getTypeBindRelations(['boundType'], 'dc.type');
const relatedModels = service.getRelatedFormModel(testModel);
const relatedModels = service.getRelatedFormModel(testModel, undefined);
expect(relatedModels).toHaveSize(1);
});
});
Expand All @@ -95,7 +98,8 @@ describe('DSDynamicTypeBindRelationService test suite', () => {
testModel.typeBindRelations = getTypeBindRelations(['boundType'], 'dc.type');
const dcTypeControl = new UntypedFormControl();
dcTypeControl.setValue('boundType');
let subscriptions = service.subscribeRelations(testModel, dcTypeControl);
const compRef = jasmine.createSpyObj<ChangeDetectorRef>('ChangeDetectorRef', ['markForCheck']);
let subscriptions = service.subscribeRelations(testModel, dcTypeControl, dcTypeControl.parent as any, compRef);
expect(subscriptions).toHaveSize(1);
});

Expand All @@ -108,7 +112,7 @@ describe('DSDynamicTypeBindRelationService test suite', () => {
const relation = dynamicFormRelationService.findRelationByMatcher((testModel as any).typeBindRelations, HIDDEN_MATCHER);
const matcher = HIDDEN_MATCHER;
if (relation !== undefined) {
const hasMatch = service.matchesCondition(relation, matcher);
const hasMatch = service.matchesCondition(relation, matcher,[{ 'dc.type': 'boundType' }]);
matcher.onChange(hasMatch, testModel, dcTypeControl, injector);
expect(hasMatch).toBeTruthy();
}
Expand All @@ -123,7 +127,7 @@ describe('DSDynamicTypeBindRelationService test suite', () => {
const relation = dynamicFormRelationService.findRelationByMatcher((testModel as any).typeBindRelations, HIDDEN_MATCHER);
const matcher = HIDDEN_MATCHER;
if (relation !== undefined) {
const hasMatch = service.matchesCondition(relation, matcher);
const hasMatch = service.matchesCondition(relation, matcher, [{ 'dc.type': 'boundType' }]);
matcher.onChange(hasMatch, testModel, dcTypeControl, injector);
expect(hasMatch).toBeFalsy();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import {
ChangeDetectorRef,
Inject,
Injectable,
Injector,
Optional,
} from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import {
FormArray,
UntypedFormControl,
UntypedFormGroup,
} from '@angular/forms';
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from '@dspace/core/shared/form/ds-dynamic-form-constants';
import { FormFieldMetadataValueObject } from '@dspace/core/shared/form/models/form-field-metadata-value.model';
import {
hasNoValue,
hasValue,
isNotNull,
} from '@dspace/shared/utils/empty.util';
import {
AND_OPERATOR,
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
DYNAMIC_MATCHERS,
DynamicFormControlCondition,
DynamicFormControlMatcher,
Expand All @@ -22,7 +29,11 @@ import {
OR_OPERATOR,
} from '@ng-dynamic-forms/core';
import { Subscription } from 'rxjs';
import { startWith } from 'rxjs/operators';
import {
debounceTime,
distinctUntilChanged,
startWith,
} from 'rxjs/operators';

import { FormBuilderService } from '../form-builder.service';

Expand All @@ -32,6 +43,7 @@ import { FormBuilderService } from '../form-builder.service';
*/
@Injectable({ providedIn: 'root' })
export class DsDynamicTypeBindRelationService {
activatedSubscriptionsByMetadataKey = new Set<string>();

constructor(@Optional() @Inject(DYNAMIC_MATCHERS) private dynamicMatchers: DynamicFormControlMatcher[],
protected dynamicFormRelationService: DynamicFormRelationService,
Expand Down Expand Up @@ -60,26 +72,39 @@ export class DsDynamicTypeBindRelationService {


/**
* Get models for this bind type
* Get models or (parent array-)controls for a model with type bind
*
* @param model
* @param formGroup
*/
public getRelatedFormModel(model: DynamicFormControlModel): DynamicFormControlModel[] {
public getRelatedFormModel(model: DynamicFormControlModel, formGroup: UntypedFormGroup): Array<DynamicFormControlModel | FormArray> {

const models: DynamicFormControlModel[] = [];
const models: Array<DynamicFormControlModel | FormArray> = [];
let parentControl: any;

(model as any).typeBindRelations.forEach((relGroup) => relGroup.when.forEach((rel) => {

if (model.id === rel.id) {
throw new Error(`FormControl ${model.id} cannot depend on itself`);
}

const bindModel: DynamicFormControlModel = this.formBuilderService.getTypeBindModel();
const bindModel: DynamicFormControlModel = this.formBuilderService.getTypeBindModels(rel.id);

if (!parentControl && formGroup && bindModel && this.isPartOfArrayWithRepeatableItems(bindModel)) {
parentControl = this.formBuilderService.findControlByModel((bindModel.parent as any).context, formGroup);
}

if (model && !models.some((modelElement) => modelElement === bindModel)) {
models.push(bindModel);
if (!parentControl) {
if (model && !models.some((modelElement) => modelElement === bindModel)) {
models.push(bindModel);
}
}
}));

if (parentControl && !models.some((modelElement) => modelElement === parentControl)) {
models.push(parentControl);
}

return models;
}

Expand All @@ -89,20 +114,20 @@ export class DsDynamicTypeBindRelationService {
* component, the negation of the comparison is returned.
* @param relation type bind relation (eg. {MATCH_VISIBLE, OR, ['book', 'book part']})
* @param matcher contains 'match' value and an onChange() event listener
* @param latestChanges
*/
public matchesCondition(relation: DynamicFormControlRelation, matcher: DynamicFormControlMatcher): boolean {
public matchesCondition(relation: DynamicFormControlRelation, matcher: DynamicFormControlMatcher, latestChanges?: any): boolean {

// Default to OR for operator (OR is explicitly set in field-parser.ts anyway)
const operator = relation.operator || OR_OPERATOR;


return relation.when.reduce((hasAlreadyMatched: boolean, condition: DynamicFormControlCondition, index: number) => {
// Get the DynamicFormControlModel (typeBindModel) from the form builder service, set in the form builder
// in the form model at init time in formBuilderService.modelFromConfiguration (called by other form components
// like relation group component and submission section form component).
// This model (DynamicRelationGroupModel) contains eg. mandatory field, formConfiguration, relationFields,
// submission scope, form/section type and other high level properties
const bindModel: any = this.formBuilderService.getTypeBindModel();
const bindModel: any = this.formBuilderService.getTypeBindModels(condition.id);

let values: string[];
let bindModelValue = bindModel.value;
Expand All @@ -112,8 +137,17 @@ export class DsDynamicTypeBindRelationService {
if (bindModel.type === DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP) {
bindModelValue = bindModel.value.map((entry) => entry[bindModel.mandatoryField]);
}

// Support multiple bind models
if (Array.isArray(bindModelValue)) {
if (latestChanges && Array.isArray(latestChanges)) {
values = latestChanges.map((entry) => {
if (entry[condition.id]) {
return this.getTypeBindValue(entry[condition.id]);
} else {
return this.getTypeBindValue(entry);
}
});
} else if (Array.isArray(bindModelValue)) {
values = [...bindModelValue.map((entry) => this.getTypeBindValue(entry))];
} else {
values = [this.getTypeBindValue(bindModelValue)];
Expand Down Expand Up @@ -177,42 +211,68 @@ export class DsDynamicTypeBindRelationService {
* Return an array of subscriptions to a calling component
* @param model
* @param control
* @param formGroup
* @param compRef
*/
subscribeRelations(model: DynamicFormControlModel, control: UntypedFormControl): Subscription[] {

const relatedModels = this.getRelatedFormModel(model);
const subscriptions: Subscription[] = [];

Object.values(relatedModels).forEach((relatedModel: any) => {

if (hasValue(relatedModel)) {
const initValue = (hasNoValue(relatedModel.value) || typeof relatedModel.value === 'string') ? relatedModel.value :
(Array.isArray(relatedModel.value) ? relatedModel.value : relatedModel.value.value);

const updateSubject = (relatedModel.type === 'CHECKBOX_GROUP' ? relatedModel.valueUpdates : relatedModel.valueChanges);
const valueChanges = updateSubject.pipe(
startWith(initValue),
);

// Build up the subscriptions to watch for changes;
subscriptions.push(valueChanges.subscribe(() => {
// Iterate each matcher
if (hasValue(this.dynamicMatchers)) {
this.dynamicMatchers.forEach((matcher) => {
// Find the relation
const relation = this.dynamicFormRelationService.findRelationByMatcher((model as any).typeBindRelations, matcher);
// If the relation is defined, get matchesCondition result and pass it to the onChange event listener
if (relation !== undefined) {
const hasMatch = this.matchesCondition(relation, matcher);
matcher.onChange(hasMatch, model, control, this.injector);
}
});
}
}));
subscribeRelations(model: DynamicFormControlModel, control: UntypedFormControl, formGroup: UntypedFormGroup, compRef: ChangeDetectorRef): Subscription[] {
const relatedControlsOrModels: Array<DynamicFormControlModel | FormArray> = this.getRelatedFormModel(model, formGroup);

return Object.values(relatedControlsOrModels).filter((relatedModel) => hasValue(relatedModel)).map((relatedModel) => {
const isFormArray = relatedModel instanceof FormArray;

const initialValue = this.getInitialRelatedModelValue(relatedModel);
let valueUpdates$;

if (isFormArray) {
valueUpdates$ = relatedModel.valueChanges;
} else if ((relatedModel as any).type === 'CHECKBOX_GROUP') {
valueUpdates$ = (relatedModel as any).valueUpdates;
} else {
valueUpdates$ = (relatedModel as any).valueChanges;
}

return valueUpdates$.pipe(
startWith(initialValue),
distinctUntilChanged(),
debounceTime(150),
).subscribe((changedValues: any) => {
this.applyMatcher(model, control, compRef, isFormArray ? changedValues : undefined);
});
});
}

private getInitialRelatedModelValue(relatedModel: DynamicFormControlModel | FormArray) {
const value = (relatedModel as any).value;

if (hasNoValue(value) || typeof value === 'string' || Array.isArray(value)) {
return value;
}

return value.value;
}

return subscriptions;
private applyMatcher(model: DynamicFormControlModel, control: UntypedFormControl, compRef: ChangeDetectorRef, latestChanges: any) {
if (!hasValue(this.dynamicMatchers)) {
return;
}

this.dynamicMatchers.forEach((matcher) => {
const relation = this.dynamicFormRelationService.findRelationByMatcher((model as any).typeBindRelations, matcher);

if (relation === undefined) {
return;
}

const hasMatch = this.matchesCondition(relation, matcher, latestChanges);
matcher.onChange(hasMatch, model, control, this.injector);
compRef.markForCheck();
});
}

private isPartOfArrayWithRepeatableItems(model: DynamicFormControlModel): boolean {
return (isNotNull(model.parent) &&
(model.parent as any).context &&
(model.parent as any).context.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY &&
(model.parent as any).context.notRepeatable === false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,6 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent
{}, // @Input model.value
this.model.submissionScope,
this.model.readOnly,
null,
true);
const fieldId = fieldName.replace(/\./g, '_');
const model = this.formBuilderService.findById(fieldId, formModel);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@ export class DsDynamicRelationGroupModalComponent extends DynamicFormControlComp
{},
this.model.submissionScope,
this.model.readOnly,
null,
true,
this.metadataSecurityConfiguration);
this.formBuilderService.addFormModel(this.formId, this.formModel);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ export class DsDynamicRelationInlineGroupComponent extends DynamicFormControlCom
initValues,
this.model.submissionScope,
this.model.readOnly,
this.formBuilderService.getTypeBindModel(),
true,
this.metadataSecurityConfiguration)[0];

Expand Down
Loading
Loading