import { Component, ChangeDetectionStrategy, OnDestroy, Input, ChangeDetectorRef, OnInit, ViewChildren } from '@angular/core'; import { SqueezedValueAccessor } from '../../../../shared/interfaces/squeeze-value-accessor.inteface'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormGroup, FormBuilder, FormArray } from '@angular/forms'; import { TargetingSpec } from '../../interfaces/targeting-spec.interface'; import { Subject , BehaviorSubject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { FormControlToken } from '../../../../shared/constants/form-control-token'; import { detailedTargetingSpecInitial, detailedSpecInitial } from '../../interfaces/targeting-spec-detailed.interface'; import isEqual from 'lodash-es/isEqual'; import isEmpty from 'lodash-es/isEmpty'; @Component({ selector: 'app-detailed-targeting', templateUrl: './detailed-targeting.html', styleUrls: ['./detailed-targeting.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: DetailedTargetingComponent, multi: true }, {provide: FormControlToken, useExisting: DetailedTargetingComponent}], changeDetection: ChangeDetectionStrategy.OnPush }) export class DetailedTargetingComponent implements ControlValueAccessor, SqueezedValueAccessor, OnInit, OnDestroy { destroy$ = new Subject(); squeezedValue$ = new BehaviorSubject('–'); @Input() adaccountId = 'act_944874195534529'; @ViewChildren(FormControlToken) detailedComponents; detailedTargetingForm: FormGroup; // ==== value ==== _value: TargetingSpec = {}; set value (value: any) { this._value = value || this._value; this.updateSqueezedValue(); this.propagateChange(this._value); } get value () { return this._value; } // ==== value ==== // noinspection JSMethodCanBeStatic /** * Will be replaced when implementing registerOnChange * @param _ {TargetingSpec} */ propagateChange (_: TargetingSpec) { return _; } // ==== implement ControlValueAccessor ==== writeValue (value: TargetingSpec) { if (!value) { return; } this._value = value || this._value; this.updateForm(); } registerOnChange (fn: any) { this.propagateChange = fn; } registerOnTouched () { return; } // ==== implement ControlValueAccessor ==== // ==== implement SqueezedValueAccessor ==== updateSqueezedValue () { const squeezedValue = this.detailedComponents.reduce((acc, control, index) => { let prefix = 'People Who Match: '; if (index > 0) { prefix = control.type === 'flexible_spec' ? 'And Must Also Match: ' : 'Exclude: '; } return acc + prefix + control.getSqueezedValue() + '
'; }, ''); this.squeezedValue$.next(squeezedValue || '–'); } getSqueezedValue () { return this.squeezedValue$.getValue(); } focus () { return; } // ==== implement SqueezedValueAccessor ==== updateForm (formValue = this.value) { if (isEqual(formValue, this.detailedTargetingForm.value)) { return; } this.detailedTargetingForm = this.getForm(formValue); this.watchFormValueChanges(); this.changeDetectorRef.markForCheck(); this.changeDetectorRef.detectChanges(); } /** * Set model driven form using passed form value or initial form value * @param formValue */ getForm (formValue = detailedTargetingSpecInitial) { const groupData = {}; /*Extend with initial values*/ formValue = Object.assign({}, detailedTargetingSpecInitial, formValue); /* * Set or extend flexible specs from detailed targeting keys from upper level * e.g. interests, behaviors, etc. That's how we support old format targetings * (before flexible specs become available). * Important! Targeting segments e.g. interests/behaviors specified inside flexible_spec * are not available for use outside of flexible_spec. * */ const generatedFlexibleSpecs = []; for (const name in detailedSpecInitial) { if (detailedSpecInitial.hasOwnProperty(name) && formValue[name]) { // Add to flexible spec generatedFlexibleSpecs.push({[name]: formValue[name]}); } } /** * If no true flexible specs, but has generated flexible spec, update! */ formValue['flexible_spec'] = formValue['flexible_spec'] .filter((flexibleSpec) => !generatedFlexibleSpecs.length || !isEmpty(flexibleSpec)) .concat(generatedFlexibleSpecs); /** * Iterate through initial detailedTargeting keys */ for (const name in detailedTargetingSpecInitial) { if (detailedTargetingSpecInitial.hasOwnProperty(name)) { if (Array.isArray(formValue[name])) { groupData[name] = this.formBuilder.array( formValue[name].map((data) => this.formBuilder.control(data)) ); } else if (formValue[name] !== null) { // ignore null value groupData[name] = this.formBuilder.control(formValue[name]); } } } return this.formBuilder.group(groupData); } /** * Narrow audience by adding another control */ addControl (name: string) { if (this.detailedTargetingForm.controls[name] instanceof FormArray) { const control = this.detailedTargetingForm.controls[name]; control.push(this.formBuilder.control({})); } else { this.detailedTargetingForm.addControl(name, this.formBuilder.control({})); } } /** * Remove control from by index * @param name * @param i */ removeControl ({name, i}: {name: string, i?: number}) { if (this.detailedTargetingForm.controls[name] instanceof FormArray) { const control = this.detailedTargetingForm.controls[name]; control.removeAt(i); } else { this.detailedTargetingForm.removeControl(name); } } /** * Subscribe to form value changes and update component's value when it is changed */ watchFormValueChanges () { this.detailedTargetingForm.valueChanges .pipe( takeUntil(this.destroy$) ) .subscribe((formValue) => { const newValue = Object.assign({}, {flexible_spec: null, exclusions: null}, formValue); // Filter out empty flexible specs if (Array.isArray(newValue['flexible_spec'])) { newValue['flexible_spec'] = newValue['flexible_spec'].filter((flexibleSpec, i) => { const isEmptySpec = isEmpty(flexibleSpec); /*Remove control if current spec is empty, it is not the first and the only one spec * and previously had a value*/ if (isEmptySpec && this.value['flexible_spec'] && !isEmpty(this.value['flexible_spec'][i]) && newValue['flexible_spec'].length > 1) { this.removeControl({name: 'flexible_spec', i}); } // Ignore first flexible_spec return !isEmptySpec; }); } // Filter out empty exclusions if (newValue['exclusions'] !== null && isEmpty(newValue['exclusions'])) { newValue['exclusions'] = null; // Remove exclusions if previously it had values if (!isEmpty(this.value['exclusions'])) { this.removeControl({name: 'exclusions'}); } } this.value = newValue; this.changeDetectorRef.markForCheck(); this.changeDetectorRef.detectChanges(); }); } ngOnDestroy () { this.destroy$.next(); } ngOnInit () { this.detailedTargetingForm = this.getForm(); this.watchFormValueChanges(); } constructor (private formBuilder: FormBuilder, private changeDetectorRef: ChangeDetectorRef) {} }