# Angular Primitives Documentation
> Angular Primitives is a collection of unstyled, accessible Angular components and utilities for building design systems and web applications. Built with Angular, TypeScript, and a focus on accessibility, customization, and developer experience. It provides headless UI primitives that can be styled with any CSS framework or design system.
Generated on: 2026-01-20T10:15:01.614Z
---
## Getting-started
### Get Started
File: getting-started/get-started.md
URL: https://angularprimitives.com/getting-started/get-started
# Get Started
## Installation
Angular Primitives is distributed as a single package with entrypoints for each primitive.
This makes it easy to install and update, while keeping the bundle size as small as possible.
To install Angular Primitives using the Angular CLI, run the following command:
```bash npm
ng add ng-primitives
```
This command will install Angular Primitives and add the necessary dependencies to your project.
To install Angular Primitives using Nx, run the following command:
```bash npm
nx add ng-primitives
```
This command will install Angular Primitives and add the necessary dependencies to your project.
To manually install Angular Primitives, run the following command:
```bash npm
npm i ng-primitives
```
## Peer Dependencies
Angular Primitives relies on a few peer dependencies that need to be installed. If you are using NPM version 7 or higher, these dependencies will be installed automatically. If you are using an older version of NPM, or a different package manager, you may need to install them manually.
| Package | Version |
| ---------------- | -------- |
| @angular/cdk | >=20.0.0 |
| @floating-ui/dom | >=1.6.0 |
To install these dependencies, run the following command:
```bash
npm i @angular/cdk@^21.0.0 @floating-ui/dom@^1.6.0
```
## Usage
Once Angular Primitives is installed, you can start using the primitives in your Angular application.
It is best to create a reusable component that encapsulates the primitives, so that you can easily reuse it throughout your application.
Primitives can be used both within a template or as a host directive on a component giving you the flexibility to choose the best approach for your use case.
Primitives add `data-` attributes to the elements they are applied to based on their current state. This allows you to easily style the primitives using CSS without having to rely on JavaScript.
---
### Introduction
File: getting-started/introduction.md
URL: https://angularprimitives.com/getting-started/introduction
# Introduction
Angular Primitives is a low-level headless UI component library with a focus on accessibility, customization, and developer experience. Whether you're building a robust design system from scratch or looking to enhance your existing one, our primitives are here to support you every step of the way.
## Key Features
1. **Accessibility First:** We believe that all users should have equal access to information and functionality. Our primitives are crafted with accessibility best practices, ensuring that your applications are inclusive and usable by everyone.
2. **Customization Made Easy:** We understand the importance of maintaining a consistent look and feel across your applications. With Angular Primitives, we don't provide any styles, you provide your own to seamlessly blend with your brand's identity and visual guidelines.
3. **Developer Experience:** We know developers value simplicity and efficiency. Our library is designed to be intuitive and straightforward, making it a breeze for you to integrate and work with our primitives in your Angular projects. We follow the best practices and standards to ensure the highest quality code and documentation and support SSR (Server-Side Rendering) and Zoneless out of the box.
## Versioning
Angular Primitives follow [Semantic Versioning](https://semver.org/). As a result you can expect the following:
- **Patch releases** contain bug fixes and minor improvements.
- **Minor releases** contain new features and improvements.
- **Major releases** contain breaking changes.
We release a new major version each time Angular releases a new major version.
This ensures that Angular Primitives is always up-to-date with the latest Angular features and best practices.
## Compatibility
Angular Primitives versions are compatible with the following Angular versions:
| Angular Primitives | Angular |
| ------------------ | -------------- |
| 0.1.0 - 0.18.0 | 18.x.x |
| 0.19.0+ | 19.x.x, 20.x.x |
| 0.90.0+ | 20.x.x, 21.x.x |
### Version Support Policy
Starting from v1.0.0, Angular Primitives will support the two latest major Angular versions. For example, when Angular 21 is released, Angular Primitives will support Angular 20 and 21. New major versions will be supported upon release (typically every six months), ensuring compatibility with the latest Angular features while allowing you to upgrade independently.
## Community
If you would like to stay up-to-date with the latest news and updates, or have any questions, feedback, or suggestions, we invite you to join our community on [Discord](https://discord.gg/NTdjc5r3gC).
## Acknowledgements
Angular Primitives would not have been possible without inspiration from many of the great libraries that came before it.
We would like to thank the following projects for their contributions to the open-source community:
- [Angular CDK](https://material.angular.io/cdk)
- [Radix UI](https://radix-ui.com/)
- [Headless UI](https://headlessui.com/)
- [React Aria](https://react-spectrum.adobe.com/react-aria/)
## Support
If you have any questions, feedback, or need help, feel free to reach out to us on [GitHub](https://github.com/ng-primitives/ng-primitives). We're always happy to help and improve our library based on your feedback.
If you're interested in contributing to Angular Primitives, please check out our [Contributing Guide](https://github.com/ng-primitives/ng-primitives/blob/main/CONTRIBUTING.md).
If you would like to support Angular Primitives to help us maintain and improve the library, please consider [sponsoring us](https://github.com/sponsors/ng-primitives). Your support is greatly appreciated!
---
### llms.txt
File: getting-started/llms.md
URL: https://angularprimitives.com/getting-started/llms
# llms.txt
Angular Primitives provides comprehensive documentation files optimized for Large Language Models (LLMs) and AI assistants. These files enable AI tools to understand the component library and provide better assistance with code generation, documentation, and implementation guidance.
## Available Files
### llms.txt
A structured index file that provides an overview of all components, organized by category. This file follows industry standards and includes direct links to the live documentation.
- **File Size:** ~8KB
- **Purpose:** Quick reference and component discovery
- **Best For:** Initial exploration and finding specific components
Download llms.txt
### llms-full.txt
A comprehensive documentation file containing the complete content of all documentation pages, including:
- Full component documentation
- Complete TypeScript source code for reusable components
- API reference tables with inputs, outputs, and properties
- Usage examples and implementation details
The llms-full.txt file is ideal for in-depth understanding and reference when working with Angular Primitives, however, due to its size, it may consume more of your AI tool's context window.
- **File Size:** ~500KB
- **Purpose:** Complete reference with all implementation details
- **Best For:** In-depth analysis and code generation, offline use or use in environments with limited internet access
Download llms-full.txt
## Usage with AI Tools
### Cursor
Enhance your Angular Primitives development with Cursor's `@Docs` feature:
1. Type `@Docs` in your chat prompt
2. Reference the Angular Primitives documentation: `https://angularprimitives.com/assets/llms/llms.txt`
3. Ask questions about specific primitives, implementation patterns, or accessibility features
Example: _"@Docs How do I implement a custom button using Angular Primitives button primitive?"_
### Windsurf
Integrate Angular Primitives documentation in Windsurf:
- Use `https://angularprimitives.com/assets/llms/llms.txt` in your prompts for direct reference
- Add the documentation URL to your `.windsurfrules` file for persistent access across sessions
- Leverage the comprehensive API documentation for accurate code generation
### ChatGPT & Claude
Maximize AI assistance when working with Angular Primitives:
- Specify that you're building with **Angular Primitives** (headless UI library)
- Include the documentation URL: `https://angularprimitives.com/assets/llms/llms.txt` in your conversations
- The AI can then provide context-aware suggestions for primitive usage, accessibility patterns, and Angular-specific implementations
### GitHub Copilot
Copilot doesn't currently support external documentation, but you can still enhance its suggestions by:
- Mentioning that you're using **Angular Primitives** in your code comments
- Providing snippets of relevant documentation in your comments for context
---
### MCP
File: getting-started/mcp.md
URL: https://angularprimitives.com/getting-started/mcp
# Model Context Protocol (MCP)
The Angular Primitives MCP Server allows AI assistants to interact with our headless UI primitives. You can browse available components, search for specific ones, and get implementation help using natural language.
For example, you can ask an AI assistant to _"Show me all form primitives"_ or _"Add a dialog component to my Angular app"_ or _"Get the API details for the button primitive"_.
## Configuration
### Automatic Setup
```bash
npx ng g ng-primitives:mcp-setup
```
Select your AI tools and the schematic will create the necessary configuration files.
### Manual Setup
First, install the `@ng-primitives/mcp` package:
```bash
npm install @ng-primitives/mcp
```
Then, create the configuration file for your AI tool:
Create `.mcp.json` in your project root:
```json
{
"mcpServers": {
"ngp-mcp": {
"command": "npx",
"args": ["-y", "@ng-primitives/mcp"]
}
}
}
```
Create `.cursor/mcp.json` in your project root:
```json
{
"mcpServers": {
"ngp-mcp": {
"command": "npx",
"args": ["-y", "@ng-primitives/mcp"]
}
}
}
```
Create `.vscode/mcp.json` in your project root:
```json
{
"servers": {
"ngp-mcp": {
"command": "npx",
"args": ["-y", "@ng-primitives/mcp"]
}
}
}
```
Create `.codex/config.toml` in your project root:
```toml
[mcp_servers.ngp-mcp]
command = "npx"
args = ["-y", "@ng-primitives/mcp"]
```
## Example Usage
- _"Show me all form primitives"_
- _"Add a dialog to my Angular app"_
- _"What are the button primitive's inputs?"_
- _"Generate the command to create a reusable button component"_
- _"Show me reusable component implementations"_
## Troubleshooting
**MCP not responding?**
- Restart your AI tool after configuration changes
- Verify your configuration file is valid JSON/TOML
---
### Styling
File: getting-started/styling.md
URL: https://angularprimitives.com/getting-started/styling
# Styling
Unlike traditional component libraries, Angular Primitives doesn't come with any built-in styles. This is because no matter how flexible a component library's theming system is, it can never meet the needs of every project or design system.
Angular Primitives gives you a set of basic building blocks that you can structure however you want, letting you style them exactly as you like.
Sure, this means you'll need to put in more effort than you would with an off the shelf component library. If you're just looking to quickly build something without worrying much about its appearance, Angular Primitives might not be the best fit. But if you want a way to quickly create custom components that look and behave exactly the way you want, Angular Primitives is a great choice.
## Approach
Angular Primitives has an opinionated way of handling styling. Each primitive uses `data-*` attributes to reflect its important state. This lets you style the primitives based on their state using CSS, so you don't have to manually toggle classes.
This works with any styling system, whether you're using plain CSS, SCSS, or a utility-first CSS framework like Tailwind CSS.
## Example
If you are using CSS or SCSS, you can style the primitives like this:
```scss
button[data-selected] {
background-color: blue;
color: white;
}
button[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
```
If you are using Tailwind CSS, you can style the primitives like this:
```html
```
---
### Usage
File: getting-started/usage.md
URL: https://angularprimitives.com/getting-started/usage
# Usage
Angular Primitives are built entirely using Angular directives, offering a flexible and composable way to enhance your components.
There are two main ways to use them:
- Apply the directives directly in your component templates.
- Use the directives as **host directives** in your own components.
Let’s explore both approaches, along with some limitations and the solutions we provide.
---
## Using Directives in Templates
To use a directive in a template, simply apply it to the element you want to enhance. For example, here’s how you might use the `ngpSwitch` directive:
```html
```
This applies the `ngpSwitch` directive to the button, enabling correct behavior and accessibility. You have full control over the inputs and outputs, making this approach ideal for many use cases.
However, this method has a few limitations. Some directives — like `ngpButton` — are intended to be used on specific elements (e.g. `button`). Wrapping such elements in your own component can make it difficult for consumers to add attributes like `type`, since you'd need to expose every possible attribute as an input manually.
To solve this, you can use **host directives**.
## Using Directives as Host Directives
Host directives let you enhance your components by attaching existing directives at the class level. Here's how you can use `ngpButton` as a host directive:
```typescript
import { Component } from '@angular/core';
import { NgpButton } from 'ng-primitives/button';
@Component({
selector: 'button[my-button]',
template: `
`,
hostDirectives: [NgpButton],
})
export class MyButtonComponent {
// You can add any additional inputs or outputs you want here
}
```
This approach allows consumers to apply attributes directly to the `button` element while still benefiting from the functionality provided by the directive.
You can also expose specific inputs and outputs from the directive to your component. For example:
```typescript
import { Component, Input } from '@angular/core';
import { NgpButton } from 'ng-primitives/button';
@Component({
selector: 'button[my-button]',
template: `
`,
hostDirectives: [{ directive: NgpButton, inputs: ['disabled'] }],
})
export class MyButtonComponent {}
```
This lets consumers bind to the `disabled` input just as if they were using `ngpButton` directly.
## Limitations & Solutions
### Programmatically setting inputs
A key limitation of host directives in Angular is that their inputs can't be set programmatically within the component. To address this, we provide two main solutions:
#### Config Providers
Some directives support configuration providers that let you set default values. For example, to configure the default expansion type for `ngpAccordion`:
```typescript
@Component({
selector: 'my-accordion',
template: `
`,
hostDirectives: [{ directive: NgpAccordion, inputs: ['type'] }],
providers: [provideAccordionConfig({ type: 'multiple' })],
})
export class MyAccordionComponent {}
```
Consumers can still override this default by explicitly binding to the `type` input.
#### State Providers
Other directives provide a **state provider**, allowing you to programmatically control their inputs. For example:
```typescript
import { Component } from '@angular/core';
import { NgpSwitch, injectSwitchState } from 'ng-primitives/switch';
@Component({
selector: 'my-switch',
template: `
`,
hostDirectives: [NgpSwitch],
})
export class MySwitchComponent {
private readonly state = injectSwitchState();
constructor() {
this.state.checked.set(true);
}
}
```
Internally, all directive inputs are converted into **linked signals**, enabling both binding and programmatic updates.
### Splitting into Multiple Components
Some features are best implemented as multiple components. Take an accordion, for example:
```html
Content for item 1
Content for item 2
```
If you're using `ngpAccordion` as a directive in the `my-accordion` template, the child `my-accordion-item` components won’t be able to locate it in the dependency injection tree. This is a known Angular limitation.
To solve this, Angular Primitives supports **state hoisting**, which allows you to share directive state between components.
#### State Hoisting Example
To hoist the state of `ngpAccordion` from the `my-accordion` component:
```typescript
import { Component } from '@angular/core';
import { NgpAccordion, provideAccordionState } from 'ng-primitives/accordion';
@Component({
selector: 'my-accordion',
template: `
`,
providers: [provideAccordionState()],
})
export class MyAccordionComponent {}
```
Now, child directives like `ngpAccordionItem` can correctly locate and interact with the hoisted `ngpAccordion` state.
---
## Interactions
### Focus Visible
File: interactions/focus-visible.md
URL: https://angularprimitives.com/interactions/focus-visible
# Focus Visible
Determine whether an element should display a visible focus indicator and exposes the origin of that focus (keyboard, mouse, touch, program).
This is useful for accessibility and user experience, as it allows you to provides accessible visual feedback aligned with the `:focus-visible` standard.
## Example
```typescript
import { Component } from '@angular/core';
import { NgpFocusVisible } from 'ng-primitives/interactions';
@Component({
selector: 'app-focus-visible',
imports: [NgpFocusVisible],
template: `
`,
styles: `
button {
padding-left: 1rem;
padding-right: 1rem;
border-radius: 0.5rem;
color: var(--ngp-text-primary);
outline: none;
height: 2.5rem;
font-weight: 500;
background-color: var(--ngp-background);
border: 1px solid var(--ngp-border);
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--ngp-shadow);
}
button[data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
`,
})
export default class FocusVisibleExample {}
```
## Import
Import the Focus Visible primitives from `ng-primitives/interactions`.
```ts
import { NgpFocusVisible } from 'ng-primitives/interactions';
```
## Usage
Assemble the focus-visible directives in your template.
```html
```
## API Reference
The following directives are available to import from the `ng-primitives/interactions` package:
### NgpFocusVisible
## API Reference
### NgpFocusVisible
Apply the `ngpFocusVisible` directive to an element that should be visually focused. This is similar to `ngpFocus`
but it will only apply the focus visible styles when the element is focused via keyboard navigation.
**Selector:** `[ngpFocusVisible]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpFocusVisibleDisabled` | `boolean` | `-` | Whether focus events are listened to. |
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpFocusVisible` | `boolean` | Emit when the element is visually focused. |
#### Export As
`ngpFocusVisible`
#### Data Attributes
The following data attributes are applied to the `ngpFocusVisible` directive:
| Attribute | Description | Value |
| -------------------- | ------------------------------------------------------------------ | --------------------------------------- |
| `data-focus-visible` | Applied when the element should display a visible focus indicator. | `keyboard \| mouse \| touch \| program` |
### Disabling Focus Visible Interaction
Many primitives automatically add focus handling to components. If you want to disable focus handling, either globally or on a per-component/per-directive basis, you can do so by registering the `provideInteractionConfig` provider and setting the `focusVisible` option to `false`.
```ts
import { provideInteractionConfig } from 'ng-primitives/interactions';
providers: [
provideInteractionConfig({
focusVisible: false,
}),
],
```
---
### Focus
File: interactions/focus.md
URL: https://angularprimitives.com/interactions/focus
# Focus
Normalizes the focus event across different browsers and devices.
## Example
```typescript
import { Component, signal } from '@angular/core';
import { NgpFocus } from 'ng-primitives/interactions';
@Component({
selector: 'app-focus',
imports: [NgpFocus],
template: `
Input is {{ isFocused() ? 'focused' : 'blurred' }}.
`,
styles: `
:host {
display: flex;
flex-direction: column;
}
input {
height: 36px;
padding: 0 12px;
border: 1px solid var(--ngp-border);
border-radius: 0.5rem;
box-shadow: var(--ngp-shadow);
outline: none;
}
input[data-focus] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
p {
font-size: 0.75rem;
color: var(--ngp-text-secondary);
margin-top: 0.25rem;
}
`,
})
export default class FocusExample {
/**
* Whether the input is currently focused.
*/
readonly isFocused = signal(false);
}
```
## Import
Import the Focus primitives from `ng-primitives/interactions`.
```ts
import { NgpFocus } from 'ng-primitives/interactions';
```
## Usage
Assemble the focus directives in your template.
```html
```
## API Reference
The following directives are available to import from the `ng-primitives/interactions` package:
### NgpFocus
## API Reference
### NgpFocus
This was inspired by the React Aria useFocus hook.
https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/interactions/src/useFocus.ts#L20
**Selector:** `[ngpFocus]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpFocusDisabled` | `boolean` | `-` | Whether listening for focus events is disabled. |
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpFocus` | `boolean` | Emit when the focus state changes. |
#### Export As
`ngpFocus`
#### Data Attributes
The following data attributes are applied to the `ngpFocus` directive:
| Attribute | Description |
| ------------ | ------------------------------------ |
| `data-focus` | Applied when the element is focused. |
### Disabling Focus Interaction
Many primitives automatically add focus handling to components. If you want to disable focus handling, either globally or on a per-component/per-directive basis, you can do so by registering the `provideInteractionConfig` provider and setting the `focus` option to `false`.
```ts
import { provideInteractionConfig } from 'ng-primitives/interactions';
providers: [
provideInteractionConfig({
focus: false,
}),
],
```
---
### Hover
File: interactions/hover.md
URL: https://angularprimitives.com/interactions/hover
# Hover
Normalizes the hover event across different browsers and devices.
## Example
```typescript
import { Component, signal } from '@angular/core';
import { NgpHover } from 'ng-primitives/interactions';
@Component({
selector: 'app-hover',
imports: [NgpHover],
styles: `
div {
display: flex;
width: 10rem;
height: 6rem;
background-color: var(--ngp-background);
border: 1px solid var(--ngp-border);
align-items: center;
justify-content: center;
border-radius: 0.5rem;
box-shadow: var(--ngp-shadow);
transition: all 0.2s;
cursor: pointer;
}
div[data-hover] {
background-color: var(--ngp-background-blue);
border-color: var(--ngp-border-blue);
}
`,
template: `
{{ isHovering() ? 'Hovering' : 'Not Hovering' }}
`,
})
export default class HoverExample {
isHovering = signal(false);
}
```
## Import
Import the Hover primitive from `ng-primitives/interactions`.
```ts
import { NgpHover } from 'ng-primitives/interactions';
```
## Usage
Assemble the hover directives in your template.
```html
```
## API Reference
The following directives are available to import from the `ng-primitives/interactions` package:
### NgpHover
## API Reference
### NgpHover
Apply the `ngpHover` directive to an element that you want to listen for hover events. T
his is particulaly useful for supporting hover events on touch devices, where hover events are not handled consistently.
On iOS relying on the `:hover` pseudo-class can result in the hover state being stuck until the user taps elsewhere on the screen.
**Selector:** `[ngpHover]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpHoverDisabled` | `boolean` | `-` | Whether hoving should be disabled. |
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpHoverStart` | `void` | Emit an event when hovering starts. |
| `ngpHoverEnd` | `void` | Emit an event when hovering ends. |
| `ngpHover` | `boolean` | Emit an event when the hover state changes. |
#### Export As
`ngpHover`
#### Data Attributes
| Attribute | Description |
| ------------ | ------------------------------------------ |
| `data-hover` | Added to the element when hovering occurs. |
### Disabling Hover Interaction
Many primitives automatically add hover handling to components. If you want to disable hover handling, either globally or on a per-component/per-directive basis, you can do so by registering the `provideInteractionConfig` provider and setting the `hover` option to `false`.
```ts
import { provideInteractionConfig } from 'ng-primitives/interactions';
providers: [
provideInteractionConfig({
hover: false,
}),
],
```
---
### Move
File: interactions/move.md
URL: https://angularprimitives.com/interactions/move
# Move
The Move primitives enable pointer and keyboard move interactions on an element.
## Example
```typescript
import { Component, signal } from '@angular/core';
import { NgpMove, NgpMoveEvent } from 'ng-primitives/interactions';
@Component({
selector: 'app-move',
imports: [NgpMove],
template: `
Move me!
`,
styles: `
div {
padding: 1rem;
border-radius: 0.5rem;
color: var(--ngp-text-primary);
border: 1px solid var(--ngp-border);
font-weight: 500;
background-color: var(--ngp-background);
box-shadow: none;
cursor: move;
user-select: none;
touch-action: none;
position: absolute;
width: 100px;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
outline: none;
}
div:focus-visible {
outline: 2px solid var(--ngp-focus-ring);
}
div[data-move] {
box-shadow: var(--ngp-button-shadow);
}
`,
})
export default class MoveExample {
readonly x = signal(60);
readonly y = signal(60);
onMove(event: NgpMoveEvent) {
this.x.update(x => x + event.deltaX);
this.y.update(y => y + event.deltaY);
}
}
```
## Import
Import the Move primitives from `ng-primitives/interactions`.
```ts
import { NgpMove } from 'ng-primitives/interactions';
```
## Usage
Assemble the move directives in your template.
```html
```
## API Reference
The following directives are available to import from the `ng-primitives/interactions` package:
### NgpMove
## API Reference
### NgpMove
The `NgpMove` directive is used to enable the pointer and keyboard move interactions on an element.
**Selector:** `[ngpMove]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpMoveDisabled` | `boolean` | `-` | Whether movement is disabled. |
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpMoveStart` | `NgpMoveBaseEvent` | Emit when the move event begins. |
| `ngpMove` | `NgpMoveEvent` | Emit when the element is moved. |
| `ngpMoveEnd` | `NgpMoveBaseEvent` | Emit when the move event ends. |
#### Export As
`ngpMove`
#### Data Attributes
The following data attributes are available to use with the `NgpMove` directive:
| Attribute | Description |
| ----------- | ---------------------------------------- |
| `data-move` | Applied when the element is being moved. |
---
### Press
File: interactions/press.md
URL: https://angularprimitives.com/interactions/press
# Press
Normalizes the press event across different browsers and devices.
## Example
```typescript
import { Component, signal } from '@angular/core';
import { NgpPress } from 'ng-primitives/interactions';
@Component({
selector: 'app-press',
imports: [NgpPress],
styles: `
div {
display: flex;
width: 10rem;
height: 6rem;
background-color: var(--ngp-background);
align-items: center;
justify-content: center;
border-radius: 0.5rem;
box-shadow: none;
transition: all 0.2s;
cursor: pointer;
user-select: none;
border: 1px solid var(--ngp-border);
}
div[data-press] {
background-color: var(--ngp-background-active);
box-shadow: var(--ngp-button-shadow);
}
`,
template: `
{{ isPressed() ? 'Pressed' : 'Not Pressed' }}
`,
})
export default class PressExample {
isPressed = signal(false);
}
```
## Import
Import the Press primitives from `ng-primitives/interactions`.
```ts
import { NgpPress } from 'ng-primitives/interactions';
```
## Usage
Assemble the press directives in your template.
```html
```
## API Reference
The following directives are available to import from the `ng-primitives/interactions` package:
### NgpPress
## API Reference
### NgpPress
The `ngpPress` directive listens for press events on an element. This is particularly useful for supporting press events on touch devices, where press events are not handled consistently.
**Selector:** `[ngpPress]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpPressDisabled` | `boolean` | `-` | Whether listening for press events is disabled. |
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpPressStart` | `void` | Emit when the press begins. |
| `ngpPressEnd` | `void` | Emit when the press ends. |
| `ngpPress` | `boolean` | Emit when the press changes. |
#### Export As
`ngpPress`
#### Data Attributes
| Attribute | Description |
| ------------ | ------------------------------------------ |
| `data-press` | Applied when the element is being pressed. |
### Disabling Press Interaction
Many primitives automatically add press handling to components. If you want to disable press handling, either globally or on a per-component/per-directive basis, you can do so by registering the `provideInteractionConfig` provider and setting the `press` option to `false`.
```ts
import { provideInteractionConfig } from 'ng-primitives/interactions';
providers: [
provideInteractionConfig({
press: false,
}),
],
```
---
## Primitives
### Accordion
File: primitives/accordion.md
URL: https://angularprimitives.com/primitives/accordion
# Accordion
Display a series of panels that can be expanded or collapsed.
## Example
```typescript
import { Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroChevronDownMini } from '@ng-icons/heroicons/mini';
import {
NgpAccordion,
NgpAccordionContent,
NgpAccordionItem,
NgpAccordionTrigger,
} from 'ng-primitives/accordion';
import { NgpButton } from 'ng-primitives/button';
@Component({
selector: 'app-accordion',
imports: [
NgpButton,
NgIcon,
NgpAccordion,
NgpAccordionItem,
NgpAccordionContent,
NgpAccordionTrigger,
],
providers: [provideIcons({ heroChevronDownMini })],
styles: `
:host {
display: flex;
justify-content: center;
width: 100%;
}
[ngpAccordion] {
width: 100%;
max-width: 24rem;
border-radius: 0.75rem;
border: 1px solid var(--ngp-border);
background-color: var(--ngp-background);
box-shadow: var(--ngp-shadow);
}
[ngpAccordionItem]:has(+ [ngpAccordionItem]) {
border-bottom: 1px solid var(--ngp-border);
}
/* Reset default heading margins inside accordion items to avoid layout shifts */
[ngpAccordionItem] h3 {
margin: 0;
}
[ngpAccordionTrigger] {
display: flex;
padding-left: 1rem;
padding-right: 1rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
justify-content: space-between;
align-items: center;
width: 100%;
height: 2.75rem;
border-radius: 0.75rem;
outline: none;
color: var(--ngp-text-primary);
background-color: var(--ngp-background);
border: none;
}
[ngpAccordionTrigger][data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
}
[ngpAccordionContent] {
font-size: 0.875rem;
color: var(--ngp-text-secondary);
overflow: hidden;
}
[ngpAccordionContent][data-open] {
animation: slideDown 0.2s ease-in-out forwards;
}
[ngpAccordionContent][data-closed] {
animation: slideUp 0.2s ease-in-out forwards;
}
.accordion-content {
padding: 0 16px 16px;
}
ng-icon {
font-size: 1.25rem;
color: var(--ngp-text-secondary);
}
[ngpAccordionTrigger][data-open] ng-icon {
transform: rotate(180deg);
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--ngp-accordion-content-height);
}
}
@keyframes slideUp {
from {
height: var(--ngp-accordion-content-height);
}
to {
height: 0;
}
}
`,
template: `
If you would like to learn more please reach out to us on GitHub.
Yes, this is open source and you can use it in your project.
`,
})
export default class AccordionExample {}
```
## Import
Import the Accordion primitives from `ng-primitives/accordion`.
```ts
import {
NgpAccordion,
NgpAccordionItem,
NgpAccordionTrigger,
NgpAccordionContent,
} from 'ng-primitives/accordion';
```
## Usage
Assemble the accordion directives in your template.
```html
If you would like to learn more please reach out to us on GitHub.
Yes, this is open source and you can use it in your project.
```
## Reusable Component
Create reusable components that uses the `NgpAccordion` and `NgpAccordionItem` directives.
## Reusable Component
```typescript
import { Component } from '@angular/core';
import { NgpAccordion } from 'ng-primitives/accordion';
@Component({
selector: 'app-accordion',
hostDirectives: [
{
directive: NgpAccordion,
inputs: [
'ngpAccordionValue:value',
'ngpAccordionType:type',
'ngpAccordionCollapsible:collapsible',
'ngpAccordionDisabled:disabled',
'ngpAccordionOrientation:orientation',
],
},
],
template: `
`,
styles: `
:host {
display: block;
width: 100%;
max-width: 24rem;
border-radius: 0.75rem;
border: 1px solid var(--ngp-border);
background-color: var(--ngp-background);
box-shadow: var(--ngp-shadow);
}
`,
})
export class Accordion {}
```
## Schematics
Generate a reusable accordion component using the Angular CLI.
```bash npm
ng g ng-primitives:primitive accordion
```
### Options
- `path`: The path at which to create the component file.
- `prefix`: The prefix to apply to the generated component selector.
- `componentSuffix`: The suffix to apply to the generated component class name.
- `fileSuffix`: The suffix to apply to the generated component file name. Defaults to `component`.
- `exampleStyles`: Whether to include example styles in the generated component file. Defaults to `true`.
## API Reference
The following directives are available to import from the `ng-primitives/accordion` package:
### NgpAccordion
## API Reference
### NgpAccordion
Apply the `ngpAccordion` directive to an element that represents the group of accordion items.
**Selector:** `[ngpAccordion]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpAccordionType` | `NgpAccordionType` | `-` | The type of the accordion. |
| `ngpAccordionCollapsible` | `boolean` | `-` | Whether the accordion is collapsible. |
| `ngpAccordionValue` | `T | T[] | null` | `-` | The value of the accordion. |
| `ngpAccordionDisabled` | `boolean` | `-` | Whether the accordion is disabled. |
| `ngpAccordionOrientation` | `NgpOrientation` | `-` | The accordion orientation. |
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpAccordionValueChange` | `T | T[] | null` | Event emitted when the accordion value changes. |
#### Export As
`ngpAccordion`
#### Data Attributes
The following data attributes are applied to the `ngpAccordion` directive:
| Attribute | Description | Value |
| ------------------ | ------------------------------------- | -------------------------- |
| `data-orientation` | The orientation of the accordion. | `horizontal` \| `vertical` |
| `data-disabled` | Applied when the element is disabled. | `-` |
### NgpAccordionItem
## API Reference
### NgpAccordionItem
Apply the `ngpAccordionItem` directive to an element that represents an accordion item.
**Selector:** `[ngpAccordionItem]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpAccordionItemValue` | `T` | `-` | The value of the accordion item. |
| `ngpAccordionItemDisabled` | `boolean` | `-` | Whether the accordion item is disabled. |
#### Export As
`ngpAccordionItem`
#### Data Attributes
The following data attributes are applied to the `ngpAccordionItem` directive:
| Attribute | Description | Value |
| ------------------ | -------------------------------------------- | -------------------------- |
| `data-orientation` | The orientation of the accordion. | `horizontal` \| `vertical` |
| `data-open` | Applied when the accordion item is open. | `-` |
| `data-disabled` | Applied when the accordion item is disabled. | `-` |
### NgpAccordionTrigger
## API Reference
### NgpAccordionTrigger
Apply the `ngpAccordionTrigger` directive to an element that represents the trigger for an accordion item, such as a button.
**Selector:** `[ngpAccordionTrigger]`
#### Export As
`ngpAccordionTrigger`
#### Data Attributes
The following data attributes are applied to the `ngpAccordionTrigger` directive:
| Attribute | Description | Value |
| ------------------ | -------------------------------------------- | -------------------------- |
| `data-orientation` | The orientation of the accordion. | `horizontal` \| `vertical` |
| `data-open` | Applied when the accordion item is open. | `-` |
| `data-disabled` | Applied when the accordion item is disabled. | `-` |
### NgpAccordionContent
## API Reference
### NgpAccordionContent
Apply the `ngpAccordionContent` directive to an element that represents the content of an accordion item.
**Selector:** `[ngpAccordionContent]`
#### Export As
`ngpAccordionContent`
#### Data Attributes
The following data attributes are applied to the `ngpAccordionContent` directive:
| Attribute | Description | Value |
| ------------------ | ---------------------------------------- | -------------------------- |
| `data-orientation` | The orientation of the accordion. | `horizontal` \| `vertical` |
| `data-open` | Applied when the accordion item is open. | `-` |
The following CSS custom properties are applied to the `ngpAccordionContent` directive:
| Property | Description |
| -------------------------------- | ------------------------------------ |
| `--ngp-accordion-content-width` | The width of the accordion content. |
| `--ngp-accordion-content-height` | The height of the accordion content. |
## Animations
The `ngpAccordionContent` primitive adds several CSS custom properties `--ngp-accordion-content-width` and `--ngp-accordion-content-height` to the element that can be used to animate the accordion content on open and close.
## Global Configuration
You can configure the default options for all accordions in your application by using the `provideAccordionConfig` function in a providers array.
```ts
import { provideAccordionConfig } from 'ng-primitives/accordion';
bootstrapApplication(AppComponent, {
providers: [
provideAccordionConfig({
type: 'multiple',
collapsible: true,
orientation: 'horizontal',
}),
],
});
```
### NgpAccordionConfig
Define whether only one or multiple accordion items can be open at a time.
Define an accordion can be collapsed. This is only applicable when `type` is set to `single`.
Define the orientation of the accordion.
## Accessibility
Adheres to the [Accordion WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion).
Tip: Per APG, wrap each trigger button in a heading element (e.g., `h3`) or an element with `role="heading"` and an appropriate `aria-level`. Ensure the button is the only child of the heading.
### Keyboard Interactions
- Space - Toggle the expanded state of the accordion item when the trigger is focused.
- Enter - Toggle the expanded state of the accordion item when the trigger is focused.
### Hidden Until Found
The `ngpAccordionContent` primitive uses the `until-found` attribute to allow the browser to search text within the hidden region and reveal the section if a match is found. If the browser does not support this functionality, the attribute is ignored.
More information about the `until-found` attribute can be found on [Can I use](https://caniuse.com/?search=hidden%20until-found).
---
### AI Assistant
File: primitives/ai-assistant.md
URL: https://angularprimitives.com/primitives/ai-assistant
# AI Assistant
Build a customizable AI chat assistant by composing multiple primitives. These primitives provide accessible, flexible building blocks for chat threads, message display, prompt input, dictation, and submission—allowing you to create rich conversational interfaces tailored to your application's needs.
**Note:** The AI Assistant primitives are currently experimental. The API may change in future releases.
## Example
```typescript
import { Component, computed, signal } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideArrowUp, lucideMic, lucidePlus, lucideX } from '@ng-icons/lucide';
import {
NgpPromptComposer,
NgpPromptComposerDictation,
NgpPromptComposerInput,
NgpPromptComposerSubmit,
NgpThread,
NgpThreadMessage,
NgpThreadSuggestion,
NgpThreadViewport,
} from 'ng-primitives/ai';
import { NgpButton } from 'ng-primitives/button';
import { NgpFileUpload } from 'ng-primitives/file-upload';
interface Attachment {
id: string;
file: File;
preview: string; // Data URL for image preview
type: 'image' | 'file';
}
interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
attachments?: Attachment[];
isStreaming?: boolean;
}
@Component({
selector: 'app-ai',
imports: [
NgpThread,
NgpThreadViewport,
NgpThreadMessage,
NgpThreadSuggestion,
NgpPromptComposer,
NgpPromptComposerInput,
NgpPromptComposerSubmit,
NgpPromptComposerDictation,
NgpFileUpload,
NgIcon,
NgpButton,
],
providers: [provideIcons({ lucideArrowUp, lucideMic, lucidePlus, lucideX })],
template: `
@if (!hasMessages()) {
{{ welcomeMessage }}
Choose a suggestion below or type your own message to get started.
@for (suggestion of suggestions; track suggestion) {
}
`,
styles: `
:host {
display: contents;
}
/* Container */
.ai-container {
height: 700px;
width: 100%;
}
.ai-chat-wrapper {
display: flex;
height: 100%;
flex-direction: column;
align-items: stretch;
border-radius: 1rem;
background-color: var(--ngp-background);
padding: 0 1rem;
box-shadow: var(--ngp-shadow-border);
}
.ai-chat-content {
display: flex;
flex-grow: 1;
flex-direction: column;
gap: 1rem;
overflow: hidden;
padding-top: 1rem;
}
.ai-viewport {
display: flex;
flex-grow: 1;
flex-direction: column;
gap: 1rem;
overflow-y: auto;
padding: 0 0.5rem 1rem;
}
/* Welcome State */
.ai-welcome-container {
display: flex;
flex-grow: 1;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
text-align: center;
}
.ai-welcome-content {
max-width: 28rem;
}
.ai-welcome-title {
margin-bottom: 0.5rem;
font-size: 1.5rem;
font-weight: 600;
color: var(--ngp-text-emphasis);
line-height: 32px;
}
.ai-welcome-subtitle {
font-size: 0.875rem;
color: var(--ngp-text-secondary);
line-height: 24px;
}
.ai-suggestions-grid {
display: grid;
width: 100%;
max-width: 32rem;
grid-template-columns: 1fr;
gap: 0.75rem;
}
@media (min-width: 768px) {
.ai-suggestions-grid {
grid-template-columns: 1fr 1fr;
}
}
.ai-suggestion-button {
border-radius: 0.5rem;
box-shadow: var(--ngp-shadow-border);
padding: 10px 0.75rem;
text-align: left;
font-size: 0.875rem;
background-color: var(--ngp-background);
color: var(--ngp-text-secondary);
transition: all 0.2s ease;
cursor: pointer;
}
.ai-suggestion-button:hover,
.ai-suggestion-button[data-hover] {
border-color: var(--ngp-border);
background-color: var(--ngp-background-hover);
}
/* Messages */
.ai-message {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.ai-message-user {
align-items: flex-end;
}
.ai-message-assistant {
align-items: flex-start;
}
.ai-attachments {
display: flex;
max-width: 80%;
flex-wrap: wrap;
gap: 0.5rem;
}
.ai-attachment-image {
max-height: 8rem;
max-width: 20rem;
border-radius: 0.5rem;
border: 1px solid var(--ngp-border-secondary);
object-fit: cover;
}
.ai-attachment-file {
display: flex;
align-items: center;
gap: 0.5rem;
border-radius: 0.5rem;
border: 1px solid var(--ngp-border-secondary);
background-color: var(--ngp-background-hover);
padding: 0.5rem;
font-size: 0.75rem;
color: var(--ngp-text-secondary);
}
.ai-message-bubble {
max-width: 80%;
border-radius: 1rem;
padding: 0.75rem 1rem;
font-size: 0.875rem;
}
.ai-message-bubble-user {
background-color: var(--ngp-background-inverse);
color: var(--ngp-text-inverse);
}
.ai-message-bubble-assistant {
background-color: var(--ngp-background-active);
color: var(--ngp-text-primary);
}
.ai-streaming-wrapper {
margin-left: 0.25rem;
display: inline-flex;
}
.streaming-indicator {
height: 0.5rem;
width: 0.5rem;
border-radius: 50%;
background-color: var(--ngp-text-tertiary);
animation: pulse 1.5s ease-in-out infinite;
}
/* Attachment Previews */
.ai-attachment-previews-container {
margin: 0 auto;
width: 100%;
max-width: 768px;
padding: 0 1rem 0.5rem;
}
.ai-attachment-previews {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.ai-attachment-preview-item {
position: relative;
}
.ai-attachment-preview-item:hover .ai-attachment-remove {
opacity: 1;
}
.ai-attachment-preview-image {
height: 4rem;
width: 4rem;
cursor: pointer;
border-radius: 0.5rem;
border: 1px solid var(--ngp-border-secondary);
object-fit: cover;
transition: opacity 0.2s ease;
}
.ai-attachment-preview-image:hover {
opacity: 0.8;
}
.ai-attachment-preview-file {
display: flex;
height: 4rem;
width: 4rem;
align-items: center;
justify-content: center;
border-radius: 0.5rem;
border: 1px solid var(--ngp-border-secondary);
background-color: var(--ngp-background-hover);
}
.ai-attachment-extension {
font-size: 0.75rem;
color: var(--ngp-text-secondary);
}
.ai-attachment-remove {
position: absolute;
right: -0.25rem;
top: -0.25rem;
display: flex;
height: 1.25rem;
width: 1.25rem;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: var(--ngp-text-red);
color: var(--ngp-text-inverse);
opacity: 0;
transition: opacity 0.2s ease;
border: none;
cursor: pointer;
font-size: 0.75rem;
}
/* Composer */
.ai-composer {
margin: 0 auto;
display: flex;
width: 100%;
max-width: 768px;
align-items: flex-end;
border-radius: 1.5rem;
background-color: rgba(255, 255, 255, 0.05);
box-shadow: var(--ngp-shadow-border);
}
.ai-composer-button {
margin: 0.5rem;
display: flex;
height: 2rem;
width: 2rem;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.2s ease;
border: none;
cursor: pointer;
background-color: transparent;
}
.ai-file-button:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.ai-dictation-button {
display: none;
}
.ai-dictation-button[data-dictation-supported]:not([data-prompt]) {
display: flex;
}
.ai-dictation-button[data-prompt]:not([data-dictating]) {
display: none;
}
.ai-dictation-button:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.ai-dictation-button[data-dictating] {
background-color: rgba(0, 0, 0, 0.05);
}
.ai-dictation-button[data-dictating]:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.ai-send-button {
display: none;
background-color: var(--ngp-background-inverse);
color: var(--ngp-text-inverse);
}
.ai-send-button[data-prompt] {
display: flex;
}
.ai-textarea {
max-height: 10rem;
min-height: 3rem;
grow: 1;
resize: none;
background-color: transparent;
padding: 0.75rem 0;
font-size: 0.875rem;
outline: none;
border: none;
color: var(--ngp-text-primary);
}
.ai-textarea::placeholder {
color: var(--ngp-text-placeholder);
}
.ai-icon {
font-size: 14px;
color: var(--ngp-text-secondary);
}
.ai-send-button .ai-icon {
color: var(--ngp-text-inverse);
}
/* Disclaimer */
.ai-disclaimer {
margin: 0.25rem 0;
padding: 0.5rem;
text-align: center;
font-size: 0.75rem;
color: var(--ngp-text-placeholder);
}
/* Animations */
@keyframes pulse {
0%,
100% {
opacity: 0.4;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.1);
}
}
`,
})
export default class AiExample {
readonly messages = signal([]);
readonly attachments = signal([]);
// Check if there are any non-system messages to show welcome/suggestions
readonly hasMessages = computed(() => this.messages().some(message => message.role !== 'system'));
// Welcome message and suggestions
readonly welcomeMessage = "Hello! I'm ChatNGP, your AI assistant. How can I help you today?";
readonly suggestions = [
'Explain Angular components',
'Help with TypeScript types',
'Best practices for testing',
'Web development concepts',
'Angular routing guide',
'State management patterns',
];
// Simulated chat scenarios
private readonly chatScenarios = [
{
keywords: ['angular', 'component', 'directive'],
responses: [
'Angular is a powerful framework! I can help you with components, services, routing, and more.',
'Components are the building blocks of Angular applications. They control views and handle user interactions.',
'Directives are classes that add additional behavior to elements in your Angular applications.',
'Angular provides great tools for building scalable applications with TypeScript.',
],
},
{
keywords: ['help', 'assist', 'support'],
responses: [
'I am here to help! You can ask me about web development, Angular, or any other programming topics.',
'Feel free to ask me anything! I can assist with coding questions, best practices, or debugging.',
'How can I assist you today? I am knowledgeable about many programming topics.',
],
},
{
keywords: ['typescript', 'ts', 'type'],
responses: [
'TypeScript is a great language that adds static typing to JavaScript!',
'TypeScript helps catch errors at compile time and improves developer experience.',
'With TypeScript, you get better IntelliSense, refactoring, and code navigation.',
],
},
{
keywords: ['web', 'frontend', 'ui', 'interface'],
responses: [
'Modern web development offers many exciting possibilities for creating great user interfaces.',
'Frontend development has evolved significantly with frameworks like Angular, React, and Vue.',
'User interface design is crucial for creating engaging web applications.',
],
},
{
keywords: ['test', 'testing', 'unit', 'e2e'],
responses: [
'Testing is essential for maintaining code quality. Angular provides great testing utilities.',
'Unit tests help ensure individual components work correctly in isolation.',
'End-to-end testing validates that your application works as expected from the user perspective.',
],
},
];
private readonly fallbackResponses = [
'That is an interesting question! Could you tell me more about what you are trying to achieve?',
'I would love to help you with that. Can you provide some more context?',
'That is a great topic to explore. What specific aspect interests you most?',
'Interesting! I am curious to learn more about your use case.',
'That sounds like something worth discussing further. What is your current approach?',
];
sendMessage(prompt: string): void {
// Add user message with attachments if any
const userMessage: Message = {
id: Date.now().toString(),
content: prompt,
role: 'user',
attachments: this.attachments().length > 0 ? [...this.attachments()] : undefined,
};
this.messages.update(messages => [...messages, userMessage]);
// Clear attachments after sending
this.attachments.set([]);
// Generate AI response
this.generateAiResponse(prompt);
}
private async generateAiResponse(userMessage: string): Promise {
const aiMessageId = (Date.now() + 1).toString();
// Create initial AI message
const aiMessage: Message = {
id: aiMessageId,
content: '',
role: 'assistant',
isStreaming: true,
};
this.messages.update(messages => [...messages, aiMessage]);
try {
// Simulate streaming response
await this.streamSimulatedResponse(userMessage, aiMessageId);
} catch (error) {
console.error('Error generating response:', error);
// Update with error message
this.messages.update(messages =>
messages.map(msg =>
msg.id === aiMessageId
? {
...msg,
content: 'Sorry, I encountered an error. Please try again.',
isStreaming: false,
}
: msg,
),
);
}
}
private async streamSimulatedResponse(userMessage: string, messageId: string): Promise {
// Add a small delay to simulate thinking
await new Promise(resolve => setTimeout(resolve, 500));
// Generate response based on user message
const response = this.getSimulatedResponse(userMessage);
// Simulate streaming by adding characters with delay
let currentContent = '';
for (let i = 0; i < response.length; i++) {
await new Promise(resolve => setTimeout(resolve, 10)); // 10ms delay between characters
currentContent += response[i];
this.messages.update(messages =>
messages.map(msg =>
msg.id === messageId ? { ...msg, content: currentContent, isStreaming: true } : msg,
),
);
}
// Mark streaming as complete
this.messages.update(messages =>
messages.map(msg => (msg.id === messageId ? { ...msg, isStreaming: false } : msg)),
);
}
private getSimulatedResponse(userMessage: string): string {
const lowerMessage = userMessage.toLowerCase();
// Check for matching scenario based on keywords
for (const scenario of this.chatScenarios) {
for (const keyword of scenario.keywords) {
if (lowerMessage.includes(keyword)) {
// Return a random response from the matching scenario
const randomIndex = Math.floor(Math.random() * scenario.responses.length);
return scenario.responses[randomIndex];
}
}
}
// If no keywords match, return a random fallback response
const randomIndex = Math.floor(Math.random() * this.fallbackResponses.length);
return this.fallbackResponses[randomIndex];
}
addAttachment(files: FileList | null): void {
if (!files || files.length === 0) return;
Array.from(files).forEach(file => {
const attachment: Attachment = {
id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
file,
preview: '',
type: file.type.startsWith('image/') ? 'image' : 'file',
};
// Create preview for images
if (attachment.type === 'image') {
const reader = new FileReader();
reader.onload = e => {
attachment.preview = e.target?.result as string;
this.attachments.update(attachments => [...attachments, attachment]);
};
reader.readAsDataURL(file);
} else {
this.attachments.update(attachments => [...attachments, attachment]);
}
});
}
removeAttachment(attachmentId: string): void {
this.attachments.update(attachments =>
attachments.filter(attachment => attachment.id !== attachmentId),
);
}
}
```
## Import
Import the AI primitives from `ng-primitives/ai`.
```ts
import {
NgpThread,
NgpThreadViewport,
NgpThreadMessage,
NgpThreadSuggestion,
NgpPromptComposer,
NgpPromptComposerInput,
NgpPromptComposerSubmit,
NgpPromptComposerDictation,
} from 'ng-primitives/ai';
```
## Usage
Assemble the AI directives in your template.
```html
Message content
...
```
## API Reference
The following directives are available to import from the `ng-primitives/ai` package:
### NgpThread
The `NgpThread` directive is wrapper around the thread viewport, messages and composer in the AI assistant chat.
## API Reference
### NgpThread
**Selector:** `[ngpThread]`
#### Export As
`ngpThread`
### NgpThreadViewport
The `NgpThreadViewport` directive creates a scrollable container for displaying the messages in the AI assistant chat thread.
## API Reference
### NgpThreadViewport
**Selector:** `[ngpThreadViewport]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpThreadViewportThreshold` | `number` | `70` | The distance in pixels from the bottom of the scrollable container that is considered "at the bottom".
When the user scrolls within this threshold, the thread is treated as being at the bottom.
This value is used to determine whether automatic scrolling to the bottom should occur,
for example when new content is added or the container is resized. |
| `ngpThreadViewportAutoScroll` | `boolean` | `-` | Whether the thread should automatically scroll to the bottom when new content is added. |
#### Export As
`ngpThreadViewport`
### NgpThreadMessage
The `NgpThreadMessage` directive represents an individual message within a thread in the AI assistant chat.
## API Reference
### NgpThreadMessage
**Selector:** `[ngpThreadMessage]`
#### Export As
`ngpThreadMessage`
### NgpThreadSuggestion
The `NgpThreadSuggestion` directive displays suggested text that the user can click to populate the prompt input field.
## API Reference
### NgpThreadSuggestion
**Selector:** `button[ngpThreadSuggestion]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpThreadSuggestion` | `string` | `-` | The suggested text to display in the input field. |
| `ngpThreadSuggestionSetPromptOnClick` | `boolean` | `-` | Whether the suggestion should populate the prompt when clicked. |
#### Export As
`ngpThreadSuggestion`
### NgpPromptComposer
The `NgpPromptComposer` directive creates a container for composing and submitting prompts to the AI assistant.
## API Reference
### NgpPromptComposer
**Selector:** `[ngpPromptComposer]`
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpPromptComposerSubmit` | `string` | Emits whenever the user submits the prompt. |
#### Export As
`ngpPromptComposer`
#### Data Attributes
| Attribute | Description |
| -------------------------- | -------------------------------------------------------- |
| `data-prompt` | Added when there is text content in the prompt. |
| `data-dictating` | Added when speech dictation is active. |
| `data-dictation-supported` | Added when speech dictation is supported by the browser. |
### NgpPromptComposerInput
The `NgpPromptComposerInput` directive is used for the text input field where users type their messages.
## API Reference
### NgpPromptComposerInput
**Selector:** `input[ngpPromptComposerInput], textarea[ngpPromptComposerInput]`
#### Export As
`ngpPromptComposerInput`
### NgpPromptComposerSubmit
The `NgpPromptComposerSubmit` directive handles the submission of composed prompts to the AI assistant.
## API Reference
### NgpPromptComposerSubmit
**Selector:** `button[ngpPromptComposerSubmit]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `disabled` | `boolean` | `-` | Whether the submit button should be disabled |
#### Export As
`ngpPromptComposerSubmit`
#### Data Attributes
| Attribute | Description |
| -------------------------- | -------------------------------------------------------- |
| `data-prompt` | Added when there is text content in the prompt. |
| `data-dictating` | Added when speech dictation is active. |
| `data-dictation-supported` | Added when speech dictation is supported by the browser. |
### NgpPromptComposerDictation
The `NgpPromptComposerDictation` directive enables voice input functionality for composing prompts using speech-to-text.
## API Reference
### NgpPromptComposerDictation
**Selector:** `button[ngpPromptComposerDictation]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `disabled` | `boolean` | `-` | Whether the submit button should be disabled. |
#### Export As
`ngpPromptComposerDictation`
#### Data Attributes
| Attribute | Description |
| -------------------------- | -------------------------------------------------------- |
| `data-dictating` | Added when speech dictation is active. |
| `data-dictation-supported` | Added when speech dictation is supported by the browser. |
| `data-prompt` | Added when there is text content in the prompt. |
## Accessibility
### Keyboard Interactions
- Enter - Submits the prompt when focused on the input field.
- Shift+Enter - Inserts a newline in the input field.
---
### Avatar
File: primitives/avatar.md
URL: https://angularprimitives.com/primitives/avatar
# Avatar
Display an image that represents a user with a text fallback.
## Example
```typescript
import { Component } from '@angular/core';
import { NgpAvatar, NgpAvatarFallback, NgpAvatarImage } from 'ng-primitives/avatar';
@Component({
selector: 'app-avatar',
imports: [NgpAvatar, NgpAvatarImage, NgpAvatarFallback],
styles: `
[ngpAvatar] {
position: relative;
display: inline-flex;
width: 3rem;
height: 3rem;
align-items: center;
justify-content: center;
border-radius: 9999px;
border-width: 2px;
border-color: var(--ngp-avatar-border);
background-color: var(--ngp-avatar-background);
vertical-align: middle;
}
[ngpAvatar]:before {
content: '';
position: absolute;
inset: 0;
border-radius: 9999px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}
[ngpAvatarImage] {
width: 100%;
height: 100%;
}
[ngpAvatarFallback] {
text-align: center;
font-weight: 500;
color: var(--ngp-text-emphasis);
}
`,
template: `
NG
`,
})
export default class AvatarExample {}
```
## Import
Import the Avatar primitives from `ng-primitives/avatar`.
```ts
import { NgpAvatar, NgpAvatarImage, NgpAvatarFallback } from 'ng-primitives/avatar';
```
## Usage
Assemble the avatar directives in your template.
```html
NG
```
## Reusable Component
Create a reusable component that uses the `NgpAvatar` directive.
## Reusable Component
```typescript
import { Component, input } from '@angular/core';
import { NgpAvatar, NgpAvatarFallback, NgpAvatarImage } from 'ng-primitives/avatar';
@Component({
selector: 'app-avatar',
hostDirectives: [NgpAvatar],
imports: [NgpAvatarImage, NgpAvatarFallback],
template: `
@if (image()) {
}
{{ fallback() }}
`,
styles: `
:host {
position: relative;
display: inline-flex;
width: 3rem;
height: 3rem;
align-items: center;
justify-content: center;
border-radius: 9999px;
border-width: 2px;
border-color: var(--ngp-avatar-border);
background-color: var(--ngp-avatar-background);
vertical-align: middle;
overflow: hidden;
}
:host:before {
content: '';
position: absolute;
inset: 0;
border-radius: 9999px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}
[ngpAvatarImage] {
width: 100%;
height: 100%;
}
[ngpAvatarFallback] {
text-align: center;
font-weight: 500;
color: var(--ngp-text-emphasis);
}
`,
})
export class Avatar {
/** Define the avatar image source */
readonly image = input();
/** Define the avatar fallback text */
readonly fallback = input();
}
```
## Schematics
Generate a reusable avatar component using the Angular CLI.
```bash npm
ng g ng-primitives:primitive avatar
```
### Options
- `path`: The path at which to create the component file.
- `prefix`: The prefix to apply to the generated component selector.
- `componentSuffix`: The suffix to apply to the generated component class name.
- `fileSuffix`: The suffix to apply to the generated component file name. Defaults to `component`.
- `exampleStyles`: Whether to include example styles in the generated component file. Defaults to `true`.
## API Reference
The following directives are available to import from the `ng-primitives/avatar` package:
### NgpAvatar
## API Reference
### NgpAvatar
Apply the `ngpAvatar` directive to an element that represents the avatar. This directive is a container for the image and/or fallback.
**Selector:** `[ngpAvatar]`
#### Export As
`ngpAvatar`
#### Data Attributes
| Attribute | Description | Value |
| ------------- | --------------------------------------- | ------------------------------------------ |
| `data-status` | The loading status of the avatar image. | `idle` \| `loading` \| `loaded` \| `error` |
### NgpAvatarImage
## API Reference
### NgpAvatarImage
Apply the `ngpAvatarImage` directive to an element that represents the avatar image. This would typically be an `img` element or a `div` with a background image.
**Selector:** `img[ngpAvatarImage]`
#### Export As
`ngpAvatarImage`
### NgpAvatarFallback
## API Reference
### NgpAvatarFallback
Apply the `ngpAvatarFallback` directive to an element that represents the user in the absence of an image. This is typically the user's initials.
**Selector:** `[ngpAvatarFallback]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpAvatarFallbackDelay` | `number` | `0` | Define a delay before the fallback is shown. This is useful to only show the fallback for those with slower connections. |
#### Export As
`ngpAvatarFallback`
## Global Configuration
You can configure the default options for all avatars in your application by using the `provideAvatarConfig` function in a providers array.
```ts
import { provideAvatarConfig } from 'ng-primitives/avatar';
bootstrapApplication(AppComponent, {
providers: [provideAvatarConfig({ delay: 1000 })],
});
```
### NgpAvatarConfig
Define a delay before the fallback is shown. This is useful to only show the fallback for those
with slower connections.
---
### Breadcrumbs
File: primitives/breadcrumbs.md
URL: https://angularprimitives.com/primitives/breadcrumbs
# Breadcrumbs
Help users understand their location within a hierarchy with a fully accessible breadcrumb trail.
## Example
```typescript
import { Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideChevronRight, lucideMoreHorizontal } from '@ng-icons/lucide';
import {
NgpBreadcrumbEllipsis,
NgpBreadcrumbItem,
NgpBreadcrumbLink,
NgpBreadcrumbList,
NgpBreadcrumbPage,
NgpBreadcrumbs,
NgpBreadcrumbSeparator,
} from 'ng-primitives/breadcrumbs';
import { NgpMenu, NgpMenuItem, NgpMenuTrigger } from 'ng-primitives/menu';
@Component({
selector: 'app-breadcrumbs',
imports: [
NgpBreadcrumbs,
NgpBreadcrumbList,
NgpBreadcrumbItem,
NgpBreadcrumbLink,
NgpBreadcrumbPage,
NgpBreadcrumbSeparator,
NgpBreadcrumbEllipsis,
NgpMenu,
NgpMenuTrigger,
NgpMenuItem,
NgIcon,
],
providers: [provideIcons({ lucideChevronRight, lucideMoreHorizontal })],
styles: `
:host {
display: contents;
}
[ngpBreadcrumbs] {
display: block;
}
[ngpBreadcrumbList] {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
list-style: none;
margin: 0;
padding: 0;
font-size: 0.875rem;
color: var(--ngp-text-secondary);
}
[ngpBreadcrumbItem] {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
[ngpBreadcrumbLink] {
color: inherit;
text-decoration: none;
transition: color 150ms ease;
}
[ngpBreadcrumbLink][data-hover] {
color: var(--ngp-text-primary);
}
[ngpBreadcrumbLink][data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
[ngpBreadcrumbPage] {
font-weight: 500;
color: var(--ngp-text-primary);
}
[ngpBreadcrumbSeparator] {
color: var(--ngp-border-strong);
display: inline-flex;
align-items: center;
padding: 0;
}
[ngpBreadcrumbEllipsis] {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 999px;
color: var(--ngp-text-secondary);
padding: 0;
border: none;
background: none;
cursor: pointer;
}
[ngpBreadcrumbEllipsis] ng-icon {
--ng-icon__size: 1.1rem;
}
[ngpBreadcrumbEllipsis][data-hover] {
color: var(--ngp-text-primary);
}
[ngpBreadcrumbEllipsis][data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
[ngpBreadcrumbSeparator] ng-icon {
--ng-icon__size: 0.85rem;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
[ngpMenu] {
position: fixed;
display: flex;
flex-direction: column;
width: max-content;
background: var(--ngp-background);
border: 1px solid var(--ngp-border);
box-shadow: var(--ngp-shadow-lg);
border-radius: 8px;
padding: 4px;
transform-origin: var(--ngp-menu-transform-origin);
}
[ngpMenuItem] {
padding: 6px 14px;
border: none;
background: none;
cursor: pointer;
transition: background-color 0.2s;
border-radius: 4px;
min-width: 140px;
text-align: start;
outline: none;
font-size: 14px;
font-weight: 400;
}
[ngpMenuItem][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpMenuItem][data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
}
`,
template: `
`,
})
export default class BreadcrumbsExample {}
```
## Import
Import the Breadcrumb primitives from `ng-primitives/breadcrumbs`.
```ts
import {
NgpBreadcrumbs,
NgpBreadcrumbList,
NgpBreadcrumbItem,
NgpBreadcrumbLink,
NgpBreadcrumbSeparator,
NgpBreadcrumbEllipsis,
NgpBreadcrumbPage,
} from 'ng-primitives/breadcrumbs';
```
## Usage
Assemble the directives to build the breadcrumb structure, applying your own `aria-label` to describe the navigation context. You can optionally wire the ellipsis into the Menu primitives to expose collapsed sections.
```html
```
## API Reference
The following directives are available to import from the `ng-primitives/breadcrumbs` package:
### NgpBreadcrumbs
## API Reference
### NgpBreadcrumbs
Apply `ngpBreadcrumbs` to the navigation element that wraps the breadcrumb trail.
**Selector:** `[ngpBreadcrumbs]`
#### Export As
`ngpBreadcrumbs`
### NgpBreadcrumbList
## API Reference
### NgpBreadcrumbList
Apply `ngpBreadcrumbList` to the ordered list that groups breadcrumb items.
**Selector:** `[ngpBreadcrumbList]`
#### Export As
`ngpBreadcrumbList`
### NgpBreadcrumbItem
## API Reference
### NgpBreadcrumbItem
Apply `ngpBreadcrumbItem` to each list item in the breadcrumb trail.
**Selector:** `[ngpBreadcrumbItem]`
#### Export As
`ngpBreadcrumbItem`
### NgpBreadcrumbLink
## API Reference
### NgpBreadcrumbLink
Apply `ngpBreadcrumbLink` to anchors or buttons that navigate to a breadcrumb destination.
**Selector:** `[ngpBreadcrumbLink]`
#### Export As
`ngpBreadcrumbLink`
#### Data Attributes
| Attribute | Description |
| -------------------- | --------------------------------------------- |
| `data-hover` | Applied when the breadcrumb link is hovered. |
| `data-press` | Applied when the breadcrumb link is pressed. |
| `data-focus-visible` | Applied when the link receives focus visibly. |
| `data-current` | Applied when the link represents the page. |
### NgpBreadcrumbPage
## API Reference
### NgpBreadcrumbPage
Apply `ngpBreadcrumbPage` to non-link content that represents the active page.
**Selector:** `[ngpBreadcrumbPage]`
#### Export As
`ngpBreadcrumbPage`
#### Data Attributes
| Attribute | Description |
| -------------- | ----------------------------------------- |
| `data-current` | Applied to indicate the current location. |
### NgpBreadcrumbSeparator
## API Reference
### NgpBreadcrumbSeparator
Apply `ngpBreadcrumbSeparator` between breadcrumb items to render a visual divider.
**Selector:** `[ngpBreadcrumbSeparator]`
#### Export As
`ngpBreadcrumbSeparator`
### NgpBreadcrumbEllipsis
## API Reference
### NgpBreadcrumbEllipsis
Apply `ngpBreadcrumbEllipsis` to elements that represent collapsed breadcrumb items.
**Selector:** `[ngpBreadcrumbEllipsis]`
#### Export As
`ngpBreadcrumbEllipsis`
## Accessibility
Adheres to the [WAI-ARIA Breadcrumb Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/).
- Breadcrumbs are rendered within a `nav` landmark with an accessible label.
- Separators are hidden from assistive technologies via `role="presentation"` and `aria-hidden`.
- The current location uses `aria-current="page"` for accurate announcements.
---
### Button
File: primitives/button.md
URL: https://angularprimitives.com/primitives/button
# Button
A button is a clickable element that can be used to trigger an action. This primitive enhances the native button element with improved accessibility and interaction handling for hover, press and focus.
## Example
```typescript
import { Component } from '@angular/core';
import { NgpButton } from 'ng-primitives/button';
@Component({
selector: 'app-button',
imports: [NgpButton],
template: `
`,
styles: `
[ngpButton] {
padding-left: 1rem;
padding-right: 1rem;
border-radius: 0.5rem;
color: var(--ngp-text-primary);
border: none;
height: 2.5rem;
font-weight: 500;
background-color: var(--ngp-background);
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--ngp-button-shadow);
}
[ngpButton][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpButton][data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
}
[ngpButton][data-press] {
background-color: var(--ngp-background-active);
}
`,
})
export default class ButtonExample {}
```
## Import
Import the Button primitives from `ng-primitives/button`.
```ts
import { NgpButton } from 'ng-primitives/button';
```
## Usage
Assemble the button directives in your template.
```html
```
## Reusable Component
Create a button component that uses the `NgpButton` directive.
## Reusable Component
```typescript
import { Component, input } from '@angular/core';
import { NgpButton } from 'ng-primitives/button';
/**
* The size of the button.
*/
export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
/**
* The variant of the button.
*/
export type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link';
@Component({
selector: 'button[app-button]',
hostDirectives: [{ directive: NgpButton, inputs: ['disabled'] }],
template: `
`,
host: {
'[attr.data-size]': 'size()',
'[attr.data-variant]': 'variant()',
},
styles: `
:host {
padding-left: 1rem;
padding-right: 1rem;
border-radius: 0.5rem;
color: var(--ngp-text-primary);
border: none;
height: 2.5rem;
font-weight: 500;
background-color: var(--ngp-background);
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--ngp-button-shadow);
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
gap: 0.5rem;
}
:host[data-hover] {
background-color: var(--ngp-background-hover);
}
:host[data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
}
:host[data-press] {
background-color: var(--ngp-background-active);
}
/* Size variants */
:host[data-size='sm'] {
height: 2rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
font-size: 0.875rem;
--ng-icon__size: 0.875rem;
}
:host[data-size='md'],
:host:not([data-size]) {
height: 2.5rem;
padding-left: 1rem;
padding-right: 1rem;
font-size: 0.875rem;
--ng-icon__size: 0.875rem;
}
:host[data-size='lg'] {
height: 3rem;
padding-left: 1.25rem;
padding-right: 1.25rem;
font-size: 1rem;
--ng-icon__size: 1rem;
}
:host[data-size='xl'] {
height: 3.5rem;
padding-left: 1.5rem;
padding-right: 1.5rem;
font-size: 1.125rem;
--ng-icon__size: 1.125rem;
}
/* Variant styles */
:host[data-variant='primary'],
:host:not([data-variant]) {
background-color: var(--ngp-background);
color: var(--ngp-text-primary);
border: none;
}
:host[data-variant='primary'][data-hover],
:host:not([data-variant])[data-hover] {
background-color: var(--ngp-background-hover);
}
:host[data-variant='primary'][data-press],
:host:not([data-variant])[data-press] {
background-color: var(--ngp-background-active);
}
:host[data-variant='secondary'] {
background-color: var(--ngp-secondary-background, #f1f5f9);
color: var(--ngp-secondary-text, #0f172a);
border: none;
}
:host[data-variant='secondary'][data-hover] {
background-color: var(--ngp-secondary-background-hover, #e2e8f0);
}
:host[data-variant='secondary'][data-press] {
background-color: var(--ngp-secondary-background-active, #cbd5e1);
}
:host[data-variant='destructive'] {
background-color: var(--ngp-destructive-background, #ef4444);
color: var(--ngp-destructive-text, #ffffff);
border: none;
}
:host[data-variant='destructive'][data-hover] {
background-color: var(--ngp-destructive-background-hover, #dc2626);
}
:host[data-variant='destructive'][data-press] {
background-color: var(--ngp-destructive-background-active, #b91c1c);
}
:host[data-variant='outline'] {
background-color: transparent;
color: var(--ngp-text-primary);
border: 1px solid var(--ngp-outline-border, #e2e8f0);
box-shadow: none;
}
:host[data-variant='outline'][data-hover] {
background-color: var(--ngp-background-hover);
border-color: var(--ngp-outline-border-hover, #cbd5e1);
}
:host[data-variant='outline'][data-press] {
background-color: var(--ngp-outline-background-active, rgba(15, 23, 42, 0.1));
}
:host[data-variant='ghost'] {
background-color: transparent;
color: var(--ngp-text-primary);
border: none;
box-shadow: none;
}
:host[data-variant='ghost'][data-hover] {
background-color: var(--ngp-background-hover);
}
:host[data-variant='ghost'][data-press] {
background-color: var(--ngp-background-active);
}
:host[data-variant='link'] {
background-color: transparent;
color: var(--ngp-link-color, #3b82f6);
border: none;
box-shadow: none;
text-decoration: underline;
text-underline-offset: 4px;
}
:host[data-variant='link'][data-hover] {
text-decoration-thickness: 2px;
}
:host[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
`,
})
export class Button {
/**
* The size of the button.
*/
readonly size = input('md');
/**
* The variant of the button.
*/
readonly variant = input('primary');
}
```
## Examples
### Button Sizes
You can add size support to your reusable button component. This is implemented at the component level rather than in the primitive to provide more flexibility for different design systems.
## Example
```typescript
import { Component, input } from '@angular/core';
import { NgpButton } from 'ng-primitives/button';
/**
* The size of the button.
*/
export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
@Component({
selector: 'button[app-button]',
hostDirectives: [{ directive: NgpButton, inputs: ['disabled'] }],
template: '',
host: {
'[attr.data-size]': 'size()',
},
styles: `
:host {
padding-left: 1rem;
padding-right: 1rem;
border-radius: 0.5rem;
color: var(--ngp-text-primary);
border: none;
height: 2.5rem;
font-weight: 500;
background-color: var(--ngp-background);
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--ngp-button-shadow);
box-sizing: border-box;
}
:host[data-hover] {
background-color: var(--ngp-background-hover);
}
:host[data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
}
:host[data-press] {
background-color: var(--ngp-background-active);
}
/* Size variants */
:host[data-size='sm'] {
height: 2rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
font-size: 0.875rem;
}
:host[data-size='md'],
:host:not([data-size]) {
height: 2.5rem;
padding-left: 1rem;
padding-right: 1rem;
font-size: 0.875rem;
}
:host[data-size='lg'] {
height: 3rem;
padding-left: 1.25rem;
padding-right: 1.25rem;
font-size: 1rem;
}
:host[data-size='xl'] {
height: 3.5rem;
padding-left: 1.5rem;
padding-right: 1.5rem;
font-size: 1.125rem;
}
`,
})
export class Button {
/**
* The size of the button.
*/
readonly size = input('md');
}
@Component({
selector: 'app-button-sizes-example',
imports: [Button],
template: `
`,
styles: `
.button-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.button-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
}
h3 {
margin-bottom: 0.5rem;
font-size: 1rem;
font-weight: 500;
}
`,
})
export default class ButtonIconExample {}
```
## Schematics
Generate a reusable button component using the Angular CLI.
```bash npm
ng g ng-primitives:primitive button
```
### Options
- `path`: The path at which to create the component file.
- `prefix`: The prefix to apply to the generated component selector.
- `componentSuffix`: The suffix to apply to the generated component class name.
- `fileSuffix`: The suffix to apply to the generated component file name. Defaults to `component`.
- `exampleStyles`: Whether to include example styles in the generated component file. Defaults to `true`.
## API Reference
The following directives are available to import from the `ng-primitives/button` package:
### NgpButton
## API Reference
### NgpButton
**Selector:** `[ngpButton]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `disabled` | `boolean` | `-` | Whether the button is disabled. |
#### Export As
`ngpButton`
#### Data Attributes
| Attribute | Description |
| -------------------- | ---------------------------------- |
| `data-hover` | Added to the button when hovered. |
| `data-focus-visible` | Added to the button when focused. |
| `data-press` | Added to the button when pressed. |
| `data-disabled` | Added to the button when disabled. |
---
### Checkbox
File: primitives/checkbox.md
URL: https://angularprimitives.com/primitives/checkbox
# Checkbox
Perform state toggling.
## Example
```typescript
import { Component, signal } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroCheckMini } from '@ng-icons/heroicons/mini';
import { NgpCheckbox } from 'ng-primitives/checkbox';
@Component({
selector: 'app-checkbox',
imports: [NgIcon, NgpCheckbox],
providers: [provideIcons({ heroCheckMini })],
styles: `
[ngpCheckbox] {
display: flex;
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
border: 1px solid var(--ngp-border);
background-color: transparent;
padding: 0;
outline: none;
flex: none;
color: var(--ngp-text-inverse);
font-size: 0.75rem;
}
[ngpCheckbox][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpCheckbox][data-checked],
[ngpCheckbox][data-indeterminate] {
border-color: var(--ngp-background-inverse);
background-color: var(--ngp-background-inverse);
}
[ngpCheckbox][data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
`,
template: `
@if (checked()) {
}
`,
})
export default class CheckboxExample {
/**
* The checked state of the checkbox.
*/
readonly checked = signal(true);
}
```
## Import
Import the Checkbox primitives from `ng-primitives/checkbox`.
```ts
import { NgpCheckbox } from 'ng-primitives/checkbox';
```
## Usage
Assemble the checkbox directives in your template.
```html
```
## Reusable Component
Create a reusable component that uses the `NgpCheckbox` directive.
## Reusable Component
```typescript
import { Component } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroCheckMini, heroMinusMini } from '@ng-icons/heroicons/mini';
import { injectCheckboxState, NgpCheckbox } from 'ng-primitives/checkbox';
import { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';
@Component({
selector: 'app-checkbox',
hostDirectives: [
{
directive: NgpCheckbox,
inputs: [
'ngpCheckboxChecked:checked',
'ngpCheckboxIndeterminate:indeterminate',
'ngpCheckboxDisabled:disabled',
],
outputs: [
'ngpCheckboxCheckedChange:checkedChange',
'ngpCheckboxIndeterminateChange:indeterminateChange',
],
},
],
providers: [provideValueAccessor(Checkbox), provideIcons({ heroCheckMini, heroMinusMini })],
imports: [NgIcon],
template: `
@if (state().indeterminate()) {
} @else if (state().checked()) {
}
`,
styles: `
:host {
display: flex;
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
border: 1px solid var(--ngp-border);
background-color: transparent;
padding: 0;
outline: none;
flex: none;
color: var(--ngp-text-inverse);
font-size: 0.75rem;
}
:host[data-hover] {
background-color: var(--ngp-background-hover);
}
:host[data-checked],
:host[data-indeterminate] {
border-color: var(--ngp-background-inverse);
background-color: var(--ngp-background-inverse);
}
:host[data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
`,
host: {
'(focusout)': 'onTouchedFn?.()',
},
})
export class Checkbox implements ControlValueAccessor {
/**
* The checked state of the checkbox.
*/
protected readonly state = injectCheckboxState();
/**
* The onChange function for the checkbox.
*/
protected onChangeFn?: ChangeFn;
/**
* The onTouched function for the checkbox.
*/
protected onTouchedFn?: TouchedFn;
constructor() {
// Whenever the user interacts with the checkbox, call the onChange function with the new value.
this.state().checkedChange.subscribe(checked => this.onChangeFn?.(checked));
}
writeValue(checked: boolean): void {
this.state().setChecked(checked);
}
registerOnChange(fn: ChangeFn): void {
this.onChangeFn = fn;
}
registerOnTouched(fn: TouchedFn): void {
this.onTouchedFn = fn;
}
setDisabledState(isDisabled: boolean): void {
this.state().setDisabled(isDisabled);
}
}
```
## Schematics
Generate a reusable checkbox component using the Angular CLI.
```bash npm
ng g ng-primitives:primitive checkbox
```
### Options
- `path`: The path at which to create the component file.
- `prefix`: The prefix to apply to the generated component selector.
- `componentSuffix`: The suffix to apply to the generated component class name.
- `fileSuffix`: The suffix to apply to the generated component file name. Defaults to `component`.
- `exampleStyles`: Whether to include example styles in the generated component file. Defaults to `true`.
## Examples
Here are some additional examples of how to use the Checkbox primitives.
### Checkbox Form Field
The checkbox automatically integrates with the form field primitives.
## Example
```typescript
import { Component, signal } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroCheckMini } from '@ng-icons/heroicons/mini';
import { NgpCheckbox } from 'ng-primitives/checkbox';
import { NgpDescription, NgpFormField, NgpLabel } from 'ng-primitives/form-field';
@Component({
selector: 'app-checkbox-form-control',
imports: [NgIcon, NgpCheckbox, NgpFormField, NgpLabel, NgpDescription],
providers: [provideIcons({ heroCheckMini })],
styles: `
[ngpFormField] {
display: flex;
column-gap: 0.75rem;
align-items: baseline;
}
[ngpCheckbox] {
display: flex;
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
border: 1px solid var(--ngp-border);
background-color: transparent;
padding: 0;
outline: none;
flex: none;
}
[ngpCheckbox][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpCheckbox][data-checked] {
border-color: var(--ngp-background-inverse);
background-color: var(--ngp-background-inverse);
}
[ngpCheckbox][data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
ng-icon {
color: var(--ngp-text-inverse);
font-size: 0.75rem;
}
[ngpLabel] {
display: flex;
flex-direction: column;
row-gap: 0.125rem;
font-weight: 500;
font-size: 14px;
line-height: 14px;
color: var(--ngp-text-primary);
}
[ngpDescription] {
font-size: 12px;
line-height: 16px;
color: var(--ngp-text-secondary);
}
`,
template: `
@if (checked()) {
}
`,
})
export default class CheckboxFormFieldExample {
/**
* The checked state of the checkbox.
*/
readonly checked = signal(true);
}
```
## API Reference
The following directives are available to import from the `ng-primitives/checkbox` package:
### NgpCheckbox
## API Reference
### NgpCheckbox
Apply the `ngpCheckbox` directive to an element to that represents the checkbox, such as a `button`.
**Selector:** `[ngpCheckbox]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpCheckboxChecked` | `boolean` | `-` | Defines whether the checkbox is checked. |
| `ngpCheckboxIndeterminate` | `boolean` | `-` | Defines whether the checkbox is indeterminate. |
| `ngpCheckboxRequired` | `boolean` | `-` | Whether the checkbox is required. |
| `ngpCheckboxDisabled` | `boolean` | `-` | Defines whether the checkbox is disabled. |
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpCheckboxCheckedChange` | `boolean` | The event that is emitted when the checkbox value changes. |
| `ngpCheckboxIndeterminateChange` | `boolean` | The event that is emitted when the indeterminate value changes. |
| Attribute | Description |
| -------------------- | ------------------------------------------- |
| `data-checked` | Applied when the checkbox is checked. |
| `data-indeterminate` | Applied when the checkbox is indeterminate. |
| `data-disabled` | Applied when the checkbox is disabled. |
| `data-hover` | Applied when the checkbox is hovered. |
| `data-focus-visible` | Applied when the checkbox is focused. |
| `data-press` | Applied when the checkbox is pressed. |
## Accessibility
Adheres to the [Tri-State Checkbox WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox).
### Keyboard Interactions
- Space - Toggle the checked state.
---
### Combobox
File: primitives/combobox.md
URL: https://angularprimitives.com/primitives/combobox
# Combobox
The Combobox primitive is a combination of a dropdown and an input field. It allows users to select from a list of options while filtering the list based on their input.
## Example
```typescript
import { Component, computed, signal } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroChevronDown } from '@ng-icons/heroicons/outline';
import {
NgpCombobox,
NgpComboboxButton,
NgpComboboxDropdown,
NgpComboboxInput,
NgpComboboxOption,
NgpComboboxPortal,
} from 'ng-primitives/combobox';
@Component({
selector: 'app-combobox',
imports: [
NgpCombobox,
NgpComboboxDropdown,
NgpComboboxOption,
NgpComboboxInput,
NgpComboboxPortal,
NgpComboboxButton,
NgIcon,
],
providers: [provideIcons({ heroChevronDown })],
template: `
@for (option of filteredOptions(); track option) {
```
### Without Input Field
You can also create a combobox without an input field, which is useful for select-like behavior with keyboard navigation:
```html
@for (option of options; track option) {
{{ option }}
}
```
When no input is present, the combobox element itself becomes focusable and supports full keyboard navigation.
## Reusable Component
Create a reusable component that uses the `NgpCombobox` directive.
## Reusable Component
```typescript
import { BooleanInput } from '@angular/cdk/coercion';
import { booleanAttribute, Component, computed, input, model, signal } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroChevronDown } from '@ng-icons/heroicons/outline';
import {
NgpCombobox,
NgpComboboxButton,
NgpComboboxDropdown,
NgpComboboxInput,
NgpComboboxOption,
NgpComboboxPortal,
} from 'ng-primitives/combobox';
import { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';
@Component({
selector: 'app-combobox',
imports: [
NgpCombobox,
NgpComboboxDropdown,
NgpComboboxOption,
NgpComboboxInput,
NgpComboboxPortal,
NgpComboboxButton,
NgIcon,
],
providers: [provideIcons({ heroChevronDown }), provideValueAccessor(Combobox)],
template: `
@for (option of filteredOptions(); track option) {
{{ option }}
} @empty {
No options found
}
`,
styles: `
[ngpCombobox] {
display: flex;
justify-content: space-between;
align-items: center;
height: 36px;
width: 300px;
border-radius: 8px;
border: none;
background-color: var(--ngp-background);
box-shadow: var(--ngp-input-shadow);
box-sizing: border-box;
}
[ngpCombobox][data-focus] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
[ngpComboboxInput] {
flex: 1;
padding: 0 16px;
border: none;
background-color: transparent;
color: var(--ngp-text);
font-family: inherit;
font-size: 14px;
padding: 0 16px;
outline: none;
height: 100%;
}
[ngpComboboxButton] {
display: inline-flex;
justify-content: center;
align-items: center;
height: 100%;
width: 36px;
background-color: transparent;
border: none;
color: var(--ngp-text);
cursor: pointer;
box-sizing: border-box;
}
[ngpComboboxDropdown] {
background-color: var(--ngp-background);
border: 1px solid var(--ngp-border);
padding: 0.25rem;
border-radius: 0.75rem;
outline: none;
position: absolute;
animation: popover-show 0.1s ease-out;
width: var(--ngp-combobox-width);
box-shadow: var(--ngp-shadow-lg);
box-sizing: border-box;
margin-top: 4px;
max-height: 240px;
overflow-y: auto;
transform-origin: var(--ngp-combobox-transform-origin);
}
[ngpComboboxDropdown][data-enter] {
animation: combobox-show 0.1s ease-out;
}
[ngpComboboxDropdown][data-exit] {
animation: combobox-hide 0.1s ease-out;
}
[ngpComboboxOption] {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.75rem;
cursor: pointer;
border-radius: 0.5rem;
width: 100%;
height: 36px;
font-size: 14px;
color: var(--ngp-text-primary);
box-sizing: border-box;
}
[ngpComboboxOption][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpComboboxOption][data-press] {
background-color: var(--ngp-background-active);
}
[ngpComboboxOption][data-active] {
background-color: var(--ngp-background-active);
}
.empty-message {
display: flex;
justify-content: center;
align-items: center;
padding: 0.5rem;
color: var(--ngp-text-secondary);
font-size: 14px;
font-weight: 500;
text-align: center;
}
@keyframes combobox-show {
0% {
opacity: 0;
transform: translateY(-10px) scale(0.9);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes combobox-hide {
0% {
opacity: 1;
transform: translateY(0) scale(1);
}
100% {
opacity: 0;
transform: translateY(-10px) scale(0.9);
}
}
`,
})
export class Combobox implements ControlValueAccessor {
/** The options for the combobox. */
readonly options = input([]);
/** The selected value. */
readonly value = model();
/** The placeholder for the input. */
readonly placeholder = input('');
/** The disabled state of the combobox. */
readonly disabled = input(false, {
transform: booleanAttribute,
});
/** The filter value. */
protected readonly filter = signal('');
/** Get the filtered options. */
protected readonly filteredOptions = computed(() =>
this.options().filter(option => option.toLowerCase().includes(this.filter().toLowerCase())),
);
/** Store the form disabled state */
protected readonly formDisabled = signal(false);
/** The on change callback */
private onChange?: ChangeFn;
/** The on touch callback */
protected onTouched?: TouchedFn;
onFilterChange(event: Event): void {
const input = event.target as HTMLInputElement;
this.filter.set(input.value);
}
writeValue(value: string | undefined): void {
this.value.set(value);
this.filter.set(value ?? '');
}
registerOnChange(fn: ChangeFn): void {
this.onChange = fn;
}
registerOnTouched(fn: TouchedFn): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.formDisabled.set(isDisabled);
}
protected onValueChange(value: string): void {
this.onChange?.(value);
// update the filter value
this.filter.set(value);
}
protected resetOnClose(open: boolean): void {
// if the dropdown is closed, reset the filter value
if (open) {
return;
}
// if the filter value is empty, set the value to undefined
if (this.filter() === '') {
this.value.set(undefined);
} else {
// otherwise set the filter value to the selected value
this.filter.set(this.value() ?? '');
}
}
}
```
## Examples
### Button-only Combobox
This example demonstrates a combobox without an input field. The combobox element itself becomes focusable.
## Example
```typescript
import { Component, signal } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroChevronDown } from '@ng-icons/heroicons/outline';
import {
NgpCombobox,
NgpComboboxButton,
NgpComboboxDropdown,
NgpComboboxOption,
NgpComboboxPortal,
} from 'ng-primitives/combobox';
@Component({
selector: 'app-combobox-button',
imports: [
NgpCombobox,
NgpComboboxDropdown,
NgpComboboxOption,
NgpComboboxPortal,
NgpComboboxButton,
NgIcon,
],
providers: [provideIcons({ heroChevronDown })],
template: `
Select All
@if (selectAllState() === 'all') {
} @else if (selectAllState() === 'some') {
}
@for (option of filteredOptions(); track option) {
{{ option }}
@if (isSelected(option)) {
}
} @empty {
No options found
}
`,
styles: `
[ngpCombobox] {
display: flex;
justify-content: space-between;
align-items: center;
height: 36px;
width: 300px;
border-radius: 8px;
border: none;
background-color: var(--ngp-background);
box-shadow: var(--ngp-input-shadow);
box-sizing: border-box;
}
[ngpCombobox][data-focus] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
[ngpComboboxInput] {
flex: 1;
padding: 0 16px;
border: none;
background-color: transparent;
color: var(--ngp-text);
font-family: inherit;
font-size: 14px;
padding: 0 16px;
outline: none;
height: 100%;
}
[ngpComboboxButton] {
display: inline-flex;
justify-content: center;
align-items: center;
height: 100%;
width: 36px;
background-color: transparent;
border: none;
color: var(--ngp-text);
cursor: pointer;
box-sizing: border-box;
}
[ngpComboboxDropdown] {
background-color: var(--ngp-background);
border: 1px solid var(--ngp-border);
padding: 0.25rem;
border-radius: 0.75rem;
outline: none;
position: absolute;
animation: popover-show 0.1s ease-out;
width: var(--ngp-combobox-width);
box-shadow: var(--ngp-shadow-lg);
box-sizing: border-box;
margin-top: 4px;
max-height: 240px;
overflow-y: auto;
z-index: 1001;
}
[ngpComboboxDropdown][data-enter] {
animation: combobox-show 0.1s ease-out;
}
[ngpComboboxDropdown][data-exit] {
animation: combobox-hide 0.1s ease-out;
}
[ngpComboboxOption] {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.375rem 0.75rem;
cursor: pointer;
border-radius: 0.5rem;
width: 100%;
height: 36px;
font-size: 14px;
color: var(--ngp-text-primary);
box-sizing: border-box;
}
[ngpComboboxOption][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpComboboxOption][data-press] {
background-color: var(--ngp-background-active);
}
[ngpComboboxOption][data-active] {
background-color: var(--ngp-background-active);
}
[ngpComboboxOption][data-selected] {
background-color: var(--ngp-background-active);
}
.select-all-option {
font-weight: 600;
}
.divider {
height: 1px;
background-color: var(--ngp-border);
margin: 0.25rem 0;
}
.empty-message {
display: flex;
justify-content: center;
align-items: center;
padding: 0.5rem;
color: var(--ngp-text-secondary);
font-size: 14px;
font-weight: 500;
text-align: center;
}
ng-icon {
width: 16px;
height: 16px;
color: var(--ngp-primary);
}
@keyframes combobox-show {
0% {
opacity: 0;
transform: translateY(-10px) scale(0.9);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes combobox-hide {
0% {
opacity: 1;
transform: translateY(0) scale(1);
}
100% {
opacity: 0;
transform: translateY(-10px) scale(0.9);
}
}
`,
})
export default class ComboboxSelectAllExample {
/** The options for the combobox */
readonly options: string[] = [
'Marty McFly',
'Doc Brown',
'Biff Tannen',
'George McFly',
'Jennifer Parker',
'Emmett Brown',
'Einstein',
'Clara Clayton',
'Needles',
'Goldie Wilson',
'Marvin Berry',
'Lorraine Baines',
'Strickland',
];
/** The selected values */
readonly value = signal([]);
/** The filter value */
readonly filter = signal('');
/** Get the filtered options */
protected readonly filteredOptions = computed(() =>
this.filter()
? this.options.filter(option => option.toLowerCase().includes(this.filter().toLowerCase()))
: this.options,
);
/** Get the display value for the input */
protected readonly displayValue = computed(() => {
const selected = this.value();
if (selected.length === 0) {
return '';
}
if (selected.length === 1) {
return selected[0];
}
return `${selected.length} selected`;
});
/** Get the select all state */
protected readonly selectAllState = computed(() => {
const selected = this.value();
const filtered = this.filteredOptions();
if (filtered.length === 0) {
return 'none';
}
const allSelected = filtered.every(option => selected.includes(option));
const someSelected = filtered.some(option => selected.includes(option));
if (allSelected) {
return 'all';
}
if (someSelected) {
return 'some';
}
return 'none';
});
/** Check if an option is selected */
protected isSelected(option: string): boolean {
return this.value().includes(option);
}
/** Handle filter change */
protected onFilterChange(event: Event): void {
const input = event.target as HTMLInputElement;
this.filter.set(input.value);
}
/** Reset filter when dropdown closes */
protected resetOnClose(open: boolean): void {
if (!open) {
this.filter.set('');
}
}
}
```
#### Select All Features
- **Toggle All**: Click to select all options if not all are selected, or deselect all if all are selected
- **Visual State**: The "Select All" option shows as selected when all individual options are selected
- **Filtering Support**: When options are filtered, "Select All" only affects currently visible options
- **Keyboard Navigation**: Full keyboard support with Arrow keys and Enter
- **Multiple Mode Only**: Automatically disabled in single selection mode
To implement Select All, use the special value `'all'` for your Select All option:
```html
Select All
```
### Virtualized Large Lists
When dealing with large datasets (thousands of items), you can use TanStack Virtual or other virtualization libraries to efficiently render only the visible options, improving performance:
## Example
```typescript
import { Component, computed, ElementRef, signal, viewChild } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroChevronDown } from '@ng-icons/heroicons/outline';
import { injectVirtualizer } from '@tanstack/angular-virtual';
import {
NgpCombobox,
NgpComboboxButton,
NgpComboboxDropdown,
NgpComboboxInput,
NgpComboboxOption,
NgpComboboxPortal,
} from 'ng-primitives/combobox';
@Component({
selector: 'app-combobox-virtual',
imports: [
NgpCombobox,
NgpComboboxDropdown,
NgpComboboxOption,
NgpComboboxInput,
NgpComboboxPortal,
NgpComboboxButton,
NgIcon,
],
providers: [provideIcons({ heroChevronDown })],
template: `
@if (filteredOptions().length > 0) {
@for (virtualRow of virtualizer.getVirtualItems(); track virtualRow.index) {
{{ filteredOptions()[virtualRow.index] }}
}
} @else {
No options found
}
Showing {{ filteredOptions().length }} of {{ options.length }} results
@for (option of filteredOptions(); track option) {
{{ option }}
} @empty {
No options found
}
`,
styles: `
[ngpCombobox] {
display: flex;
justify-content: space-between;
align-items: center;
height: 36px;
width: 300px;
border-radius: 8px;
border: none;
background-color: var(--ngp-background);
box-shadow: var(--ngp-input-shadow);
box-sizing: border-box;
}
[ngpCombobox][data-focus] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
[ngpComboboxInput] {
flex: 1;
padding: 0 16px;
border: none;
background-color: transparent;
color: var(--ngp-text);
font-family: inherit;
font-size: 14px;
padding: 0 16px;
outline: none;
height: 100%;
}
[ngpComboboxButton] {
display: inline-flex;
justify-content: center;
align-items: center;
height: 100%;
width: 36px;
background-color: transparent;
border: none;
color: var(--ngp-text);
cursor: pointer;
box-sizing: border-box;
}
[ngpComboboxDropdown] {
background-color: var(--ngp-background);
border: 1px solid var(--ngp-border);
padding: 0.25rem;
border-radius: 0.75rem;
outline: none;
position: absolute;
animation: popover-show 0.1s ease-out;
width: var(--ngp-combobox-width);
box-shadow: var(--ngp-shadow-lg);
box-sizing: border-box;
margin-top: 4px;
max-height: 240px;
overflow-y: auto;
z-index: 1001;
}
[ngpComboboxDropdown][data-enter] {
animation: combobox-show 0.1s ease-out;
}
[ngpComboboxDropdown][data-exit] {
animation: combobox-hide 0.1s ease-out;
}
[ngpComboboxOption] {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.75rem;
cursor: pointer;
border-radius: 0.5rem;
width: 100%;
height: 36px;
font-size: 14px;
color: var(--ngp-text-primary);
box-sizing: border-box;
}
[ngpComboboxOption][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpComboboxOption][data-press] {
background-color: var(--ngp-background-active);
}
[ngpComboboxOption][data-active] {
background-color: var(--ngp-background-active);
}
.clear-option {
color: var(--ngp-text-secondary);
font-style: italic;
}
.divider {
height: 1px;
background-color: var(--ngp-border);
margin: 0.25rem 0;
}
.empty-message {
display: flex;
justify-content: center;
align-items: center;
padding: 0.5rem;
color: var(--ngp-text-secondary);
font-size: 14px;
font-weight: 500;
text-align: center;
}
@keyframes combobox-show {
0% {
opacity: 0;
transform: translateY(-10px) scale(0.9);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes combobox-hide {
0% {
opacity: 1;
transform: translateY(0) scale(1);
}
100% {
opacity: 0;
transform: translateY(-10px) scale(0.9);
}
}
`,
})
export default class ComboboxCustomOptionExample {
/** The options for the combobox. */
readonly options: string[] = [
'Marty McFly',
'Doc Brown',
'Biff Tannen',
'George McFly',
'Jennifer Parker',
'Emmett Brown',
'Einstein',
'Clara Clayton',
'Needles',
'Goldie Wilson',
'Marvin Berry',
'Lorraine Baines',
'Strickland',
];
/** The selected value. */
readonly value = signal(undefined);
/** The filter value. */
readonly filter = signal('');
/** Get the filtered options. */
protected readonly filteredOptions = computed(() =>
this.options.filter(option => option.toLowerCase().includes(this.filter().toLowerCase())),
);
protected onFilterChange(event: Event): void {
const input = event.target as HTMLInputElement;
this.filter.set(input.value);
}
protected resetOnClose(open: boolean): void {
// if the dropdown is closed, reset the filter value
if (open) {
return;
}
// if the filter value is empty, set the value to undefined
if (this.filter() === '') {
this.value.set(undefined);
} else {
// otherwise set the filter value to the selected value
this.filter.set(this.value() ?? '');
}
}
/** Clear the selection. */
clear(): void {
this.value.set(undefined);
this.filter.set('');
}
}
```
## Schematics
Generate a reusable combobox component using the Angular CLI.
```bash npm
ng g ng-primitives:primitive combobox
```
### Options
- `path`: The path at which to create the component file.
- `prefix`: The prefix to apply to the generated component selector.
- `componentSuffix`: The suffix to apply to the generated component class name.
- `fileSuffix`: The suffix to apply to the generated component file name. Defaults to `component`.
- `exampleStyles`: Whether to include example styles in the generated component file. Defaults to `true`.
## API Reference
The following directives are available to import from the `ng-primitives/combobox` package:
### NgpCombobox
The main container for the combobox.
## API Reference
### NgpCombobox
**Selector:** `[ngpCombobox]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpComboboxValue` | `any` | `-` | The value of the combobox. |
| `ngpComboboxMultiple` | `boolean` | `-` | Whether the combobox is multiple selection. |
| `ngpComboboxDisabled` | `boolean` | `-` | Whether the combobox is disabled. |
| `ngpComboboxAllowDeselect` | `boolean` | `-` | Whether the combobox allows deselection in single selection mode. |
| `ngpComboboxCompareWith` | `(a: any, b: any) => boolean` | `-` | The comparator function used to compare options. |
| `ngpComboboxDropdownPlacement` | `NgpComboboxPlacement` | `-` | The position of the dropdown. |
| `ngpComboboxDropdownContainer` | `string | HTMLElement | null` | `-` | The container for the dropdown. |
| `ngpComboboxScrollToOption` | `((index: number) => void) | undefined` | `-` | A function that will scroll the active option into view. This can be overridden
for cases such as virtual scrolling where we cannot scroll the option directly because
it may not be rendered. |
| `ngpComboboxOptions` | `any[] | undefined` | `-` | Provide all the option values to the combobox. This is useful for virtual scrolling scenarios
where not all options are rendered in the DOM. This is not an alternative to adding the options
in the DOM, it is only to provide the combobox with the full list of options. This list should match
the order of the options as they would appear in the DOM. |
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpComboboxValueChange` | `any` | Event emitted when the value changes. |
| `ngpComboboxOpenChange` | `boolean` | Emit when the dropdown open state changes. |
#### Export As
`ngpCombobox`
#### Data Attributes
The following data attributes are applied to the `ngpCombobox` directive:
| Attribute | Description |
| --------------- | -------------------------------------------------------------- |
| `data-open` | Applied when the combobox is open. |
| `data-disabled` | Applied when the combobox is disabled. |
| `data-multiple` | Applied when the combobox is in multiple mode. |
| `data-hover` | Applied when the combobox is hovered. |
| `data-press` | Applied when the combobox is pressed. |
| `data-focus` | Applied when the combobox has focus within it. |
| `data-invalid` | Applied when the combobox is invalid. |
| `data-valid` | Applied when the combobox is valid. |
| `data-touched` | Applied when the combobox has been touched. |
| `data-pristine` | Applied when the combobox is pristine (not modified). |
| `data-dirty` | Applied when the combobox has been modified. |
| `data-pending` | Applied when the combobox is pending (e.g., async validation). |
| `data-disabled` | Applied when the combobox is disabled. |
#### Focus Management
When no `ngpComboboxInput` is present, the combobox element itself receives:
- `tabindex="0"` to make it focusable via keyboard navigation
- `tabindex="-1"` when disabled or when an input is present
- Full keyboard navigation support
### NgpComboboxButton
The button that toggles the combobox dropdown.
## API Reference
### NgpComboboxButton
**Selector:** `button[ngpComboboxButton]`
#### Export As
`ngpComboboxButton`
#### Data Attributes
The following data attributes are applied to the `ngpComboboxButton` directive:
| Attribute | Description |
| --------------- | ---------------------------------------------- |
| `data-open` | Applied when the combobox is open. |
| `data-disabled` | Applied when the combobox is disabled. |
| `data-multiple` | Applied when the combobox is in multiple mode. |
### NgpComboboxDropdown
The dropdown that contains the combobox options.
## API Reference
### NgpComboboxDropdown
**Selector:** `[ngpComboboxDropdown]`
#### Export As
`ngpComboboxDropdown`
#### CSS Custom Properties
The following CSS custom properties are applied to the `ngpComboboxDropdown` directive:
| Property | Description |
| --------------------------------- | ------------------------------------------------ |
| `--ngp-combobox-transform-origin` | The transform origin for the dropdown animation. |
| `--ngp-combobox-width` | The width of the combobox dropdown. |
| `--ngp-combobox-input-width` | The width of the combobox input field. |
| `--ngp-combobox-button-width` | The width of the combobox button. |
### NgpComboboxInput
The input field for the combobox.
## API Reference
### NgpComboboxInput
**Selector:** `input[ngpComboboxInput]`
#### Export As
`ngpComboboxInput`
#### Data Attributes
The following data attributes are applied to the `ngpComboboxInput` directive:
| Attribute | Description |
| --------------- | ----------------------------------------------------------- |
| `data-open` | Applied when the combobox is open. |
| `data-disabled` | Applied when the combobox is disabled. |
| `data-multiple` | Applied when the combobox is in multiple mode. |
| `data-invalid` | Applied when the input is invalid. |
| `data-valid` | Applied when the input is valid. |
| `data-touched` | Applied when the input has been touched. |
| `data-pristine` | Applied when the input is pristine (not modified). |
| `data-dirty` | Applied when the input has been modified. |
| `data-pending` | Applied when the input is pending (e.g., async validation). |
| `data-disabled` | Applied when the input is disabled. |
### NgpComboboxOption
The individual options within the combobox dropdown.
## API Reference
### NgpComboboxOption
**Selector:** `[ngpComboboxOption]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpComboboxOptionValue` | `any` | `-` | - |
| `ngpComboboxOptionDisabled` | `boolean` | `-` | The disabled state of the option. |
| `ngpComboboxOptionIndex` | `number | undefined` | `-` | The index of the option in the combobox. This can be used to define the order of options
when virtual scrolling is used or when the order is not determined by DOM order. |
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpComboboxOptionActivated` | `void` | Event emitted when the option is activated via click or keyboard.
This is useful for options without values that need custom behavior. |
#### Export As
`ngpComboboxOption`
#### Data Attributes
The following data attributes are applied to the `ngpComboboxOption` directive:
| Attribute | Description |
| --------------- | ------------------------------------ |
| `data-selected` | Applied when the option is selected. |
| `data-active` | Applied when the option is active. |
| `data-disabled` | Applied when the option is disabled. |
### NgpComboboxPortal
The portal for rendering the combobox dropdown in an overlay.
## API Reference
### NgpComboboxPortal
**Selector:** `[ngpComboboxPortal]`
#### Export As
`ngpComboboxPortal`
## Animations
The `ngpComboboxDropdown` primitive adds a CSS custom property `--ngp-combobox-transform-origin` to the element that can be used to animate the menu from the trigger element.
The `ngpComboboxDropdown` will also add the `data-enter` and `data-exit` attributes to the element when it is being added or removed from the DOM. This can be used to trigger animations.
```css
:host[data-enter] {
animation: fade-in 0.2s ease-in-out;
}
:host[data-exit] {
animation: fade-out 0.2s ease-in-out;
}
```
## Global Configuration
You can configure the default options for all comboboxes in your application by using the `provideComboboxConfig` function in a providers array.
```ts
import { provideComboboxConfig } from 'ng-primitives/combobox';
bootstrapApplication(AppComponent, {
providers: [provideComboboxConfig({ placement: 'bottom', container: document.body })],
});
```
### NgpComboboxConfig
Define the placement of the combobox dropdown.
Define the container element for the combobox dropdown. This is useful for rendering the dropdown in a specific part of the DOM.
## Accessibility
Adheres to the [WAI-ARIA](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) guidelines for comboboxes.
### Keyboard Interactions
The combobox supports comprehensive keyboard navigation whether or not an input field is present:
#### With Input Field
- ArrowDown: Open the dropdown and focus the first option. If the dropdown is already open, move focus to the next option.
- ArrowUp: Open the dropdown and focus the last option. If the dropdown is already open, move focus to the previous option.
- Home: Move focus to the first option (when dropdown is open).
- End: Move focus to the last option (when dropdown is open).
- Enter: Toggle the selection state of the focused option. In single selection mode, this will select the option and close the dropdown. In multiple selection mode, this will toggle the option without closing the dropdown.
- Escape: Close the dropdown without selecting an option.
- Any character key: Open the dropdown and filter options based on typed text.
#### Without Input Field
When no `ngpComboboxInput` is present, the combobox container becomes focusable and supports:
- Tab: Focus the combobox container.
- ArrowDown: Open the dropdown and focus the first option. If already open, move to the next option.
- ArrowUp: Open the dropdown and focus the last option. If already open, move to the previous option.
- Home: Move focus to the first option (when dropdown is open).
- End: Move focus to the last option (when dropdown is open).
- Enter: Select the focused option and close the dropdown.
- Escape: Close the dropdown without selecting an option.
---
### Date Picker
File: primitives/date-picker.md
URL: https://angularprimitives.com/primitives/date-picker
# Date Picker
A date picker is a component that allows users to select a date from a calendar and navigate through months and years.
## Example
```typescript
import { Component, computed, signal } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroChevronLeftMini, heroChevronRightMini } from '@ng-icons/heroicons/mini';
import {
NgpDatePicker,
NgpDatePickerCell,
NgpDatePickerCellRender,
NgpDatePickerDateButton,
NgpDatePickerGrid,
NgpDatePickerLabel,
NgpDatePickerNextMonth,
NgpDatePickerPreviousMonth,
NgpDatePickerRowRender,
} from 'ng-primitives/date-picker';
@Component({
selector: 'app-date-picker',
imports: [
NgIcon,
NgpDatePicker,
NgpDatePickerLabel,
NgpDatePickerNextMonth,
NgpDatePickerPreviousMonth,
NgpDatePickerGrid,
NgpDatePickerCell,
NgpDatePickerRowRender,
NgpDatePickerCellRender,
NgpDatePickerDateButton,
],
providers: [provideIcons({ heroChevronRightMini, heroChevronLeftMini })],
template: `
`,
styles: `
:host {
display: inline-block;
background-color: var(--ngp-background);
border-radius: 12px;
padding: 16px;
box-shadow: var(--ngp-shadow);
border: 1px solid var(--ngp-border);
}
.date-picker-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 36px;
margin-bottom: 16px;
}
th {
font-size: 14px;
font-weight: 500;
width: 40px;
height: 40px;
text-align: center;
color: var(--ngp-text-secondary);
}
[ngpDatePickerLabel] {
font-size: 14px;
font-weight: 500;
color: var(--ngp-text-primary);
}
[ngpDatePickerPreviousMonth],
[ngpDatePickerNextMonth] {
all: unset;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
font-size: 20px;
border: 1px solid var(--ngp-border);
cursor: pointer;
}
[ngpDatePickerPreviousMonth][data-hover],
[ngpDatePickerNextMonth][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpDatePickerPreviousMonth][data-focus-visible],
[ngpDatePickerNextMonth][data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
}
[ngpDatePickerPreviousMonth][data-press],
[ngpDatePickerNextMonth][data-press] {
background-color: var(--ngp-background-active);
}
[ngpDatePickerPreviousMonth][data-disabled],
[ngpDatePickerNextMonth][data-disabled] {
cursor: not-allowed;
color: var(--ngp-text-disabled);
}
[ngpDatePickerDateButton] {
all: unset;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
cursor: pointer;
}
[ngpDatePickerDateButton][data-today] {
color: var(--ngp-text-blue);
}
[ngpDatePickerDateButton][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpDatePickerDateButton][data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
[ngpDatePickerDateButton][data-press] {
background-color: var(--ngp-background-active);
}
[ngpDatePickerDateButton][data-outside-month] {
color: var(--ngp-text-disabled);
}
[ngpDatePickerDateButton][data-selected] {
background-color: var(--ngp-background-inverse);
color: var(--ngp-text-inverse);
}
[ngpDatePickerDateButton][data-selected][data-outside-month] {
background-color: var(--ngp-background-disabled);
color: var(--ngp-text-disabled);
}
[ngpDatePickerDateButton][data-disabled] {
cursor: not-allowed;
color: var(--ngp-text-disabled);
}
`,
host: {
'(focusout)': 'onTouched?.()',
},
})
export class DatePicker implements ControlValueAccessor {
/** Access the date picker host directive */
private readonly state = injectDatePickerState();
/**
* Get the current focused date in string format.
* @returns The focused date in "February 2024" format.
*/
readonly label = computed(
() =>
`${this.state().focusedDate().toLocaleString('default', { month: 'long' })} ${this.state().focusedDate().getFullYear()}`,
);
/**
* The onChange callback function for the date picker.
*/
protected onChange?: ChangeFn;
/**
* The onTouched callback function for the date picker.
*/
protected onTouched?: TouchedFn;
constructor() {
// Whenever the user interacts with the date picker, call the onChange function with the new value.
this.state().dateChange.subscribe(date => this.onChange?.(date));
}
writeValue(date: Date): void {
this.state().select(date);
}
registerOnChange(fn: ChangeFn): void {
this.onChange = fn;
}
registerOnTouched(fn: TouchedFn): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.state().disabled.set(isDisabled);
}
}
```
## Schematics
Generate a reusable date-picker component using the Angular CLI.
```bash npm
ng g ng-primitives:primitive date-picker
```
### Options
- `path`: The path at which to create the component file.
- `prefix`: The prefix to apply to the generated component selector.
- `componentSuffix`: The suffix to apply to the generated component class name.
- `fileSuffix`: The suffix to apply to the generated component file name. Defaults to `component`.
- `exampleStyles`: Whether to include example styles in the generated component file. Defaults to `true`.
## Examples
Here are some additional examples of how to use the Date Picker primitives.
### Date Range Picker
The date range picker allows users to select a range of dates by selecting the start and end dates.
## Example
```typescript
import { Component, computed, signal } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroChevronLeftMini, heroChevronRightMini } from '@ng-icons/heroicons/mini';
import {
NgpDatePickerCell,
NgpDatePickerCellRender,
NgpDatePickerDateButton,
NgpDatePickerGrid,
NgpDatePickerLabel,
NgpDatePickerNextMonth,
NgpDatePickerPreviousMonth,
NgpDatePickerRowRender,
NgpDateRangePicker,
} from 'ng-primitives/date-picker';
@Component({
selector: 'app-date-range-picker',
imports: [
NgIcon,
NgpDateRangePicker,
NgpDatePickerLabel,
NgpDatePickerNextMonth,
NgpDatePickerPreviousMonth,
NgpDatePickerGrid,
NgpDatePickerCell,
NgpDatePickerRowRender,
NgpDatePickerCellRender,
NgpDatePickerDateButton,
],
providers: [provideIcons({ heroChevronRightMini, heroChevronLeftMini })],
template: `
{{ label() }}
S
M
T
W
T
F
S
`,
styles: `
[ngpDateRangePicker] {
display: inline-block;
background-color: var(--ngp-background);
border-radius: 12px;
padding: 16px;
box-shadow: var(--ngp-shadow);
border: 1px solid var(--ngp-border);
}
.date-picker-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 36px;
margin-bottom: 16px;
}
th {
font-size: 14px;
font-weight: 500;
width: 40px;
height: 40px;
text-align: center;
color: var(--ngp-text-secondary);
}
[ngpDatePickerLabel] {
font-size: 14px;
font-weight: 500;
color: var(--ngp-text-primary);
}
[ngpDatePickerPreviousMonth],
[ngpDatePickerNextMonth] {
all: unset;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
font-size: 20px;
border: 1px solid var(--ngp-border);
cursor: pointer;
}
[ngpDatePickerPreviousMonth][data-hover],
[ngpDatePickerNextMonth][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpDatePickerPreviousMonth][data-focus-visible],
[ngpDatePickerNextMonth][data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
}
[ngpDatePickerPreviousMonth][data-press],
[ngpDatePickerNextMonth][data-press] {
background-color: var(--ngp-background-active);
}
[ngpDatePickerPreviousMonth][data-disabled],
[ngpDatePickerNextMonth][data-disabled] {
cursor: not-allowed;
color: var(--ngp-text-disabled);
}
[ngpDatePickerCell] {
padding: 0;
}
[ngpDatePickerDateButton] {
all: unset;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
cursor: pointer;
}
[ngpDatePickerDateButton][data-today] {
color: var(--ngp-text-blue);
}
[ngpDatePickerDateButton][data-hover] {
background: var(--ngp-background-hover);
}
[ngpDatePickerDateButton][data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
[ngpDatePickerDateButton][data-press] {
background: var(--ngp-background-active);
}
[ngpDatePickerDateButton][data-outside-month] {
color: var(--ngp-text-disabled);
}
[ngpDatePickerDateButton][data-selected] {
background: var(--ngp-background-inverse);
color: var(--ngp-text-inverse);
}
[ngpDatePickerDateButton][data-selected]:not([data-range-end]) {
border-radius: 8px 0 0 8px;
}
[ngpDatePickerDateButton][data-selected][data-range-end] {
border-radius: 0 8px 8px 0;
}
[ngpDatePickerDateButton][data-selected][data-range-start][data-range-end] {
border-radius: 8px;
}
[ngpDatePickerDateButton][data-range-between] {
background: color-mix(in srgb, var(--ngp-background-inverse) 5%, transparent);
border-radius: 0;
}
[ngpDatePickerDateButton][data-selected][data-outside-month] {
background-color: var(--ngp-background-disabled);
color: var(--ngp-text-disabled);
}
[ngpDatePickerDateButton][data-disabled] {
cursor: not-allowed;
color: var(--ngp-text-disabled);
}
`,
})
export default class DateRangePickerExample {
/**
* The start date of the range.
*/
readonly startDate = signal(new Date(2025, 7, 10));
/**
* The end date of the range.
*/
readonly endDate = signal(new Date(2025, 7, 14));
/**
* Store the current focused date.
*/
readonly focused = signal(new Date(2025, 7, 10));
/**
* Get the current focused date in string format.
* @returns The focused date in "February 2024" format.
*/
readonly label = computed(
() =>
`${this.focused().toLocaleString('default', { month: 'long' })} ${this.focused().getFullYear()}`,
);
}
```
## API Reference
By default, the date picker uses the native JavaScript `Date` object, however the date picker is designed to work with any date library. To use a date library, such as Luxon, you need to specify the appropriate date adapter. The date adapter is an abstraction layer that allows components to use date objects from any date library, ensuring compatibility and easy integration. To learn more about the date adapter, see the [Date Adapter](/utilities/date-adapter) documentation.
The following directives are available to import from the `ng-primitives/date-picker` package:
### NgpDatePicker
## API Reference
### NgpDatePicker
The outermost container for the date picker.
**Selector:** `[ngpDatePicker]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpDatePickerMin` | `T | undefined` | `-` | The minimum date that can be selected. |
| `ngpDatePickerMax` | `T | undefined` | `-` | The maximum date that can be selected. |
| `ngpDatePickerDisabled` | `boolean` | `-` | Determine if the date picker is disabled. |
| `ngpDatePickerDateDisabled` | `(date: T) => boolean` | `-` | A function that is called to determine if a specific date should be disabled. |
| `ngpDatePickerFirstDayOfWeek` | `NgpDatePickerFirstDayOfWeekNumber` | `7 (Sunday)` | Sets which day starts the week in the calendar.
Accepts 0-7 where 1=Monday, 2=Tuesday, 3=Wednesday, 4=Thursday, 5=Friday, 6=Saturday, 7=Sunday.
Defaults to NgpDatePickerConfig.firstDayOfWeek (default 7 if not overridden).
Note: Update calendar header column order when changing from Sunday start. |
| `ngpDatePickerDate` | `T | undefined` | `-` | The selected value. |
| `ngpDatePickerFocusedDate` | `T` | `-` | The focused value. |
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpDatePickerDateChange` | `T | undefined` | Emit when the date changes. |
| `ngpDatePickerFocusedDateChange` | `T` | Emit when the focused date changes. |
#### Export As
`ngpDatePicker`
#### Data Attributes
The following data attributes are available on the `ngpDatePicker` directive:
| Attribute | Description |
| --------------- | ----------------------------------------- |
| `data-disabled` | Applied when the date picker is disabled. |
### NgpDateRangePicker
## API Reference
### NgpDateRangePicker
**Selector:** `[ngpDateRangePicker]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpDateRangePickerMin` | `T | undefined` | `-` | The minimum date that can be selected. |
| `ngpDateRangePickerMax` | `T | undefined` | `-` | The maximum date that can be selected. |
| `ngpDateRangePickerDisabled` | `boolean` | `-` | Determine if the date picker is disabled. |
| `ngpDateRangePickerDateDisabled` | `(date: T) => boolean` | `-` | A function that is called to determine if a specific date should be disabled. |
| `ngpDateRangePickerFirstDayOfWeek` | `NgpDatePickerFirstDayOfWeekNumber` | `7 (Sunday)` | Sets which day starts the week in the calendar.
Accepts 0-7 where 1=Monday, 2=Tuesday, 3=Wednesday, 4=Thursday, 5=Friday, 6=Saturday, 7=Sunday.
Defaults to NgpDatePickerConfig.firstDayOfWeek (default 7 if not overridden).
Note: Update calendar header column order when changing from Sunday start. |
| `ngpDateRangePickerStartDate` | `T | undefined` | `-` | The selected start date |
| `ngpDateRangePickerEndDate` | `T | undefined` | `-` | The selected end date |
| `ngpDateRangePickerFocusedDate` | `T` | `-` | The focused value. |
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpDateRangePickerStartDateChange` | `T | undefined` | Emit when the date changes. |
| `ngpDateRangePickerEndDateChange` | `T | undefined` | Emit when the end date changes. |
| `ngpDateRangePickerFocusedDateChange` | `T` | Emit when the focused date changes. |
#### Export As
`ngpDateRangePicker`
#### Data Attributes
The following data attributes are available on the `ngpDateRangePicker` directive:
| Attribute | Description |
| --------------- | ----------------------------------------------- |
| `data-disabled` | Applied when the date range picker is disabled. |
### NgpDatePickerLabel
## API Reference
### NgpDatePickerLabel
The label that displays the current month and year typically in the header of the date picker. This will be announced by screen readers when the date changes.
**Selector:** `[ngpDatePickerLabel]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `aria-live` | `string` | `-` | Define the aria live attribute. |
#### Export As
`ngpDatePickerLabel`
#### Data Attributes
The following data attributes are available on the `ngpDatePickerLabel` directive:
| Attribute | Description |
| --------------- | ----------------------------------------- |
| `data-disabled` | Applied when the date picker is disabled. |
### NgpDatePickerPreviousMonth
## API Reference
### NgpDatePickerPreviousMonth
A button that navigates to the previous month.
**Selector:** `[ngpDatePickerPreviousMonth]`
#### Export As
`ngpDatePickerPreviousMonth`
#### Data Attributes
The following data attributes are available on the `ngpDatePickerPreviousMonth` directive:
| Attribute | Description |
| -------------------- | ------------------------------------ |
| `data-hover` | Applied when the button is hovered. |
| `data-focus-visible` | Applied when the button is focused. |
| `data-press` | Applied when the button is pressed. |
| `data-disabled` | Applied when the button is disabled. |
### NgpDatePickerNextMonth
## API Reference
### NgpDatePickerNextMonth
A button that navigates to the next month.
**Selector:** `[ngpDatePickerNextMonth]`
#### Export As
`ngpDatePickerNextMonth`
#### Data Attributes
The following data attributes are available on the `ngpDatePickerNextMonth` directive:
| Attribute | Description |
| -------------------- | ------------------------------------ |
| `data-hover` | Applied when the button is hovered. |
| `data-focus-visible` | Applied when the button is focused. |
| `data-press` | Applied when the button is pressed. |
| `data-disabled` | Applied when the button is disabled. |
### NgpDatePickerGrid
## API Reference
### NgpDatePickerGrid
The grid that contains the days of the month.
**Selector:** `[ngpDatePickerGrid]`
#### Export As
`ngpDatePickerGrid`
#### Data Attributes
The following data attributes are available on the `ngpDatePickerGrid` directive:
| Attribute | Description |
| --------------- | ----------------------------------------- |
| `data-disabled` | Applied when the date picker is disabled. |
### NgpDatePickerRowRender
## API Reference
### NgpDatePickerRowRender
A structural directive that renders a row of weekdays in the date picker grid.
**Selector:** `[ngpDatePickerRowRender]`
#### Export As
`ngpDatePickerRowRender`
### NgpDatePickerCellRender
A structural directive that renders a cell in the date picker grid.
- Selector: `*ngpDatePickerCellRender`
- Exported As: `ngpDatePickerCellRender`
The following context fields are available on the `ngpDatePickerCellRender` directive:
The date value for the cell.
### NgpDatePickerCell
## API Reference
### NgpDatePickerCell
A cell in the date picker grid.
**Selector:** `[ngpDatePickerCell]`
#### Export As
`ngpDatePickerCell`
#### Data Attributes
The following data attributes are available on the `ngpDatePickerCell` directive:
| Attribute | Description |
| --------------- | ---------------------------------- |
| `data-disabled` | Applied when the cell is disabled. |
| `data-selected` | Applied when the cell is selected. |
### NgpDatePickerDateButton
## API Reference
### NgpDatePickerDateButton
A button that represents a date in the date picker grid.
**Selector:** `[ngpDatePickerDateButton]`
#### Export As
`ngpDatePickerDateButton`
#### Data Attributes
The following data attributes are available on the `ngpDatePickerDateButton` directive:
| Attribute | Description |
| -------------------- | --------------------------------------------------------------------- |
| `data-selected` | Applied when the button is selected. |
| `data-outside-month` | Applied when the button is outside the current month. |
| `data-today` | Applied when the button represents the current date. |
| `data-hover` | Applied when the button is hovered. |
| `data-focus-visible` | Applied when the button is focused. |
| `data-press` | Applied when the button is pressed. |
| `data-disabled` | Applied when the button is disabled. |
| `data-range-start` | Applied when the button is the start of a date range. |
| `data-range-end` | Applied when the button is the end of a date range. |
| `data-range-between` | Applied when the button is between the start and end of a date range. |
## Accessibility
Adheres to the [WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/).
### Keyboard Interactions
- Space - Selects the focused date.
- Enter - Selects the focused date.
- ArrowUp - Moves focus to the same day of the previous week.
- ArrowDown - Moves focus to the same day of the next week.
- ArrowLeft - Moves focus to the previous day.
- ArrowRight - Moves focus to the next day.
- Home - Moves focus to the first day of the month.
- End - Moves focus to the last day of the month.
- PageUp - Moves focus to the same date in the previous month.
- PageDown - Moves focus to the same date in the next month.
## Global Configuration
You can configure the default options for all `NgpDatePicker` and `NgpDateRangePicker` calendars in your application by using the `provideDatePickerConfig` function in a providers array.
```ts
import { provideDatePickerConfig } from 'ng-primitives/date-picker';
bootstrapApplication(AppComponent, {
providers: [
provideDatePickerConfig({
firstDayOfWeek: 1, // Monday
}),
],
});
```
### NgpDatePickerConfig
Sets which day starts the week in date picker and date range picker calendars.
Accepts 1-7 where:
1=Monday,
2=Tuesday,
3=Wednesday,
4=Thursday,
5=Friday,
6=Saturday,
7=Sunday (default).
Choose based on your users' cultural expectations - most international
applications use 1 (Monday), while US applications typically use
7 (Sunday).
Note: When using a non-Sunday start day, update your calendar header column order accordingly.
---
### Dialog
File: primitives/dialog.md
URL: https://angularprimitives.com/primitives/dialog
# Dialog
A dialog is a floating window that can be used to display information or prompt the user for input.
## Example
```typescript
import { Component } from '@angular/core';
import { NgpButton } from 'ng-primitives/button';
import {
NgpDialog,
NgpDialogDescription,
NgpDialogOverlay,
NgpDialogTitle,
NgpDialogTrigger,
} from 'ng-primitives/dialog';
@Component({
selector: 'app-dialog',
imports: [
NgpButton,
NgpDialog,
NgpDialogOverlay,
NgpDialogTitle,
NgpDialogDescription,
NgpDialogTrigger,
],
template: `
Publish this article?
Are you sure you want to publish this article? This action is irreversible.
Are you sure you want to publish this article? This action is irreversible.
```
## Reusable Component
Create reusable components that uses the `NgpDialog` and `NgpDialogTrigger` directives.
## Reusable Component
```typescript
import { Component, input } from '@angular/core';
import {
NgpDialog,
NgpDialogDescription,
NgpDialogOverlay,
NgpDialogTitle,
provideDialogState,
} from 'ng-primitives/dialog';
@Component({
selector: 'app-dialog',
hostDirectives: [NgpDialogOverlay],
imports: [NgpDialog, NgpDialogTitle, NgpDialogDescription],
providers: [
// We need to hoist the dialog state to the host component so that it can be used
// within ng-content
provideDialogState(),
],
template: `
{{ header() }}
`,
styles: `
:host {
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
position: fixed;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
animation: fadeIn 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
:host[data-exit] {
animation: fadeOut 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
[ngpDialog] {
background-color: var(--ngp-background);
padding: 24px;
border-radius: 8px;
box-shadow: var(--ngp-shadow);
animation: slideIn 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
[ngpDialog][data-exit] {
animation: slideOut 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
[ngpDialogTitle] {
font-size: 18px;
line-height: 28px;
font-weight: 600;
color: var(--ngp-text-primary);
margin: 0 0 4px;
}
[ngpDialogDescription] {
font-size: 14px;
line-height: 20px;
color: var(--ngp-text-secondary);
margin: 0;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes slideIn {
0% {
transform: translateY(-20px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideOut {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(-20px);
opacity: 0;
}
}
`,
})
export class Dialog {
/** The dialog title */
readonly header = input.required();
}
```
## Schematics
Generate a reusable dialog component using the Angular CLI.
```bash npm
ng g ng-primitives:primitive dialog
```
### Options
- `path`: The path at which to create the component file.
- `prefix`: The prefix to apply to the generated component selector.
- `componentSuffix`: The suffix to apply to the generated component class name.
- `fileSuffix`: The suffix to apply to the generated component file name. Defaults to `component`.
- `exampleStyles`: Whether to include example styles in the generated component file. Defaults to `true`.
## API Reference
The following directives are available to import from the `ng-primitives/dialog` package:
### NgpDialog
## API Reference
### NgpDialog
**Selector:** `[ngpDialog]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpDialogRole` | `NgpDialogRole | undefined` | `-` | The dialog role. |
| `ngpDialogModal` | `boolean` | `-` | Whether the dialog is a modal. |
#### Export As
`ngpDialog`
| Attribute | Description |
| ----------- | ----------------------------------- |
| `data-exit` | Applied when the dialog is closing. |
### NgpDialogTitle
## API Reference
### NgpDialogTitle
**Selector:** `[ngpDialogTitle]`
#### Export As
`ngpDialogTitle`
### NgpDialogDescription
## API Reference
### NgpDialogDescription
**Selector:** `[ngpDialogDescription]`
#### Export As
`ngpDialogDescription`
### NgpDialogTrigger
## API Reference
### NgpDialogTrigger
**Selector:** `[ngpDialogTrigger]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpDialogTrigger` | `TemplateRef
Opens a dialog with the specified component or template reference.
Closes all open dialogs.
## Examples
### Dialog with external data
Data can be passed to the dialog using the `NgpDialogManager`.
## Example
```typescript
import { Component, inject } from '@angular/core';
import { NgpButton } from 'ng-primitives/button';
import {
injectDialogRef,
NgpDialog,
NgpDialogDescription,
NgpDialogManager,
NgpDialogOverlay,
NgpDialogTitle,
} from 'ng-primitives/dialog';
@Component({
selector: 'app-dialog',
imports: [NgpButton],
template: `
`,
styles: `
button {
padding-left: 1rem;
padding-right: 1rem;
border-radius: 0.5rem;
color: var(--ngp-text-primary);
border: none;
outline: none;
height: 2.5rem;
font-weight: 500;
background-color: var(--ngp-background);
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--ngp-button-shadow);
}
button[data-hover] {
background-color: var(--ngp-background-hover);
}
button[data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
button[data-press] {
background-color: var(--ngp-background-active);
}
`,
})
export default class DialogDataExample {
private dialogManager = inject(NgpDialogManager);
openDialog() {
this.dialogManager.open(Dialog, {
data: 'This came from the dialog opener!',
});
}
}
@Component({
imports: [NgpButton, NgpDialog, NgpDialogOverlay, NgpDialogTitle, NgpDialogDescription],
template: `
`,
styles: `
[ngpFileUpload] {
display: flex;
cursor: pointer;
flex-direction: column;
align-items: center;
justify-content: center;
row-gap: 0.25rem;
border-radius: 0.5rem;
border: 1px dashed var(--ngp-border-secondary);
background-color: var(--ngp-background);
padding: 2rem 3rem;
}
[ngpFileUpload][data-dragover] {
border-color: var(--ngp-border);
background-color: var(--ngp-background-hover);
}
ng-icon {
color: var(--ngp-text-primary);
font-size: 20px;
margin-bottom: 0.25rem;
}
.heading {
font-size: 0.875rem;
font-weight: 500;
color: var(--ngp-text-primary);
line-height: 1.25rem;
text-align: center;
margin: 0;
}
.subheading {
font-size: 0.75rem;
color: var(--ngp-text-secondary);
line-height: 1rem;
text-align: center;
margin: 0;
}
`,
})
export default class FileUploadExample {
onFilesSelected(files: FileList | null): void {
if (files) {
alert(`Selected ${files.length} files.`);
}
}
onFilesRejected(): void {
alert('File type not supported.');
}
}
```
## Import
Import the FileUpload primitives from `ng-primitives/file-upload`.
```ts
import { NgpFileUpload } from 'ng-primitives/file-upload';
```
## Usage
Assemble the file-upload directives in your template.
```html
```
## Examples
### File Dropzone
The file dropzone primitive allows you to create a dropzone for files. This functionality is built into the file upload primitive, but can also be used separately if you don't want to show the file upload dialog on click.
## Example
```typescript
import { Component } from '@angular/core';
import { NgpFileDropzone } from 'ng-primitives/file-upload';
@Component({
selector: 'app-file-dropzone',
imports: [NgpFileDropzone],
template: `
Drag and drop files anywhere here!
But clicking won't open a file selection dialog.
`,
styles: `
[ngpFileDropzone] {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 3rem;
flex-direction: column;
row-gap: 0.25rem;
width: 100%;
height: 100%;
border-radius: 0.5rem;
border: 1px dashed var(--ngp-border-secondary);
background-color: var(--ngp-background);
padding: 2rem 3rem;
}
[ngpFileDropzone][data-dragover] {
border-color: var(--ngp-border);
background-color: var(--ngp-background-hover);
}
h3 {
font-size: 0.875rem;
font-weight: 500;
color: var(--ngp-text-primary);
line-height: 1.25rem;
text-align: center;
margin: 0;
}
p {
font-size: 0.75rem;
color: var(--ngp-text-secondary);
line-height: 1rem;
text-align: center;
margin: 0;
}
`,
})
export default class FileDropzoneExample {
onFilesSelected(files: FileList | null): void {
if (files) {
alert(`Selected ${files.length} files.`);
}
}
onFilesRejected(): void {
alert('File type not supported.');
}
}
```
## Reusable Component
Create a file upload component that uses the `NgpFileUpload` directive.
## Reusable Component
```typescript
import { Component } from '@angular/core';
import { NgpFileUpload } from 'ng-primitives/file-upload';
@Component({
selector: 'app-file-upload',
hostDirectives: [
{
directive: NgpFileUpload,
inputs: [
'ngpFileUploadFileTypes:types',
'ngpFileUploadMultiple:multiple',
'ngpFileUploadDirectory:directory',
'ngpFileUploadDragDrop:dragDrop',
'ngpFileUploadDisabled:disabled',
],
outputs: ['ngpFileUploadSelected:selected', 'ngpFileUploadCanceled:canceled'],
},
],
template: `
Drop files here or click to upload
`,
styles: `
:host {
display: flex;
cursor: pointer;
flex-direction: column;
align-items: center;
justify-content: center;
row-gap: 0.25rem;
border-radius: 0.5rem;
border: 1px dashed var(--ngp-border-secondary);
background-color: var(--ngp-background);
padding: 2rem 3rem;
}
:host[data-dragover] {
border-color: var(--ngp-border);
background-color: var(--ngp-background-hover);
}
`,
})
export class FileUpload {}
```
## Schematics
Generate a reusable file upload component using the Angular CLI.
```bash npm
ng g ng-primitives:primitive file-upload
```
### Options
- `path`: The path at which to create the component file.
- `prefix`: The prefix to apply to the generated component selector.
- `componentSuffix`: The suffix to apply to the generated component class name.
- `fileSuffix`: The suffix to apply to the generated component file name. Defaults to `component`.
- `exampleStyles`: Whether to include example styles in the generated component file. Defaults to `true`.
## API Reference
The following directives are available to import from the `ng-primitives/file-upload` package:
### NgpFileUpload
## API Reference
### NgpFileUpload
A directive that allows you to turn any element into a file upload trigger.
**Selector:** `[ngpFileUpload]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpFileUploadFileTypes` | `string[] | undefined` | `-` | The accepted file types. This can be an array of strings or a comma-separated string.
Accepted types can either be file extensions (e.g. `.jpg`) or MIME types (e.g. `image/jpeg`). |
| `ngpFileUploadMultiple` | `boolean` | `-` | Whether to allow multiple files to be selected. |
| `ngpFileUploadDirectory` | `boolean` | `-` | Whether to allow the user to select directories. |
| `ngpFileUploadDragDrop` | `boolean` | `-` | Whether drag-and-drop is enabled. |
| `ngpFileUploadDisabled` | `boolean` | `-` | Whether the file upload is disabled. |
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpFileUploadSelected` | `FileList | null` | Emits when the user selects files. |
| `ngpFileUploadCanceled` | `void` | Emits when the user cancel the file selection. |
| `ngpFileUploadRejected` | `void` | Emits when uploaded files are rejected because they do not match the allowed {@link fileTypes}. |
| `ngpFileUploadDragOver` | `boolean` | Emits when the user drags a file over the file upload. |
#### Export As
`ngpFileUpload`
#### Data Attributes
| Attribute | Description |
| -------------------- | ------------------------------------------------ |
| `data-hover` | Applied when the element is hovered. |
| `data-focus-visible` | Applied when the element is focus visible. |
| `data-press` | Applied when the element is pressed. |
| `data-dragover` | Applied when a file is dragged over the element. |
| `data-disabled` | Applied when the element is disabled. |
### NgpFileDropzone
## API Reference
### NgpFileDropzone
Capture files dropped on the element.
**Selector:** `[ngpFileDropzone]`
#### Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ngpFileDropzoneFileTypes` | `string[] | undefined` | `-` | The accepted file types. This can be an array of strings or a comma-separated string.
Accepted types can either be file extensions (e.g. `.jpg`) or MIME types (e.g. `image/jpeg`). |
| `ngpFileDropzoneMultiple` | `boolean` | `-` | Whether to allow multiple files to be selected. |
| `ngpFileDropzoneDirectory` | `boolean` | `-` | Whether to allow the user to select directories. |
| `ngpFileDropzoneDisabled` | `boolean` | `-` | Whether the file upload is disabled. |
#### Outputs
| Event | Type | Description |
|-------|------|-------------|
| `ngpFileDropzoneSelected` | `FileList | null` | Emits when the user selects files. |
| `ngpFileDropzoneRejected` | `void` | Emits when uploaded files are rejected because they do not match the allowed {@link fileTypes}. |
| `ngpFileDropzoneDragOver` | `boolean` | Emits when the user drags a file over the file upload. |
#### Export As
`ngpFileDropzone`
#### Data Attributes
| Attribute | Description |
| --------------- | ------------------------------------------------ |
| `data-hover` | Applied when the element is hovered. |
| `data-dragover` | Applied when a file is dragged over the element. |
| `data-disabled` | Applied when the element is disabled. |
---
### Form Field
File: primitives/form-field.md
URL: https://angularprimitives.com/primitives/form-field
# Form Field
## Example
```typescript
import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import {
NgpDescription,
NgpError,
NgpFormControl,
NgpFormField,
NgpLabel,
} from 'ng-primitives/form-field';
@Component({
selector: 'app-form-field',
imports: [NgpFormField, NgpLabel, NgpError, NgpDescription, NgpFormControl, ReactiveFormsModule],
template: `
Please include any middle names, no matter how ridiculous.
```
## Reusable Component
Create a reusable component that uses the `NgpFormField` directive.
## Reusable Component
```typescript
import { Component } from '@angular/core';
import { NgpFormField } from 'ng-primitives/form-field';
@Component({
selector: 'app-form-field',
hostDirectives: [NgpFormField],
template: `
`,
styles: `
:host {
display: flex;
flex-direction: column;
gap: 6px;
width: 90%;
}
`,
})
export class FormField {}
```
## Schematics
Generate a reusable form-field component using the Angular CLI.
```bash npm
ng g ng-primitives:primitive form-field
```
### Options
- `path`: The path at which to create the component file.
- `prefix`: The prefix to apply to the generated component selector.
- `componentSuffix`: The suffix to apply to the generated component class name.
- `fileSuffix`: The suffix to apply to the generated component file name. Defaults to `component`.
- `exampleStyles`: Whether to include example styles in the generated component file. Defaults to `true`.
## API Reference
The following directives are available to import from the `ng-primitives/form-field` package:
### NgpFormField
## API Reference
### NgpFormField
The `NgpFormField` directive is a container for form field elements. Any labels, form controls, or descriptions should be placed within this directive.
**Selector:** `[ngpFormField]`
#### Export As
`ngpFormField`
#### Data Attributes
| Attribute | Description |
| --------------- | ------------------------------------------ |
| `data-invalid` | Applied when the form control is invalid. |
| `data-valid` | Applied when the form control is valid. |
| `data-touched` | Applied when the form control is touched. |
| `data-pristine` | Applied when the form control is pristine. |
| `data-dirty` | Applied when the form control is dirty. |
| `data-pending` | Applied when the form control is pending. |
| `data-disabled` | Applied when the form control is disabled. |
### NgpLabel
## API Reference
### NgpLabel
The `NgpLabel` directive is used to mark a label element within a form field. Preferably, there should use an HTML `