diff --git a/package-lock.json b/package-lock.json
index c2e7e6f9..47057cc5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -462,7 +462,6 @@
"integrity": "sha512-1KocmjmBP0qlKQGRhRGN0MGvLxf1q2KDWbvzn7ZGdQrIDLC/hFJ8YmnOWsPrM9RxiZi0o5BxCCu9D7KlbthxIg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@angular-eslint/bundled-angular-compiler": "21.0.1",
"eslint-scope": "^9.0.0"
@@ -492,7 +491,6 @@
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.0.9.tgz",
"integrity": "sha512-vKt+HT5louWK0/MMm/D7kLDmpG6/5OeG8FaF4WTHc2ZI7vn6V4UGgYNdfUZFTn6NWsK1I8tykL3+ExCCfn51eg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -672,7 +670,6 @@
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.0.6.tgz",
"integrity": "sha512-5Gw8mXtKXvcvDMWEciPLRYB6Ja5vsikLAidZsdCEIF6Bc51GmoqT5Tk/Ke+ciCd5Hq9Aco/IcHxT1RC3470lZg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"parse5": "^8.0.0",
"tslib": "^2.3.0"
@@ -807,7 +804,6 @@
"resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.9.tgz",
"integrity": "sha512-yU9D5qgZGGhlhaM9p0YZEcFSMq84sZHKUqOjL5a2+bxpT01gO1ZhoOGeyLccClgjaNALH23J9hCvmv05ufhXlw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -824,7 +820,6 @@
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.9.tgz",
"integrity": "sha512-wQVaNWZM/iyIggJ6lExxnJG8ProqNp4fDNCinejnJbjiQVH7oLU57uAD153nqe6lHhz9oHVMtHx4qpPh1h/ptQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -838,7 +833,6 @@
"integrity": "sha512-yjXdY6PAw9HR3YcWGiGSwPw7sJmnW8ziCqP+5DKJkHDIAHmTWFyxffmWys+cZRDjRuk6GvBmyWemvEjNyfMVMw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/core": "7.28.4",
"@jridgewell/sourcemap-codec": "^1.4.14",
@@ -901,7 +895,6 @@
"resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.9.tgz",
"integrity": "sha512-3RAkc0esb5fuerAJcjgv+rdsaOQfjkWIRckyvDFecAFyeouYWn8x3pAp9rDI02PTMnup+qSgZOqPN1mG3BAjgg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -927,7 +920,6 @@
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.9.tgz",
"integrity": "sha512-FyTkjXbdN+jJLhg1YlTtsmwtiyTMT/jWYFFWG9NIRvKTIMEJqkxl3pi8/idhsuzbZmvBVTQf6xOyAY1QnBWQig==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"tslib": "^2.3.0"
@@ -964,7 +956,6 @@
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.9.tgz",
"integrity": "sha512-NXOiDAlGXex8O1D6ZsHinZsloA/ngvSqcg/g2IzVUtxM/PRKYIyk6DeRnWXx1QCQmUVh96WbcAnFoF2PUj2Z8A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -1104,7 +1095,6 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -1384,6 +1374,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=0.1.90"
}
@@ -1476,7 +1467,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
},
@@ -1517,7 +1507,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -2536,7 +2525,6 @@
"integrity": "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@inquirer/checkbox": "^4.3.0",
"@inquirer/confirm": "^5.1.19",
@@ -4892,7 +4880,8 @@
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"dev": true,
"license": "MIT",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
@@ -4976,6 +4965,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"@types/node": "*"
}
@@ -5136,7 +5126,6 @@
"integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==",
"dev": true,
"license": "BSD-2-Clause",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.16.0",
"@typescript-eslint/types": "8.16.0",
@@ -5180,6 +5169,7 @@
"integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.53.1",
"@typescript-eslint/types": "^8.53.1",
@@ -5234,6 +5224,7 @@
"integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -5430,6 +5421,7 @@
"integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/types": "8.53.1",
"@typescript-eslint/visitor-keys": "8.53.1"
@@ -5448,6 +5440,7 @@
"integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/project-service": "8.53.1",
"@typescript-eslint/tsconfig-utils": "8.53.1",
@@ -5476,6 +5469,7 @@
"integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/types": "8.53.1",
"eslint-visitor-keys": "^4.2.1"
@@ -5494,6 +5488,7 @@
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -5717,7 +5712,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5868,6 +5862,7 @@
"dev": true,
"license": "ISC",
"optional": true,
+ "peer": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@@ -5883,6 +5878,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=8.6"
},
@@ -5964,6 +5960,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": "^4.5.0 || >= 5.9"
}
@@ -6025,6 +6022,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=8"
},
@@ -6107,7 +6105,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -6661,6 +6658,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"debug": "2.6.9",
"finalhandler": "1.1.2",
@@ -6678,6 +6676,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"ms": "2.0.0"
}
@@ -6689,6 +6688,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">= 0.8"
}
@@ -6700,6 +6700,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
@@ -6719,7 +6720,8 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/connect/node_modules/on-finished": {
"version": "2.3.0",
@@ -6728,6 +6730,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"ee-first": "1.1.1"
},
@@ -6742,6 +6745,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -6934,7 +6938,8 @@
"integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==",
"dev": true,
"license": "MIT",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/data-uri-to-buffer": {
"version": "6.0.2",
@@ -6984,6 +6989,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=4.0"
}
@@ -7062,6 +7068,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
@@ -7084,7 +7091,8 @@
"integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==",
"dev": true,
"license": "MIT",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/dom-serialize": {
"version": "2.2.1",
@@ -7093,6 +7101,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"custom-event": "~1.0.0",
"ent": "~2.2.0",
@@ -7237,6 +7246,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
@@ -7259,6 +7269,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=10.0.0"
}
@@ -7270,6 +7281,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
@@ -7285,6 +7297,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -7296,6 +7309,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -7310,6 +7324,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -7321,6 +7336,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"call-bound": "^1.0.3",
"es-errors": "^1.3.0",
@@ -7539,7 +7555,6 @@
"integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7855,7 +7870,8 @@
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"dev": true,
"license": "MIT",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/eventsource": {
"version": "3.0.7",
@@ -7903,7 +7919,6 @@
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -7964,7 +7979,8 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true,
"license": "MIT",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
@@ -8207,6 +8223,7 @@
],
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=4.0"
},
@@ -8243,6 +8260,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
@@ -8271,7 +8289,8 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true,
"license": "ISC",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/fsevents": {
"version": "2.3.3",
@@ -8393,6 +8412,7 @@
"dev": true,
"license": "ISC",
"optional": true,
+ "peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -8435,6 +8455,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -8447,6 +8468,7 @@
"dev": true,
"license": "ISC",
"optional": true,
+ "peer": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -8524,6 +8546,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
@@ -8662,6 +8685,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
@@ -8811,6 +8835,7 @@
"dev": true,
"license": "ISC",
"optional": true,
+ "peer": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -8870,6 +8895,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
@@ -8976,6 +9002,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
@@ -9016,6 +9043,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">= 8.0.0"
},
@@ -9222,6 +9250,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
@@ -9243,6 +9272,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"@colors/colors": "1.5.0",
"body-parser": "^1.19.0",
@@ -9283,6 +9313,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=8"
}
@@ -9294,6 +9325,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
@@ -9320,6 +9352,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -9332,6 +9365,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -9358,6 +9392,7 @@
"dev": true,
"license": "ISC",
"optional": true,
+ "peer": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
@@ -9371,6 +9406,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"ms": "2.0.0"
}
@@ -9381,7 +9417,8 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/karma/node_modules/glob-parent": {
"version": "5.1.2",
@@ -9390,6 +9427,7 @@
"dev": true,
"license": "ISC",
"optional": true,
+ "peer": true,
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -9404,6 +9442,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
@@ -9418,6 +9457,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=8"
}
@@ -9429,6 +9469,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -9440,6 +9481,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -9451,6 +9493,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -9465,6 +9508,7 @@
"dev": true,
"license": "ISC",
"optional": true,
+ "peer": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -9478,7 +9522,8 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/karma/node_modules/picomatch": {
"version": "2.3.1",
@@ -9487,6 +9532,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=8.6"
},
@@ -9501,6 +9547,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
@@ -9518,6 +9565,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"picomatch": "^2.2.1"
},
@@ -9532,6 +9580,7 @@
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9543,6 +9592,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -9559,6 +9609,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -9573,6 +9624,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
@@ -9588,6 +9640,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -9607,6 +9660,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
@@ -9627,6 +9681,7 @@
"dev": true,
"license": "ISC",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -9648,7 +9703,6 @@
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"copy-anything": "^2.0.1",
"parse-node-version": "^1.0.1",
@@ -9741,7 +9795,6 @@
"integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"cli-truncate": "^5.0.0",
"colorette": "^2.0.20",
@@ -9961,6 +10014,7 @@
"dev": true,
"license": "Apache-2.0",
"optional": true,
+ "peer": true,
"dependencies": {
"date-format": "^4.0.14",
"debug": "^4.3.4",
@@ -10141,6 +10195,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"bin": {
"mime": "cli.js"
},
@@ -10211,6 +10266,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -10375,6 +10431,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"minimist": "^1.2.6"
},
@@ -10534,7 +10591,6 @@
"integrity": "sha512-2lMGkmS91FyP+p/Tzmu49hY+p1PDgHBNM+Fce8yrzZo8/EbybNPBYfJnwFfl0lwGmqpYLevH2oh12+ikKCLv9g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@rollup/plugin-json": "^6.1.0",
@@ -11208,6 +11264,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -11719,6 +11776,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -11907,7 +11965,6 @@
"integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -12017,7 +12074,8 @@
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
"dev": true,
"license": "MIT",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/qjobs": {
"version": "1.2.0",
@@ -12026,6 +12084,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=0.9"
}
@@ -12126,7 +12185,8 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true,
"license": "MIT",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/resolve": {
"version": "1.22.11",
@@ -12254,6 +12314,7 @@
"dev": true,
"license": "ISC",
"optional": true,
+ "peer": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -12303,7 +12364,6 @@
"integrity": "sha512-y9yUpfQvetAjiDLtNMf1hL9NXchIJgWt6zIKeoB+tCd3npX08Eqfzg60V9DhIGVMtQ0AlMkFw5xa+AQ37zxnAA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -12412,7 +12472,6 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -12424,6 +12483,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@@ -12783,6 +12843,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
@@ -12803,6 +12864,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"debug": "~4.4.1",
"ws": "~8.18.3"
@@ -12815,6 +12877,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1"
@@ -12830,6 +12893,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
@@ -12845,6 +12909,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -12856,6 +12921,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -12870,6 +12936,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -13044,6 +13111,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"date-format": "^4.0.14",
"debug": "^4.3.4",
@@ -13233,6 +13301,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=14.14"
}
@@ -13323,8 +13392,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD",
- "peer": true
+ "license": "0BSD"
},
"node_modules/tuf-js": {
"version": "4.1.0",
@@ -13375,7 +13443,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -13405,6 +13472,7 @@
],
"license": "MIT",
"optional": true,
+ "peer": true,
"bin": {
"ua-parser-js": "script/cli.js"
},
@@ -13462,6 +13530,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">= 4.0.0"
}
@@ -13534,6 +13603,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">= 0.4.0"
}
@@ -13589,7 +13659,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -14149,7 +14218,6 @@
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@vitest/expect": "4.0.18",
"@vitest/mocker": "4.0.18",
@@ -14229,6 +14297,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -14447,6 +14516,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -14588,7 +14658,6 @@
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/src/app/app.navigation.ts b/src/app/app.navigation.ts
index 34ab9e3e..d50544b5 100644
--- a/src/app/app.navigation.ts
+++ b/src/app/app.navigation.ts
@@ -38,6 +38,48 @@ const messageBarNavigationItem: NavigationItem = {
fullRouterPath: getFullRoutePath(ROUTE_MAP.components.messageBar)
}
+const asyncResultNavigationItem: NavigationItem = {
+ label: 'navigation.async_result',
+ icon: 'fa-solid fa-spinner',
+ fullRouterPath: getFullRoutePath(ROUTE_MAP.components.asyncResult)
+}
+
+const fileDownloadNavigationItem: NavigationItem = {
+ label: 'navigation.file_download',
+ icon: 'fa-solid fa-download',
+ fullRouterPath: getFullRoutePath(ROUTE_MAP.components.fileDownload)
+}
+
+const draggableDialogNavigationItem: NavigationItem = {
+ label: 'navigation.draggable_dialog',
+ icon: 'fa-solid fa-arrows-up-down-left-right',
+ fullRouterPath: getFullRoutePath(ROUTE_MAP.components.draggableDialog)
+}
+
+const formsDemoNavigationItem: NavigationItem = {
+ label: 'navigation.forms_demo',
+ icon: 'fa-solid fa-pen-to-square',
+ fullRouterPath: getFullRoutePath(ROUTE_MAP.components.formsDemo)
+}
+
+const routeMapNavigationItem: NavigationItem = {
+ label: 'navigation.route_map',
+ icon: 'fa-solid fa-route',
+ fullRouterPath: getFullRoutePath(ROUTE_MAP.components.routeMap)
+}
+
+const signalStoreNavigationItem: NavigationItem = {
+ label: 'navigation.signal_store',
+ icon: 'fa-solid fa-database',
+ fullRouterPath: getFullRoutePath(ROUTE_MAP.components.signalStore)
+}
+
+const utilsDemoNavigationItem: NavigationItem = {
+ label: 'navigation.utils_demo',
+ icon: 'fa-solid fa-wrench',
+ fullRouterPath: getFullRoutePath(ROUTE_MAP.components.utilsDemo)
+}
+
const componentsNavigationItemContainer: NavigationItem = {
label: 'navigation.components',
icon: 'fa-solid fa-cubes',
@@ -46,7 +88,14 @@ const componentsNavigationItemContainer: NavigationItem = {
expandableCardNavigationItem,
tableNavigationItem,
formTableNavigationItem,
- messageBarNavigationItem
+ messageBarNavigationItem,
+ asyncResultNavigationItem,
+ fileDownloadNavigationItem,
+ draggableDialogNavigationItem,
+ formsDemoNavigationItem,
+ routeMapNavigationItem,
+ signalStoreNavigationItem,
+ utilsDemoNavigationItem
]
}
@@ -62,6 +111,18 @@ const globalErrorHandlerNavigationItem: NavigationItem = {
fullRouterPath: getFullRoutePath(ROUTE_MAP.globalErrorHandler)
}
+const subscriptionHandlingNavigationItem: NavigationItem = {
+ label: 'navigation.subscription_handling',
+ icon: 'fa-solid fa-link-slash',
+ fullRouterPath: getFullRoutePath(ROUTE_MAP.subscriptionHandling)
+}
+
+const localStorageNavigationItem: NavigationItem = {
+ label: 'navigation.local_storage',
+ icon: 'fa-solid fa-hard-drive',
+ fullRouterPath: getFullRoutePath(ROUTE_MAP.localStorage)
+}
+
const peoplewareWebsiteNavigationItem: NavigationItem = {
label: 'navigation.peopleware_website',
icon: 'fa-solid fa-earth-europe',
@@ -74,6 +135,8 @@ export const getNavigationItems = () => {
dashboardNavigationItem,
componentsNavigationItemContainer,
inMemoryLoggingNavigationItem,
+ subscriptionHandlingNavigationItem,
+ localStorageNavigationItem,
globalErrorHandlerNavigationItem,
peoplewareWebsiteNavigationItem
]
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 4f09af3e..a1f1bf74 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -12,11 +12,20 @@ export const ROUTE_MAP = {
expandableCard: defineRoute('expandable-card'),
table: defineRoute('table'),
formTable: defineRoute('form-table'),
- messageBar: defineRoute('message-bar')
+ messageBar: defineRoute('message-bar'),
+ asyncResult: defineRoute('async-result'),
+ fileDownload: defineRoute('file-download'),
+ draggableDialog: defineRoute('draggable-dialog'),
+ formsDemo: defineRoute('forms'),
+ routeMap: defineRoute('route-map'),
+ signalStore: defineRoute('signal-store'),
+ utilsDemo: defineRoute('utils')
}),
dashboardItem: defineRoute('dashboard-item'),
globalErrorHandler: defineRoute('global-error-handler'),
- inMemoryLogging: defineRoute('in-memory-logging')
+ inMemoryLogging: defineRoute('in-memory-logging'),
+ subscriptionHandling: defineRoute('subscription-handling'),
+ localStorage: defineRoute('local-storage')
}
export const routes: Routes = [
@@ -43,5 +52,17 @@ export const routes: Routes = [
path: getRouteSegment(ROUTE_MAP.globalErrorHandler),
component: GlobalErrorHandlerComponent,
title: 'navigation.global_error_handler'
+ },
+ {
+ path: getRouteSegment(ROUTE_MAP.subscriptionHandling),
+ loadComponent: () =>
+ import('./subscription-handling-demo/subscription-handling-demo.component').then((m) => m.SubscriptionHandlingDemoComponent),
+ title: 'navigation.subscription_handling'
+ },
+ {
+ path: getRouteSegment(ROUTE_MAP.localStorage),
+ loadComponent: () =>
+ import('./local-storage-demo/local-storage-demo.component').then((m) => m.LocalStorageDemoComponent),
+ title: 'navigation.local_storage'
}
]
diff --git a/src/app/async-result-demo/async-result-demo.component.html b/src/app/async-result-demo/async-result-demo.component.html
new file mode 100644
index 00000000..6c520704
--- /dev/null
+++ b/src/app/async-result-demo/async-result-demo.component.html
@@ -0,0 +1,33 @@
+
+
Use the buttons below to switch between AsyncResult states. Demonstrates createSuccessAsyncResult, createFailedAsyncResult, executeAsyncOperation, and isAsyncResult.
+
+ Initial
+ Success
+ Empty
+ Failed
+ Simulate load
+ Check isAsyncResult
+
+ @if (isAsyncResultGuardResult(); as msg) {
+ @if (msg) {
+
isAsyncResult guard: {{ msg }}
+ }
+ }
+
+
+ Click a button above or "Simulate load" to see content.
+
+
+ @if (data.entity; as items) {
+
+ @for (item of items; track item.id) {
+ {{ item.name }}
+ }
+
+ }
+
+
+ No items.
+
+
+
diff --git a/src/app/async-result-demo/async-result-demo.component.scss b/src/app/async-result-demo/async-result-demo.component.scss
new file mode 100644
index 00000000..7f26ddcb
--- /dev/null
+++ b/src/app/async-result-demo/async-result-demo.component.scss
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/src/app/async-result-demo/async-result-demo.component.ts b/src/app/async-result-demo/async-result-demo.component.ts
new file mode 100644
index 00000000..d9abf327
--- /dev/null
+++ b/src/app/async-result-demo/async-result-demo.component.ts
@@ -0,0 +1,83 @@
+import { AsyncPipe } from '@angular/common'
+import { ChangeDetectionStrategy, Component, signal } from '@angular/core'
+import { MatButtonModule } from '@angular/material/button'
+import { AsyncResultModule, createFailedAsyncResult, createSuccessAsyncResult, AsyncResult, isAsyncResult } from '@ppwcode/ng-async'
+import { BehaviorSubject } from 'rxjs'
+import { delay, map, of } from 'rxjs'
+import { executeAsyncOperation } from '@ppwcode/ng-async'
+
+interface DemoItem {
+ id: number
+ name: string
+}
+
+@Component({
+ selector: 'ppw-async-result-demo',
+ standalone: true,
+ imports: [AsyncResultModule, MatButtonModule, AsyncPipe],
+ templateUrl: './async-result-demo.component.html',
+ styleUrl: './async-result-demo.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class AsyncResultDemoComponent {
+ readonly asyncResult = signal | null>(null)
+ readonly isAsyncResultGuardResult = signal('')
+
+ readonly isLoading$ = new BehaviorSubject(false)
+
+ setInitial(): void {
+ this.asyncResult.set({
+ status: 'initial',
+ entity: null,
+ filters: null
+ })
+ this.isLoading$.next(false)
+ }
+
+ setSuccess(): void {
+ this.asyncResult.set(
+ createSuccessAsyncResult([
+ { id: 1, name: 'Alpha' },
+ { id: 2, name: 'Beta' }
+ ])
+ )
+ this.isLoading$.next(false)
+ }
+
+ setEmpty(): void {
+ this.asyncResult.set(createSuccessAsyncResult(null))
+ this.isLoading$.next(false)
+ }
+
+ setFailed(): void {
+ this.asyncResult.set(
+ createFailedAsyncResult(new Error('Something went wrong'), [], null)
+ )
+ this.isLoading$.next(false)
+ }
+
+ async simulateLoad(): Promise {
+ const result$ = of(
+ createSuccessAsyncResult([{ id: 1, name: 'Loaded' }])
+ ).pipe(
+ delay(1500),
+ map((r) => r)
+ )
+ await executeAsyncOperation(
+ result$,
+ {
+ success: (r) => this.asyncResult.set(r),
+ error: (r) => this.asyncResult.set(r)
+ },
+ this.isLoading$,
+ true,
+ false
+ )
+ }
+
+ checkIsAsyncResult(): void {
+ const current = this.asyncResult()
+ const result = isAsyncResult(current) ? `Yes, status: ${current.status}` : 'No'
+ this.isAsyncResultGuardResult.set(result)
+ }
+}
diff --git a/src/app/components-dashboard-demo/components-dashboard-demo.component.ts b/src/app/components-dashboard-demo/components-dashboard-demo.component.ts
index f3df7788..4560abf3 100644
--- a/src/app/components-dashboard-demo/components-dashboard-demo.component.ts
+++ b/src/app/components-dashboard-demo/components-dashboard-demo.component.ts
@@ -4,6 +4,7 @@ import { MatProgressSpinner } from '@angular/material/progress-spinner'
import { Router } from '@angular/router'
import { DashboardItem, DashboardItemAction, DashboardItemsTableComponent } from '@ppwcode/ng-common-components'
import { getFullRoutePath } from '@ppwcode/ng-router'
+import type { RouteMapRoute } from '@ppwcode/ng-router'
import { ROUTE_MAP } from '../app.routes'
@Component({
@@ -23,7 +24,16 @@ export default class ComponentsDashboardDemoComponent {
this.#getConfirmationDemoItem(),
this.#getExpandableCardDemoItem(),
this.#getTableDemoItem(),
- this.#getMessageBarDemoItem()
+ this.#getMessageBarDemoItem(),
+ this.#getAsyncResultDemoItem(),
+ this.#getFileDownloadDemoItem(),
+ this.#getDraggableDialogDemoItem(),
+ this.#getFormsDemoItem(),
+ this.#getRouteMapDemoItem(),
+ this.#getSignalStoreDemoItem(),
+ this.#getUtilsDemoItem(),
+ this.#getSubscriptionHandlingDemoItem(),
+ this.#getLocalStorageDemoItem()
])
#getConfirmationDemoItem(): DashboardItem {
@@ -109,4 +119,59 @@ export default class ComponentsDashboardDemoComponent {
#openMessageBarDemo(): void {
this.#router.navigateByUrl(getFullRoutePath(ROUTE_MAP.components.messageBar))
}
+
+ #getAsyncResultDemoItem(): DashboardItem {
+ return this.#item('navigation.async_result', 'dashboard.async-result-description', 'fa-solid fa-spinner', ROUTE_MAP.components.asyncResult)
+ }
+
+ #getFileDownloadDemoItem(): DashboardItem {
+ return this.#item('navigation.file_download', 'dashboard.file-download-description', 'fa-solid fa-download', ROUTE_MAP.components.fileDownload)
+ }
+
+ #getDraggableDialogDemoItem(): DashboardItem {
+ return this.#item('navigation.draggable_dialog', 'dashboard.draggable-dialog-description', 'fa-solid fa-arrows-up-down-left-right', ROUTE_MAP.components.draggableDialog)
+ }
+
+ #getFormsDemoItem(): DashboardItem {
+ return this.#item('navigation.forms_demo', 'dashboard.forms-demo-description', 'fa-solid fa-pen-to-square', ROUTE_MAP.components.formsDemo)
+ }
+
+ #getRouteMapDemoItem(): DashboardItem {
+ return this.#item('navigation.route_map', 'dashboard.route-map-description', 'fa-solid fa-route', ROUTE_MAP.components.routeMap)
+ }
+
+ #getSignalStoreDemoItem(): DashboardItem {
+ return this.#item('navigation.signal_store', 'dashboard.signal-store-description', 'fa-solid fa-database', ROUTE_MAP.components.signalStore)
+ }
+
+ #getUtilsDemoItem(): DashboardItem {
+ return this.#item('navigation.utils_demo', 'dashboard.utils-demo-description', 'fa-solid fa-wrench', ROUTE_MAP.components.utilsDemo)
+ }
+
+ #getSubscriptionHandlingDemoItem(): DashboardItem {
+ return this.#item('navigation.subscription_handling', 'dashboard.subscription-handling-description', 'fa-solid fa-link-slash', ROUTE_MAP.subscriptionHandling)
+ }
+
+ #getLocalStorageDemoItem(): DashboardItem {
+ return this.#item('navigation.local_storage', 'dashboard.local-storage-description', 'fa-solid fa-hard-drive', ROUTE_MAP.localStorage)
+ }
+
+ #item(
+ titleKey: string,
+ descriptionKey: string,
+ iconClass: string,
+ route: RouteMapRoute
+ ): DashboardItem {
+ const openAction: DashboardItemAction = {
+ labelKey: 'button.open',
+ clickFn: () => this.#router.navigateByUrl(getFullRoutePath(route))
+ }
+ return {
+ iconClass,
+ titleKey,
+ descriptionKey,
+ actions: [openAction],
+ defaultAction: openAction
+ }
+ }
}
diff --git a/src/app/components.routes.ts b/src/app/components.routes.ts
index 29f1f88c..b6ab37a1 100644
--- a/src/app/components.routes.ts
+++ b/src/app/components.routes.ts
@@ -30,5 +30,41 @@ export const componentsRoutes: Routes = [
path: getRouteSegment(ROUTE_MAP.components.messageBar),
component: MessageBarComponent,
title: 'navigation.message_bar'
+ },
+ {
+ path: getRouteSegment(ROUTE_MAP.components.asyncResult),
+ loadComponent: () => import('./async-result-demo/async-result-demo.component').then((m) => m.AsyncResultDemoComponent),
+ title: 'navigation.async_result'
+ },
+ {
+ path: getRouteSegment(ROUTE_MAP.components.fileDownload),
+ loadComponent: () => import('./file-download-demo/file-download-demo.component').then((m) => m.FileDownloadDemoComponent),
+ title: 'navigation.file_download'
+ },
+ {
+ path: getRouteSegment(ROUTE_MAP.components.draggableDialog),
+ loadComponent: () =>
+ import('./draggable-dialog-demo/draggable-dialog-demo.component').then((m) => m.DraggableDialogDemoComponent),
+ title: 'navigation.draggable_dialog'
+ },
+ {
+ path: getRouteSegment(ROUTE_MAP.components.formsDemo),
+ loadComponent: () => import('./forms-demo/forms-demo.component').then((m) => m.FormsDemoComponent),
+ title: 'navigation.forms_demo'
+ },
+ {
+ path: getRouteSegment(ROUTE_MAP.components.routeMap),
+ loadComponent: () => import('./route-map-demo/route-map-demo.component').then((m) => m.RouteMapDemoComponent),
+ title: 'navigation.route_map'
+ },
+ {
+ path: getRouteSegment(ROUTE_MAP.components.signalStore),
+ loadComponent: () => import('./signal-store-demo/signal-store-demo.component').then((m) => m.SignalStoreDemoComponent),
+ title: 'navigation.signal_store'
+ },
+ {
+ path: getRouteSegment(ROUTE_MAP.components.utilsDemo),
+ loadComponent: () => import('./utils-demo/utils-demo.component').then((m) => m.UtilsDemoComponent),
+ title: 'navigation.utils_demo'
}
]
diff --git a/src/app/dashboard-item-demo/dashboard-item-demo.component.html b/src/app/dashboard-item-demo/dashboard-item-demo.component.html
index c6a1e8e3..e3964730 100644
--- a/src/app/dashboard-item-demo/dashboard-item-demo.component.html
+++ b/src/app/dashboard-item-demo/dashboard-item-demo.component.html
@@ -1,3 +1,6 @@
+
+ This demo showcases the ppwcode Angular SDK: ng-async, ng-common, ng-common-components, ng-dialogs, ng-forms, ng-router, ng-state-management, ng-utils, ng-wireframe, and ng-unit-testing. Use the sidebar or the cards below to open each demo.
+
+ Draggable Dialog
+
+ This dialog can be dragged by the title bar. Uses ppwDraggableDialog and cdkDragHandle from ng-dialogs.
+
+
+ Close
+
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class DraggableDialogContentComponent {}
diff --git a/src/app/draggable-dialog-demo/draggable-dialog-demo.component.html b/src/app/draggable-dialog-demo/draggable-dialog-demo.component.html
new file mode 100644
index 00000000..1794ddc8
--- /dev/null
+++ b/src/app/draggable-dialog-demo/draggable-dialog-demo.component.html
@@ -0,0 +1,4 @@
+
+
Demonstrates the DraggableDialogDirective from ng-dialogs. Click the button to open a dialog that can be dragged by its title bar.
+
Open draggable dialog
+
diff --git a/src/app/draggable-dialog-demo/draggable-dialog-demo.component.scss b/src/app/draggable-dialog-demo/draggable-dialog-demo.component.scss
new file mode 100644
index 00000000..7f26ddcb
--- /dev/null
+++ b/src/app/draggable-dialog-demo/draggable-dialog-demo.component.scss
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/src/app/draggable-dialog-demo/draggable-dialog-demo.component.ts b/src/app/draggable-dialog-demo/draggable-dialog-demo.component.ts
new file mode 100644
index 00000000..72e45027
--- /dev/null
+++ b/src/app/draggable-dialog-demo/draggable-dialog-demo.component.ts
@@ -0,0 +1,22 @@
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
+import { MatButtonModule } from '@angular/material/button'
+import { MatDialog } from '@angular/material/dialog'
+import { DraggableDialogContentComponent } from './draggable-dialog-content.component'
+
+@Component({
+ selector: 'ppw-draggable-dialog-demo',
+ standalone: true,
+ imports: [MatButtonModule],
+ templateUrl: './draggable-dialog-demo.component.html',
+ styleUrl: './draggable-dialog-demo.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class DraggableDialogDemoComponent {
+ private readonly dialog = inject(MatDialog)
+
+ openDraggableDialog(): void {
+ this.dialog.open(DraggableDialogContentComponent, {
+ width: '400px'
+ })
+ }
+}
diff --git a/src/app/examples/http-call-tester-example.spec.ts b/src/app/examples/http-call-tester-example.spec.ts
new file mode 100644
index 00000000..30786f1a
--- /dev/null
+++ b/src/app/examples/http-call-tester-example.spec.ts
@@ -0,0 +1,45 @@
+import { HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
+import { provideHttpClientTesting } from '@angular/common/http/testing'
+import { TestBed } from '@angular/core/testing'
+import { HttpCallTester } from '@ppwcode/ng-unit-testing'
+
+/**
+ * Example spec demonstrating HttpCallTester from @ppwcode/ng-unit-testing.
+ * Use this as reference when testing services that perform HTTP calls.
+ */
+describe('HttpCallTester example', () => {
+ let httpClient: HttpClient
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()]
+ })
+ httpClient = TestBed.inject(HttpClient)
+ })
+
+ it('should verify a successful GET request', () => {
+ const mockUsers = [{ id: 1, name: 'Alpha' }]
+
+ HttpCallTester.expectOneCallToUrl('/api/users')
+ .whenSubscribingTo(httpClient.get('/api/users'))
+ .expectRequestTo((request) => {
+ expect(request.request.method).toBe('GET')
+ })
+ .withResponse(mockUsers)
+ .expectStreamResultTo((result) => {
+ expect(result).toEqual(mockUsers)
+ expect(result.length).toBe(1)
+ })
+ .verify()
+ })
+
+ it('should verify an HTTP error is handled', () => {
+ HttpCallTester.expectOneCallToUrl('/api/users/999')
+ .whenSubscribingTo(httpClient.get('/api/users/999'))
+ .withResponse(null, { status: 404, statusText: 'Not Found' })
+ .expectErrorTo((error) => {
+ expect((error as { status?: number }).status).toBe(404)
+ })
+ .verifyFailure()
+ })
+})
diff --git a/src/app/file-download-demo/file-download-demo.component.html b/src/app/file-download-demo/file-download-demo.component.html
new file mode 100644
index 00000000..afe9ac24
--- /dev/null
+++ b/src/app/file-download-demo/file-download-demo.component.html
@@ -0,0 +1,11 @@
+
+
Demonstrates file download helpers from ng-async: saveDownloadedFile (fixed filename) and saveFileDownload (filename from response / FileDownload).
+
+
+ Download with fixed name (saveDownloadedFile)
+
+
+ Download with filename from response (saveFileDownload)
+
+
+
diff --git a/src/app/file-download-demo/file-download-demo.component.scss b/src/app/file-download-demo/file-download-demo.component.scss
new file mode 100644
index 00000000..7f26ddcb
--- /dev/null
+++ b/src/app/file-download-demo/file-download-demo.component.scss
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/src/app/file-download-demo/file-download-demo.component.ts b/src/app/file-download-demo/file-download-demo.component.ts
new file mode 100644
index 00000000..cc2d4f2b
--- /dev/null
+++ b/src/app/file-download-demo/file-download-demo.component.ts
@@ -0,0 +1,34 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core'
+import { MatButtonModule } from '@angular/material/button'
+import {
+ createSuccessAsyncResult,
+ saveDownloadedFile,
+ saveFileDownload
+} from '@ppwcode/ng-async'
+
+@Component({
+ selector: 'ppw-file-download-demo',
+ standalone: true,
+ imports: [MatButtonModule],
+ templateUrl: './file-download-demo.component.html',
+ styleUrl: './file-download-demo.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class FileDownloadDemoComponent {
+ downloadWithFixedName(): void {
+ const content = 'Demo content from ppwcode Angular SDK – saveDownloadedFile demo.'
+ const blob = new Blob([content], { type: 'text/plain' })
+ const result = createSuccessAsyncResult(blob)
+ saveDownloadedFile('demo-download.txt')(result)
+ }
+
+ downloadWithFileNameFromResponse(): void {
+ const content = 'Content with filename from response – saveFileDownload demo.'
+ const blob = new Blob([content], { type: 'text/plain' })
+ const result = createSuccessAsyncResult({
+ blob,
+ fileName: 'server-filename-demo.txt'
+ })
+ saveFileDownload()(result)
+ }
+}
diff --git a/src/app/forms-demo/forms-demo.component.html b/src/app/forms-demo/forms-demo.component.html
new file mode 100644
index 00000000..2fe10f6b
--- /dev/null
+++ b/src/app/forms-demo/forms-demo.component.html
@@ -0,0 +1,27 @@
+
+
Demonstrates ng-forms: createNonNullableControl (required, notOnlySpacesValidator), createNullableControl (optional nickname). Reset restores non-nullable to initial value and nullable to null.
+
+
diff --git a/src/app/forms-demo/forms-demo.component.scss b/src/app/forms-demo/forms-demo.component.scss
new file mode 100644
index 00000000..7f26ddcb
--- /dev/null
+++ b/src/app/forms-demo/forms-demo.component.scss
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/src/app/forms-demo/forms-demo.component.ts b/src/app/forms-demo/forms-demo.component.ts
new file mode 100644
index 00000000..9352f54d
--- /dev/null
+++ b/src/app/forms-demo/forms-demo.component.ts
@@ -0,0 +1,50 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core'
+import { FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'
+import { MatButtonModule } from '@angular/material/button'
+import { MatFormFieldModule } from '@angular/material/form-field'
+import { MatInputModule } from '@angular/material/input'
+import {
+ createNonNullableControl,
+ createNullableControl,
+ ValidationService
+} from '@ppwcode/ng-forms'
+
+@Component({
+ selector: 'ppw-forms-demo',
+ standalone: true,
+ imports: [
+ ReactiveFormsModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatButtonModule
+ ],
+ templateUrl: './forms-demo.component.html',
+ styleUrl: './forms-demo.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class FormsDemoComponent {
+ readonly form = new FormGroup({
+ name: createNonNullableControl('', [
+ Validators.required,
+ ValidationService.notOnlySpacesValidator
+ ]),
+ nickname: createNullableControl(null),
+ email: createNonNullableControl('', {
+ validators: [Validators.required, Validators.email]
+ })
+ })
+
+ submit(): void {
+ if (this.form.valid) {
+ console.log('Submitted:', this.form.getRawValue())
+ }
+ }
+
+ reset(): void {
+ this.form.reset({
+ name: '',
+ nickname: null,
+ email: ''
+ })
+ }
+}
diff --git a/src/app/local-storage-demo/local-storage-demo.component.html b/src/app/local-storage-demo/local-storage-demo.component.html
new file mode 100644
index 00000000..512866db
--- /dev/null
+++ b/src/app/local-storage-demo/local-storage-demo.component.html
@@ -0,0 +1,13 @@
+
+
Demonstrates LOCAL_STORAGE_TOKEN from ng-common: getItem, setItem, removeItem. Uses provideLocalStorage() from app config.
+
+ Value to store
+
+
+
+ Save to localStorage
+ Read from localStorage
+ Clear
+
+
Last read value: {{ lastRead() ?? '(empty)' }}
+
diff --git a/src/app/local-storage-demo/local-storage-demo.component.scss b/src/app/local-storage-demo/local-storage-demo.component.scss
new file mode 100644
index 00000000..7f26ddcb
--- /dev/null
+++ b/src/app/local-storage-demo/local-storage-demo.component.scss
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/src/app/local-storage-demo/local-storage-demo.component.ts b/src/app/local-storage-demo/local-storage-demo.component.ts
new file mode 100644
index 00000000..48b7b993
--- /dev/null
+++ b/src/app/local-storage-demo/local-storage-demo.component.ts
@@ -0,0 +1,44 @@
+import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'
+import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { MatButtonModule } from '@angular/material/button'
+import { MatFormFieldModule } from '@angular/material/form-field'
+import { MatInputModule } from '@angular/material/input'
+import { LOCAL_STORAGE_TOKEN } from '@ppwcode/ng-common'
+
+const DEMO_KEY = 'ppw-demo-storage-key'
+
+@Component({
+ selector: 'ppw-local-storage-demo',
+ standalone: true,
+ imports: [
+ MatButtonModule,
+ MatFormFieldModule,
+ MatInputModule,
+ ReactiveFormsModule,
+ FormsModule
+ ],
+ templateUrl: './local-storage-demo.component.html',
+ styleUrl: './local-storage-demo.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class LocalStorageDemoComponent {
+ private readonly storage = inject(LOCAL_STORAGE_TOKEN)
+
+ readonly value = new FormControl(this.storage.getItem(DEMO_KEY) ?? '', { nonNullable: true })
+ readonly lastRead = signal(this.storage.getItem(DEMO_KEY))
+
+ save(): void {
+ this.storage.setItem(DEMO_KEY, this.value.value)
+ this.lastRead.set(this.value.value)
+ }
+
+ clear(): void {
+ this.storage.removeItem(DEMO_KEY)
+ this.value.setValue('')
+ this.lastRead.set(null)
+ }
+
+ read(): void {
+ this.lastRead.set(this.storage.getItem(DEMO_KEY))
+ }
+}
diff --git a/src/app/route-map-demo/route-map-demo.component.html b/src/app/route-map-demo/route-map-demo.component.html
new file mode 100644
index 00000000..272dce71
--- /dev/null
+++ b/src/app/route-map-demo/route-map-demo.component.html
@@ -0,0 +1,8 @@
+
+
Type-safe routing with ng-router: getFullRoutePath(route) and ppwRouteMapRoute pipe for routerLink. Links below use the route map.
+
+
diff --git a/src/app/route-map-demo/route-map-demo.component.scss b/src/app/route-map-demo/route-map-demo.component.scss
new file mode 100644
index 00000000..3b16ab16
--- /dev/null
+++ b/src/app/route-map-demo/route-map-demo.component.scss
@@ -0,0 +1,8 @@
+:host {
+ display: block;
+}
+
+.route-links {
+ list-style: disc;
+ padding-left: 1.5rem;
+}
diff --git a/src/app/route-map-demo/route-map-demo.component.ts b/src/app/route-map-demo/route-map-demo.component.ts
new file mode 100644
index 00000000..714b9a73
--- /dev/null
+++ b/src/app/route-map-demo/route-map-demo.component.ts
@@ -0,0 +1,18 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core'
+import { RouterLink } from '@angular/router'
+import { getFullRoutePath } from '@ppwcode/ng-router'
+import { RouteMapRoutePipe } from '@ppwcode/ng-router'
+import { ROUTE_MAP } from '../app.routes'
+
+@Component({
+ selector: 'ppw-route-map-demo',
+ standalone: true,
+ imports: [RouterLink, RouteMapRoutePipe],
+ templateUrl: './route-map-demo.component.html',
+ styleUrl: './route-map-demo.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class RouteMapDemoComponent {
+ readonly ROUTE_MAP = ROUTE_MAP
+ readonly getFullRoutePath = getFullRoutePath
+}
diff --git a/src/app/signal-store-demo/demo-item.store.ts b/src/app/signal-store-demo/demo-item.store.ts
new file mode 100644
index 00000000..e26f25c5
--- /dev/null
+++ b/src/app/signal-store-demo/demo-item.store.ts
@@ -0,0 +1,50 @@
+import { Injectable, inject, computed } from '@angular/core'
+import { SignalStore } from '@ppwcode/ng-state-management'
+
+export interface DemoItem {
+ id: number
+ name: string
+}
+
+interface DemoItemState extends Record {
+ items: DemoItem[]
+ isLoading: boolean
+ searchTerm: string
+}
+
+@Injectable({ providedIn: 'root' })
+export class DemoItemStore extends SignalStore {
+ constructor() {
+ super()
+ this.initialize({
+ items: [],
+ isLoading: false,
+ searchTerm: ''
+ })
+ }
+
+ filteredItems = this.selectMany(['items', 'searchTerm'], ({ items, searchTerm }) =>
+ searchTerm
+ ? items.filter((item) =>
+ item.name.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+ : items
+ )
+
+ async loadItems(): Promise {
+ this.patch({ isLoading: true })
+ await new Promise((r) => setTimeout(r, 800))
+ this.patch({
+ items: [
+ { id: 1, name: 'Alpha' },
+ { id: 2, name: 'Beta' },
+ { id: 3, name: 'Gamma' }
+ ],
+ isLoading: false
+ })
+ }
+
+ setSearchTerm(searchTerm: string): void {
+ this.patch({ searchTerm })
+ }
+}
diff --git a/src/app/signal-store-demo/signal-store-demo.component.html b/src/app/signal-store-demo/signal-store-demo.component.html
new file mode 100644
index 00000000..f88e430e
--- /dev/null
+++ b/src/app/signal-store-demo/signal-store-demo.component.html
@@ -0,0 +1,15 @@
+
+
SignalStore from ng-state-management: initialize, patch, select, selectMany. Load triggers async patch; filter uses selectMany.
+
Load items
+
+ Filter
+
+
+
+
+ @for (item of store.filteredItems(); track item.id) {
+ {{ item.name }}
+ }
+
+
+
diff --git a/src/app/signal-store-demo/signal-store-demo.component.scss b/src/app/signal-store-demo/signal-store-demo.component.scss
new file mode 100644
index 00000000..7f26ddcb
--- /dev/null
+++ b/src/app/signal-store-demo/signal-store-demo.component.scss
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/src/app/signal-store-demo/signal-store-demo.component.ts b/src/app/signal-store-demo/signal-store-demo.component.ts
new file mode 100644
index 00000000..d512f5b0
--- /dev/null
+++ b/src/app/signal-store-demo/signal-store-demo.component.ts
@@ -0,0 +1,22 @@
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
+import { MatButtonModule } from '@angular/material/button'
+import { MatFormFieldModule } from '@angular/material/form-field'
+import { MatInputModule } from '@angular/material/input'
+import { LoaderComponent } from '@ppwcode/ng-common-components'
+import { DemoItemStore } from './demo-item.store'
+
+@Component({
+ selector: 'ppw-signal-store-demo',
+ standalone: true,
+ imports: [MatButtonModule, MatFormFieldModule, MatInputModule, LoaderComponent],
+ templateUrl: './signal-store-demo.component.html',
+ styleUrl: './signal-store-demo.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class SignalStoreDemoComponent {
+ readonly store = inject(DemoItemStore)
+
+ onSearchTermChange(value: string): void {
+ this.store.setSearchTerm(value)
+ }
+}
diff --git a/src/app/subscription-handling-demo/subscription-handling-demo.component.html b/src/app/subscription-handling-demo/subscription-handling-demo.component.html
new file mode 100644
index 00000000..a94d6c7f
--- /dev/null
+++ b/src/app/subscription-handling-demo/subscription-handling-demo.component.html
@@ -0,0 +1,4 @@
+
+
This component extends mixinHandleSubscriptions() and uses stopOnDestroy(interval(1000)) so the interval is automatically unsubscribed when the component is destroyed. Navigate away and back to see the counter reset.
+
Counter (ticks every second): {{ counter() }}
+
diff --git a/src/app/subscription-handling-demo/subscription-handling-demo.component.scss b/src/app/subscription-handling-demo/subscription-handling-demo.component.scss
new file mode 100644
index 00000000..7f26ddcb
--- /dev/null
+++ b/src/app/subscription-handling-demo/subscription-handling-demo.component.scss
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/src/app/subscription-handling-demo/subscription-handling-demo.component.ts b/src/app/subscription-handling-demo/subscription-handling-demo.component.ts
new file mode 100644
index 00000000..649291d7
--- /dev/null
+++ b/src/app/subscription-handling-demo/subscription-handling-demo.component.ts
@@ -0,0 +1,25 @@
+import { ChangeDetectionStrategy, Component, signal } from '@angular/core'
+import { MatButtonModule } from '@angular/material/button'
+import { mixinHandleSubscriptions } from '@ppwcode/ng-common'
+import { interval } from 'rxjs'
+
+const SubscriptionHandlerBase = mixinHandleSubscriptions()
+
+@Component({
+ selector: 'ppw-subscription-handling-demo',
+ standalone: true,
+ imports: [MatButtonModule],
+ templateUrl: './subscription-handling-demo.component.html',
+ styleUrl: './subscription-handling-demo.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class SubscriptionHandlingDemoComponent extends SubscriptionHandlerBase {
+ readonly counter = signal(0)
+
+ constructor() {
+ super()
+ this.stopOnDestroy(interval(1000)).subscribe((value) => {
+ this.counter.set(value)
+ })
+ }
+}
diff --git a/src/app/utils-demo/utils-demo.component.html b/src/app/utils-demo/utils-demo.component.html
new file mode 100644
index 00000000..9bcada59
--- /dev/null
+++ b/src/app/utils-demo/utils-demo.component.html
@@ -0,0 +1,36 @@
+
+
Assertion utilities from ng-utils: natural, noDuplicates, notUndefined, notNull.
+
+
natural(n?)
+
Run natural() examples
+ @if (naturalResults(); as r) {
+
+ @for (entry of r | keyvalue; track entry.key) {
+ {{ entry.key }} = {{ entry.value }}
+ }
+
+ }
+
+
+
noDuplicates(arr)
+
Run noDuplicates() examples
+ @if (noDuplicatesResults(); as r) {
+
+ @for (entry of r | keyvalue; track entry.key) {
+ {{ entry.key }} = {{ entry.value }}
+ }
+
+ }
+
+
+
notUndefined / notNull
+
Run notUndefined
+
Run notNull
+ @if (notUndefinedResult()) {
+
{{ notUndefinedResult() }}
+ }
+ @if (notNullResult()) {
+
{{ notNullResult() }}
+ }
+
+
diff --git a/src/app/utils-demo/utils-demo.component.scss b/src/app/utils-demo/utils-demo.component.scss
new file mode 100644
index 00000000..7f26ddcb
--- /dev/null
+++ b/src/app/utils-demo/utils-demo.component.scss
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/src/app/utils-demo/utils-demo.component.ts b/src/app/utils-demo/utils-demo.component.ts
new file mode 100644
index 00000000..ca9e960c
--- /dev/null
+++ b/src/app/utils-demo/utils-demo.component.ts
@@ -0,0 +1,57 @@
+import { KeyValuePipe } from '@angular/common'
+import { ChangeDetectionStrategy, Component, signal } from '@angular/core'
+import { MatButtonModule } from '@angular/material/button'
+import { natural, noDuplicates, notNull, notUndefined } from '@ppwcode/ng-utils'
+
+@Component({
+ selector: 'ppw-utils-demo',
+ standalone: true,
+ imports: [MatButtonModule, KeyValuePipe],
+ templateUrl: './utils-demo.component.html',
+ styleUrl: './utils-demo.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class UtilsDemoComponent {
+ readonly naturalResults = signal>({})
+ readonly noDuplicatesResults = signal>({})
+ readonly notUndefinedResult = signal('')
+ readonly notNullResult = signal('')
+
+ runNatural(): void {
+ this.naturalResults.set({
+ 'natural(0)': natural(0),
+ 'natural(5)': natural(5),
+ 'natural(-1)': natural(-1),
+ 'natural(1.5)': natural(1.5),
+ 'natural(undefined)': natural(undefined)
+ })
+ }
+
+ runNoDuplicates(): void {
+ this.noDuplicatesResults.set({
+ 'noDuplicates([1,2,3])': noDuplicates([1, 2, 3]),
+ 'noDuplicates([1,2,2,3])': noDuplicates([1, 2, 2, 3]),
+ "noDuplicates(['a','b'])": noDuplicates(['a', 'b'])
+ })
+ }
+
+ runNotUndefined(): void {
+ const val: string | undefined = 'hello'
+ try {
+ const result = notUndefined(val)
+ this.notUndefinedResult.set(`notUndefined('hello') = ${result}`)
+ } catch (e) {
+ this.notUndefinedResult.set(`Threw: ${(e as Error).message}`)
+ }
+ }
+
+ runNotNull(): void {
+ const val: string | null = 'world'
+ try {
+ const result = notNull(val)
+ this.notNullResult.set(`notNull('world') = ${result}`)
+ } catch (e) {
+ this.notNullResult.set(`Threw: ${(e as Error).message}`)
+ }
+ }
+}
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 216c46f4..69517532 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -12,7 +12,16 @@
"confirmation-dialog-description": "A demo page showcasing the confirm dialog component",
"expandable-card-description": "A demo page that shows all possibilities with the expandable card component",
"global-error-handler-description": "A demo page that shows all possibilities with the global error handler",
- "in-memory-logging-description": "A demo page that shows all possibilities with the in-memory logging provider"
+ "in-memory-logging-description": "A demo page that shows all possibilities with the in-memory logging provider",
+ "async-result-description": "AsyncResult states and factories from ng-async",
+ "file-download-description": "File download helpers saveDownloadedFile and saveFileDownload",
+ "draggable-dialog-description": "Draggable Material dialog with ppwDraggableDialog",
+ "forms-demo-description": "Form control generators createNonNullableControl and createNullableControl",
+ "route-map-description": "Type-safe routing with getRouteUrl and RouteMapRoutePipe",
+ "signal-store-description": "Signal-based state store with select and selectMany",
+ "utils-demo-description": "Assertion utilities notUndefined, notNull, natural, noDuplicates",
+ "subscription-handling-description": "mixinHandleSubscriptions and stopOnDestroy",
+ "local-storage-description": "LOCAL_STORAGE_TOKEN getItem/setItem/removeItem"
},
"global-error-dialog": {
"copy-all-errors": "Copy All Errors",
@@ -35,7 +44,16 @@
"global_error_handler": "Global Error Handler",
"in_memory_logging": "In Memory Logging",
"message_bar": "Message Bar",
- "peopleware_website": "PeopleWare Website"
+ "peopleware_website": "PeopleWare Website",
+ "async_result": "Async Result",
+ "file_download": "File Download",
+ "draggable_dialog": "Draggable Dialog",
+ "forms_demo": "Form Controls",
+ "route_map": "Route Map",
+ "signal_store": "Signal Store",
+ "utils_demo": "Assertion Utils",
+ "subscription_handling": "Subscription Handling",
+ "local_storage": "Local Storage"
},
"table": {
"age": "Age",
diff --git a/src/assets/i18n/nl.json b/src/assets/i18n/nl.json
index cad54242..d4b3a9e7 100644
--- a/src/assets/i18n/nl.json
+++ b/src/assets/i18n/nl.json
@@ -12,7 +12,16 @@
"confirmation-dialog-description": "Een demo pagina die de confirmation dialog component laat zien",
"expandable-card-description": "Een demo pagina die alle mogelijkheden met de expandable card component laat zien",
"global-error-handler-description": "Een demo pagina die alle mogelijkheden van de global error handler laat zien",
- "in-memory-logging-description": "Een demo pagina die alle mogelijkheden van de in-memory logging provider laat zien"
+ "in-memory-logging-description": "Een demo pagina die alle mogelijkheden van de in-memory logging provider laat zien",
+ "async-result-description": "AsyncResult staten en factories van ng-async",
+ "file-download-description": "File download helpers saveDownloadedFile en saveFileDownload",
+ "draggable-dialog-description": "Versleepbare Material dialog met ppwDraggableDialog",
+ "forms-demo-description": "Form control generators createNonNullableControl en createNullableControl",
+ "route-map-description": "Type-veilige routing met getRouteUrl en RouteMapRoutePipe",
+ "signal-store-description": "Signal-based state store met select en selectMany",
+ "utils-demo-description": "Assertion utilities notUndefined, notNull, natural, noDuplicates",
+ "subscription-handling-description": "mixinHandleSubscriptions en stopOnDestroy",
+ "local-storage-description": "LOCAL_STORAGE_TOKEN getItem/setItem/removeItem"
},
"global-error-dialog": {
"copy-all-errors": "Kopieer alle fouten",
@@ -35,7 +44,16 @@
"global_error_handler": "Global Error Handler",
"in_memory_logging": "In Memory Logging",
"message_bar": "Message Bar",
- "peopleware_website": "PeopleWare Website"
+ "peopleware_website": "PeopleWare Website",
+ "async_result": "Async Result",
+ "file_download": "File Download",
+ "draggable_dialog": "Draggable Dialog",
+ "forms_demo": "Form Controls",
+ "route_map": "Route Map",
+ "signal_store": "Signal Store",
+ "utils_demo": "Assertion Utils",
+ "subscription_handling": "Subscription Handling",
+ "local_storage": "Local Storage"
},
"table": {
"age": "Leeftijd",
diff --git a/src/main.ts b/src/main.ts
index f22c8fad..86e359d0 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -13,7 +13,7 @@ import {
PpwAsyncResultDefaultOptions,
ppwHttpErrorExtractorWithTranslatedMessages
} from '@ppwcode/ng-async'
-import { provideGlobalErrorHandler } from '@ppwcode/ng-common'
+import { provideGlobalErrorHandler, provideLocalStorage, provideLogger } from '@ppwcode/ng-common'
import { PPW_TABLE_DEFAULT_OPTIONS } from '@ppwcode/ng-common-components'
import { provideBreadcrumbOptions, providePaginationOptions, TranslatedPageTitleStrategy } from '@ppwcode/ng-router'
import { AppComponent } from './app/app.component'
@@ -51,6 +51,8 @@ bootstrapApplication(AppComponent, {
useValue: { emptyResultComponent: EmptyAsyncResultComponent } as PpwAsyncResultDefaultOptions
},
provideRouter(routes, withViewTransitions()),
+ provideLogger({ prefix: '[PPW Demo]', debug: true }),
+ provideLocalStorage(),
provideGlobalErrorHandler({
errorDialogOptions: {
allowIgnore: true,