@dsivd/prestations-ng 19.0.5 → 19.0.6-beta.2

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.
@@ -47,11 +47,16 @@ increment(): void {
47
47
  }
48
48
  ```
49
49
 
50
- | Method | Description |
51
- | ------------- | --------------------------------- |
52
- | `.set(value)` | Replace the value |
53
- | `.update(fn)` | Derive next value from current |
54
- | `.mutate(fn)` | Mutate in place (arrays, objects) |
50
+ | Method | Description |
51
+ | ------------- | ------------------------------ |
52
+ | `.set(value)` | Replace the value |
53
+ | `.update(fn)` | Derive next value from current |
54
+
55
+ > ⚠️ `.mutate()` is **not** part of the public API. It was part of the
56
+ > original signals proposal but was removed from `WritableSignal` before
57
+ > the stable release — mutating an object without a `.set()`/`.update()`
58
+ > call is invisible to the signal graph. Always produce a new reference
59
+ > via `.update()` (see [pitfall #8](angular-signals#common_pitfalls)).
55
60
 
56
61
  ---
57
62
 
@@ -302,8 +307,10 @@ readonly selectedOption = linkedSignal<string[], string>({
302
307
  });
303
308
  ```
304
309
 
305
- `previous` contains `{ value: T }` — the last value of the linked signal
306
- before the source changed, or `undefined` on first run.
310
+ `previous` contains `{ source: S; value: D }` — the previous value of the
311
+ `source` signal **and** the previous value of the linked signal itself, or
312
+ `undefined` on first run. Explicit generic type arguments (`<S, D>`) are
313
+ required as soon as you use `previous`.
307
314
 
308
315
  ---
309
316
 
@@ -393,12 +400,15 @@ Usage in a parent template:
393
400
  <!-- two-way -->
394
401
  <my-component [(isVisible)]="open" />
395
402
 
396
- <!-- one-way — will cause a compiler error with model() -->
403
+ <!-- one-way binding compiles fine, but changes made by the -->
404
+ <!-- component are not propagated back to `open` -->
397
405
  <my-component [isVisible]="open" />
398
406
  ```
399
407
 
400
- > ⚠️ `model()` does not accept one-way `[binding]` alone.
401
- > Use `input()` if the child never needs to write back to the parent.
408
+ > ⚠️ A one-way `[binding]` on a `model()` input is valid and compiles —
409
+ > it's up to the consumer to decide whether they use the input, the
410
+ > output, or both. It's just pointless if the component never needs to
411
+ > write back: use `input()` instead to communicate intent clearly.
402
412
 
403
413
  > ⚠️ `model()` holds object values by reference. Mutating a nested property
404
414
  > of a `model()`'s value (e.g. `this.address().locality.name = 'x'`) does
@@ -828,15 +838,24 @@ effect(() => {
828
838
  });
829
839
  ```
830
840
 
831
- ### 8. Mutating a `model()`'s nested object instead of reassigning
841
+ ### 8. Mutating a signal's value instead of reassigning
832
842
 
833
843
  This is the signals-era version of "mutating `@Input()` used to work."
834
844
  Pre-signals, Zone.js-driven change detection re-read template expressions
835
- on virtually every async event, so mutating a nested property and letting
836
- CD "catch it eventually" appeared to work. Signals only propagate on
837
- `.set()`/`.update()` — mutating `this.address().locality.name = 'x'` is
838
- invisible to the signal, the template, and any `effect()`/`computed()`
839
- depending on it.
845
+ on virtually every async event, so mutating a nested property or an array
846
+ in place and letting CD "catch it eventually" appeared to work. Signals
847
+ only notify subscribers on `.set()`/`.update()` — any in-place mutation of
848
+ the value returned by `signal()`/`model()`/`input()` is invisible to the
849
+ signal, the template, and any `effect()`/`computed()` depending on it,
850
+ because the object/array reference never changes and signals compare by
851
+ reference by default.
852
+
853
+ The fix is always the same shape: read the current value, produce a
854
+ **new top-level reference** with the change applied, and pass that to
855
+ `.set()` or `.update()`. Never call a mutating method or assign a nested
856
+ property directly on `signalOrModel()`.
857
+
858
+ #### 8a. Nested object property
840
859
 
841
860
  ```ts
842
861
  // 🔴 Silent no-op from the signal's perspective
@@ -848,3 +867,142 @@ this.address.update((current) => ({
848
867
  locality: { ...current.locality, name: 'newName' },
849
868
  }));
850
869
  ```
870
+
871
+ #### 8b. Two-way binding directly on an array element inside `@for`
872
+
873
+ Binding `[(model)]` straight to an indexed read of a signal
874
+ (`model()[i]`) is a variant of the same mistake: the "write" side of the
875
+ banana-in-a-box syntax tries to assign into `model()[i]`, which mutates
876
+ the array in place instead of going through `.set()`/`.update()`, so
877
+ nothing propagates and Angular may also warn about an unsupported
878
+ two-way expression.
879
+
880
+ ```html
881
+ <!-- 🔴 Tries to write through model()[i] — mutates in place, breaks reactivity -->
882
+ @for (item of model(); track item.id; let i = $index) {
883
+ <app-address-row [(model)]="model()[i]" />
884
+ }
885
+
886
+ <!-- 🟢 One-way in, explicit handler out -->
887
+ @for (item of model(); track item.id; let i = $index) {
888
+ <app-address-row [model]="model()[i]" (modelChange)="updateItem(i, $event)" />
889
+ }
890
+ ```
891
+
892
+ ```ts
893
+ updateItem(index: number, newValue: NomAdresse): void {
894
+ this.model.update((current) =>
895
+ current.map((item, i) => (i === index ? newValue : item)),
896
+ );
897
+ }
898
+ ```
899
+
900
+ #### 8c. Array mutating methods (`push`, `splice`, `pop`, `shift`, `unshift`, `sort`, `reverse`)
901
+
902
+ All of these mutate the array in place and return either the mutated
903
+ array, the removed element(s), or nothing — never a new reference. Since
904
+ they operate on `this.model()` (the _value_, not the signal), Angular has
905
+ no way to know the array changed.
906
+
907
+ ```ts
908
+ // 🔴 Adds an item, but the signal reference is untouched — no re-render
909
+ this.model().push({ name: null, address: null });
910
+
911
+ // 🟢 New array reference via spread
912
+ this.model.update((current) => [...current, { name: null, address: null }]);
913
+ ```
914
+
915
+ ```ts
916
+ // 🔴 Removes an item in place — invisible to the signal
917
+ this.model().splice(index, 1);
918
+
919
+ // 🟢 filter() naturally returns a new array
920
+ this.model.update((current) => current.filter((_, i) => i !== index));
921
+ ```
922
+
923
+ ```ts
924
+ // 🔴 Same problem with pop() / shift() / unshift()
925
+ this.model().pop();
926
+ this.model().shift();
927
+ this.model().unshift(newItem);
928
+
929
+ // 🟢 Equivalent immutable forms
930
+ this.model.update((current) => current.slice(0, -1)); // pop
931
+ this.model.update((current) => current.slice(1)); // shift
932
+ this.model.update((current) => [newItem, ...current]); // unshift
933
+ ```
934
+
935
+ ```ts
936
+ // 🔴 sort() / reverse() mutate AND return the same array reference —
937
+ // even risky to combine with spread if you forget it: [...current].sort()
938
+ // mutates the *new* array in place, which is fine, but sorting
939
+ // `this.model()` directly still mutates the one held by the signal
940
+ this.model().sort((a, b) => a.name.localeCompare(b.name));
941
+
942
+ // 🟢 Copy first, then sort/reverse the copy
943
+ this.model.update((current) =>
944
+ [...current].sort((a, b) => a.name.localeCompare(b.name)),
945
+ );
946
+ ```
947
+
948
+ #### 8d. Direct index assignment
949
+
950
+ ```ts
951
+ // 🔴 Mutates the array element in place
952
+ this.model()[index] = newValue;
953
+
954
+ // 🟢 map() to swap out just that index
955
+ this.model.update((current) =>
956
+ current.map((item, i) => (i === index ? newValue : item)),
957
+ );
958
+ ```
959
+
960
+ #### 8e. `Object.assign()` on the current value
961
+
962
+ `Object.assign(target, ...)` mutates and returns `target`. Passing
963
+ `this.model()` as the target mutates the value in place, even though the
964
+ call "looks" like it's producing something.
965
+
966
+ ```ts
967
+ // 🔴 target is this.model() itself — mutated in place
968
+ Object.assign(this.model(), { name: 'newName' });
969
+
970
+ // 🟢 target is a fresh object — this is what Object.assign() is for
971
+ this.model.update((current) => Object.assign({}, current, { name: 'newName' }));
972
+ // or, more idiomatically:
973
+ this.model.update((current) => ({ ...current, name: 'newName' }));
974
+ ```
975
+
976
+ #### 8f. `Map` / `Set` values
977
+
978
+ `Map` and `Set` are reference types too, and their mutating methods
979
+ (`.set()` on a `Map`, `.add()`/`.delete()` on a `Set`) don't return a new
980
+ `Map`/`Set` — they mutate and return `this`, which is easy to miss since
981
+ `Map.prototype.set` shares a name with `WritableSignal.set`.
982
+
983
+ ```ts
984
+ // 🔴 This is Map.prototype.set, not the signal's — mutates the same Map instance
985
+ this.cache().set(key, value);
986
+
987
+ // 🟢 Build a new Map from the old one
988
+ this.cache.update((current) => new Map(current).set(key, value));
989
+ ```
990
+
991
+ ```ts
992
+ // 🔴 Set.prototype.delete mutates in place
993
+ this.selectedIds().delete(id);
994
+
995
+ // 🟢 Copy into a new Set, then mutate the copy
996
+ this.selectedIds.update((current) => {
997
+ const next = new Set(current);
998
+ next.delete(id);
999
+ return next;
1000
+ });
1001
+ ```
1002
+
1003
+ > 💡 General rule of thumb: if a method's name suggests it changes
1004
+ > something "in place" and doesn't hand you back a brand-new
1005
+ > object/array/Map/Set, assume it mutates and wrap it in a copy first.
1006
+ > When unsure, check the return type — a genuinely immutable operation
1007
+ > (`map`, `filter`, `concat`, `slice`, spread) always returns a fresh
1008
+ > reference you can pass straight to `.set()`/`.update()`.
package/UPGRADING_V19.md CHANGED
@@ -1144,6 +1144,9 @@ export default tseslint.config(
1144
1144
  style: 'camelCase',
1145
1145
  },
1146
1146
  ],
1147
+ '@angular-eslint/no-uncalled-signals': ['error'],
1148
+ '@angular-eslint/prefer-signal-model': ['error'],
1149
+ '@angular-eslint/prefer-signals': ['error'],
1147
1150
 
1148
1151
  // ── @typescript-eslint ───────────────────────────────────────────
1149
1152
  '@typescript-eslint/explicit-function-return-type': [
@@ -42,6 +42,10 @@ describe('no-direct-signal-mutation', () => {
42
42
  'let x = value',
43
43
  'const obj = { prop: value }',
44
44
  'array[0] = value',
45
+ 'pendingFilesByFormKey.get(formKey).files = []',
46
+ 'this.pendingFilesByFormKey.get(formKey).url = baseUrl',
47
+ 'const autocompleteComponent = viewChild("auto"); autocompleteComponent().inputElement().nativeElement.hidden = true',
48
+ 'component.inputElement().nativeElement.value = ""',
45
49
 
46
50
  // Method chaining without assignment
47
51
  'model().update((v) => v).subscribe()',
@@ -72,10 +76,6 @@ describe('no-direct-signal-mutation', () => {
72
76
  code: "this.model().deeply.nested.property = value",
73
77
  errors: [{ messageId: 'directMutation' }],
74
78
  },
75
- {
76
- code: "obj.getter().property = value",
77
- errors: [{ messageId: 'directMutation' }],
78
- },
79
79
  {
80
80
  code: "model().property += value",
81
81
  errors: [{ messageId: 'directMutation' }],
@@ -19,13 +19,23 @@ export default {
19
19
  },
20
20
 
21
21
  create(context) {
22
+ const sourceCode = context.sourceCode;
23
+ const viewQueryFactoryNames = new Set([
24
+ "viewChild",
25
+ "viewChildren",
26
+ "contentChild",
27
+ "contentChildren",
28
+ ]);
29
+ const viewQueryAccessorNames = collectViewQueryAccessorNames(sourceCode.ast);
30
+
22
31
  return {
23
32
  AssignmentExpression(node) {
24
- // Check if left side is a member expression with a call expression
25
- if (
26
- node.left.type === "MemberExpression" &&
27
- isCallExpression(node.left.object)
28
- ) {
33
+ if (node.left.type !== "MemberExpression") {
34
+ return;
35
+ }
36
+
37
+ const rootCallExpression = getFirstCallExpression(node.left.object);
38
+ if (rootCallExpression && isSignalLikeRootCall(rootCallExpression)) {
29
39
  context.report({
30
40
  node,
31
41
  messageId: "directMutation",
@@ -34,19 +44,132 @@ export default {
34
44
  },
35
45
  };
36
46
 
47
+ function isSignalLikeRootCall(node) {
48
+ if (node.arguments.length !== 0) {
49
+ return false;
50
+ }
51
+
52
+ // model().prop = ... (local signal-like accessor)
53
+ if (node.callee.type === "Identifier") {
54
+ return !viewQueryAccessorNames.has(node.callee.name);
55
+ }
56
+
57
+ // this.model().prop = ... (component/service signal accessor)
58
+ // component.model().prop = ... should not be assumed to be a signal read.
59
+ if (node.callee.type === "MemberExpression") {
60
+ if (node.callee.object?.type !== "ThisExpression") {
61
+ return false;
62
+ }
63
+ return !isViewQueryAccessorCall(node.callee);
64
+ }
65
+
66
+ return false;
67
+ }
68
+
69
+ function isViewQueryAccessorCall(callee) {
70
+ const calledName = getCalledName(callee);
71
+ return calledName ? viewQueryAccessorNames.has(calledName) : false;
72
+ }
73
+
74
+ function getCalledName(callee) {
75
+ if (!callee) {
76
+ return null;
77
+ }
78
+ if (callee.type === "Identifier") {
79
+ return callee.name;
80
+ }
81
+ if (
82
+ callee.type === "MemberExpression" &&
83
+ !callee.computed &&
84
+ callee.property.type === "Identifier"
85
+ ) {
86
+ return callee.property.name;
87
+ }
88
+ return null;
89
+ }
90
+
37
91
  /**
38
- * Recursively check if a node is a call expression.
39
- * Handles: func().prop, obj.func().prop, this.func().prop, etc.
92
+ * Finds the first/root function call used as the base of a mutation chain.
93
+ * Examples:
94
+ * - model().property = x => model()
95
+ * - this.model().deep.prop = x => this.model()
96
+ * - this.query().el().x = y => this.query()
40
97
  */
41
- function isCallExpression(node) {
42
- if (!node) return false;
43
- if (node.type === "CallExpression") {
44
- return true;
98
+ function getFirstCallExpression(node) {
99
+ if (!node) {
100
+ return null;
45
101
  }
46
102
  if (node.type === "MemberExpression") {
47
- return isCallExpression(node.object);
103
+ return getFirstCallExpression(node.object);
104
+ }
105
+ if (node.type === "CallExpression") {
106
+ if (node.callee.type === "MemberExpression") {
107
+ const nestedCall = getFirstCallExpression(node.callee.object);
108
+ return nestedCall || node;
109
+ }
110
+ return node;
111
+ }
112
+ return null;
113
+ }
114
+
115
+ function collectViewQueryAccessorNames(ast) {
116
+ const names = new Set();
117
+
118
+ walkAst(ast, (node) => {
119
+ if (
120
+ node.type === "VariableDeclarator" &&
121
+ node.id?.type === "Identifier" &&
122
+ isViewQueryFactoryCall(node.init)
123
+ ) {
124
+ names.add(node.id.name);
125
+ return;
126
+ }
127
+
128
+ if (
129
+ (node.type === "PropertyDefinition" || node.type === "ClassProperty") &&
130
+ node.key?.type === "Identifier" &&
131
+ isViewQueryFactoryCall(node.value)
132
+ ) {
133
+ names.add(node.key.name);
134
+ }
135
+ });
136
+
137
+ return names;
138
+ }
139
+
140
+ function isViewQueryFactoryCall(node) {
141
+ return (
142
+ node?.type === "CallExpression" &&
143
+ node.callee?.type === "Identifier" &&
144
+ viewQueryFactoryNames.has(node.callee.name)
145
+ );
146
+ }
147
+
148
+ function walkAst(node, visitor, visited = new WeakSet()) {
149
+ if (!node || typeof node !== "object") {
150
+ return;
151
+ }
152
+ if (visited.has(node)) {
153
+ return;
154
+ }
155
+ visited.add(node);
156
+
157
+ visitor(node);
158
+
159
+ for (const key of Object.keys(node)) {
160
+ // ESLint AST nodes include cyclic parent references.
161
+ if (key === "parent") {
162
+ continue;
163
+ }
164
+ const child = node[key];
165
+ if (Array.isArray(child)) {
166
+ for (const element of child) {
167
+ walkAst(element, visitor, visited);
168
+ }
169
+ } else {
170
+ walkAst(child, visitor, visited);
171
+ }
48
172
  }
49
- return false;
50
173
  }
51
174
  },
52
175
  };
@@ -771,6 +771,8 @@ class FoehnInputComponent {
771
771
  this.blurHandler = output({ alias: 'blur' });
772
772
  // eslint-disable-next-line @angular-eslint/no-output-native,@angular-eslint/no-output-rename
773
773
  this.focusHandler = output({ alias: 'focus' });
774
+ // not readonly, to be redefined in subcomponents
775
+ // eslint-disable-next-line @angular-eslint/prefer-signals
774
776
  this.displayClearButton = computed(() => this.clearButton() && !!this.model() && !this.disabled(), ...(ngDevMode ? [{ debugName: "displayClearButton" }] : /* istanbul ignore next */ []));
775
777
  this.validationHandlerService = inject(ValidationHandlerService);
776
778
  /**
@@ -6777,7 +6779,7 @@ class FoehnTableComponent extends FoehnInputComponent {
6777
6779
  this.columnsConfiguration = model(...(ngDevMode ? [undefined, { debugName: "columnsConfiguration" }] : /* istanbul ignore next */ []));
6778
6780
  this.itemsPerPage = input(10, ...(ngDevMode ? [{ debugName: "itemsPerPage" }] : /* istanbul ignore next */ []));
6779
6781
  this.fixedPageCount = input(...(ngDevMode ? [undefined, { debugName: "fixedPageCount" }] : /* istanbul ignore next */ []));
6780
- this.sort = input(...(ngDevMode ? [undefined, { debugName: "sort" }] : /* istanbul ignore next */ []));
6782
+ this.sort = model(...(ngDevMode ? [undefined, { debugName: "sort" }] : /* istanbul ignore next */ []));
6781
6783
  this.title = input(...(ngDevMode ? [undefined, { debugName: "title" }] : /* istanbul ignore next */ []));
6782
6784
  this.totalElements = input(...(ngDevMode ? [undefined, { debugName: "totalElements" }] : /* istanbul ignore next */ []));
6783
6785
  this.titleSrOnly = input(...(ngDevMode ? [undefined, { debugName: "titleSrOnly" }] : /* istanbul ignore next */ []));
@@ -6785,7 +6787,6 @@ class FoehnTableComponent extends FoehnInputComponent {
6785
6787
  this.nextLabel = input('Suivant', ...(ngDevMode ? [{ debugName: "nextLabel" }] : /* istanbul ignore next */ []));
6786
6788
  this.tableClass = input('', ...(ngDevMode ? [{ debugName: "tableClass" }] : /* istanbul ignore next */ []));
6787
6789
  this.trackByFn = input((index, _item) => index, ...(ngDevMode ? [{ debugName: "trackByFn" }] : /* istanbul ignore next */ []));
6788
- this.sortChange = output();
6789
6790
  this.pageChange = output();
6790
6791
  this.rowClick = output();
6791
6792
  this.currentPage = 1;
@@ -6863,7 +6864,7 @@ class FoehnTableComponent extends FoehnInputComponent {
6863
6864
  sortDirection =
6864
6865
  this.sort().sortDirection === 'DESC' ? 'ASC' : 'DESC';
6865
6866
  }
6866
- this.sortChange.emit({
6867
+ this.sort.set({
6867
6868
  sortDirection,
6868
6869
  sortAttribute,
6869
6870
  });
@@ -6891,7 +6892,7 @@ class FoehnTableComponent extends FoehnInputComponent {
6891
6892
  this.filteredList = this._list.slice(start, start + this.itemsPerPage());
6892
6893
  }
6893
6894
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FoehnTableComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
6894
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FoehnTableComponent, isStandalone: true, selector: "foehn-table", inputs: { columnsConfiguration: { classPropertyName: "columnsConfiguration", publicName: "columnsConfiguration", isSignal: true, isRequired: false, transformFunction: null }, itemsPerPage: { classPropertyName: "itemsPerPage", publicName: "itemsPerPage", isSignal: true, isRequired: false, transformFunction: null }, fixedPageCount: { classPropertyName: "fixedPageCount", publicName: "fixedPageCount", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, totalElements: { classPropertyName: "totalElements", publicName: "totalElements", isSignal: true, isRequired: false, transformFunction: null }, titleSrOnly: { classPropertyName: "titleSrOnly", publicName: "titleSrOnly", isSignal: true, isRequired: false, transformFunction: null }, previousLabel: { classPropertyName: "previousLabel", publicName: "previousLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, tableClass: { classPropertyName: "tableClass", publicName: "tableClass", isSignal: true, isRequired: false, transformFunction: null }, trackByFn: { classPropertyName: "trackByFn", publicName: "trackByFn", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { columnsConfiguration: "columnsConfigurationChange", sortChange: "sortChange", pageChange: "pageChange", rowClick: "rowClick" }, providers: [
6895
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FoehnTableComponent, isStandalone: true, selector: "foehn-table", inputs: { columnsConfiguration: { classPropertyName: "columnsConfiguration", publicName: "columnsConfiguration", isSignal: true, isRequired: false, transformFunction: null }, itemsPerPage: { classPropertyName: "itemsPerPage", publicName: "itemsPerPage", isSignal: true, isRequired: false, transformFunction: null }, fixedPageCount: { classPropertyName: "fixedPageCount", publicName: "fixedPageCount", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, totalElements: { classPropertyName: "totalElements", publicName: "totalElements", isSignal: true, isRequired: false, transformFunction: null }, titleSrOnly: { classPropertyName: "titleSrOnly", publicName: "titleSrOnly", isSignal: true, isRequired: false, transformFunction: null }, previousLabel: { classPropertyName: "previousLabel", publicName: "previousLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, tableClass: { classPropertyName: "tableClass", publicName: "tableClass", isSignal: true, isRequired: false, transformFunction: null }, trackByFn: { classPropertyName: "trackByFn", publicName: "trackByFn", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { columnsConfiguration: "columnsConfigurationChange", sort: "sortChange", pageChange: "pageChange", rowClick: "rowClick" }, providers: [
6895
6896
  {
6896
6897
  provide: FoehnInputComponent,
6897
6898
  useExisting: forwardRef(() => FoehnTableComponent),
@@ -6916,7 +6917,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
6916
6917
  FoehnIconChevronRightComponent,
6917
6918
  SdkDictionaryPipe,
6918
6919
  ], template: "<div\n class=\"form-group\"\n [class.has-danger]=\"hasErrors()\"\n [class.vd-form-group-danger]=\"hasErrors()\"\n [attr.id]=\"buildId('Container')\"\n tabindex=\"-1\"\n>\n @if (label() && type !== 'hidden') {\n <p\n [attr.id]=\"buildChildId() + 'Label'\"\n [class.visually-hidden]=\"isLabelSrOnly()\"\n [class.vd-p]=\"!isLabelSrOnly()\"\n >\n <span [innerHTML]=\"label()\"></span>\n @if (!required() && !hideNotRequiredExtraLabel()) {\n <span aria-hidden=\"true\">\n {{ 'foehn-input.optional' | fromDictionary }}\n </span>\n }\n </p>\n }\n\n <foehn-validation-alerts [component]=\"this\" />\n\n @if (helpText() && type !== 'hidden') {\n <small\n [attr.id]=\"buildChildId() + 'Help'\"\n class=\"form-text text-secondary\"\n [innerHTML]=\"helpText()\"\n ></small>\n }\n\n <!-- Fake input with NgModel to be registered into Form controls -->\n <input type=\"hidden\" [name]=\"name() || label()\" [ngModel]=\"model()\" />\n\n <!-- Templates must be stored inside component so that \"@ViewChildren subComponents\" can be filled up with FoehnInputComponent -->\n <ng-content />\n\n <div class=\"row\" [id]=\"buildId() || 'foehn-table'\">\n <div class=\"col-12\">\n @if (!!totalElements() && totalElements() === 1) {\n <h4>\n {{ 'foehn-table.totalElements.1' | fromDictionary }}\n </h4>\n }\n @if (!!totalElements() && totalElements() !== 1) {\n <h4>\n {{\n 'foehn-table.totalElements'\n | fromDictionary\n : { total: totalElements().toString() }\n }}\n </h4>\n }\n </div>\n\n <div class=\"col-12 table-responsive\">\n <table class=\"table table-hover {{ tableClass() }}\" #entryComponent>\n @if (title()?.length) {\n <caption [class.visually-hidden]=\"titleSrOnly()\">\n {{\n title()\n }}\n </caption>\n }\n <thead>\n <tr>\n @for (\n col of columnsConfiguration();\n track trackFoehnTableColumnConfiguration(\n $index,\n col\n )\n ) {\n <th\n class=\"vd-bg-pattern-bars-gray\"\n scope=\"col\"\n [id]=\"col.id\"\n >\n @if (\n !filteredList.length || !col.sortAttribute\n ) {\n <span\n [innerHTML]=\"\n col.columnLabelKey | fromDictionary\n \"\n ></span>\n }\n @if (\n !!filteredList.length && !!col.sortAttribute\n ) {\n <a\n href=\"#\"\n class=\"vd-text-thin\"\n (click)=\"\n $event.preventDefault();\n triggerSort(col.sortAttribute)\n \"\n [title]=\"\n 'Trier par ' +\n (col.columnLabelKey\n | fromDictionary)\n \"\n >\n <span\n [innerHTML]=\"\n col.columnLabelKey\n | fromDictionary\n \"\n ></span>\n @if (\n sort().sortAttribute ===\n col.sortAttribute\n ) {\n @if (\n sort().sortDirection === 'ASC'\n ) {\n <span class=\"ms-3\">\n <foehn-icon-chevron-up />\n </span>\n }\n @if (\n sort().sortDirection === 'DESC'\n ) {\n <span class=\"ms-3\">\n <foehn-icon-chevron-down />\n </span>\n }\n }\n </a>\n }\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (\n item of filteredList;\n track trackByFn()(index, item);\n let index = $index\n ) {\n <tr\n #tableRowElement\n (click)=\"manageRowClick(tableRowElement, item)\"\n >\n @for (\n col of columnsConfiguration();\n track trackFoehnTableColumnConfiguration(\n $index,\n col\n )\n ) {\n <td [id]=\"col.id + '-' + index\">\n @if (!!col.isImportant) {\n @if (col.isImportant(item)) {\n <span\n class=\"cell-vertical-align-middle\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"30\"\n height=\"30\"\n fill=\"currentColor\"\n class=\"bi bi-exclamation text-danger\"\n viewBox=\"0 0 16 16\"\n >\n <path\n d=\"M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0L7.1 4.995z\"\n />\n </svg>\n </span>\n }\n }\n @if (!!col.iconGetter) {\n @if (col.iconGetter(item); as iconDef) {\n <span\n class=\"cell-vertical-align-middle me-2\"\n >\n <fa-icon\n aria-hidden=\"true\"\n [icon]=\"iconDef.icon\"\n [title]=\"iconDef.label\"\n />\n <span class=\"visually-hidden\">\n {{ iconDef.label }}\n </span>\n </span>\n }\n }\n @if (!!col.template) {\n <ng-template\n [ngTemplateOutlet]=\"col.template\"\n [ngTemplateOutletContext]=\"{\n item: item,\n index: index,\n }\"\n />\n }\n @if (!col.template) {\n <span\n [innerHTML]=\"col.valueGetter(item)\"\n class=\"cell-vertical-align-middle\"\n ></span>\n }\n </td>\n }\n </tr>\n }\n @if (!!columnsConfiguration() && !filteredList.length) {\n <tr>\n <td [colSpan]=\"columnsConfiguration().length\">\n <div class=\"w-100 text-center\">\n {{\n 'foehn-table.totalElements.0'\n | fromDictionary\n }}\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n\n @if (hasPreviousPage() || hasNextPage()) {\n <nav\n [id]=\"buildId('_navContainer')\"\n class=\"vd-pagination\"\n aria-label=\"Pagination\"\n >\n <ul class=\"vd-pagination__list\">\n @if (hasPreviousPage()) {\n <li\n class=\"vd-pagination__item vd-pagination__item--previous\"\n >\n <button\n [id]=\"buildId('_prevButton')\"\n type=\"button\"\n class=\"btn btn-link vd-pagination__link btn-no-extra vd-pagination__link-reset\"\n (click)=\"previousPage()\"\n >\n <span class=\"vd-pagination__title\">\n <foehn-icon-chevron-left />\n {{ previousLabel() }}\n </span>\n <span class=\"visually-hidden\">:</span>\n <span class=\"vd-pagination__label\">\n {{ currentPage - 1 }} sur\n {{ pagesCount() }}\n </span>\n </button>\n </li>\n }\n @if (hasNextPage()) {\n <li\n class=\"vd-pagination__item vd-pagination__item--next\"\n >\n <button\n [id]=\"buildId('_nextButton')\"\n type=\"button\"\n class=\"btn btn-link vd-pagination__link btn-no-extra vd-pagination__link-reset\"\n (click)=\"nextPage()\"\n >\n <span class=\"vd-pagination__title\">\n {{ nextLabel() }}\n <foehn-icon-chevron-right />\n </span>\n <span class=\"visually-hidden\">:</span>\n <span class=\"vd-pagination__label\">\n {{ currentPage + 1 }} sur\n {{ pagesCount() }}\n </span>\n </button>\n </li>\n }\n </ul>\n </nav>\n }\n </div>\n </div>\n</div>\n", styles: [".btn-no-extra{padding:0;border:none;font-weight:400;text-decoration-line:none}.vd-pagination{margin-bottom:1rem}.vd-pagination__link-reset{background:none;border:none;font-weight:inherit;text-decoration-line:none;text-align:inherit;cursor:pointer}.vd-pagination__link-reset:focus{background-color:var(--vd-focus)}:host ::ng-deep .btn.btn-link .svg-inline--fa{color:var(--vd-primary-dark)}:host ::ng-deep .btn.btn-link:hover .svg-inline--fa{color:var(--vd-neutral-dark)}.btn.btn-link{text-transform:none}.cell-vertical-align-middle{vertical-align:middle}\n"] }]
6919
- }], propDecorators: { columnsConfiguration: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnsConfiguration", required: false }] }, { type: i0.Output, args: ["columnsConfigurationChange"] }], itemsPerPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemsPerPage", required: false }] }], fixedPageCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixedPageCount", required: false }] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], totalElements: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalElements", required: false }] }], titleSrOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleSrOnly", required: false }] }], previousLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], tableClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "tableClass", required: false }] }], trackByFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackByFn", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }] } });
6920
+ }], propDecorators: { columnsConfiguration: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnsConfiguration", required: false }] }, { type: i0.Output, args: ["columnsConfigurationChange"] }], itemsPerPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemsPerPage", required: false }] }], fixedPageCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixedPageCount", required: false }] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }, { type: i0.Output, args: ["sortChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], totalElements: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalElements", required: false }] }], titleSrOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleSrOnly", required: false }] }], previousLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], tableClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "tableClass", required: false }] }], trackByFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackByFn", required: false }] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }] } });
6920
6921
 
6921
6922
  class DraftsService {
6922
6923
  constructor() {
@@ -9330,12 +9331,11 @@ class FoehnMultiselectAutocompleteComponent extends FoehnAutocompleteComponent {
9330
9331
  manageSuggestionListAndFocusAfterRemovingAnItem(removedIndex) {
9331
9332
  // Hide message saying no more suggestions to display
9332
9333
  this.showEmptyListMessage = false;
9333
- const autocompleteComponent = this.autocompleteComponent();
9334
- if (autocompleteComponent) {
9334
+ if (this.autocompleteComponent()) {
9335
9335
  // Manually show autocomplete because there is at least one suggestions to display
9336
- autocompleteComponent.inputElement().nativeElement.hidden = false;
9336
+ this.autocompleteComponent().inputElement().nativeElement.hidden = false;
9337
9337
  // Add back selected item to autocomplete suggestion list
9338
- autocompleteComponent.elements.set(this.filterAutocompleteElements(this.elements()));
9338
+ this.autocompleteComponent().elements.set(this.filterAutocompleteElements(this.elements()));
9339
9339
  }
9340
9340
  if (removedIndex !== null) {
9341
9341
  // When deleting a selected item, check were to set focus back when no more items to delete
@@ -14498,7 +14498,7 @@ class FoehnListSummaryComponent extends FoehnInputComponent {
14498
14498
  })
14499
14499
  .then(() => {
14500
14500
  const updatedList = this.internalList().filter((_item, itemIndex) => itemIndex !== index);
14501
- this.internalList.set(updatedList);
14501
+ this.updateNgModel(updatedList);
14502
14502
  this.updateListCopyForTable(updatedList);
14503
14503
  this.itemRemoved.emit();
14504
14504
  }, () => {
@@ -14507,7 +14507,7 @@ class FoehnListSummaryComponent extends FoehnInputComponent {
14507
14507
  }
14508
14508
  changeSort(sortEvent) {
14509
14509
  const sortedList = this.sortNow(this.internalList(), sortEvent);
14510
- this.internalList.set(sortedList);
14510
+ this.updateNgModel(sortedList);
14511
14511
  this.updateListCopyForTable(sortedList);
14512
14512
  this.tableSort = sortEvent;
14513
14513
  }