The project follows a modular file organization pattern where components are organized by their functional area:
Feature modules live as top-level directories under apps/lfx-one/src/app/modules/ (not nested under a project/ parent). The tree below shows a representative slice of how each module is organized β see CLAUDE.md for the full current inventory (badges, documents, events, trainings, transactions, etc. follow the same pattern).
apps/lfx-one/src/app/modules/
βββ committees/ # Committee management
β βββ committee-dashboard/ # Main committees route component
β βββ committee-view/ # Committee detail route component
β βββ committee-manage/ # Committee create/edit
β βββ components/ # Committee-specific components
βββ dashboards/ # Role-based dashboards
β βββ board-member/ # Board member dashboard
β βββ contributor/ # Contributor dashboard
β βββ maintainer/ # Maintainer dashboard
β βββ components/ # Dashboard-specific components (drawers, cards)
βββ meetings/ # Meetings management
β βββ meetings-dashboard/ # Main meetings route component
β βββ meeting-manage/ # Meeting create/edit
β βββ meeting-join/ # Public meeting join page
β βββ meeting-not-found/ # Meeting 404 page
β βββ components/ # Meeting-specific components
βββ mailing-lists/ # Mailing lists
β βββ mailing-list-dashboard/ # Main mailing lists route component
β βββ mailing-list-view/ # Mailing list detail
β βββ mailing-list-manage/ # Mailing list create/edit
β βββ components/ # Mailing list components
βββ votes/ # Voting system
β βββ votes-dashboard/ # Main votes route component
β βββ vote-manage/ # Vote create/edit
β βββ components/ # Vote-specific components
βββ surveys/ # Survey management
β βββ surveys-dashboard/ # Main surveys route component
β βββ survey-manage/ # Survey create/edit
β βββ components/ # Survey-specific components
βββ profile/ # User profile management
β βββ profile-overview/ # Profile overview tab
β βββ manage-profile/ # Profile editing
β βββ affiliations/ # User affiliations
β βββ developer/ # Developer settings
β βββ email/ # Email management
β βββ password/ # Password management
β βββ components/ # Profile components
βββ settings/ # Application settings
β βββ settings-dashboard/ # Main settings route component
β βββ components/ # Settings-specific components
βββ pages/ # Static pages
βββ home/ # Home/projects listing
Note: Routes are FLAT under
MainLayoutComponentβ there is no/project/:slugnesting.
- Section Organization: Each major feature area (meetings, committees, etc.) has its own folder
- Route Components: Components that have routes live directly in their section folder
- Shared Components Within Section: Components used only within a section live in that section's
componentsfolder - Truly Shared Components: Only components used across multiple sections remain in
apps/lfx-one/src/app/shared/components
When importing section-specific components:
// From within the same section (e.g., committee-view importing committee-form)
import { CommitteeFormComponent } from '../components/committee-form/committee-form.component';
// From another section (e.g., project dashboard importing committee-form)
import { CommitteeFormComponent } from '../../committees/components/committee-form/committee-form.component';
// Truly shared components still use the alias
import { ButtonComponent } from '@app/shared/components/button/button.component';When creating new components, follow these guidelines:
- Route Components: If the component has its own route, place it directly in the section folder
- Section-Specific Components: If used only within one section, place in that section's
componentsfolder - Cross-Section Components: If used across multiple sections, place in
app/shared/components - UI Wrapper Components: Generic UI components (buttons, cards, etc.) always go in
app/shared/components
All PrimeNG components are abstracted through LFX wrapper components for UI library independence and consistent API.
Application Code β LFX Wrapper β PrimeNG Component β DOM
βββ Clean API βββ Abstraction βββ UI Library βββ Rendered UI
- UI Library Independence: Easy migration from PrimeNG to other libraries
- Consistent API: All components follow Angular signals pattern
- Type Safety: Proper TypeScript interfaces and validation
- Template Flexibility: Full support for all PrimeNG template options
- Brand Consistency: LFX-specific styling and behavior
Features: Intelligent priority system with automatic fallback logic
Priority Chain: image β icon β label (first character, uppercase)
// Avatar with full fallback chain
<lfx-avatar
[image]="user.picture"
[icon]="'fa-light fa-user'"
[label]="user.name"
[shape]="'circle'"
(onClick)="handleAvatarClick($event)">
</lfx-avatar>
// Component implementation with computed signals
@Component({
selector: 'lfx-avatar',
imports: [CommonModule, AvatarModule],
})
export class AvatarComponent {
// Input signals
public readonly image = input<string>('');
public readonly icon = input<string>('');
public readonly label = input<string>('');
// Error handling
private readonly imageErrorSignal = signal<boolean>(false);
// Computed display logic
public readonly displayImage = computed(() => {
return this.image() && !this.imageErrorSignal() ? this.image() : '';
});
public readonly displayIcon = computed(() => {
return !this.displayImage() && this.icon() ? this.icon() : '';
});
public readonly displayLabel = computed(() => {
const image = this.displayImage();
const icon = this.displayIcon();
const label = this.label();
if (!image && !icon && label) {
return label.charAt(0).toUpperCase();
}
return '';
});
}Additional wrappers follow the same pattern: BadgeComponent, ButtonComponent, BreadcrumbComponent, CardComponent, MenuComponent, MenubarComponent, TableComponent. Each uses input() signals for properties, output() for events, and @ContentChild with descendants: false for template projection. See the source files in app/shared/components/ for each wrapper's specific API.
Two layout components ship today under apps/lfx-one/src/app/layouts/:
The primary layout that wraps every authenticated route (header + sidebar + content outlet). Routes are flat under this layout β there is no nested /project/:slug routing pattern. Lens context (Me / Foundation / Project / Org) is supplied via LensService and the route's data.lens value rather than through nested routing.
// apps/lfx-one/src/app/app.routes.ts (excerpt)
{
path: '',
canActivate: [authGuard],
loadComponent: () => import('./layouts/main-layout/main-layout.component').then((m) => m.MainLayoutComponent),
children: [
{ path: '', pathMatch: 'full', data: { lens: 'me' }, loadComponent: () => import('./modules/dashboards/dashboard.component').then((m) => m.DashboardComponent) },
{ path: 'meetings', loadChildren: () => import('./modules/meetings/meetings.routes').then((m) => m.MEETING_ROUTES) },
// ... every feature route registers as a flat child of MainLayoutComponent
],
}Wraps the /profile sub-tree to render tabbed profile pages (overview, edit, affiliations, developer, email, password). Used only by profile-feature routes.
@Component({
selector: 'lfx-[component-name]',
imports: [CommonModule, [PrimeNGModule]],
templateUrl: './[component-name].component.html',
})
export class [ComponentName]Component {
// Input signals for all PrimeNG properties
public readonly [property] = input<[Type]>([defaultValue]);
// Output signals for all PrimeNG events
public readonly [event] = output<[EventType]>();
// Template references for content projection
@ContentChild('[templateName]', {
static: false,
descendants: false // Critical for template scoping
}) [templateName]Template?: TemplateRef<any>;
// Event handlers
protected handle[Event](event: [EventType]): void {
this.[event].emit(event);
}
}<p-[component] [property]="property()" (event)="handleEvent($event)">
<!-- For templates with context -->
<ng-template #[templateName] let-[contextVar]="[contextVar]">
<ng-container
*ngTemplateOutlet="[templateName]Template || null;
context: { $implicit: [contextVar] }">
</ng-container>
</ng-template>
<!-- For templates without context -->
<ng-template #[templateName] *ngIf="[templateName]Template">
<ng-container *ngTemplateOutlet="[templateName]Template || null"></ng-container>
</ng-template>
<!-- For default content projection -->
<ng-content></ng-content>
</p-[component]>Problem: Angular's @ContentChild by default searches through all descendant elements, which can cause template conflicts when components are nested.
Example of the Problem:
<lfx-card>
<ng-template #header>Card Header</ng-template>
<lfx-table>
<ng-template #header>Table Header</ng-template>
<!-- This conflicts! -->
</lfx-table>
</lfx-card>Solution: Always use descendants: false in all @ContentChild decorators:
// β
Correct - only finds direct child templates
@ContentChild('header', { static: false, descendants: false }) headerTemplate?: TemplateRef<any>;
// β Incorrect - searches all descendants, causing conflicts
@ContentChild('header', { static: false }) headerTemplate?: TemplateRef<any>;This ensures:
- Each wrapper component only finds its own direct child templates
- Nested components don't interfere with parent templates
- Template scoping works as expected in complex component hierarchies
Before creating a wrapper, always research the PrimeNG component thoroughly:
- Study PrimeNG Documentation: Visit the official PrimeNG documentation for the component
- Review Properties & Events: Identify all available properties, events, and methods
- Identify Templates: Check supported template options (
pTemplatedirectives) - Template Context: Study what context data templates receive
# In the apps/lfx-one directory
ng generate component shared/components/[component-name] --skip-testsNote: The
--standaloneflag is no longer needed β Angular defaults components, directives, and pipes to standalone.
// Required inputs
public readonly requiredProperty = input.required<string>();
// Optional inputs with defaults
public readonly optionalProperty = input<boolean>(false);
public readonly arrayProperty = input<Item[]>([]);
// Union type inputs with defaults
public readonly severity = input<'success' | 'info' | 'warning' | 'danger'>('info');
// Events should match PrimeNG event names exactly
public readonly onClick = output<MouseEvent>();
public readonly onSelectionChange = output<SelectionChangeEvent>();// For each pTemplate supported by the PrimeNG component
// CRITICAL: Always use descendants: false
@ContentChild('header', { static: false, descendants: false }) headerTemplate?: TemplateRef<any>;
@ContentChild('body', { static: false, descendants: false }) bodyTemplate?: TemplateRef<any>;
@ContentChild('item', { static: false, descendants: false }) itemTemplate?: TemplateRef<any>;protected handleClick(event: MouseEvent): void {
this.onClick.emit(event);
}
protected handleSelectionChange(event: SelectionChangeEvent): void {
this.onSelectionChange.emit(event);
}<p-[component] [property1]="property1()" [property2]="property2()" (onClick)="handleClick($event)" (onSelectionChange)="handleSelectionChange($event)">
<!-- Template outlets for each supported template -->
<ng-template #header *ngIf="headerTemplate">
<ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
</ng-template>
<ng-template #item let-item let-index="index">
<ng-container *ngTemplateOutlet="itemTemplate || null; context: { $implicit: item, index: index }"> </ng-container>
</ng-template>
<!-- Default content projection -->
<ng-content></ng-content>
</p-[component]>- Layout Templates:
header,footer,title,subtitle - Item Templates:
item,option,selectedItem(receive context data) - Content Templates:
start,end,content - State Templates:
empty,loading,error - Navigation Templates:
paginatorleft,paginatorright,summary
- Research: Check PrimeNG documentation for all properties, events, and templates
- Dependencies: Identify required PrimeNG modules and imports
- Context: Understand template context structure from PrimeNG source
- Component Selector: Use
lfx-prefix (enforced by ESLint) - Standalone Component: Import dependencies explicitly
- Input Signals: Use
input()andinput.required()for properties with proper types - Output Signals: Use
output()for events with correct event types - Template References: Use
@ContentChild()for all template references - Template Scoping: CRITICAL - Always use
descendants: falsein@ContentChild() - Context: Ensure template context matches PrimeNG's structure exactly
- Fallbacks: Use
|| nullfor template outlets to handle undefined cases
- Type Safety: Import and use interfaces from
@lfx-one/sharedpackage - Event Handling: Proper event emission and parameter passing
- Accessibility: Include ARIA labels and roles where applicable
- Nested Testing: Test component works when nested with other wrappers
- Build Verification: Ensure build passes and no TypeScript errors
- Documentation: Update this documentation with usage examples
- Shared Interfaces: Add any new interfaces to
@lfx-one/shared/interfaces - Export Path: Ensure component is exported correctly
- Usage Guidelines: Update project documentation
- Component Hierarchy: Verify component fits properly in app structure
AppComponent
βββ RouterOutlet
βββ MainLayoutComponent (authGuard protected, provides header + sidebar + content area)
βββ / β DashboardComponent (role-based dashboard)
βββ /projects β HomeComponent (project listing)
βββ /meetings β MeetingsDashboardComponent (lazy loaded)
βββ /groups β CommitteeDashboardComponent (lazy loaded)
βββ /mailing-lists β MailingListDashboardComponent (lazy loaded)
βββ /votes β VotesDashboardComponent (lazy loaded)
βββ /surveys β SurveysDashboardComponent (lazy loaded)
βββ /settings β SettingsDashboardComponent (lazy loaded)
βββ /profile β ProfileOverviewComponent (lazy loaded)
Standalone routes (outside MainLayoutComponent):
βββ /meetings/not-found β MeetingNotFoundComponent
βββ /meetings/:id β MeetingJoinComponent (public meeting join)
- Always use LFX wrapper components instead of PrimeNG directly
- Import wrapper components directly - no barrel exports
- Follow the established patterns for consistency
- Use shared interfaces for type safety
- Support template projection for flexibility
- Maintain accessibility standards
- Test component isolation and integration
- Follow module organization - place components in section-specific folders when appropriate
- Minimize shared components - only truly cross-cutting components belong in shared/components