All files / app/formly child-plugin.type.ts

9.23% Statements 6/65
11.53% Branches 6/52
4.16% Functions 1/24
9.75% Lines 4/41

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 12211x         11x 11x                                                     11x                                                                                                                                                                                
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { FieldType } from '@ngx-formly/core';
import { cloneDeep } from 'lodash-es';
import { Plugin } from '../model/plugin';
import { convertFeature } from '../util/geo';
import { directChild, hasPrefix } from '../util/tag';
 
/**
 * Select a single child plugin of the parent plugin (props.parent).
 * Changing the selection swaps the child plugin tag on the Ref, converting
 * the GeoJSON geometry of the previous child where possible.
 * Children in props.keep stay on the Ref when another child is selected,
 * so a kept child can be combined with one other.
 * Has no key, so nothing is stored in the parent plugin data.
 */
@Component({
  selector: 'formly-field-child-plugin',
  host: { 'class': 'field child-plugin-field' },
  template: `
    <select class="child-plugin-select"
            [id]="id"
            [attr.aria-label]="parentName"
            [disabled]="!togglePlugin"
            (input)="select($any($event.target).value)">
      <option value="" [selected]="!current">{{ parentName }}</option>
      @for (p of visibleChildren; track p.tag) {
        <option [value]="p.tag" [selected]="p.tag === current">{{ p.name || '#' + p.tag }}</option>
      }
    </select>
  `,
  changeDetection: ChangeDetectionStrategy.Eager,
})
export class FormlyFieldChildPlugin extends FieldType {
 
  get parent(): string {
    return this.props.parent;
  }
 
  get parentName(): string {
    return this.formState?.admin?.getPlugin(this.parent)?.name || this.parent;
  }
 
  get children(): Plugin[] {
    const plugins: Record<string, Plugin | undefined> = this.formState?.admin?.status?.plugins || {};
    return Object.values(plugins)
      .filter(p => !!p && directChild(p.tag, this.parent)) as Plugin[];
  }
 
  /**
   * Children shown in the dropdown. Hidden children are only shown when selected.
   */
  get visibleChildren(): Plugin[] {
    const current = this.current;
    return this.children.filter(p => !p.config?.hideChild || p.tag === current);
  }
 
  get togglePlugin(): ((tag: string) => void) | undefined {
    return this.formState?.togglePlugin;
  }
 
  get setPlugin(): ((tag: string, value: any) => void) | undefined {
    return this.formState?.setPlugin;
  }
 
  private get plugins(): AbstractControl | null | undefined {
    return this.form?.parent;
  }
 
  /**
   * All child plugins currently on the Ref.
   */
  get selected(): string[] {
    const tags: string[] = this.plugins?.parent?.get('tags')?.value
      || Object.keys((this.plugins as any)?.controls || {});
    return this.children.map(p => p.tag).filter(t => tags.some(tag => hasPrefix(tag, t)));
  }
 
  /**
   * Child plugins kept on the Ref when another child is selected.
   */
  get keep(): string[] {
    return this.props.keep || [];
  }
 
  /**
   * The child plugin currently selected. A kept child is only shown
   * when no other child is on the Ref.
   */
  get current(): string | undefined {
    const selected = this.selected;
    return selected.find(t => !this.keep.includes(t)) || selected[0];
  }
 
  select(tag: string) {
    const selected = this.selected;
    if (this.current === tag && (selected.length === 1 || tag && !this.keep.includes(tag))) return;
    const others = selected.filter(t => !this.keep.includes(t));
    // Convert from the other geometry being replaced, or a kept one when adding
    const source = others.find(t => t !== tag) || selected.find(t => t !== tag);
    const sourceValue = source && cloneDeep(this.plugins?.get(source)?.value);
    // Remove other children, keeping the selected one and kept children if a child is selected
    for (const t of selected) {
      if (t === tag) continue;
      if (tag && this.keep.includes(t)) continue;
      this.togglePlugin?.(t);
    }
    if (!tag || selected.includes(tag)) return;
    this.togglePlugin?.(tag);
    const value = this.convert(sourceValue, tag);
    if (value) this.setPlugin?.(tag, value);
  }
 
  /**
   * Convert the GeoJSON data of one child plugin to the type of another.
   */
  private convert(value: any, tag: string) {
    const plugin = this.children.find(p => p.tag === tag);
    return convertFeature(value, plugin?.defaults, !!(plugin?.schema as any)?.optionalProperties?.properties);
  }
}