ngx-mq 2.11.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +186 -110
- package/fesm2022/ngx-mq.mjs +428 -8
- package/fesm2022/ngx-mq.mjs.map +1 -1
- package/index.d.ts +427 -3
- package/package.json +20 -5
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,182 +1,235 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/martsinlabs/ngx-mq/refs/heads/main/assets/logo.svg" width="
|
|
2
|
+
<img src="https://raw.githubusercontent.com/martsinlabs/ngx-mq/refs/heads/main/assets/logo.svg" width="140" alt="ngx-mq" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h3 align="center">
|
|
6
|
-
Signal-Powered Breakpoints & Media Queries
|
|
7
|
-
</h3>
|
|
5
|
+
<h3 align="center">Signal-Powered Breakpoints & Media Queries for Angular</h3>
|
|
8
6
|
|
|
9
|
-
<
|
|
7
|
+
<p align="center">
|
|
8
|
+
Reactive <code>matchMedia</code> as Angular signals. SSR-safe, zoneless-ready, and free of RxJS.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
10
12
|
<a href="https://github.com/martsinlabs/ngx-mq/actions/workflows/ci.yml">
|
|
11
|
-
<img src="https://img.shields.io/github/actions/workflow/status/martsinlabs/ngx-mq/ci.yml?branch=main&label=CI&color=44cc11&logo=github" alt="CI
|
|
13
|
+
<img src="https://img.shields.io/github/actions/workflow/status/martsinlabs/ngx-mq/ci.yml?branch=main&label=CI&color=44cc11&logo=github" alt="CI status" />
|
|
12
14
|
</a>
|
|
13
15
|
<a href="https://codecov.io/gh/martsinlabs/ngx-mq">
|
|
14
16
|
<img src="https://codecov.io/gh/martsinlabs/ngx-mq/branch/main/graph/badge.svg" alt="coverage" />
|
|
15
17
|
</a>
|
|
16
|
-
<br>
|
|
17
18
|
<a href="https://www.npmjs.com/package/ngx-mq">
|
|
18
19
|
<img src="https://img.shields.io/npm/v/ngx-mq.svg?color=007ec6" alt="npm version" />
|
|
19
20
|
</a>
|
|
20
21
|
<a href="https://www.npmjs.com/package/ngx-mq">
|
|
21
22
|
<img src="https://img.shields.io/npm/dm/ngx-mq.svg?color=44cc11" alt="npm downloads" />
|
|
22
23
|
</a>
|
|
24
|
+
<a href="https://bundlephobia.com/package/ngx-mq">
|
|
25
|
+
<img src="https://img.shields.io/bundlephobia/minzip/ngx-mq.svg?color=44cc11&label=minzip" alt="minzipped size" />
|
|
26
|
+
</a>
|
|
23
27
|
<a href="https://opensource.org/license/MIT">
|
|
24
28
|
<img src="https://img.shields.io/npm/l/ngx-mq.svg?color=44cc11" alt="license" />
|
|
25
29
|
</a>
|
|
26
|
-
</
|
|
30
|
+
</p>
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
---
|
|
29
33
|
|
|
30
34
|
## Features
|
|
31
35
|
|
|
32
|
-
-
|
|
33
|
-
- SSR-safe
|
|
34
|
-
- Auto-cleanup
|
|
35
|
-
-
|
|
36
|
-
-
|
|
36
|
+
- **Signal-native**: every query is a `Signal<boolean>` that updates as the viewport changes.
|
|
37
|
+
- **SSR-safe**: returns a configurable static value on the server, then hydrates on the client.
|
|
38
|
+
- **Auto-cleanup**: listeners are tied to Angular's `DestroyRef`; no manual teardown.
|
|
39
|
+
- **Efficient**: one shared `matchMedia` listener per unique query, reused across the app.
|
|
40
|
+
- **Batteries included**: Tailwind, Bootstrap and Material breakpoint presets out of the box.
|
|
41
|
+
- **Composable**: combine any signals with `and` / `or` / `not`.
|
|
42
|
+
- **Tiny and tested**: ~1.9 kB gzipped, 100% line coverage.
|
|
37
43
|
|
|
38
|
-
##
|
|
44
|
+
## Why ngx-mq?
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
Angular's CDK ships [`BreakpointObserver`](https://material.angular.io/cdk/layout/overview), which
|
|
47
|
+
works well but is built around RxJS and raw query strings. `ngx-mq` is designed for the signals era:
|
|
48
|
+
templates read a value directly, there is nothing to subscribe to, and cleanup is automatic.
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
| | `ngx-mq` | CDK `BreakpointObserver` |
|
|
51
|
+
| --------------------- | ------------------------------------------------- | ----------------------------------------- |
|
|
52
|
+
| Reactivity | `Signal<boolean>` | `Observable<BreakpointState>` |
|
|
53
|
+
| Cleanup | Automatic via `DestroyRef` | Manual (`unsubscribe` / `takeUntilDestroyed`) |
|
|
54
|
+
| Template usage | `@if (isDesktop())` | `async` pipe or manual subscription |
|
|
55
|
+
| Named breakpoints | Tailwind, Bootstrap, Material presets or your own | Material breakpoints or raw strings |
|
|
56
|
+
| Media-feature helpers | `colorScheme`, `hover`, `pointer`, ... | Raw query strings |
|
|
57
|
+
| Composition | `and` / `or` / `not` | RxJS operators |
|
|
58
|
+
| SSR | Configurable static value | Handle it yourself |
|
|
59
|
+
| Footprint | ~1.9 kB standalone | Part of `@angular/cdk` |
|
|
43
60
|
|
|
44
|
-
|
|
61
|
+
If you already pull in `@angular/cdk` and live in RxJS, `BreakpointObserver` is a fine choice. If you
|
|
62
|
+
want a signals-first, zoneless-friendly API with batteries included, reach for `ngx-mq`.
|
|
45
63
|
|
|
46
|
-
|
|
64
|
+
## Documentation
|
|
47
65
|
|
|
48
|
-
|
|
66
|
+
- **API reference and guides:** https://martsinlabs.github.io/ngx-mq
|
|
67
|
+
- **Live demo:** [StackBlitz](https://stackblitz.com/github/martsinlabs/ngx-mq-demo/tree/demo/v2?file=src%2Fapp%2Fapp.component.ts)
|
|
49
68
|
|
|
50
|
-
|
|
69
|
+
## Installation
|
|
51
70
|
|
|
52
|
-
|
|
53
|
-
# For Angular 16–18
|
|
54
|
-
npm install ngx-mq@1
|
|
71
|
+
Install the major version that matches your Angular version:
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
| Angular | Install |
|
|
74
|
+
| ------- | ------------------ |
|
|
75
|
+
| 20 - 22 | `npm i ngx-mq@3` |
|
|
76
|
+
| 19 | `npm i ngx-mq@2` |
|
|
77
|
+
| 16 - 18 | `npm i ngx-mq@1` |
|
|
59
78
|
|
|
60
|
-
##
|
|
79
|
+
## Quick start
|
|
61
80
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Create a custom breakpoint map or use one of the built-in presets (e.g. `provideTailwindBreakpoints()`).
|
|
81
|
+
**1. Provide a breakpoint map** at bootstrap (a custom map or a preset):
|
|
65
82
|
|
|
66
83
|
```ts
|
|
67
84
|
import { bootstrapApplication } from '@angular/platform-browser';
|
|
68
|
-
import { AppComponent } from './app/app.component';
|
|
69
85
|
import { provideBreakpoints } from 'ngx-mq';
|
|
86
|
+
import { AppComponent } from './app/app.component';
|
|
70
87
|
|
|
71
88
|
bootstrapApplication(AppComponent, {
|
|
72
|
-
providers: [
|
|
73
|
-
// Define a custom map (keys are named ranges, values are widths in pixels)
|
|
74
|
-
provideBreakpoints({
|
|
75
|
-
sm: 640,
|
|
76
|
-
md: 768,
|
|
77
|
-
lg: 1024,
|
|
78
|
-
}),
|
|
79
|
-
],
|
|
89
|
+
providers: [provideBreakpoints({ sm: 640, md: 768, lg: 1024 })],
|
|
80
90
|
});
|
|
81
91
|
```
|
|
82
92
|
|
|
83
|
-
**
|
|
93
|
+
**2. Use the helpers** as signals inside any component:
|
|
84
94
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
```ts
|
|
96
|
+
import { Component } from '@angular/core';
|
|
97
|
+
import { up, down, between } from 'ngx-mq';
|
|
88
98
|
|
|
89
|
-
|
|
99
|
+
@Component({
|
|
100
|
+
selector: 'app-root',
|
|
101
|
+
template: `
|
|
102
|
+
@if (isDesktop()) {
|
|
103
|
+
<app-sidebar />
|
|
104
|
+
}
|
|
105
|
+
`,
|
|
106
|
+
})
|
|
107
|
+
export class AppComponent {
|
|
108
|
+
readonly isMobile = down('md');
|
|
109
|
+
readonly isTablet = between('md', 'lg');
|
|
110
|
+
readonly isDesktop = up('lg');
|
|
111
|
+
}
|
|
112
|
+
```
|
|
90
113
|
|
|
91
|
-
|
|
92
|
-
| --------- | ----------------------------------------------------------------- | ----------------- | --------------------------------------------- |
|
|
93
|
-
| `up` | `bp: string, options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when viewport width ≥ breakpoint |
|
|
94
|
-
| `down` | `bp: string, options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when viewport width < breakpoint |
|
|
95
|
-
| `between` | `minBp: string, maxBp: string, options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when viewport width is in range [min, max) |
|
|
114
|
+
> **Tip:** Call the helpers within Angular's [injection context](https://angular.dev/guide/di/dependency-injection-context) (component fields, `inject`, factories) so their lifecycle stays in sync with the framework.
|
|
96
115
|
|
|
97
|
-
|
|
116
|
+
## API
|
|
98
117
|
|
|
99
|
-
|
|
118
|
+
Every query helper returns a `Signal<boolean>` and accepts an optional `options` argument
|
|
119
|
+
([`CreateMediaQueryOptions`](#options)). The `options` column is omitted below for brevity.
|
|
100
120
|
|
|
101
|
-
|
|
102
|
-
// viewport-utils.ts
|
|
103
|
-
import { Signal } from '@angular/core';
|
|
104
|
-
import { up, down, between } from 'ngx-mq';
|
|
121
|
+
### Configuration
|
|
105
122
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
Register breakpoints once, then refer to them by name. Use a custom map or a preset:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import {
|
|
127
|
+
provideBreakpoints,
|
|
128
|
+
provideTailwindBreakpoints,
|
|
129
|
+
provideBootstrapBreakpoints,
|
|
130
|
+
provideMaterialBreakpoints,
|
|
131
|
+
} from 'ngx-mq';
|
|
109
132
|
```
|
|
110
133
|
|
|
111
|
-
|
|
134
|
+
| Preset | Breakpoints |
|
|
135
|
+
| ----------- | ------------------------------------------------ |
|
|
136
|
+
| Tailwind | `sm: 640, md: 768, lg: 1024, xl: 1280, 2xl: 1536` |
|
|
137
|
+
| Bootstrap | `sm: 576, md: 768, lg: 992, xl: 1200, xxl: 1400` |
|
|
138
|
+
| Material | `sm: 600, md: 905, lg: 1240, xl: 1440` |
|
|
112
139
|
|
|
113
|
-
|
|
140
|
+
### Breakpoints
|
|
114
141
|
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `reducedMotion` | `options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the user has enabled reduced motion. |
|
|
121
|
-
| `hover` | `options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the user's primary input device supports hover capability. |
|
|
122
|
-
| `anyHover` | `options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when any available input device supports hover capability. |
|
|
123
|
-
| `pointer` | `value: 'fine' \| 'coarse' \| 'none', options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the user's primary input device matches the specified pointer type. |
|
|
124
|
-
| `anyPointer` | `value: 'fine' \| 'coarse' \| 'none', options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when any available input device matches the specified pointer type. |
|
|
125
|
-
| `colorGamut` | `value: 'srgb' \| 'p3' \| 'rec2020', options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the user's display supports the specified color gamut. |
|
|
142
|
+
| Helper | Arguments | `true` when |
|
|
143
|
+
| --------- | ---------------- | ------------------------------------ |
|
|
144
|
+
| `up` | `bp` | viewport width `>=` `bp` |
|
|
145
|
+
| `down` | `bp` | viewport width `<` `bp` |
|
|
146
|
+
| `between` | `minBp`, `maxBp` | viewport width is in `[minBp, maxBp)` |
|
|
126
147
|
|
|
127
|
-
|
|
148
|
+
> **Note:** `down` and `between` upper bounds are **exclusive**: a small epsilon is subtracted
|
|
149
|
+
> from the max so adjacent ranges (e.g. `down('md')` and `up('md')`) never overlap. Tune it with
|
|
150
|
+
> [`provideBreakpointEpsilon`](#providers).
|
|
128
151
|
|
|
129
|
-
|
|
152
|
+
### Media features
|
|
130
153
|
|
|
131
|
-
|
|
154
|
+
| Helper | Arguments | `true` when |
|
|
155
|
+
| --------------- | ------------------------------ | ------------------------------------ |
|
|
156
|
+
| `orientation` | `'portrait' \| 'landscape'` | the screen orientation matches |
|
|
157
|
+
| `colorScheme` | `'light' \| 'dark'` | the system color scheme matches |
|
|
158
|
+
| `displayMode` | `DisplayModeOption` | the display mode matches (PWA detection) |
|
|
159
|
+
| `reducedMotion` | none | the user prefers reduced motion |
|
|
160
|
+
| `hover` | none | the primary pointer can hover |
|
|
161
|
+
| `anyHover` | none | any available pointer can hover |
|
|
162
|
+
| `pointer` | `'fine' \| 'coarse' \| 'none'` | the primary pointer matches |
|
|
163
|
+
| `anyPointer` | `'fine' \| 'coarse' \| 'none'` | any available pointer matches |
|
|
164
|
+
| `colorGamut` | `'srgb' \| 'p3' \| 'rec2020'` | the display covers the gamut |
|
|
132
165
|
|
|
133
|
-
|
|
134
|
-
| ------------------ | -------------------------------------------------- | ----------------- | --------------------------------------------------------- |
|
|
135
|
-
| `matchMediaSignal` | `query: string, options?: CreateMediaQueryOptions` | `Signal<boolean>` | Provides a signal representing the state of a media query |
|
|
166
|
+
### Custom queries
|
|
136
167
|
|
|
137
|
-
|
|
168
|
+
For anything without a dedicated helper, pass a raw CSS media query:
|
|
138
169
|
|
|
139
170
|
```ts
|
|
140
|
-
import { Signal } from '@angular/core';
|
|
141
171
|
import { matchMediaSignal } from 'ngx-mq';
|
|
142
172
|
|
|
143
|
-
|
|
144
|
-
export const isLandscape = (): Signal<boolean> => matchMediaSignal('(orientation: landscape)');
|
|
173
|
+
readonly isRetina = matchMediaSignal('(min-resolution: 2dppx)');
|
|
145
174
|
```
|
|
146
175
|
|
|
147
|
-
|
|
176
|
+
### Composition
|
|
148
177
|
|
|
149
|
-
|
|
178
|
+
Combine boolean signals into derived ones. Combinators work at the signal level, so the underlying
|
|
179
|
+
listeners stay shared and are still cleaned up automatically.
|
|
150
180
|
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
| `
|
|
156
|
-
| `provideMaterialBreakpoints()` | none | Registers the default Material 2 breakpoints. |
|
|
157
|
-
| `provideBreakpointEpsilon()` | `epsilon: number` | Sets the epsilon threshold used when comparing breakpoint values. |
|
|
158
|
-
| `provideSsrValue()` | `value: boolean` | Defines the static signal value used during SSR, since media queries are not available on the server. Defaults to `false`. |
|
|
181
|
+
| Helper | Arguments | `true` when |
|
|
182
|
+
| ------ | ---------------------------------- | ------------------------------------------ |
|
|
183
|
+
| `and` | `...conditions: Signal<boolean>[]` | every condition is `true` (empty: `true`) |
|
|
184
|
+
| `or` | `...conditions: Signal<boolean>[]` | any condition is `true` (empty: `false`) |
|
|
185
|
+
| `not` | `condition: Signal<boolean>` | the condition is `false` |
|
|
159
186
|
|
|
160
|
-
|
|
187
|
+
```ts
|
|
188
|
+
import { and, or, not, up, down, hover, orientation, reducedMotion } from 'ngx-mq';
|
|
161
189
|
|
|
162
|
-
|
|
190
|
+
// Large screen, in landscape, with a hover-capable pointer
|
|
191
|
+
readonly isLandscapeDesktop = and(up('lg'), orientation('landscape'), hover());
|
|
163
192
|
|
|
164
|
-
|
|
165
|
-
|
|
193
|
+
// Small screens OR a reduced-motion preference
|
|
194
|
+
readonly prefersSimpleUi = or(down('md'), reducedMotion());
|
|
166
195
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
*/
|
|
171
|
-
ssrValue?: boolean;
|
|
196
|
+
// Devices without hover (touch-like): `hover()` has no direct inverse helper
|
|
197
|
+
readonly isTouchLike = not(hover());
|
|
198
|
+
```
|
|
172
199
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
200
|
+
### Providers
|
|
201
|
+
|
|
202
|
+
Each returns a standard Angular `Provider` you can register at any injector level.
|
|
203
|
+
|
|
204
|
+
| Provider | Argument | Description |
|
|
205
|
+
| ------------------------------- | -------------------- | ----------------------------------------------------- |
|
|
206
|
+
| `provideBreakpoints` | `bps: MqBreakpoints` | Registers a custom breakpoint map. |
|
|
207
|
+
| `provideTailwindBreakpoints` | none | Registers the Tailwind preset. |
|
|
208
|
+
| `provideBootstrapBreakpoints` | none | Registers the Bootstrap preset. |
|
|
209
|
+
| `provideMaterialBreakpoints` | none | Registers the Material 2 preset. |
|
|
210
|
+
| `provideBreakpointEpsilon` | `epsilon: number` | Sets the exclusive-bound epsilon (default `0.02`). |
|
|
211
|
+
| `provideSsrValue` | `value: boolean` | Sets the value signals report during SSR (default `false`). |
|
|
212
|
+
|
|
213
|
+
> **Tip:** To register these as environment providers, wrap them with
|
|
214
|
+
> [`makeEnvironmentProviders`](https://angular.dev/api/core/makeEnvironmentProviders).
|
|
215
|
+
|
|
216
|
+
### Options
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
interface CreateMediaQueryOptions {
|
|
220
|
+
/** Value the signal reports during SSR. Overrides the app-wide `provideSsrValue`. */
|
|
221
|
+
ssrValue?: boolean;
|
|
222
|
+
/** Debug name shown for the signal in Angular DevTools. */
|
|
176
223
|
debugName?: string;
|
|
177
224
|
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Types
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
type MqBreakpoints = Record<string, number>;
|
|
178
231
|
|
|
179
|
-
|
|
232
|
+
type DisplayModeOption =
|
|
180
233
|
| 'browser'
|
|
181
234
|
| 'fullscreen'
|
|
182
235
|
| 'standalone'
|
|
@@ -185,6 +238,33 @@ export type DisplayModeOption =
|
|
|
185
238
|
| 'picture-in-picture';
|
|
186
239
|
```
|
|
187
240
|
|
|
241
|
+
## Server-side rendering
|
|
242
|
+
|
|
243
|
+
`matchMedia` does not exist on the server, so during SSR every signal returns a static value and
|
|
244
|
+
no listeners are created. Set the default with `provideSsrValue`, or override it per call:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
provideSsrValue(false); // app-wide default
|
|
248
|
+
up('lg', { ssrValue: true }); // per-call override
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Once the app hydrates in the browser, each signal switches to the live query result.
|
|
252
|
+
|
|
253
|
+
## How it works
|
|
254
|
+
|
|
255
|
+
`ngx-mq` keeps a single `MediaQueryList` and writable signal per unique query in a global registry
|
|
256
|
+
(Multiton + Flyweight). Callers share that signal, and a reference count tied to `DestroyRef`
|
|
257
|
+
removes the listener and registry entry once the last consumer is destroyed. No manual cleanup, no
|
|
258
|
+
duplicate listeners.
|
|
259
|
+
|
|
260
|
+
## Contributing
|
|
261
|
+
|
|
262
|
+
Contributions are welcome. See [CONTRIBUTING.md](https://github.com/martsinlabs/ngx-mq/blob/main/CONTRIBUTING.md).
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
[MIT](https://github.com/martsinlabs/ngx-mq/blob/main/LICENSE) © Martsin Labs
|
|
267
|
+
|
|
188
268
|
## Sponsors
|
|
189
269
|
|
|
190
270
|
<table>
|
|
@@ -194,7 +274,7 @@ export type DisplayModeOption =
|
|
|
194
274
|
<img
|
|
195
275
|
src="https://raw.githubusercontent.com/getsentry/sentry/7b65f0f23d7eb5ccc035b12776bf5d8f3d9f8965/static/images/logo-sentry.svg"
|
|
196
276
|
width="120"
|
|
197
|
-
alt="Sentry
|
|
277
|
+
alt="Sentry" />
|
|
198
278
|
</p>
|
|
199
279
|
<p>
|
|
200
280
|
<a href="https://sentry.io" target="_blank"><strong>Sentry</strong></a>
|
|
@@ -204,7 +284,3 @@ export type DisplayModeOption =
|
|
|
204
284
|
</td>
|
|
205
285
|
</tr>
|
|
206
286
|
</table>
|
|
207
|
-
|
|
208
|
-
## Contributing
|
|
209
|
-
|
|
210
|
-
[CONTRIBUTING.md](https://github.com/martsinlabs/ngx-mq/blob/main/CONTRIBUTING.md)
|