Creating Frontend Parts

1. Add Part Model

▶️ (1) in your library, under src/lib, add the part model file, named <PARTNAME>.ts (e.g. cod-bindings-part.ts), like this:

import { Part } from "@myrmidon/cadmus-core";

/**
 * The __NAME__ part model.
 */
export interface __NAME__Part extends Part {
  // TODO: add properties
}

/**
 * The type ID used to identify the __NAME__Part type.
 */
export const __NAME___PART_TYPEID = "it.vedph.__PRJ__.__NAME__";

/**
 * JSON schema for the __NAME__ part.
 * You can use the JSON schema tool at https://jsonschema.net/.
 */
export const __NAME___PART_SCHEMA = {
  $schema: "http://json-schema.org/draft-07/schema#",
  $id:
    "www.vedph.it/cadmus/parts/__PRJ__/__LIB__/" +
    __NAME___PART_TYPEID +
    ".json",
  type: "object",
  title: "__NAME__Part",
  required: [
    "id",
    "itemId",
    "typeId",
    "timeCreated",
    "creatorId",
    "timeModified",
    "userId",
    // TODO: add other required properties here...
  ],
  properties: {
    timeCreated: {
      type: "string",
      pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d+Z$",
    },
    creatorId: {
      type: "string",
    },
    timeModified: {
      type: "string",
      pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d+Z$",
    },
    userId: {
      type: "string",
    },
    id: {
      type: "string",
      pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
    },
    itemId: {
      type: "string",
      pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
    },
    typeId: {
      type: "string",
      pattern: "^[a-z][-0-9a-z._]*$",
    },
    roleId: {
      type: ["string", "null"],
      pattern: "^([a-z][-0-9a-z._]*)?$",
    },

    // TODO: add properties and fill the "required" array as needed
  },
};

💡 If you want to infer a schema in the JSON schema tool, which is usually the quickest way of writing the schema, you can use this JSON template, adding your model’s properties to it:

{
  "id": "009dcbd9-b1f1-4dc2-845d-1d9c88c83269",
  "itemId": "2c2eadb7-1972-4415-9a43-b8036b6fa685",
  "typeId": "it.vedph.thetype",
  "roleId": null,
  "timeCreated": "2019-11-29T16:48:49.694Z",
  "creatorId": "zeus",
  "timeModified": "2019-11-29T16:48:49.694Z",
  "userId": "zeus",
  "TODO": "add properties here"
}

▶️ (2) add the new file to the exports of the “barrel” public-api.ts file in the module, like export * from './lib/<NAME>-part';.

2. Add Part Editor

The part editor UI is a dumb component which essentially uses a form to represent the data of a part’s model. These data are adapted to the form when loading them, and converted back to the part’s model when saving.

▶️ (1) in src/lib, add a part editor dumb component named after the part (e.g. ng g component note-part for NotePartComponent after the model NotePart), and extending ModelEditorComponentBase<T> where T is the part’s type. Here we usually have two cases: - a generic part - a part consisting only of a list of entities.

Two different templates are provided here.

2.1. Generic Part Editor Template

▶️ (1) write code and HTML template:

  • 📁 part editor code:
// NAME-part.component.ts

import { Component, OnInit } from "@angular/core";
import {
  FormControl,
  FormBuilder,
  FormGroup,
  UntypedFormGroup,
  ReactiveFormsModule,
} from "@angular/forms";

import { CommonModule } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { MatCardModule } from "@angular/material/card";
import { MatExpansionModule } from "@angular/material/expansion";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatIconModule } from "@angular/material/icon";
import { MatInputModule } from "@angular/material/input";
import { MatSelectModule } from "@angular/material/select";
import { MatTooltipModule } from "@angular/material/tooltip";
// ... etc.

import { AuthJwtService } from "@myrmidon/auth-jwt-login";
import { ThesauriSet, ThesaurusEntry } from "@myrmidon/cadmus-core";
import { EditedObject, ModelEditorComponentBase } from "@myrmidon/cadmus-ui";

import { __NAME__Part, __NAME___PART_TYPEID } from "../__NAME__-part";

/**
 * __NAME__ part editor component.
 * Thesauri: ...TODO list of thesauri IDs...
 */
@Component({
  selector: "cadmus-__NAME__-part",
  imports: [
    CommonModule,
    ReactiveFormsModule,
    MatButtonModule,
    MatCardModule,
    MatExpansionModule,
    MatFormFieldModule,
    MatIconModule,
    MatInputModule,
    MatSelectModule,
    MatTooltipModule,
    // ... etc.
    // cadmus
    CloseSaveButtonsComponent,
  ],
  templateUrl: "./__NAME__-part.component.html",
  styleUrls: ["./__NAME__-part.component.scss"],
})
export class __NAME__PartComponent
  extends ModelEditorComponentBase<__NAME__Part>
  implements OnInit
{
  // TODO: add your form controls here, e.g.:
  // public tag: FormControl<string | null>;
  // public text: FormControl<string | null>;

  // TODO: add your thesauri entries here, e.g.:
  // public tagEntries?: ThesaurusEntry[];

  constructor(authService: AuthJwtService, formBuilder: FormBuilder) {
    super(authService, formBuilder);
    // form
    // TODO: create your form controls (but NOT the form itself), e.g.:
    // this.tag = formBuilder.control(null, Validators.maxLength(100));
    // this.text = formBuilder.control('', Validators.required, { nonNullable: true });
  }

  public override ngOnInit(): void {
    super.ngOnInit();
  }

  protected buildForm(formBuilder: FormBuilder): FormGroup | UntypedFormGroup {
    return formBuilder.group({
      // TODO: assign your created form controls to the form returned here, e.g.:
      // tag: this.tag,
      // text: this.text,
    });
  }

  private updateThesauri(thesauri: ThesauriSet): void {
    // TODO: setup thesauri entries here, e.g.:
    // const key = 'note-tags';
    // if (this.hasThesaurus(key)) {
    //  this.tagEntries = thesauri[key].entries;
    // } else {
    //  this.tagEntries = undefined;
    // }
  }

  private updateForm(part?: __NAME__Part | null): void {
    if (!part) {
      this.form.reset();
      return;
    }
    // TODO: set values of your form controls, e.g.:
    // this.tag.setValue(part.tag || null);
    // this.text.setValue(part.text);
    this.form.markAsPristine();
  }

  protected override onDataSet(data?: EditedObject<__NAME__Part>): void {
    // thesauri
    if (data?.thesauri) {
      this.updateThesauri(data.thesauri);
    }

    // form
    this.updateForm(data?.value);
  }

  protected getValue(): __NAME__Part {
    let part = this.getEditedPart(__NAME___PART_TYPEID) as __NAME__Part;
    // TODO: assign values to your part properties from form controls, e.g.:
    // part.tag = this.tag.value || undefined;
    // part.text = this.text.value?.trim() || '';
    return part;
  }
}
  • 📁 part editor HTML template:
<!-- NAME-part.component.html -->

<form [formGroup]="form" (submit)="save()">
  <mat-card>
    <mat-card-header>
      <div mat-card-avatar>
        <mat-icon>picture_in_picture</mat-icon>
      </div>
      <mat-card-title
        ></mat-card-title
      >
    </mat-card-header>
    <mat-card-content> TODO: your template here... </mat-card-content>
    <mat-card-actions>
      <cadmus-close-save-buttons
        [form]="form"
        [noSave]="userLevel < 2"
        (closeRequest)="close()"
      />
    </mat-card-actions>
  </mat-card>
</form>

Note that the modelName() human-friendly part name property is dynamically defined according to the model-types thesaurus for both pure parts and parts with a specific role. For instance, if you are going to use a categories part with role “eras”, you should add to that thesaurus an entry with ID it.vedph.categories:eras whose value will be used as the human-friendly name for that part type with that specific role.

▶️ (2) ensure the component has been added to the public-api.ts barrel file (and, if still using modules, to the library module’s declarations and exports).

If your editor needs to be customized with specific settings, you can add them to the backend JSON profile and retrieve them in the editor’s code. To this end, inject the AppRepository service and request the setting object for the editor of the part/fragment type ID and role via its getSettingFor(typeId, roleId?) method. This will return an object with any model, representing all the settings for that specific editor.

2.2. List Part Editor Template

▶️ (1) write code and HTML template:

  • 📁 list part editor code:
// NAME-part.component.ts

import { Component, OnInit } from "@angular/core";
import {
  FormControl,
  FormBuilder,
  FormGroup,
  UntypedFormGroup,
  ReactiveFormsModule,
} from "@angular/forms";
import { take } from "rxjs/operators";

import { CommonModule } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { MatCardModule } from "@angular/material/card";
import { MatExpansionModule } from "@angular/material/expansion";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatIconModule } from "@angular/material/icon";
import { MatInputModule } from "@angular/material/input";
import { MatSelectModule } from "@angular/material/select";
import { MatTooltipModule } from "@angular/material/tooltip";
// ... etc.

import { NgxToolsValidators } from "@myrmidon/ngx-tools";
import { DialogService } from "@myrmidon/ngx-mat-tools";
import { AuthJwtService } from "@myrmidon/auth-jwt-login";
import { EditedObject, ModelEditorComponentBase } from "@myrmidon/cadmus-ui";
import { ThesauriSet, ThesaurusEntry } from "@myrmidon/cadmus-core";

import {
  __NAME__,
  __NAME__sPart,
  __NAME__S_PART_TYPEID,
} from "../__NAME__s-part";

/**
 * __NAME__sPart editor component.
 * Thesauri: ...TODO list of thesauri IDs...
 */
@Component({
  selector: "cadmus-__NAME__s-part",
  imports: [
    CommonModule,
    ReactiveFormsModule,
    MatButtonModule,
    MatCardModule,
    MatExpansionModule,
    MatFormFieldModule,
    MatIconModule,
    MatInputModule,
    MatSelectModule,
    MatTooltipModule,
    // ... etc.
    // cadmus
    CloseSaveButtonsComponent,
  ],
  templateUrl: "./__NAME__s-part.component.html",
  styleUrls: ["./__NAME__s-part.component.scss"],
})
export class __NAME__sPartComponent
  extends ModelEditorComponentBase<__NAME__sPart>
  implements OnInit
{
  public editedIndex: number;
  public edited: __NAME__ | undefined;

  // TODO: add your thesauri entries here, e.g.:
  // cod-binding-tags
  // public tagEntries: ThesaurusEntry[] | undefined;

  public entries: FormControl<__NAME__[]>;

  constructor(
    authService: AuthJwtService,
    formBuilder: FormBuilder,
    private _dialogService: DialogService
  ) {
    super(authService, formBuilder);
    this.editedIndex = -1;
    // form
    this.entries = formBuilder.control([], {
      // at least 1 entry
      validators: NgxToolsValidators.strictMinLengthValidator(1),
      nonNullable: true,
    });
  }

  public override ngOnInit(): void {
    super.ngOnInit();
  }

  protected buildForm(formBuilder: FormBuilder): FormGroup | UntypedFormGroup {
    return formBuilder.group({
      entries: this.entries,
    });
  }

  private updateThesauri(thesauri: ThesauriSet): void {
    // TODO setup your thesauri entries here, e.g.:
    // let key = 'cod-binding-tags';
    // if (this.hasThesaurus(key)) {
    //   this.tagEntries = thesauri[key].entries;
    // } else {
    //   this.tagEntries = undefined;
    // }
  }

  private updateForm(part?: __NAME__sPart | null): void {
    if (!part) {
      this.form.reset();
      return;
    }
    this.entries.setValue(part.__NAME__s || []);
    this.form.markAsPristine();
  }

  protected override onDataSet(data?: EditedObject<__NAME__sPart>): void {
    // thesauri
    if (data?.thesauri) {
      this.updateThesauri(data.thesauri);
    }

    // form
    this.updateForm(data?.value);
  }

  protected getValue(): __NAME__sPart {
    let part = this.getEditedPart(__NAME__S_PART_TYPEID) as __NAME__sPart;
    part.__NAME__s = this.entries.value || [];
    return part;
  }

  public add__NAME__(): void {
    const entry: __NAME__ = {
      // TODO: set your entry default properties...
    };
    this.edit__NAME__(entry, -1);
  }

  public edit__NAME__(entry: __NAME__, index: number): void {
    this.editedIndex = index;
    this.edited = entry;
  }

  public close__NAME__(): void {
    this.editedIndex = -1;
    this.edited = undefined;
  }

  public save__NAME__(entry: __NAME__): void {
    const entries = [...this.entries.value];
    if (this.editedIndex === -1) {
      entries.push(entry);
    } else {
      entries.splice(this.editedIndex, 1, entry);
    }
    this.entries.setValue(entries);
    this.entries.markAsDirty();
    this.entries.updateValueAndValidity();
    this.close__NAME__();
  }

  public delete__NAME__(index: number): void {
    this._dialogService
      .confirm("Confirmation", "Delete __NAME__?")
      .subscribe((yes: boolean | undefined) => {
        if (yes) {
          if (this.editedIndex === index) {
            this.close__NAME__();
          }
          const entries = [...this.entries.value];
          entries.splice(index, 1);
          this.entries.setValue(entries);
          this.entries.markAsDirty();
          this.entries.updateValueAndValidity();
        }
      });
  }

  public move__NAME__Up(index: number): void {
    if (index < 1) {
      return;
    }
    const entry = this.entries.value[index];
    const entries = [...this.entries.value];
    entries.splice(index, 1);
    entries.splice(index - 1, 0, entry);
    this.entries.setValue(entries);
    this.entries.markAsDirty();
    this.entries.updateValueAndValidity();
  }

  public move__NAME__Down(index: number): void {
    if (index + 1 >= this.entries.value.length) {
      return;
    }
    const entry = this.entries.value[index];
    const entries = [...this.entries.value];
    entries.splice(index, 1);
    entries.splice(index + 1, 0, entry);
    this.entries.setValue(entries);
    this.entries.markAsDirty();
    this.entries.updateValueAndValidity();
  }
}
  • 📁 list part editor HTML template:
<!-- NAME-part.component.html -->

<form [formGroup]="form" (submit)="save()">
  <mat-card>
    <mat-card-header>
      <div mat-card-avatar>
        <mat-icon>picture_in_picture</mat-icon>
      </div>
      <mat-card-title
        ></mat-card-title
      >
    </mat-card-header>
    <mat-card-content>
      <div>
        <button
          type="button"
          mat-flat-button
          color="primary"
          (click)="add__NAME__()"
        >
          <mat-icon>add_circle</mat-icon> __NAME__
        </button>
      </div>
      @if (entries.value.length) {
      <table>
        <thead>
          <tr>
            <th></th>
            TODO: add model properties
          </tr>
        </thead>
        <tbody>
          @for (entry of entries.value; track entry; let i = $index; let first =
          $first; let last = $last) {
          <tr [class.selected]="entry === edited">
            <td class="fit-width">
              <button
                type="button"
                mat-icon-button
                color="primary"
                matTooltip="Edit this __NAME__"
                (click)="edit__NAME__(entry, i)"
              >
                <mat-icon class="mat-primary">edit</mat-icon>
              </button>
              <button
                type="button"
                mat-icon-button
                matTooltip="Move this __NAME__ up"
                [disabled]="first"
                (click)="move__NAME__Up(i)"
              >
                <mat-icon>arrow_upward</mat-icon>
              </button>
              <button
                type="button"
                mat-icon-button
                matTooltip="Move this __NAME__ down"
                [disabled]="last"
                (click)="move__NAME__Down(i)"
              >
                <mat-icon>arrow_downward</mat-icon>
              </button>
              <button
                type="button"
                mat-icon-button
                color="warn"
                matTooltip="Delete this __NAME__"
                (click)="delete__NAME__(i)"
              >
                <mat-icon class="mat-warn">remove_circle</mat-icon>
              </button>
            </td>
            TODO: td's for properties
          </tr>
          }
        </tbody>
      </table>
      } @if (edited) {
      <fieldset>
        <mat-expansion-panel [expanded]="edited" [disabled]="!edited">
          <mat-expansion-panel-header>
            <mat-panel-title>__NAME__ #</mat-panel-title>
          </mat-expansion-panel-header>
          TODO: editor control with: [model]="edited"
          (modelChange)="save__NAME__($event)" (editorClose)="close__NAME__()"
        </mat-expansion-panel>
      </fieldset>
      }
    </mat-card-content>
    <mat-card-actions>
      <cadmus-close-save-buttons
        [form]="form"
        [noSave]="userLevel < 2"
        (closeRequest)="close()"
      />
    </mat-card-actions>
  </mat-card>
</form>
  • 📁 list part editor CSS styles:
table {
  width: 100%;
  border-collapse: collapse;
}
tbody tr:nth-child(odd) {
  background-color: #e2e2e2;
}
th {
  text-align: left;
  font-weight: normal;
  color: silver;
}
td.fit-width {
  width: 1px;
  white-space: nowrap;
}
tr.selected {
  background-color: #d0d0d0 !important;
}
fieldset {
  border: 1px solid silver;
  border-radius: 6px;
  padding: 6px;
}

List Entry Editor Template

Typically you should edit each single entry in a component (generated with ng g component <NAME>-editor where NAME is the model’s name, e.g. cod-binding-editor for the cod-bindings-part component - remember to export it both from the library’s module and from its barrel public-api.ts file), similar to the following template (rename model as you prefer):

  • 📁 entry editor code:
import { CommonModule } from "@angular/common";
import { Component, OnInit, effect, model, output } from "@angular/core";
import {
  FormBuilder,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from "@angular/forms";
import {
  debounceTime,
  distinctUntilChanged,
  filter,
  takeUntil,
} from "rxjs/operators";
import { Subscription } from "rxjs";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";

// material
import { MatButtonModule } from "@angular/material/button";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatIconModule } from "@angular/material/icon";
import { MatInputModule } from "@angular/material/input";
import { MatSelectModule } from "@angular/material/select";
import { MatTooltipModule } from "@angular/material/tooltip";

@Component({
  selector: "cadmus-__PRJ__-__NAME__",
  imports: [
    CommonModule,
    ReactiveFormsModule,
    MatButtonModule,
    MatCheckboxModule,
    MatFormFieldModule,
    MatIconModule,
    MatInputModule,
    MatSelectModule,
    MatTooltipModule, // ... etc.
  ],
  templateUrl: "./__NAME__.component.html",
  styleUrls: ["./__NAME__.component.scss"],
})
export class __NAME__Component {
  public readonly data = model<__TYPE__ | undefined>();
  public readonly cancelEdit = output(); // TODO: form controls...

  public form: FormGroup; // track if the form is currently being updated programmatically

  private _updatingForm = false;

  constructor(private formBuilder: FormBuilder) {
    // form
    // TODO: create controls
    this.form = formBuilder.group({
      // TODO: add controls to form model
    }); // when model changes, update form

    effect(() => {
      const data = this.data();
      this.updateForm(data);
    }); // autosave: TODO remove if manual save

    this.form.valueChanges
      .pipe(
        // react only on user changes, when form is valid
        filter(() => !this._updatingForm && this.form.valid),
        debounceTime(500), // TODO: optionally add distinctUntilChanged with a custom comparer, e.g.: // distinctUntilChanged((prev: __TYPE__, curr: __TYPE__) => { //     // perform a deep equality check on relevant properties //     return prev.name === curr.name; // }),
        takeUntilDestroyed()
      )
      .subscribe((values) => {
        // TODO: pass false if you don't consider autosave the save action
        this.save();
      });
  }

  private updateForm(data: __TYPE__ | undefined | null): void {
    this._updatingForm = true;

    if (!data) {
      this.form.reset();
    } else {
      // TODO set controls values via patch
    }

    this.form.markAsPristine();

    // reset guard only after marking controls
    this._updatingForm = false;
  }

  private getData(): __TYPE__ {
    return {
      // TODO get values from controls
    };
  }

  public cancel(): void {
    this.cancelEdit.emit();
  }  // TODO: make this private if autosave is used
  /**
   * Saves the current form data by updating the `data` model signal.
   * This method can be called manually (e.g., by a Save button) or
   * automatically (via auto-save).
   * @param pristine If true (default), the form is marked as pristine
   * after saving.
   * Set to false for auto-save if you want the form to remain dirty.
   */

  public save(pristine = true): void {
    if (this.form.invalid) {
      // show validation errors
      this.form.markAllAsTouched();
      return;
    }

    const data = this.getData();
    this.data.set(data);

    if (pristine) {
      this.form.markAsPristine();
    }
  }
}
  • 📁 HTML template:
<form [formGroup]="form" (submit)="save()">
  TODO
  <!-- buttons -->
  <div>
    <button
      type="button"
      mat-icon-button
      matTooltip="Discard changes"
      (click)="cancel()"
    >
      <mat-icon class="mat-warn">clear</mat-icon>
    </button>
    <button
      type="submit"
      mat-icon-button
      matTooltip="Accept changes"
      [disabled]="form.invalid || form.pristine"
    >
      <mat-icon class="mat-primary">check_circle</mat-icon>
    </button>
  </div>
</form>

▶️ (2) ensure the component has been added to the public-api.ts barrel file.

3. Add PG Editor Wrapper

▶️ (1) under your library’s src/lib folder, add a part editor feature component named after the part (e.g. ng g component note-part-feature for NotePartFeatureComponent after NotePart).

  • 📁 editor wrapper code:
import { Component, OnInit } from "@angular/core";
import { MatSnackBar } from "@angular/material/snack-bar";
import { Router, ActivatedRoute } from "@angular/router";

import { ItemService, ThesaurusService } from "@myrmidon/cadmus-api";
import { EditPartFeatureBase, PartEditorService } from "@myrmidon/cadmus-state";
import { CurrentItemBarComponent } from "@myrmidon/cadmus-ui-pg";

import { __NAME__PartComponent } from "@myrmidon/cadmus-lon-part-ui";

@Component({
  selector: "cadmus-__NAME__-part-feature",
  imports: [CurrentItemBarComponent, __NAME__PartComponent],
  templateUrl: "./__NAME__-part-feature.component.html",
  styleUrl: "./__NAME__-part-feature.component.css",
})
export class __NAME__PartFeatureComponent
  extends EditPartFeatureBase
  implements OnInit
{
  constructor(
    router: Router,
    route: ActivatedRoute,
    snackbar: MatSnackBar,
    itemService: ItemService,
    thesaurusService: ThesaurusService,
    editorService: PartEditorService
  ) {
    super(
      router,
      route,
      snackbar,
      itemService,
      thesaurusService,
      editorService
    );
  }

  protected override getReqThesauriIds(): string[] {
    // TODO: if role-dependent thesauri are required, add:
    // this.roleIdInThesauri = true;

    // TODO: return the IDs of all the thesauri required by the wrapped editor, e.g.:
    return ["note-tags"];
    // or just avoid overriding the function if no thesaurus required
  }
}
  • 📁 editor wrapper HTML template:
<cadmus-current-item-bar />
<cadmus-__NAME__-part
  [identity]="identity"
  [data]="$any(data)"
  (dataChange)="save($event!.value!)"
  (editorClose)="close()"
  (dirtyChange)="onDirtyChange($event)"
/>

Note that since version 12 the dataChange handler requires $event!.value! as an argument rather than just $event as before. This is because version 12 moved input/output endpoints to signals, which also implied a better alignment between the type of data and of its corresponding event.

▶️ (2) ensure that this component is exported from the public-api.ts barrel file.

4. Add Sub-Route

This is optional, and is required only when you are providing a library with a module containing sub-routes to a set of related PG editor wrapper components.

▶️ (1) add the corresponding route in the PG library’s module, e.g.:

export const RouterModuleForChild = RouterModule.forChild([
  // TODO your part route
  {
    path: `${__NAME___PART_TYPEID}/:pid`,
    pathMatch: "full",
    component: __NAME__PartFeatureComponent,
    canDeactivate: [PendingChangesGuard],
  },
]);

@NgModule({
  declarations: [__NAME__PartFeatureComponent],
  imports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    RouterModuleForChild,
  ],
  exports: [__NAME__PartFeatureComponent],
})
export class CadmusPart__PRJ__PgModule {}

5. Add Part Mapping to App

▶️ (1) In your app’s project part-editor-keys.ts, add the mapping for the part just created, like e.g.:

// this constant refers to the project-dependent portion of the route path
// (items/:iid/__PRJ__) in routes definitions
const ITINERA_LT = 'itinera_lt';

// itinera parts example
[PERSON_PART_TYPEID]: {
  part: ITINERA_LT
},

Here, the type ID of the part (from its model in the “ui” library) is mapped to the route prefix constant ITINERA_LT = itinera-lt, which is the root route to the “pg” library module for the app.

▶️ (2) Ensure that your app routes (usually app.routes.ts) include the PG component from its lazily loaded library, e.g.:

// cadmus - lon parts
{
  path: 'items/:iid/__PRJ__',
  loadComponent: () =>
    import('@myrmidon/cadmus-__PRJ__-part').then(
      (module) => module.Cadmus__PRJ____NAME__PartComponent
    ),
  canActivate: [AuthJwtGuardService],
},