@dsivd/prestations-ng 18.8.0-beta.13 → 19.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.browserslistrc +12 -21
- package/ANGULAR_SIGNALS.md +856 -0
- package/CHANGELOG.md +333 -312
- package/COMPONENTS.md +350 -0
- package/CONTRIBUTING.md +2 -4
- package/ESLINT_MIGRATION_GUIDE.md +64 -67
- package/IAM_ACV_MIGRATION_OIDC.md +138 -0
- package/MULTI_ETAPES.md +1 -1
- package/README.md +4 -2
- package/UPGRADING_V12.md +0 -10
- package/UPGRADING_V16.md +0 -1
- package/UPGRADING_V18.md +398 -233
- package/UPGRADING_V19.md +174 -0
- package/dsivd-prestations-ng-19.0.0-beta.1.tgz +0 -0
- package/fesm2022/dsivd-prestations-ng.mjs +9333 -10485
- package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
- package/index.html +1 -1
- package/package.json +18 -17
- package/schematics/utility/add-allow-synthetic-default-imports.js +2 -2
- package/schematics/utility/add-allowed-common-js-dependencies.js +3 -3
- package/schematics/utility/foehn2-bootstrap5-migration.js +21 -21
- package/schematics/utility/migrate-to-karma-coverage.js +2 -2
- package/schematics/utility/remove-emit-decorator-metadata.js +2 -2
- package/types/dsivd-prestations-ng.d.ts +3699 -0
- package/MODULES.md +0 -371
- package/abstract-page-component.d.ts +0 -57
- package/directives/currency-formatter.directive.d.ts +0 -20
- package/directives/formatter.module.d.ts +0 -11
- package/directives/iban-formatter.directive.d.ts +0 -10
- package/directives/ide-formatter.directive.d.ts +0 -17
- package/directives/ndc-formatter.directive.d.ts +0 -24
- package/dsivd-prestations-ng-18.8.0-beta.13.tgz +0 -0
- package/foehn-address/address.type.d.ts +0 -27
- package/foehn-address/foehn-address.module.d.ts +0 -15
- package/foehn-address/foehn-display-address.component.d.ts +0 -16
- package/foehn-address/foehn-input-address.component.d.ts +0 -107
- package/foehn-address/foehn-input-foreign-locality.component.d.ts +0 -16
- package/foehn-address/foehn-input-foreign-street.component.d.ts +0 -12
- package/foehn-agenda/calendar.type.d.ts +0 -9
- package/foehn-agenda/current-week.type.d.ts +0 -5
- package/foehn-agenda/day-slots.type.d.ts +0 -5
- package/foehn-agenda/foehn-agenda-navigation/foehn-agenda-navigation.component.d.ts +0 -21
- package/foehn-agenda/foehn-agenda-timeslot-panel/foehn-agenda-timeslot-panel.component.d.ts +0 -51
- package/foehn-agenda/foehn-agenda.component.d.ts +0 -29
- package/foehn-agenda/foehn-agenda.module.d.ts +0 -17
- package/foehn-agenda/pagination-week.type.d.ts +0 -5
- package/foehn-agenda/selected-slot.type.d.ts +0 -7
- package/foehn-autocomplete/foehn-autocomplete.component.d.ts +0 -126
- package/foehn-autocomplete/foehn-autocomplete.module.d.ts +0 -13
- package/foehn-boolean/foehn-boolean-checkbox.component.d.ts +0 -6
- package/foehn-boolean/foehn-boolean-radio.component.d.ts +0 -9
- package/foehn-boolean/foehn-boolean.module.d.ts +0 -12
- package/foehn-breadcrumb/breadcrumb-event.service.d.ts +0 -48
- package/foehn-breadcrumb/breadcrumb.d.ts +0 -19
- package/foehn-breadcrumb/foehn-breadcrumb.component.d.ts +0 -30
- package/foehn-breadcrumb/foehn-breadcrumb.module.d.ts +0 -10
- package/foehn-checkables/foehn-checkable-group.component.d.ts +0 -64
- package/foehn-checkables/foehn-checkables.module.d.ts +0 -17
- package/foehn-checkables/foehn-checkbox.component.d.ts +0 -9
- package/foehn-checkables/foehn-radio.component.d.ts +0 -12
- package/foehn-checkables/foehn-select.component.d.ts +0 -22
- package/foehn-checkables/form-select-option-group.d.ts +0 -4
- package/foehn-checkables/form-select-option.d.ts +0 -5
- package/foehn-confirm-modal/foehn-confirm-modal-content.d.ts +0 -9
- package/foehn-confirm-modal/foehn-confirm-modal.component.d.ts +0 -17
- package/foehn-confirm-modal/foehn-confirm-modal.module.d.ts +0 -9
- package/foehn-confirm-modal/foehn-confirm-modal.service.d.ts +0 -18
- package/foehn-date-picker/date-picker-navigation.helper.d.ts +0 -26
- package/foehn-date-picker/date-picker.helper.d.ts +0 -30
- package/foehn-date-picker/day-month.type.d.ts +0 -8
- package/foehn-date-picker/focused-day.type.d.ts +0 -6
- package/foehn-date-picker/foehn-date-picker.component.d.ts +0 -78
- package/foehn-date-picker/foehn-date-picker.module.d.ts +0 -13
- package/foehn-date-picker/month-year.type.d.ts +0 -4
- package/foehn-date-picker-button/foehn-date-picker-button.component.d.ts +0 -31
- package/foehn-date-picker-button/foehn-date-picker-button.module.d.ts +0 -12
- package/foehn-debug-summary/foehn-debug-summary.component.d.ts +0 -21
- package/foehn-debug-summary/foehn-debug-summary.component.module.d.ts +0 -9
- package/foehn-decision-electronique/foehn-decision-electronique.component.d.ts +0 -11
- package/foehn-decision-electronique/foehn-decision-electronique.module.d.ts +0 -11
- package/foehn-dropdown-menu/dropdown-menu-group.type.d.ts +0 -5
- package/foehn-dropdown-menu/dropdown-menu-item.type.d.ts +0 -9
- package/foehn-dropdown-menu/foehn-dropdown-menu.component.d.ts +0 -42
- package/foehn-dropdown-menu/foehn-dropdown-menu.module.d.ts +0 -11
- package/foehn-feedback-notification/foehn-feedback-notification.component.d.ts +0 -32
- package/foehn-feedback-notification/foehn-feedback-notification.module.d.ts +0 -11
- package/foehn-feedback-notification/foehn-feedback-notification.type.d.ts +0 -8
- package/foehn-footer/foehn-footer.component.d.ts +0 -44
- package/foehn-footer/foehn-footer.module.d.ts +0 -9
- package/foehn-footer/footer-link.type.d.ts +0 -4
- package/foehn-form/foehn-form.component.d.ts +0 -27
- package/foehn-form/foehn-form.module.d.ts +0 -10
- package/foehn-form/register-ng-model.service.d.ts +0 -9
- package/foehn-growl/foehn-growl.component.d.ts +0 -17
- package/foehn-growl/foehn-growl.module.d.ts +0 -8
- package/foehn-growl/growl-broker.service.d.ts +0 -12
- package/foehn-growl/growl-message.d.ts +0 -7
- package/foehn-growl/growl-types.d.ts +0 -6
- package/foehn-header/foehn-header.component.d.ts +0 -47
- package/foehn-header/foehn-header.module.d.ts +0 -14
- package/foehn-header/foehn-skip-link.component.d.ts +0 -23
- package/foehn-help-modal/foehn-help-modal.component.d.ts +0 -17
- package/foehn-help-modal/foehn-help-modal.module.d.ts +0 -10
- package/foehn-help-modal/foehn-help-modal.type.d.ts +0 -13
- package/foehn-icons/abstract-icon-component.d.ts +0 -9
- package/foehn-icons/foehn-icon-calendar.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-check-square-o.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-check.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-chevron-down.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-chevron-left.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-chevron-right.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-chevron-up.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-clock.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-comment-dots.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-edit.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-external-link-alt.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-file-pdf.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-info-circle.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-lock.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-map-marker.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-minus-circle.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-pencil.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-plus-circle.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-plus-square.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-search.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-times.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-trash-alt.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-unlock-alt.component.d.ts +0 -8
- package/foehn-icons/foehn-icon-user.component.d.ts +0 -8
- package/foehn-icons/foehn-icons.module.d.ts +0 -31
- package/foehn-input/foehn-input-email.component.d.ts +0 -15
- package/foehn-input/foehn-input-hidden.component.d.ts +0 -14
- package/foehn-input/foehn-input-number.component.d.ts +0 -42
- package/foehn-input/foehn-input-password.component.d.ts +0 -12
- package/foehn-input/foehn-input-phone.component.d.ts +0 -25
- package/foehn-input/foehn-input-prefixed-text.component.d.ts +0 -26
- package/foehn-input/foehn-input-string.component.d.ts +0 -9
- package/foehn-input/foehn-input-text.component.d.ts +0 -11
- package/foehn-input/foehn-input-textarea.component.d.ts +0 -22
- package/foehn-input/foehn-input.component.d.ts +0 -127
- package/foehn-input/foehn-input.module.d.ts +0 -24
- package/foehn-input/pattern.const.d.ts +0 -11
- package/foehn-input-date/foehn-input-date.component.d.ts +0 -33
- package/foehn-input-date-time/foehn-input-date-time.component.d.ts +0 -40
- package/foehn-input-time/foehn-input-time.component.d.ts +0 -19
- package/foehn-list/foehn-list.component.d.ts +0 -27
- package/foehn-list/foehn-list.module.d.ts +0 -9
- package/foehn-list/page-change-event.d.ts +0 -5
- package/foehn-menu-prestation/abstract-list-item-detail-page.component.d.ts +0 -35
- package/foehn-menu-prestation/abstract-menu-page.component.d.ts +0 -13
- package/foehn-menu-prestation/abstract-page-from-menu.component.d.ts +0 -20
- package/foehn-menu-prestation/foehn-error-pill/foehn-error-pill.component.d.ts +0 -12
- package/foehn-menu-prestation/foehn-list-summary/foehn-list-item-description.d.ts +0 -6
- package/foehn-menu-prestation/foehn-list-summary/foehn-list-item.d.ts +0 -5
- package/foehn-menu-prestation/foehn-list-summary/foehn-list-summary.component.d.ts +0 -40
- package/foehn-menu-prestation/foehn-menu-items/foehn-menu-item/foehn-menu-item.component.d.ts +0 -16
- package/foehn-menu-prestation/foehn-menu-items/foehn-menu-item-transmit/foehn-menu-item-transmit.component.d.ts +0 -23
- package/foehn-menu-prestation/foehn-menu-prestation.module.d.ts +0 -18
- package/foehn-misc/foehn-abbr.component.d.ts +0 -7
- package/foehn-misc/foehn-misc.module.d.ts +0 -7
- package/foehn-modal/foehn-modal.component.d.ts +0 -24
- package/foehn-modal/foehn-modal.module.d.ts +0 -11
- package/foehn-multiselect-autocomplete/foehn-multiselect-autocomplete.component.d.ts +0 -54
- package/foehn-multiselect-autocomplete/foehn-multiselect-autocomplete.module.d.ts +0 -13
- package/foehn-nav13/foehn-input-nav13.component.d.ts +0 -20
- package/foehn-nav13/foehn-input-nav13.module.d.ts +0 -11
- package/foehn-navigation/abstract-foehn-navigation.component.d.ts +0 -19
- package/foehn-navigation/foehn-navigation/foehn-navigation.component.d.ts +0 -22
- package/foehn-navigation/foehn-navigation.module.d.ts +0 -13
- package/foehn-navigation/foehn-navigation.service.d.ts +0 -69
- package/foehn-navigation/foehn-simple-navigation/foehn-simple-navigation.component.d.ts +0 -8
- package/foehn-notfound/foehn-notfound.component.d.ts +0 -19
- package/foehn-notfound/foehn-notfound.module.d.ts +0 -10
- package/foehn-page/foehn-page-counter.component.d.ts +0 -15
- package/foehn-page/foehn-page-expiration-timer/current-session-expiration.d.ts +0 -5
- package/foehn-page/foehn-page-expiration-timer/demande-expiration-interceptor.d.ts +0 -15
- package/foehn-page/foehn-page-expiration-timer/demande-expiration.service.d.ts +0 -16
- package/foehn-page/foehn-page-expiration-timer/foehn-page-expiration-timer.component.d.ts +0 -36
- package/foehn-page/foehn-page-expiration-timer/interval.helper.d.ts +0 -3
- package/foehn-page/foehn-page-modal.component.d.ts +0 -23
- package/foehn-page/foehn-page-modal.service.d.ts +0 -12
- package/foehn-page/foehn-page.component.d.ts +0 -80
- package/foehn-page/foehn-page.module.d.ts +0 -24
- package/foehn-page/foehn-page.service.d.ts +0 -19
- package/foehn-page/foehn-transmit-waiting-modal/demande-transmit-interceptor.d.ts +0 -16
- package/foehn-page/foehn-transmit-waiting-modal/foehn-transmit-waiting-modal.component.d.ts +0 -11
- package/foehn-page/foehn-transmit-waiting-modal/foehn-transmit-waiting-modal.service.d.ts +0 -11
- package/foehn-recap-section/foehn-recap-section.component.d.ts +0 -21
- package/foehn-recap-section/foehn-recap-section.module.d.ts +0 -10
- package/foehn-recap-section/foehn-recap-section.type.d.ts +0 -17
- package/foehn-remaining-alerts-summary/foehn-remaining-alerts-summary.component.d.ts +0 -20
- package/foehn-remaining-alerts-summary/foehn-remaining-alerts-summary.module.d.ts +0 -8
- package/foehn-status-progress-bar/foehn-status-progress-bar.component.d.ts +0 -23
- package/foehn-status-progress-bar/foehn-status-progress-bar.module.d.ts +0 -10
- package/foehn-table/foehn-table-column-configuration.d.ts +0 -14
- package/foehn-table/foehn-table-page-change-event.d.ts +0 -5
- package/foehn-table/foehn-table.component.d.ts +0 -40
- package/foehn-table/foehn-table.module.d.ts +0 -13
- package/foehn-table/row-element.type.d.ts +0 -4
- package/foehn-table/tableSort.d.ts +0 -4
- package/foehn-upload/abstract-foehn-uploader.component.d.ts +0 -68
- package/foehn-upload/document-reference.d.ts +0 -12
- package/foehn-upload/foehn-bo-multi-upload/bo-multi-upload.service.d.ts +0 -27
- package/foehn-upload/foehn-bo-multi-upload/bo-multi-upload.type.d.ts +0 -11
- package/foehn-upload/foehn-bo-multi-upload/foehn-bo-multi-upload.component.d.ts +0 -38
- package/foehn-upload/foehn-bo-multi-upload/foehn-bo-multi-upload.module.d.ts +0 -13
- package/foehn-upload/foehn-multi-upload/foehn-multi-upload.component.d.ts +0 -42
- package/foehn-upload/foehn-multi-upload/foehn-multi-upload.module.d.ts +0 -13
- package/foehn-upload/foehn-multi-upload/multi-upload.service.d.ts +0 -29
- package/foehn-upload/foehn-multi-upload/multi-upload.type.d.ts +0 -11
- package/foehn-upload/foehn-multi-upload/pending-upload.service.d.ts +0 -26
- package/foehn-upload/foehn-picture-upload/foehn-picture-upload.component.d.ts +0 -38
- package/foehn-upload/foehn-picture-upload/foehn-picture-upload.module.d.ts +0 -14
- package/foehn-upload/foehn-upload-progress-bar/foehn-upload-progress-bar.component.d.ts +0 -22
- package/foehn-upload/foehn-upload-progress-bar/foehn-upload-progress-bar.module.d.ts +0 -11
- package/foehn-upload/foehn-upload-progress-bar/upload-progress.service.d.ts +0 -19
- package/foehn-upload/foehn-upload-progress-bar/upload-progress.type.d.ts +0 -6
- package/foehn-upload/page-upload-limit.service.d.ts +0 -22
- package/foehn-upload/pending-upload.type.d.ts +0 -9
- package/foehn-upload/uploader.helper.d.ts +0 -66
- package/foehn-user-connected-as/foehn-user-connected-as.component.d.ts +0 -12
- package/foehn-user-connected-as/foehn-user-connected-as.module.d.ts +0 -9
- package/foehn-validation-alert-summary/foehn-validation-alert-summary.component.d.ts +0 -20
- package/foehn-validation-alert-summary/foehn-validation-alert-summary.module.d.ts +0 -9
- package/foehn-validation-alerts/foehn-validation-alerts.component.d.ts +0 -14
- package/foehn-validation-alerts/foehn-validation-alerts.module.d.ts +0 -8
- package/form-error.d.ts +0 -5
- package/form-post-response.d.ts +0 -19
- package/gesdem/gesdem-error-handler.service.d.ts +0 -22
- package/gesdem/gesdem-event.service.d.ts +0 -16
- package/gesdem/gesdem-handler.service.d.ts +0 -52
- package/gesdem/gesdem-loader.guard.d.ts +0 -2
- package/gesdem/gesdem-statut-utils.d.ts +0 -12
- package/gesdem-action-recovery/gesdem-action-recovery-login/gesdem-action-recovery-login.component.d.ts +0 -39
- package/gesdem-action-recovery/gesdem-action-recovery-registration/gesdem-action-recovery-registration.component.d.ts +0 -59
- package/gesdem-action-recovery/gesdem-action-recovery.module.d.ts +0 -17
- package/gesdem-action-recovery/gesdem-action-recovery.service.d.ts +0 -24
- package/gesdem-action-recovery/reprise-info.d.ts +0 -6
- package/gesdem-confirmation/gesdem-confirmation.component.d.ts +0 -65
- package/gesdem-confirmation/gesdem-confirmation.module.d.ts +0 -12
- package/gesdem-error/gesdem-error.component.d.ts +0 -34
- package/gesdem-error/gesdem-error.module.d.ts +0 -12
- package/global.const.d.ts +0 -1
- package/helpers/demande.helper.d.ts +0 -3
- package/helpers/object.helper.d.ts +0 -30
- package/i18n-form.d.ts +0 -5
- package/index.d.ts +0 -5
- package/pipes/currency.pipe.d.ts +0 -10
- package/pipes/date.pipe.d.ts +0 -12
- package/pipes/formatIde.pipe.d.ts +0 -7
- package/pipes/pipes.module.d.ts +0 -9
- package/prestations-ng-core.module.d.ts +0 -12
- package/public-api.d.ts +0 -238
- package/refinf/model/address-type-light.d.ts +0 -5
- package/refinf/model/canton.d.ts +0 -4
- package/refinf/model/district.d.ts +0 -7
- package/refinf/model/municipality.d.ts +0 -9
- package/refinf/model/place-of-origin.d.ts +0 -9
- package/refinf/model/postal-locality.d.ts +0 -11
- package/sdk-appinfo/application-info.d.ts +0 -61
- package/sdk-appinfo/application-info.service.d.ts +0 -32
- package/sdk-currency/currency.helper.d.ts +0 -9
- package/sdk-date/date.helper.d.ts +0 -20
- package/sdk-dictionary/default-dictionary.d.ts +0 -2
- package/sdk-dictionary/sdk-dictionary.module.d.ts +0 -7
- package/sdk-dictionary/sdk-dictionary.pipe.d.ts +0 -12
- package/sdk-dictionary/sdk-dictionary.service.d.ts +0 -44
- package/sdk-drafts/draft.model.d.ts +0 -5
- package/sdk-drafts/drafts-container.component.d.ts +0 -33
- package/sdk-drafts/drafts.module.d.ts +0 -14
- package/sdk-drafts/drafts.service.d.ts +0 -20
- package/sdk-epayment/model/EPaymentParameters.d.ts +0 -21
- package/sdk-epayment/pending-payment/pending-payment.component.d.ts +0 -9
- package/sdk-epayment/sdk-epayment.component.d.ts +0 -33
- package/sdk-epayment/sdk-epayment.module.d.ts +0 -11
- package/sdk-epayment/sdk-epayment.service.d.ts +0 -21
- package/sdk-events-logger/sdk-event-type.d.ts +0 -12
- package/sdk-events-logger/sdk-event.d.ts +0 -8
- package/sdk-events-logger/sdk-events-logger.service.d.ts +0 -18
- package/sdk-recaptcha/grecaptcha/grecaptcha.component.d.ts +0 -39
- package/sdk-recaptcha/recaptcha.service.d.ts +0 -44
- package/sdk-recaptcha/sdk-recaptcha.component.d.ts +0 -27
- package/sdk-recaptcha/sdk-recaptcha.module.d.ts +0 -9
- package/sdk-redirect/iam-expired-interceptor.service.d.ts +0 -12
- package/sdk-redirect/redirect.component.d.ts +0 -22
- package/sdk-redirect/sdk-redirect.module.d.ts +0 -9
- package/sdk-session-info/session-info-data.d.ts +0 -29
- package/sdk-session-info/session-info-with-application.service.d.ts +0 -20
- package/sdk-session-info/session-info.service.d.ts +0 -17
- package/sdk-statistics/sdk-statistics.service.d.ts +0 -24
- package/sdk-support-alert/support-alert-container.component.d.ts +0 -20
- package/sdk-support-alert/support-alert.component.d.ts +0 -12
- package/sdk-support-alert/support-alert.model.d.ts +0 -12
- package/sdk-support-alert/support-alert.module.d.ts +0 -10
- package/sdk-support-alert/support-alert.service.d.ts +0 -34
- package/service-locator.d.ts +0 -4
- package/supported-browser.d.ts +0 -1
- package/validation/validation-handler.service.d.ts +0 -15
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
# Angular Signals — Migration Guide
|
|
2
|
+
|
|
3
|
+
## Table of contents
|
|
4
|
+
|
|
5
|
+
- [Overview](angular-signals#overview)
|
|
6
|
+
- [Core primitives](angular-signals#core_primitives)
|
|
7
|
+
- [computed()](angular-signals#_code_computed____code_)
|
|
8
|
+
- [effect()](angular-signals#_code_effect____code_)
|
|
9
|
+
- [untracked()](angular-signals#_code_untracked____code_)
|
|
10
|
+
- [linkedSignal()](angular-signals#_code_linkedsignal____code_)
|
|
11
|
+
- [input() and model()](angular-signals#_code_input____code__and__code_model____code_)
|
|
12
|
+
- [viewChildren() and viewChild()](angular-signals#_code_viewchildren____code__and__code_viewchild____code_)
|
|
13
|
+
- [Mental model — computed() vs effect()](angular-signals#mental_model____code_computed____code__vs__code_effect____code_)
|
|
14
|
+
- [Migration patterns](angular-signals#migration_patterns)
|
|
15
|
+
- [Inheritance caveat](angular-signals#inheritance_caveat)
|
|
16
|
+
- [Testing signals](angular-signals#testing_signals)
|
|
17
|
+
- [Common pitfalls](angular-signals#common_pitfalls)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Overview
|
|
22
|
+
|
|
23
|
+
Angular Signals are a reactive primitive introduced to replace zone-based
|
|
24
|
+
change detection with a fine-grained, declarative reactivity model.
|
|
25
|
+
The goal is to make data flow explicit, predictable, and performant.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { signal, computed, effect, untracked } from '@angular/core';
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Core primitives
|
|
34
|
+
|
|
35
|
+
### `signal()`
|
|
36
|
+
|
|
37
|
+
A writable reactive value. Reading it inside a reactive context (template,
|
|
38
|
+
`computed`, `effect`) creates a dependency automatically.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
readonly count = signal(0);
|
|
42
|
+
|
|
43
|
+
increment(): void {
|
|
44
|
+
this.count.update(v => v + 1);
|
|
45
|
+
// or
|
|
46
|
+
this.count.set(5);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
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) |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## `computed()`
|
|
59
|
+
|
|
60
|
+
A **read-only derived signal**. Re-evaluates only when one of its dependencies
|
|
61
|
+
changes. Lazy and memoized.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
readonly minYear = computed(() => this.minDate()?.[0] ?? dayjs().year() - 100);
|
|
65
|
+
readonly fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
|
|
66
|
+
readonly isValid = computed(() => this.value() >= this.min() && this.value() <= this.max());
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Rules
|
|
70
|
+
|
|
71
|
+
- Pure and synchronous — no side effects
|
|
72
|
+
- Never call `.set()` inside a `computed()`
|
|
73
|
+
- Dependencies are tracked automatically — no need to declare them
|
|
74
|
+
|
|
75
|
+
### When `computed()` replaces imperative code
|
|
76
|
+
|
|
77
|
+
Before — manual update in a setter:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
@Input()
|
|
81
|
+
set minDate(value: number[]) {
|
|
82
|
+
this._minDate = value;
|
|
83
|
+
if (value?.length) {
|
|
84
|
+
this.minYear = value[0]; // manual sync
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
After — declarative derivation:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
readonly minDate = input<number[]>();
|
|
93
|
+
readonly minYear = computed(() => this.minDate()?.[0] ?? dayjs().year() - 100);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## `effect()`
|
|
99
|
+
|
|
100
|
+
Runs a **side effect** whenever its signal dependencies change.
|
|
101
|
+
Use it to synchronize signals with the outside world.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
effect(() => {
|
|
105
|
+
localStorage.setItem('theme', this.theme());
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
effect(() => {
|
|
109
|
+
this.externalHelper.value = this.mySignal();
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Rules
|
|
114
|
+
|
|
115
|
+
- Runs at least once on initialization
|
|
116
|
+
- Avoid calling `.set()` on signals inside an effect — use `computed()` instead
|
|
117
|
+
- Registered in the constructor injection context by default
|
|
118
|
+
- Cleaned up automatically when the component is destroyed
|
|
119
|
+
- Only signals read **without** `untracked()` become dependencies — see
|
|
120
|
+
[`untracked()`](angular-signals#_code_untracked____code_) below for reading
|
|
121
|
+
a signal inside an effect without subscribing to it
|
|
122
|
+
|
|
123
|
+
### Tracking the previous value
|
|
124
|
+
|
|
125
|
+
`effect()` does not provide a previous value out of the box.
|
|
126
|
+
Use a plain class field to capture it:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
private previousIsVisible = false;
|
|
130
|
+
|
|
131
|
+
private readonly visibilityEffect = effect(() => {
|
|
132
|
+
const current = this.isVisible();
|
|
133
|
+
const previous = this.previousIsVisible;
|
|
134
|
+
|
|
135
|
+
if (!previous && current) {
|
|
136
|
+
this.onOpen();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.previousIsVisible = current;
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## `untracked()`
|
|
146
|
+
|
|
147
|
+
`untracked()` lets you read a signal's value **inside a reactive context**
|
|
148
|
+
(`effect()`, `computed()`) **without** registering it as a dependency.
|
|
149
|
+
This is the main tool for controlling exactly which signals an `effect()`
|
|
150
|
+
reacts to, when the effect's body needs to read more state than it should
|
|
151
|
+
actually react to.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { untracked } from '@angular/core';
|
|
155
|
+
|
|
156
|
+
effect(() => {
|
|
157
|
+
const id = this.userId(); // tracked — effect re-runs when this changes
|
|
158
|
+
const config = untracked(() => this.appConfig()); // read, but not tracked
|
|
159
|
+
|
|
160
|
+
this.analytics.logUserView(id, config);
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Why this matters
|
|
165
|
+
|
|
166
|
+
An `effect()` automatically tracks **every** signal it reads during its
|
|
167
|
+
synchronous execution. Sometimes that's too broad: you only want the effect
|
|
168
|
+
to re-run for a subset of the signals it touches, and reading the rest is
|
|
169
|
+
just "give me the current value, don't subscribe me to it."
|
|
170
|
+
|
|
171
|
+
This is especially common when an effect needs to read the **same signal it
|
|
172
|
+
is about to write to** (directly or via a method that writes to it).
|
|
173
|
+
Reading it without `untracked()` can cause the effect to register itself as
|
|
174
|
+
its own dependency, leading to redundant re-runs or, in more complex chains,
|
|
175
|
+
an infinite loop.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// 🔴 Risky — `this.value()` is read in the same effect that may end up
|
|
179
|
+
// triggering a write back to `value`, depending on what updateNgModel does
|
|
180
|
+
effect(() => {
|
|
181
|
+
this.showLine1();
|
|
182
|
+
this.showLine2();
|
|
183
|
+
|
|
184
|
+
const current = this.value(); // tracked unintentionally
|
|
185
|
+
const sanitized = this.sanitize(current);
|
|
186
|
+
|
|
187
|
+
if (sanitized !== current) {
|
|
188
|
+
this.updateNgModel(sanitized); // writes back to `value`
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// 🟢 Safe — only the visibility signals are tracked;
|
|
193
|
+
// `value` is read but not subscribed to
|
|
194
|
+
effect(() => {
|
|
195
|
+
this.showLine1();
|
|
196
|
+
this.showLine2();
|
|
197
|
+
|
|
198
|
+
const current = untracked(() => this.value());
|
|
199
|
+
const sanitized = this.sanitize(current);
|
|
200
|
+
|
|
201
|
+
if (sanitized !== current) {
|
|
202
|
+
this.updateNgModel(sanitized);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
In the safe version, the effect's dependency list is exactly
|
|
208
|
+
`[showLine1, showLine2]`. Writing to `value` inside the effect body no longer
|
|
209
|
+
risks the effect re-triggering itself purely because it also happened to read
|
|
210
|
+
`value()` reactively.
|
|
211
|
+
|
|
212
|
+
### Other common uses
|
|
213
|
+
|
|
214
|
+
Reading a signal for one-off, non-reactive logic inside a `computed()`:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
readonly summary = computed(() => {
|
|
218
|
+
const items = this.items();
|
|
219
|
+
// logging shouldn't be a reason for `summary` to be considered "stale"
|
|
220
|
+
untracked(() => console.debug('recomputing summary', items.length));
|
|
221
|
+
return items.length;
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Calling an imperative API (router, third-party widget, focus management)
|
|
226
|
+
that internally reads signals, without wanting those internal reads to
|
|
227
|
+
become dependencies of your effect:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
effect(() => {
|
|
231
|
+
const open = this.isVisible();
|
|
232
|
+
if (open) {
|
|
233
|
+
untracked(() => this.focusManager.focusDefaultElement());
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Rules
|
|
239
|
+
|
|
240
|
+
- `untracked()` does not prevent the read — it only prevents the
|
|
241
|
+
**dependency tracking** of that read
|
|
242
|
+
- Use it when an effect or computed needs a signal's current value for
|
|
243
|
+
logic, but should not re-run merely because that signal changed
|
|
244
|
+
- A frequent signal that you need `untracked()`: an `effect()` that reads
|
|
245
|
+
and then conditionally writes back to the **same** signal — read the
|
|
246
|
+
current value with `untracked()`, keep the write outside of it
|
|
247
|
+
- Do not reach for `untracked()` as a substitute for an equality check;
|
|
248
|
+
it controls _what triggers_ the effect, not _whether the write is a no-op_.
|
|
249
|
+
Combine both when guarding against redundant or looping writes
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## `linkedSignal()`
|
|
254
|
+
|
|
255
|
+
A **writable signal** whose value resets automatically when a source signal
|
|
256
|
+
changes. It fills the gap between `computed()` (read-only) and `signal()`
|
|
257
|
+
(no automatic sync with a source).
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { linkedSignal } from '@angular/core';
|
|
261
|
+
|
|
262
|
+
readonly options = input<string[]>([]);
|
|
263
|
+
|
|
264
|
+
readonly selectedOption = linkedSignal(() => this.options()[0]);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Whenever `options` changes, `selectedOption` resets to the first item.
|
|
268
|
+
But the user (or the component) can still write to it in the meantime:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
this.selectedOption.set('other value'); // works, until options changes again
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
### When to use `linkedSignal()`
|
|
277
|
+
|
|
278
|
+
| Scenario | Tool |
|
|
279
|
+
| --------------------------------------------- | ---------------- |
|
|
280
|
+
| Read-only derivation, never written locally | `computed()` |
|
|
281
|
+
| Local state that resets when a source changes | `linkedSignal()` |
|
|
282
|
+
| Two-way binding with parent | `model()` |
|
|
283
|
+
| Independent local state | `signal()` |
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
### Full form — source + computation
|
|
288
|
+
|
|
289
|
+
The short form `linkedSignal(() => source())` is sugar for the full form,
|
|
290
|
+
which gives you access to the **previous value** of the linked signal:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
readonly selectedOption = linkedSignal<string[], string>({
|
|
294
|
+
source: this.options,
|
|
295
|
+
computation: (newOptions, previous) => {
|
|
296
|
+
// If the previously selected option still exists in the new list, keep it
|
|
297
|
+
if (previous && newOptions.includes(previous.value)) {
|
|
298
|
+
return previous.value;
|
|
299
|
+
}
|
|
300
|
+
return newOptions[0];
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`previous` contains `{ value: T }` — the last value of the linked signal
|
|
306
|
+
before the source changed, or `undefined` on first run.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
### Migration pattern — setter with local state reset
|
|
311
|
+
|
|
312
|
+
A common pattern before signals was a setter that reset local state
|
|
313
|
+
when an input changed:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
// Before
|
|
317
|
+
private _options: string[] = [];
|
|
318
|
+
selectedOption: string;
|
|
319
|
+
|
|
320
|
+
@Input()
|
|
321
|
+
set options(value: string[]) {
|
|
322
|
+
this._options = value;
|
|
323
|
+
this.selectedOption = value[0]; // reset local selection
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
After — `linkedSignal()` expresses this declaratively:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
// After
|
|
331
|
+
readonly options = input<string[]>([]);
|
|
332
|
+
readonly selectedOption = linkedSignal(() => this.options()[0]);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
### Red flag — `effect()` resetting a `signal()`
|
|
338
|
+
|
|
339
|
+
If you find yourself writing an `effect()` that resets a plain `signal()`
|
|
340
|
+
when another signal changes, that is a `linkedSignal()` in disguise:
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
// 🔴 Bad
|
|
344
|
+
readonly selectedOption = signal('');
|
|
345
|
+
|
|
346
|
+
private readonly resetEffect = effect(() => {
|
|
347
|
+
this.selectedOption.set(this.options()[0]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// 🟢 What it really is
|
|
351
|
+
readonly selectedOption = linkedSignal(() => this.options()[0]);
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## `input()` and `model()`
|
|
357
|
+
|
|
358
|
+
### `input()`
|
|
359
|
+
|
|
360
|
+
Replaces `@Input()` for one-way parent → child binding.
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
// Before
|
|
364
|
+
@Input() closeable = true;
|
|
365
|
+
|
|
366
|
+
// After
|
|
367
|
+
readonly closeable = input(true);
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
With a default value and a type:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
readonly minYear = input<number>(dayjs().year() - 100);
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### `model()`
|
|
377
|
+
|
|
378
|
+
Replaces `@Input()` + `@Output()` pairs for **two-way binding**.
|
|
379
|
+
Automatically creates the corresponding `Change` output.
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
// Before
|
|
383
|
+
@Input() isVisible = false;
|
|
384
|
+
@Output() isVisibleChange = new EventEmitter<boolean>();
|
|
385
|
+
|
|
386
|
+
// After
|
|
387
|
+
readonly isVisible = model(false);
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Usage in a parent template:
|
|
391
|
+
|
|
392
|
+
```html
|
|
393
|
+
<!-- two-way -->
|
|
394
|
+
<my-component [(isVisible)]="open" />
|
|
395
|
+
|
|
396
|
+
<!-- one-way — will cause a compiler error with model() -->
|
|
397
|
+
<my-component [isVisible]="open" />
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
> ⚠️ `model()` does not accept one-way `[binding]` alone.
|
|
401
|
+
> Use `input()` if the child never needs to write back to the parent.
|
|
402
|
+
|
|
403
|
+
> ⚠️ `model()` holds object values by reference. Mutating a nested property
|
|
404
|
+
> of a `model()`'s value (e.g. `this.address().locality.name = 'x'`) does
|
|
405
|
+
> **not** notify the signal or the parent binding. Always `.set()` or
|
|
406
|
+
> `.update()` with a new top-level reference:
|
|
407
|
+
> `this.address.update(current => ({ ...current, locality: newLocality }))`.
|
|
408
|
+
|
|
409
|
+
### Choosing between `input()` and `model()`
|
|
410
|
+
|
|
411
|
+
| Scenario | Tool |
|
|
412
|
+
| -------------------------------- | ------------ |
|
|
413
|
+
| Parent writes, child only reads | `input()` |
|
|
414
|
+
| Parent writes, child writes back | `model()` |
|
|
415
|
+
| Derived from other inputs | `computed()` |
|
|
416
|
+
|
|
417
|
+
### `input()` with alias — ESLint caveat
|
|
418
|
+
|
|
419
|
+
If you need an internal name different from the public binding name,
|
|
420
|
+
use the `alias` option. Note that `@angular-eslint/no-input-rename` will
|
|
421
|
+
flag this — disable it locally only when strictly necessary:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
// eslint-disable-next-line @angular-eslint/no-input-rename
|
|
425
|
+
readonly minYearInput = input<number>(dayjs().year() - 100, { alias: 'minYear' });
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Prefer renaming the property to match the alias whenever possible.
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## `viewChildren()` and `viewChild()`
|
|
433
|
+
|
|
434
|
+
Replaces `@ViewChildren` and `@ViewChild` with signal-based queries.
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
// Before
|
|
438
|
+
@ViewChildren('dayButtonElemRef') daysButtons: QueryList<ElementRef>;
|
|
439
|
+
|
|
440
|
+
// After
|
|
441
|
+
readonly daysButtons = viewChildren<ElementRef>('dayButtonElemRef');
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
The signal returns a `readonly T[]` — always guard against empty arrays:
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
const enabled = [...(this.daysButtons() ?? [])].filter(
|
|
448
|
+
(el) => el.nativeElement.getAttribute('disabled') === null,
|
|
449
|
+
);
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Reacting to changes — use `effect()`:
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
private readonly daysEffect = effect(() => {
|
|
456
|
+
const buttons = this.daysButtons();
|
|
457
|
+
if (buttons.length > 0) {
|
|
458
|
+
this.changeDetector.detectChanges();
|
|
459
|
+
}
|
|
460
|
+
this.helper.buttons = buttons;
|
|
461
|
+
});
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Mental model — `computed()` vs `effect()`
|
|
467
|
+
|
|
468
|
+
> **"Am I calculating something, or am I doing something?"**
|
|
469
|
+
|
|
470
|
+
| Intent | Tool |
|
|
471
|
+
| ------------------------- | ------------ |
|
|
472
|
+
| Transform data | `computed()` |
|
|
473
|
+
| Derive state | `computed()` |
|
|
474
|
+
| Synchronize with the DOM | `effect()` |
|
|
475
|
+
| Drive an external service | `effect()` |
|
|
476
|
+
| Log, track | `effect()` |
|
|
477
|
+
|
|
478
|
+
### The red flag
|
|
479
|
+
|
|
480
|
+
If you find yourself writing an `effect()` that calls `.set()` on another signal,
|
|
481
|
+
it is almost always a `computed()` in disguise:
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
// 🔴 Bad — effect producing a value
|
|
485
|
+
effect(() => {
|
|
486
|
+
this.minYear.set(this.minDate()?.[0] ?? dayjs().year() - 100);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// 🟢 What it really is
|
|
490
|
+
readonly minYear = computed(() => this.minDate()?.[0] ?? dayjs().year() - 100);
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
> 💡 If the effect genuinely needs to remain an `effect()` (e.g. it also
|
|
494
|
+
> performs a non-signal side effect like calling a service), but reads a
|
|
495
|
+
> signal purely for context rather than as a trigger, reach for
|
|
496
|
+
> [`untracked()`](angular-signals#_code_untracked____code_) instead of
|
|
497
|
+
> restructuring into a `computed()`.
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Migration patterns
|
|
502
|
+
|
|
503
|
+
### Pattern 1 — Simple `@Input()` → `input()`
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
// Before
|
|
507
|
+
@Input() id: string;
|
|
508
|
+
|
|
509
|
+
// After
|
|
510
|
+
readonly id = input<string>();
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Pattern 2 — `@Input()` with setter → `input()` + `computed()`
|
|
514
|
+
|
|
515
|
+
When the setter only derived a value:
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
// Before
|
|
519
|
+
@Input()
|
|
520
|
+
set minDate(value: number[]) {
|
|
521
|
+
this._minDate = value;
|
|
522
|
+
if (value?.length) this.minYear = value[0];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// After
|
|
526
|
+
readonly minDate = input<number[]>();
|
|
527
|
+
readonly minYear = computed(() => this.minDate()?.[0] ?? dayjs().year() - 100);
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
When the setter triggered side effects:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
// Before
|
|
534
|
+
@Input()
|
|
535
|
+
set isVisible(val: boolean) {
|
|
536
|
+
this._isVisible = val;
|
|
537
|
+
this.isVisibleChange.emit(val);
|
|
538
|
+
this.focusDefaultDay();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// After
|
|
542
|
+
readonly isVisible = model(false);
|
|
543
|
+
|
|
544
|
+
private readonly isVisibleEffect = effect(() => {
|
|
545
|
+
const val = this.isVisible();
|
|
546
|
+
this.focusDefaultDay();
|
|
547
|
+
});
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### Pattern 3 — `@Input()` + `@Output()` pair → `model()`
|
|
551
|
+
|
|
552
|
+
```ts
|
|
553
|
+
// Before
|
|
554
|
+
@Input() value: boolean;
|
|
555
|
+
@Output() valueChange = new EventEmitter<boolean>();
|
|
556
|
+
|
|
557
|
+
// After
|
|
558
|
+
readonly value = model(false);
|
|
559
|
+
|
|
560
|
+
// Writing back from inside the component
|
|
561
|
+
this.value.set(false); // automatically emits valueChange
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Pattern 4 — `@ViewChildren` → `viewChildren()`
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
// Before
|
|
568
|
+
@ViewChildren('ref') items: QueryList<ElementRef>;
|
|
569
|
+
|
|
570
|
+
ngAfterViewInit(): void {
|
|
571
|
+
this.items.changes.subscribe(list => this.helper.items = list);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// After
|
|
575
|
+
readonly items = viewChildren<ElementRef>('ref');
|
|
576
|
+
|
|
577
|
+
private readonly itemsEffect = effect(() => {
|
|
578
|
+
this.helper.items = this.items();
|
|
579
|
+
});
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Pattern 5 — Multiple trigger signals with a guarded re-sanitization
|
|
583
|
+
|
|
584
|
+
A common case in components with several "visibility" or "feature flag"
|
|
585
|
+
signals: you need to re-run some sanitization logic whenever **any** of
|
|
586
|
+
several signals change, but the logic also reads and conditionally rewrites
|
|
587
|
+
a `model()` value. Track only the trigger signals, read the model value with
|
|
588
|
+
`untracked()`, and guard the write with an equality check to avoid loops:
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
readonly showLine1 = input(true);
|
|
592
|
+
readonly showLine2 = input(true);
|
|
593
|
+
readonly value = model<Address>();
|
|
594
|
+
|
|
595
|
+
private readonly sanitizeEffect = effect(() => {
|
|
596
|
+
// tracked dependencies
|
|
597
|
+
const showLine1 = this.showLine1();
|
|
598
|
+
const showLine2 = this.showLine2();
|
|
599
|
+
|
|
600
|
+
// read without subscribing to `value` itself
|
|
601
|
+
const current = untracked(() => this.value());
|
|
602
|
+
if (!current) return;
|
|
603
|
+
|
|
604
|
+
const sanitized: Address = { ...current };
|
|
605
|
+
if (!showLine1) sanitized.addressLine1 = null;
|
|
606
|
+
if (!showLine2) sanitized.addressLine2 = null;
|
|
607
|
+
|
|
608
|
+
if (!this.addressesEqual(sanitized, current)) {
|
|
609
|
+
this.value.set(sanitized);
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## Inheritance caveat
|
|
617
|
+
|
|
618
|
+
Signal inputs are **not inherited**. Angular registers them at compile time
|
|
619
|
+
via the `@Component` decorator — TypeScript inheritance alone is not enough.
|
|
620
|
+
|
|
621
|
+
```ts
|
|
622
|
+
export class BaseComponent {
|
|
623
|
+
readonly isVisible = input<boolean>(false); // ❌ not seen by child's Angular metadata
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export class ChildComponent extends BaseComponent {
|
|
627
|
+
// isVisible is not recognized as an Angular input here
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Solutions
|
|
632
|
+
|
|
633
|
+
**Override in the child** — verbose but explicit:
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
export class ChildComponent extends BaseComponent {
|
|
637
|
+
override readonly isVisible = input<boolean>(false);
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**Prefer composition over inheritance:**
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
export class ChildComponent {
|
|
645
|
+
readonly isVisible = input<boolean>(false);
|
|
646
|
+
// shared behavior via inject() or a shared service
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
> ⚠️ This is why the Angular automatic migration tool is conservative around
|
|
651
|
+
> classes that are extended. Always audit your inheritance chain before migrating.
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## Testing signals
|
|
656
|
+
|
|
657
|
+
### Setting inputs in tests
|
|
658
|
+
|
|
659
|
+
Use `fixture.componentRef.setInput()` for both `input()` and `model()`:
|
|
660
|
+
|
|
661
|
+
```ts
|
|
662
|
+
fixture.componentRef.setInput('minDate', [2020, 7, 15]);
|
|
663
|
+
fixture.componentRef.setInput('isVisible', true);
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Writing to a `model()` from a test
|
|
667
|
+
|
|
668
|
+
```ts
|
|
669
|
+
component.isVisible.set(true);
|
|
670
|
+
fixture.detectChanges();
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Reading a signal value in an expectation
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
// 🔴 Wrong — reads the signal object, not the value
|
|
677
|
+
expect(component.isVisible).toBeTrue();
|
|
678
|
+
|
|
679
|
+
// 🟢 Correct — call the signal to read its value
|
|
680
|
+
expect(component.isVisible()).toBeTrue();
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### Effects and timing
|
|
684
|
+
|
|
685
|
+
Effects are asynchronous relative to `detectChanges()`.
|
|
686
|
+
Always use `await fixture.whenStable()` after triggering a signal change
|
|
687
|
+
that runs an effect:
|
|
688
|
+
|
|
689
|
+
```ts
|
|
690
|
+
component.isVisible.set(true);
|
|
691
|
+
await fixture.whenStable();
|
|
692
|
+
fixture.detectChanges();
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
When testing behavior triggered by `model()` changes (e.g. focus management),
|
|
696
|
+
reproduce the real toggle rather than calling internal methods directly:
|
|
697
|
+
|
|
698
|
+
```ts
|
|
699
|
+
// 🔴 Fragile — bypasses the effect
|
|
700
|
+
component.focusDefaultDay();
|
|
701
|
+
|
|
702
|
+
// 🟢 Robust — triggers the full reactive chain
|
|
703
|
+
component.isVisible.set(false);
|
|
704
|
+
await fixture.whenStable();
|
|
705
|
+
component.isVisible.set(true);
|
|
706
|
+
await fixture.whenStable();
|
|
707
|
+
fixture.detectChanges();
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
### Testing `untracked()` reads
|
|
711
|
+
|
|
712
|
+
`untracked()` is invisible to the test from the outside — there's nothing
|
|
713
|
+
to assert about it directly. What you _can_ and should assert is the
|
|
714
|
+
**dependency boundary** it creates: changing the untracked signal alone
|
|
715
|
+
should not cause the effect to re-run.
|
|
716
|
+
|
|
717
|
+
```ts
|
|
718
|
+
// component reads `this.config` via untracked() inside an effect
|
|
719
|
+
// that is otherwise only triggered by `this.userId`
|
|
720
|
+
|
|
721
|
+
component.config.set({ theme: 'dark' }); // untracked — should NOT trigger the effect
|
|
722
|
+
await fixture.whenStable();
|
|
723
|
+
expect(loggerSpy).not.toHaveBeenCalled();
|
|
724
|
+
|
|
725
|
+
component.userId.set('123'); // tracked — SHOULD trigger the effect
|
|
726
|
+
await fixture.whenStable();
|
|
727
|
+
expect(loggerSpy).toHaveBeenCalledWith('123', { theme: 'dark' });
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
## Common pitfalls
|
|
733
|
+
|
|
734
|
+
### 1. Using `model()` where `input()` is enough
|
|
735
|
+
|
|
736
|
+
`model()` implies two-way binding. If the child never writes back,
|
|
737
|
+
use `input()` — it is simpler and communicates intent clearly.
|
|
738
|
+
|
|
739
|
+
### 2. Using `computed()` where `model()` is needed
|
|
740
|
+
|
|
741
|
+
If the parent and child both need to write a value, `computed()` is read-only
|
|
742
|
+
and will not work. Use `model()`.
|
|
743
|
+
|
|
744
|
+
### 3. Calling `.set()` inside `computed()`
|
|
745
|
+
|
|
746
|
+
```ts
|
|
747
|
+
// 🔴 Runtime error
|
|
748
|
+
readonly x = computed(() => {
|
|
749
|
+
this.y.set(this.z() + 1); // forbidden
|
|
750
|
+
return this.z();
|
|
751
|
+
});
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### 4. Forgetting to call signals in templates
|
|
755
|
+
|
|
756
|
+
```html
|
|
757
|
+
<!-- 🔴 Renders the signal object -->
|
|
758
|
+
{{ mySignal }}
|
|
759
|
+
|
|
760
|
+
<!-- 🟢 Renders the value -->
|
|
761
|
+
{{ mySignal() }}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### 5. Spreading a `viewChildren()` signal without guarding
|
|
765
|
+
|
|
766
|
+
```ts
|
|
767
|
+
// 🔴 TypeError if the signal returns undefined during init
|
|
768
|
+
const items = [...this.daysButtons()];
|
|
769
|
+
|
|
770
|
+
// 🟢 Safe
|
|
771
|
+
const items = [...(this.daysButtons() ?? [])];
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### 6. Expecting `[binding]` to work with `model()`
|
|
775
|
+
|
|
776
|
+
`model()` only accepts `[(two-way)]` or the split form
|
|
777
|
+
`[value]="x" (valueChange)="x = $event"`.
|
|
778
|
+
A plain `[binding]` will produce a compiler error.
|
|
779
|
+
|
|
780
|
+
### 7. Over-tracking inside `effect()` — forgetting `untracked()`
|
|
781
|
+
|
|
782
|
+
An `effect()` tracks **every** signal it reads, not just the ones you
|
|
783
|
+
intended as triggers. Reading a signal "just to get its current value"
|
|
784
|
+
inside an effect still subscribes the effect to it, which can cause:
|
|
785
|
+
|
|
786
|
+
- redundant re-runs when an unrelated signal changes, or
|
|
787
|
+
- infinite loops when the effect reads and then (directly or indirectly)
|
|
788
|
+
writes to the same signal
|
|
789
|
+
|
|
790
|
+
```ts
|
|
791
|
+
// 🔴 `this.value()` is tracked, even though the intent was just to read
|
|
792
|
+
// the current value before writing a sanitized version of it
|
|
793
|
+
effect(() => {
|
|
794
|
+
const sanitized = this.sanitize(this.value());
|
|
795
|
+
this.value.set(sanitized); // may re-trigger this same effect
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// 🟢 Read without tracking; only intentional triggers remain tracked
|
|
799
|
+
effect(() => {
|
|
800
|
+
const current = untracked(() => this.value());
|
|
801
|
+
const sanitized = this.sanitize(current);
|
|
802
|
+
if (sanitized !== current) {
|
|
803
|
+
this.value.set(sanitized);
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### 8. Using `untracked()` instead of an equality check
|
|
809
|
+
|
|
810
|
+
`untracked()` controls what _triggers_ an effect — it does not make a write
|
|
811
|
+
inside the effect "safe" by itself. If the effect's only tracked dependencies
|
|
812
|
+
are, say, two visibility flags, and the write always replaces `value` with a
|
|
813
|
+
new object (even when nothing actually changed), the effect can still cause
|
|
814
|
+
unnecessary downstream updates (re-renders, parent `valueChange` emissions,
|
|
815
|
+
form dirty-state changes). Pair `untracked()` with an explicit equality
|
|
816
|
+
check before writing:
|
|
817
|
+
|
|
818
|
+
```ts
|
|
819
|
+
// 🔴 Always writes a new object, even when sanitization changed nothing
|
|
820
|
+
effect(() => {
|
|
821
|
+
this.showLine1();
|
|
822
|
+
const current = untracked(() => this.value());
|
|
823
|
+
this.value.set({ ...current }); // new reference every time, even if no-op
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// 🟢 Only writes when the sanitized result actually differs
|
|
827
|
+
effect(() => {
|
|
828
|
+
this.showLine1();
|
|
829
|
+
const current = untracked(() => this.value());
|
|
830
|
+
const sanitized = this.sanitize(current);
|
|
831
|
+
if (!this.isEqual(sanitized, current)) {
|
|
832
|
+
this.value.set(sanitized);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
### 9. Mutating a `model()`'s nested object instead of reassigning
|
|
838
|
+
|
|
839
|
+
This is the signals-era version of "mutating `@Input()` used to work."
|
|
840
|
+
Pre-signals, Zone.js-driven change detection re-read template expressions
|
|
841
|
+
on virtually every async event, so mutating a nested property and letting
|
|
842
|
+
CD "catch it eventually" appeared to work. Signals only propagate on
|
|
843
|
+
`.set()`/`.update()` — mutating `this.address().locality.name = 'x'` is
|
|
844
|
+
invisible to the signal, the template, and any `effect()`/`computed()`
|
|
845
|
+
depending on it.
|
|
846
|
+
|
|
847
|
+
```ts
|
|
848
|
+
// 🔴 Silent no-op from the signal's perspective
|
|
849
|
+
this.address().locality.name = 'newName';
|
|
850
|
+
|
|
851
|
+
// 🟢 New top-level reference — the signal and bound parent both update
|
|
852
|
+
this.address.update((current) => ({
|
|
853
|
+
...current,
|
|
854
|
+
locality: { ...current.locality, name: 'newName' },
|
|
855
|
+
}));
|
|
856
|
+
```
|