@myrmidon/cadmus-thesaurus-store
v0.0.6
Published
Cadmus thesauri store components.
Readme
CadmusThesaurusStore
This project was generated using Angular CLI version 21.0.0.
This library contains components related to Cadmus thesauri and using as their core the paged tree browser from @myrmidon/paged-data-browsers.
Thesauri
Thesauri represent taxonomies of any type you might be using in editing your content. For instance, in describing a writing you might have a set of ink colors to pick from. The models for the thesaurus and its entries are defined in an external package (@myrmidon/cadmus-core).
Essentially, a thesaurus is a list of terms, each having an ID and a human-readable value. Such list can be a simple, flat list, or a hierarchical list. In both cases, they are defined as objects having each a list of entries.
Each entry in a thesaurus object has:
- an arbitrarily assigned
id, which must be unique in the context of that object; - a text
valueto be displayed to the end user.
For instance, you might have a list of languages to pick from, like English, French, Italian, etc.: in this case, your thesaurus would have its own ID, e.g. languages, plus entries like eng=English, fre=French, ita=Italian, etc. Here I'm using ISO 639 for language codes, and using standards is always recommended when available; but IDs are totally arbitrary.
You can also have hierarchical thesauri, where entries are arranged in a hierarchy represented with dots in their IDs. For instance, say you want to represent this simple 2-levels hierarchy:
- language:
- language: phonology
- language: morphology
- language: syntax
You can use a dot in each entry to represent three children entries under the same node:
[
{ "id": "lang.pho", "value": "language: phonology" },
{ "id": "lang.mor", "value": "language: morphology" },
{ "id": "lang.syn", "value": "language: syntax" }
]By convention, for each dot in the ID there is a colon in the value, so that the full hierarchy is displayed in every entry.
Should you want to have a selectable entry also for the parent language node, you just have to add another one, like this:
[
{ "id": "lang.-", "value": "language" },
{ "id": "lang.pho", "value": "language: phonology" },
{ "id": "lang.mor", "value": "language: morphology" },
{ "id": "lang.syn", "value": "language: syntax" }
]Services
A couple of services are used by the components in this library:
StaticThesaurusPagedTreeStoreService, which represents an in-memory store of the thesaurus being displayed. This service serves as an adapter from an array of thesaurus entries to the paged tree store service. Its nodes are of typeThesaurusEntryPagedTreeNodeand its filter is of typeThesaurusEntryNodeFilter.EditableStaticThesaurusPagedTreeStoreService, which is the editable counterpart ofStaticThesaurusPagedTreeStoreService. It still loads all the entries of a thesaurus at once (whence "static" in its name), but it can keep track of user edits and then commit all changes at once.
Components
The higher-level components exported by this library are listed here.
ThesaurusTreeComponent
- 🔑
cadmus-thesaurus-tree
The thesaurus tree component displays a set of hierarchical thesaurus entries in a tree, provided that each entry marks its hierarchy with dots, and allows you to pick any entry.
- ▶️ input:
entries: all the entries in the thesaurus.renderLabel: the optional function for rendering node's labels. This is typically used to shorten the label of nested entries when displaying them.
- 🔥 output:
entryChange: fired when an entry is picked.
Internally, this component uses:
- an instance of the
StaticThesaurusPagedTreeStoreService. - an instance of the
ThesaurusBrowserComponent, which is a readonly paged tree browser specialized to display thesaurus entries.
ThesaurusEntriesPickerComponent
- 🔑
cadmus-thesaurus-entries-picker
This is a more compact and powerful component which allows you to manage entries picked from a hierarchical thesaurus, which is usually collapsed so that the component uses just as much space as needed to display a list of picked entries. Each picked entry can be directly removed from the list, and new entries can be added by expanding the tree and picking them.
The component also allows users to enter custom entries beyond the thesaurus entries. This is a special functionality which must be opted-in. Such entries are all marked by an initial $ prefix and do not belong to a thesaurus.
- ▶️ input:
availableEntries: all the entries in the thesaurus. Required.entries: the entries picked.hierarchicLabels: true to show the entries with labels shortened according to their hierarchy.autoSort: true to automatically sort the picked entries (disables drag-and-drop). When false, entries can be manually reordered via drag-and-drop.allowCustom: true to allow custom values (not in the entries list).minEntries: the minimum number of picked entries.maxEntries: the maximum number of picked entries.expanded: true when the picker is expanded rather than collapsed.emptyMessage: the message to show when there are no picked entries.
- 🔥 output:
entriesChange: fired whenentrieschange.
EditableThesaurusBrowserComponent
An editable paged tree browser for thesaurus entries. Editing happens in memory and when complete, the changes can be committed to the underlying data store via the EditableStaticThesaurusPagedTreeStoreService.
- ▶️ input:
service: an instance ofEditableStaticThesPagedTreeStoreServiceused to load nodes. Required.hasRootFilter: true to show the tree root nodes filter.nodePick: fired when an entry node is clicked.
- 🔥 output:
changesStateChange: fired when the changes-state changes.
History
0.0.4
- 2026-01-31: added logging to thesaurus tree browser to diagnose issues in certain scenarios.
0.0.3
2026-01-31: fixes to thesaurus browser and editable thesaurus browser. The ThesaurusTreeComponent wasn't updating when data arrived because of an Angular effect timing issue:
The @if (service() && entries()?.length) guard in thesaurus-tree.component.html:3 only creates cadmus-thesaurus-browser once entries has data
When the component is created, the effect() in the constructor is scheduled (not run immediately)
Angular completes the initial render with empty nodes() signal
The effect then runs, loads data, and updates the nodes signal via RxJS subscription callbacks
But subscription callbacks run outside Angular's change detection zone, so no re-render is triggered
On component close, Angular's final change detection cycle picks up the changes, causing the "flash"
Solution: added ChangeDetectorRef.markForCheck() calls in the subscription callbacks (thesaurus-browser.component.ts:121-137) to explicitly notify Angular when signals are updated from async contexts.
The fix needed to address:
Signal writes inside effect execution: When subscribing to a BehaviorSubject inside an effect(), the subscription callback executes synchronously (BehaviorSubject emits its current value immediately on subscribe). This means signal writes like this.nodes.set(nodes) occur during the effect's execution.
Inconsistent markForCheck() calls: The original fix only added markForCheck() in the effect's subscriptions and the initial load's .finally(), but left other async callbacks (expand, collapse, page change, filter change) without it.
The issue appears in newer Angular apps because they may use:
- Zoneless change detection (Angular 18+)
- OnPush change detection as default in project configuration
- Different timing of change detection cycles
When RxJS subscription callbacks update Angular signals, they run outside Angular's reactive tracking context. Without explicit notification via markForCheck(), Angular may not know to re-render the component.
0.0.2
- 2026-01-31: fixes to thesaurus browser and editable thesaurus browser replacing
ngOnInitwitheffectand using signals instead of observables to avoid having side effects in computed signals.
0.0.1
- 2026-01-17:
- added
thesaurus-entries-pickermoving it from@myrmidon/cadmus-uiso that all the thesaurus components can migrate into@myrmidon/cadmus-thesaurus-storein this workspace. These components are strictly dependent from the paged tree browser in this workspace, which is a generic component. So, this Cadmus-related library is developed in this workspace, rather than in Cadmus shell workspace, as the components are essentially wrappers around the functionality developed in this workspace. - renamed thesauri components (changing "thes..." into "thesaurus..." and shortening where possible).
- added
