ngx-atomic-bindings
v1.1.1
Published
Angular directive that enables molecular components to transparently expose and forward the properties of their underlying atomic components.
Downloads
8
Maintainers
Readme
ngx-atomic-bindings
ngx-atomic-bindings enables molecular components to transparently expose and forward the properties of their underlying atomic components. Instead of manually declaring and forwarding every input and output, this directive lets you dynamically bind component properties while maintaining full type safety.
Built for Angular 18+ with modern signal-based input() and output() APIs.
⚠️ Note: This directive only works with components using signal-based inputs/outputs (
input(),output()). It does not support decorator-based@Input/@Output.
Table of Contents
- The Problem
- The Solution
- Features
- Installation
- Quick Start
- Usage Examples
- API Reference
- How It Works
- Common Use Cases
- Development
- Requirements
- License
The Problem
When building composite components in Angular, you often need to wrap reusable components and expose their properties to parent components. The traditional approach requires manually declaring and forwarding each property:
The traditional approach:
- Manually declare each input you want to expose
- Manually declare each output you want to expose
- Forward every property through template bindings
- Repeat this boilerplate for every wrapped component
- Update all forwarding code whenever the underlying component changes
This becomes a maintenance nightmare when:
- Your reusable components have many properties
- You're building component libraries with multiple composition layers
- You follow Atomic Design patterns (atoms → molecules → organisms)
- You want to expose most/all properties of a child component without repetitive code
The Solution
ngx-atomic-bindings eliminates this boilerplate by dynamically rendering components and binding their properties. You define what properties to expose once using TypeScript types, and the directive handles all the binding logic.
Before and After Example
Traditional approach - Verbose and maintenance-heavy:
// ❌ Manual property forwarding for every input/output
@Component({
selector: 'app-submit-button',
imports: [ButtonComponent],
template: `
<app-button
[variant]="variant()"
[size]="size()"
[disabled]="disabled()"
[loading]="loading()"
[icon]="icon()"
(clicked)="clicked.emit($event)">
Submit Form
</app-button>
`
})
export class SubmitButtonComponent {
// Must manually declare each property...
public variant = input<'primary' | 'secondary'>('primary');
public size = input<'small' | 'medium' | 'large'>('medium');
public disabled = input<boolean>(false);
public loading = input<boolean>(false);
public icon = input<string>();
public clicked = output<MouseEvent>();
// ...and forward each one in the template!
}With ngx-atomic-bindings - Clean and maintainable:
// ✅ One input declaration, automatic property forwarding
@Component({
selector: 'app-submit-button',
imports: [AtomicBindingsDirective],
template: `
<ng-container
[atomicBindings]="ButtonComponent"
[atomicBindingsProps]="buttonProps()">
Submit Form
</ng-container>
`
})
export class SubmitButtonComponent {
protected ButtonComponent = ButtonComponent;
// Single input that exposes ALL button properties with type safety!
public buttonProps = input<ComponentProps<ButtonComponent>>();
}What you get:
- ✨ One line of code instead of dozens
- ✨ Full type safety - TypeScript knows all valid properties
- ✨ Automatic updates - No code changes needed when ButtonComponent evolves
- ✨ Zero maintenance - No manual property forwarding
Features
- ✅ Type-safe bindings - Full TypeScript support ensures you can only pass valid properties
- ✅ Signal-based reactivity - Built with Angular signals (
input()andoutput()) for automatic updates - ✅ Dynamic props - Properties update reactively when parent signals change
- ✅ Fixed props - Lock certain properties while exposing others
- ✅ Output handling - Subscribe to component events with type-safe callbacks
- ✅ Zero dependencies - Only requires Angular core packages
- ✅ Modern Angular - Designed for Angular 18+ with signal-based APIs
- ✅ IntelliSense support - Get autocompletion for all valid component properties
Installation
npm install ngx-atomic-bindingsQuick Start
Here's a simple example to get started:
1. Create your base component with signal-based inputs/outputs
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-button',
template: `
<button
[disabled]="disabled()"
[class]="'btn btn-' + variant()"
(click)="clicked.emit($event)">
{{ label() }}
</button>
`
})
export class ButtonComponent {
public label = input<string>('Click me');
public variant = input<'primary' | 'secondary'>('primary');
public disabled = input<boolean>(false);
public clicked = output<MouseEvent>();
}2. Create a wrapper component using the directive
import { Component, input } from '@angular/core';
import { AtomicBindingsDirective, ComponentPropsExcluding } from 'ngx-atomic-bindings';
@Component({
selector: 'app-form-actions',
imports: [AtomicBindingsDirective],
template: `
<ng-container
[atomicBindings]="ButtonComponent"
[atomicBindingsProps]="buttonProps()"
[atomicBindingsFixed]="{ variant: 'primary' }">
</ng-container>
`
})
export class FormActionsComponent {
protected ButtonComponent = ButtonComponent;
// Expose all properties except 'variant' (which is fixed above)
public buttonProps = input<ComponentPropsExcluding<ButtonComponent, 'variant'>>();
}3. Use your wrapper component
@Component({
selector: 'app-form',
imports: [FormActionsComponent],
template: `
<app-form-actions
[buttonProps]="{
label: 'Submit',
disabled: isSubmitting(),
clicked: handleSubmit
}">
</app-form-actions>
`
})
export class FormComponent {
protected isSubmitting = signal(false);
protected handleSubmit = (event: MouseEvent) => {
this.isSubmitting.set(true);
// Submit form...
}
}That's it! The directive automatically binds all properties with full type safety.
Usage Examples
Example 1: Expose All Properties
Transparently forward all inputs and outputs:
@Component({
selector: 'app-primary-button',
imports: [AtomicBindingsDirective],
template: `
<ng-container
[atomicBindings]="ButtonComponent"
[atomicBindingsProps]="buttonProps()">
</ng-container>
`
})
export class PrimaryButtonComponent {
protected ButtonComponent = ButtonComponent;
public buttonProps = input<ComponentProps<ButtonComponent>>();
}Example 2: Fix Some Properties, Expose Others
Lock certain properties while exposing the rest:
@Component({
selector: 'app-danger-button',
imports: [AtomicBindingsDirective],
template: `
<ng-container
[atomicBindings]="ButtonComponent"
[atomicBindingsProps]="buttonProps()"
[atomicBindingsFixed]="{ variant: 'danger', icon: 'warning' }">
</ng-container>
`
})
export class DangerButtonComponent {
protected ButtonComponent = ButtonComponent;
// Expose all properties except variant and icon (fixed above)
public buttonProps = input<ComponentPropsExcluding<ButtonComponent, 'variant' | 'icon'>>();
}Example 3: Combine Multiple Dynamic Components
Build complex composites with multiple dynamic children:
@Component({
selector: 'app-dialog-footer',
imports: [AtomicBindingsDirective],
template: `
<!-- Cancel button -->
<ng-container
[atomicBindings]="ButtonComponent"
[atomicBindingsProps]="cancelButtonProps()"
[atomicBindingsFixed]="{ variant: 'secondary', label: 'Cancel' }">
</ng-container>
<!-- Confirm button -->
<ng-container
[atomicBindings]="ButtonComponent"
[atomicBindingsProps]="confirmButtonProps()"
[atomicBindingsFixed]="{ variant: 'primary', label: 'Confirm' }">
</ng-container>
`
})
export class DialogFooterComponent {
protected ButtonComponent = ButtonComponent;
public cancelButtonProps = input<ComponentPropsExcluding<ButtonComponent, 'variant' | 'label'>>();
public confirmButtonProps = input<ComponentPropsExcluding<ButtonComponent, 'variant' | 'label'>>();
}API Reference
Directive Inputs
| Input | Type | Required | Description |
|-------|------|----------|-------------|
| atomicBindings | Type<T> | ✅ Yes | The component class to render dynamically. Pass the class reference (e.g., ButtonComponent). |
| atomicBindingsProps | ComponentProps<T> | ❌ No | Dynamic properties to bind. Can include both inputs and outputs. These update reactively when the signal changes. |
| atomicBindingsFixed | ComponentProps<T> | ❌ No | Static properties that never change. Useful for locking certain props (like variant: 'primary') while exposing others dynamically. |
Notes:
- At least one of
atomicBindingsPropsoratomicBindingsFixedshould be provided (though not required) - If the same property exists in both
atomicBindingsFixedandatomicBindingsProps, the fixed value takes precedence - Outputs in
atomicBindingsPropsshould be functions that handle the emitted events
Type Helpers
The library provides TypeScript utilities to work with component properties safely:
ComponentProps<T>
Extracts all valid properties (inputs and outputs) from a component class.
import { ComponentProps } from 'ngx-atomic-bindings';
// Define a component
@Component({ /* ... */ })
export class ButtonComponent {
label = input<string>('Button');
variant = input<'primary' | 'secondary'>('primary');
disabled = input<boolean>(false);
clicked = output<MouseEvent>();
}
// Extract all its properties
type ButtonProps = ComponentProps<ButtonComponent>;
// Result: {
// label?: string;
// variant?: 'primary' | 'secondary';
// disabled?: boolean;
// clicked?: (event: MouseEvent) => void;
// }
// Use in component input
public buttonProps = input<ComponentProps<ButtonComponent>>();ComponentPropsExcluding<T, K>
Like ComponentProps, but excludes specific properties. Useful when you want to fix certain props and expose the rest.
import { ComponentPropsExcluding } from 'ngx-atomic-bindings';
// Exclude 'variant' from ButtonComponent props
type RestrictedProps = ComponentPropsExcluding<ButtonComponent, 'variant'>;
// Result: {
// label?: string;
// disabled?: boolean;
// clicked?: (event: MouseEvent) => void;
// }
// Exclude multiple properties
type MinimalProps = ComponentPropsExcluding<ButtonComponent, 'variant' | 'disabled'>;
// Result: {
// label?: string;
// clicked?: (event: MouseEvent) => void;
// }
// Use in component input
public buttonProps = input<ComponentPropsExcluding<ButtonComponent, 'variant'>>();When to use each:
ComponentProps<T>: When you want to expose ALL properties of the child componentComponentPropsExcluding<T, K>: When you're usingatomicBindingsFixedto lock certain properties and only want to expose the remaining ones
How It Works
The directive uses Angular's dynamic component rendering (ViewContainerRef) combined with signal-based inputs/outputs to:
- Dynamically instantiate the component specified in
[atomicBindings] - Extract input/output definitions from the component using Angular's signal APIs
- Bind dynamic props from
[atomicBindingsProps]to component inputs and subscribe to outputs - Apply fixed props from
[atomicBindingsFixed]as static values - Maintain reactivity - when props change, the bindings update automatically
All of this happens with full type safety thanks to TypeScript's type inference.
Common Use Cases
1. Component Libraries
Build a component library with base components (atoms) and specialized variants (molecules):
// Base components
ButtonComponent, InputComponent, CardComponent
// Specialized variants that expose base props
PrimaryButton, DangerButton, IconButton
FormInput, SearchInput, EmailInput
InfoCard, ProductCard, UserCard2. Atomic Design Architecture
Perfect for teams following Atomic Design methodology:
- Atoms: Base reusable components (Button, Input, Label)
- Molecules: Simple composites that wrap atoms (FormField = Label + Input + Error)
- Organisms: Complex composites (LoginForm, UserProfile, Checkout)
Each layer can expose properties from the layer below without boilerplate.
3. Design System Implementation
When implementing a design system with many component variants:
- Create base components with all possible properties
- Build semantic variants (SubmitButton, CancelButton, DeleteButton)
- Fix design tokens (colors, sizes) at the variant level
- Expose functional props (disabled, loading, onClick)
4. Reducing Maintenance Burden
When you need to:
- Quickly prototype composite components
- Avoid updating multiple forwarding layers when base components change
- Maintain a large codebase with many component abstractions
- Reduce the code review surface area (less boilerplate to review)
Development
Running the Demo
# Serve the demo application
npm run startNavigate to http://localhost:4200/ to see the demo application.
Building the Library
# Build the library
npm run buildThe build artifacts will be stored in the dist/ngx-atomic-bindings directory.
Running Tests
# Run library tests
npm run testRequirements
- Angular 18.0.0 or higher
- TypeScript 5.9.0 or higher
- Components must use signal-based inputs/outputs (
input(),output()) - the directive does not work with decorator-based@Input/@Output
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT
