Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
73aef17
feat(angular): Draft first version of angular package
stefashkaa May 9, 2026
fb38844
feat(angular): Improve lib arch, add demo project, lint & tests
stefashkaa May 9, 2026
69bf499
style(angular): Format code
stefashkaa May 9, 2026
89f2946
chore(angular): Improve build script
stefashkaa May 9, 2026
f7cf745
refactor(angular): Prepare compatible public api
stefashkaa May 9, 2026
e2da124
style(angular): Format code
stefashkaa May 9, 2026
6e511d7
refactor(angular): Simplify detect prop default value
stefashkaa May 9, 2026
d4b8e42
refactor(angular): Improve copy services
stefashkaa May 9, 2026
1ae7bcc
refactor(angular): Resolve issues in internal services
stefashkaa May 9, 2026
48852c0
style(angular): Format code
stefashkaa May 9, 2026
62cd3ce
test(core): Add alias for imports
stefashkaa May 9, 2026
b05817b
test(vue): Use alias for imports
stefashkaa May 9, 2026
199ebf8
test(svelte): Use alias for imports
stefashkaa May 9, 2026
903ff0e
test(react): Use alias for imports
stefashkaa May 9, 2026
271b2e6
test(angular): Add more test cases
stefashkaa May 9, 2026
6a4f478
test(angular): Add angular related only test cases
stefashkaa May 10, 2026
d5af456
refactor(angular): Simplify country selection for component & directi…
stefashkaa May 10, 2026
83e2ed2
test(angular): Improve tests
stefashkaa May 10, 2026
062025d
test(angular): Resolve warnings from logs
stefashkaa May 10, 2026
01e0ece
chore(ci): Try to resolve dependency related issue
stefashkaa May 10, 2026
4d1c6bc
chore(ci,angular): Try to resolve dependency related issue
stefashkaa May 10, 2026
da02531
chore(ci,angular): Try to resolve dependency related issue
stefashkaa May 10, 2026
7026241
chore(ci,angular): Remove build logs
stefashkaa May 10, 2026
5f691d8
refactor(angular): Resolve Sonar issues
stefashkaa May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
.vscode
.idea
.claude
.angular
dist
dist-demo
build
coverage
node_modules
Expand Down
3 changes: 3 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ auto-install-peers=true

# Strict peer dependencies
strict-peer-dependencies=false

# Angular library compilation imports TypeScript helpers from tslib implicitly.
public-hoist-pattern[]=tslib
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,19 +108,21 @@ Ready-made plugins for your stack:
- ✅ **Nuxt** — Auto-imported, SSR-compatible
- ✅ **React** — Component & hook with modern React patterns
- ✅ **Svelte** — Component, composable, action, and attachment for Svelte 5
- ✅ **Angular** — Standalone component, directive, pipe, and service
- ✅ **TypeScript/Vanilla JS** — Framework-agnostic core

---

## 📦 Packages

| Package | Version | Description |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| [@desource/phone-mask](./packages/phone-mask) | ![npm](https://img.shields.io/npm/v/@desource/phone-mask?color=blue&logo=typescript) | Core library — TypeScript/JS |
| [@desource/phone-mask-react](./packages/phone-mask-react) | ![npm](https://img.shields.io/npm/v/@desource/phone-mask-react?color=blue&logo=react) | React component + hook |
| [@desource/phone-mask-vue](./packages/phone-mask-vue) | ![npm](https://img.shields.io/npm/v/@desource/phone-mask-vue?color=blue&logo=vuedotjs) | Vue 3 component + composable + directive |
| [@desource/phone-mask-svelte](./packages/phone-mask-svelte) | ![npm](https://img.shields.io/npm/v/@desource/phone-mask-svelte?color=blue&logo=svelte) | Svelte 5 component + composable + action + attachment |
| [@desource/phone-mask-nuxt](./packages/phone-mask-nuxt) | ![npm](https://img.shields.io/npm/v/@desource/phone-mask-nuxt?color=blue&logo=nuxt) | Nuxt module |
| Package | Version | Description |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| [@desource/phone-mask](./packages/phone-mask) | ![npm](https://img.shields.io/npm/v/@desource/phone-mask?color=blue&logo=typescript) | Core library — TypeScript/JS |
| [@desource/phone-mask-react](./packages/phone-mask-react) | ![npm](https://img.shields.io/npm/v/@desource/phone-mask-react?color=blue&logo=react) | React component + hook |
| [@desource/phone-mask-vue](./packages/phone-mask-vue) | ![npm](https://img.shields.io/npm/v/@desource/phone-mask-vue?color=blue&logo=vuedotjs) | Vue 3 component + composable + directive |
| [@desource/phone-mask-svelte](./packages/phone-mask-svelte) | ![npm](https://img.shields.io/npm/v/@desource/phone-mask-svelte?color=blue&logo=svelte) | Svelte 5 component + composable + action + attachment |
| [@desource/phone-mask-angular](./packages/phone-mask-angular) | ![npm](https://img.shields.io/npm/v/@desource/phone-mask-angular?color=blue&logo=angular) | Angular component + directive + pipe + service |
| [@desource/phone-mask-nuxt](./packages/phone-mask-nuxt) | ![npm](https://img.shields.io/npm/v/@desource/phone-mask-nuxt?color=blue&logo=nuxt) | Nuxt module |

---

Expand Down Expand Up @@ -181,6 +183,27 @@ npm install @desource/phone-mask-svelte
<PhoneInput bind:value={phone} country="US" />
```

### Angular

```bash
npm install @desource/phone-mask-angular
```

```ts
import { Component, signal } from '@angular/core';
import { PhoneInputComponent } from '@desource/phone-mask-angular';

@Component({
selector: 'app-phone',
standalone: true,
imports: [PhoneInputComponent],
template: `<desource-phone-input [(value)]="phone" country="US" />`
})
export class PhoneComponent {
readonly phone = signal('');
}
```

### Nuxt

```bash
Expand Down
2 changes: 1 addition & 1 deletion common/tests/unit/setup/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
type FireEventReturn = Promise<boolean> | Promise<void> | boolean;

export type MaybeRef<T> = T | { value: T };
export type MaybeRef<T> = T | { value: T } | (() => T);

export interface TestTools {
toValue: <T>(val: MaybeRef<T>) => T;
Expand Down
30 changes: 30 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import svelte from 'eslint-plugin-svelte';
import svelteParser from 'svelte-eslint-parser';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import angular from 'angular-eslint';
import prettier from 'eslint-config-prettier';
import globals from 'globals';

const TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'];
const JS_FILES = ['**/*.js', '**/*.mjs', '**/*.cjs', '**/*.jsx'];

const REACT_FILES = ['packages/phone-mask-react/**/*.{ts,tsx,js,jsx}'];
const ANGULAR_TS_FILES = ['packages/phone-mask-angular/**/*.ts'];
const ANGULAR_TEMPLATE_FILES = ['packages/phone-mask-angular/**/*.html'];
const VUE_SFC_FILES = ['packages/phone-mask-vue/**/*.vue', 'packages/phone-mask-nuxt/**/*.vue', 'demo/**/*.vue'];
const VUE_TS_FILES = [
'packages/phone-mask-vue/**/*.{ts,mts,cts}',
Expand All @@ -31,6 +34,7 @@ const BROWSER_FILES = [
'packages/phone-mask-react/**/*.{ts,tsx,js,jsx}',
'packages/phone-mask-vue/**/*.{ts,js,mts,cts,vue}',
'packages/phone-mask-svelte/**/*.{ts,js,mts,cts,svelte}',
'packages/phone-mask-angular/**/*.{ts,js,mts,cts}',
'demo/**/*.{ts,tsx,js,jsx,vue}'
];

Expand All @@ -56,6 +60,7 @@ export default [
ignores: [
'**/node_modules/**',
'**/dist/**',
'**/dist-demo/**',
'**/.nuxt/**',
'**/.output/**',
'**/.svelte-kit/**',
Expand Down Expand Up @@ -149,6 +154,31 @@ export default [
}
},

...angular.configs.tsRecommended.map((config) => ({
...config,
files: ANGULAR_TS_FILES
})),

{
files: ANGULAR_TS_FILES,
processor: angular.processInlineTemplates,
rules: {
'@angular-eslint/no-input-rename': 'off',
'@angular-eslint/no-output-native': 'off',
'@angular-eslint/no-output-rename': 'off'
}
},

...angular.configs.templateRecommended.map((config) => ({
...config,
files: ANGULAR_TEMPLATE_FILES
})),

...angular.configs.templateAccessibility.map((config) => ({
...config,
files: ANGULAR_TEMPLATE_FILES
})),

{
files: VUE_TS_FILES,
languageOptions: {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@typescript-eslint/eslint-plugin": "^8.59.2",
"@typescript-eslint/parser": "^8.59.2",
"@vitest/coverage-v8": "^4.1.5",
"angular-eslint": "^21.3.1",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react": "^7.37.5",
Expand Down
5 changes: 5 additions & 0 deletions packages/phone-mask-angular/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @desource/phone-mask-angular

## 1.4.0

- Initial Angular package with standalone component, directive, pipe, service, and provider APIs.
153 changes: 153 additions & 0 deletions packages/phone-mask-angular/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# @desource/phone-mask-angular

> Angular phone input component, directive, pipe, and service API with smart masking and Google libphonenumber data

[![npm version](https://img.shields.io/npm/v/@desource/phone-mask-angular?color=blue&logo=angular)](https://www.npmjs.com/package/@desource/phone-mask-angular)
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/DeSource-Labs/phone-mask/blob/main/LICENSE)

Beautiful, accessible, tree-shakeable Angular phone input with auto-formatting, country selector, validation, forms support, and signal-first APIs.

## Features

- Standalone Angular component, directive, pipe, and `UsePhoneMaskService`
- Signal inputs and `model()` two-way value binding
- Works with Angular forms through `ControlValueAccessor`
- Smart country search with keyboard navigation
- As-you-type formatting with stable caret handling
- Optional GeoIP and locale country detection
- APF package output with partial compilation
- Core mask data and kit utilities re-exported from `@desource/phone-mask-angular/core`

## Installation

```bash
npm install @desource/phone-mask-angular
# or
pnpm add @desource/phone-mask-angular
```

## Quick Start

```ts
import { Component, signal } from '@angular/core';
import { PhoneInputComponent, type PMaskPhoneNumber } from '@desource/phone-mask-angular';

@Component({
selector: 'app-checkout-phone',
standalone: true,
imports: [PhoneInputComponent],
template: `
<desource-phone-input
[(value)]="phoneDigits"
country="US"
theme="auto"
[showClear]="true"
(phoneChange)="onPhoneChange($event)"
(validationChange)="isValid.set($event)"
/>

@if (isValid()) {
<p>Valid phone number</p>
}
`
})
export class CheckoutPhoneComponent {
readonly phoneDigits = signal('');
readonly isValid = signal(false);

onPhoneChange(phone: PMaskPhoneNumber): void {
console.log(phone.digits, phone.full, phone.fullFormatted);
}
}
```

The component also works as an Angular form control:

```html
<desource-phone-input formControlName="phone" country="US" />
```

## Directive Mode

Use the directive when you want to own the input markup and styling:

```ts
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { PhoneMaskDirective, type PMaskPhoneNumber } from '@desource/phone-mask-angular';

@Component({
selector: 'app-custom-phone',
standalone: true,
imports: [FormsModule, PhoneMaskDirective],
template: `
<input phoneMask [(ngModel)]="digits" [phoneMaskCountry]="country()" (phoneMaskChange)="onPhoneChange($event)" />
`
})
export class CustomPhoneComponent {
readonly country = signal('US');
digits = '';

onPhoneChange(phone: PMaskPhoneNumber): void {
console.log(phone.fullFormatted);
}
}
```

You can also bind directive options directly:

```html
<input
[phoneMask]="{ country: 'GB', detect: false }"
[(phoneMaskValue)]="digits"
(phoneMaskCountryChange)="country = $event"
/>
```

## Pipe

```ts
import { Component } from '@angular/core';
import { PhoneMaskPipe } from '@desource/phone-mask-angular';

@Component({
selector: 'app-phone-summary',
standalone: true,
imports: [PhoneMaskPipe],
template: `
<p>{{ '2025551234' | phoneMask }}</p>
<p>{{ '2025551234' | phoneMask: { mode: 'fullFormatted' } }}</p>
`
})
export class PhoneSummaryComponent {}
```

## Custom Templates

```html
<desource-phone-input [(value)]="digits">
<ng-template #flag let-country="country">
<span>{{ country.id }}</span>
</ng-template>

<ng-template #actionsBefore>
<button type="button" class="pi-btn" (click)="openHelp()">?</button>
</ng-template>
</desource-phone-input>
```

Available template refs are `#flag`, `#actionsBefore`, `#copySvg`, and `#clearSvg`.

## Core Utilities

```ts
import { getFlagEmoji, formatDigitsWithMap } from '@desource/phone-mask-angular/core';
```

## Styles

The component ships its default styles with the component. A compiled stylesheet is also available for manual use:

```ts
import '@desource/phone-mask-angular/assets/lib.css';
```
59 changes: 59 additions & 0 deletions packages/phone-mask-angular/angular.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"phone-mask-angular": {
"projectType": "library",
"root": ".",
"sourceRoot": ".",
"prefix": "desource",
"architect": {
"build": {
"builder": "@angular/build:ng-packagr",
"options": {
"project": "ng-package.json"
}
},
"test": {
"builder": "@angular/build:unit-test",
"options": {
"buildTarget": "phone-mask-angular:build",
"tsConfig": "tsconfig.spec.json",
"runner": "vitest",
"runnerConfig": "vitest.angular.config.ts",
"setupFiles": ["tests/unit/setup/angular.ts"],
"include": ["tests/unit/**/*.test.ts"],
"coverageInclude": ["src/**/*.ts"],
"coverageExclude": ["src/**/*.d.ts"],
"coverageReporters": ["lcov"]
}
}
}
},
"demo": {
"projectType": "application",
"root": "demo",
"sourceRoot": "demo/src",
"prefix": "demo",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "demo/src/main.ts",
"index": "demo/src/index.html",
"outputPath": "dist-demo",
"tsConfig": "demo/tsconfig.json",
"styles": []
}
},
"serve": {
"builder": "@angular/build:dev-server",
"options": {
"buildTarget": "demo:build"
}
}
}
}
}
}
6 changes: 6 additions & 0 deletions packages/phone-mask-angular/core/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "../node_modules/ng-packagr/ng-entrypoint.schema.json",
"lib": {
"entryFile": "src/public-api.ts"
}
}
2 changes: 2 additions & 0 deletions packages/phone-mask-angular/core/src/public-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from '@desource/phone-mask';
export * from '@desource/phone-mask/kit';
Loading