- {{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle
- } }}
+
+ {{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle} }}
+
diff --git a/src/addons/mod/workshop/components/assessment-strategy/assessment-strategy.ts b/src/addons/mod/workshop/components/assessment-strategy/assessment-strategy.ts
index 418f94540a1..aa163aed4d1 100644
--- a/src/addons/mod/workshop/components/assessment-strategy/assessment-strategy.ts
+++ b/src/addons/mod/workshop/components/assessment-strategy/assessment-strategy.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, Input, OnInit, ViewChild, ElementRef, Type, OnDestroy } from '@angular/core';
+import { Component, Input, OnInit, ElementRef, Type, OnDestroy, viewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { CoreError } from '@classes/errors/error';
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
@@ -69,7 +69,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
@Input({ required: true }) strategy!: string;
@Input({ transform: toBoolean }) edit = false;
- @ViewChild('assessmentForm') formElement!: ElementRef;
+ readonly formElement = viewChild.required('assessmentForm');
componentClass?: Type;
data: AddonModWorkshopAssessmentStrategyData = {
@@ -385,7 +385,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
);
}
- CoreForms.triggerFormSubmittedEvent(this.formElement, !!gradeUpdated, CoreSites.getCurrentSiteId());
+ CoreForms.triggerFormSubmittedEvent(this.formElement(), !!gradeUpdated, CoreSites.getCurrentSiteId());
const promises: Promise[] = [];
diff --git a/src/addons/mod/workshop/pages/assessment/assessment.ts b/src/addons/mod/workshop/pages/assessment/assessment.ts
index c672f50ab92..4152c04160c 100644
--- a/src/addons/mod/workshop/pages/assessment/assessment.ts
+++ b/src/addons/mod/workshop/pages/assessment/assessment.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnInit, OnDestroy, ViewChild, ElementRef, inject } from '@angular/core';
+import { Component, OnInit, OnDestroy, ElementRef, inject, viewChild } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { CoreCourse } from '@features/course/services/course';
import { CoreGradesHelper, CoreGradesMenuItem } from '@features/grades/services/grades-helper';
@@ -65,7 +65,7 @@ import { CoreErrorHelper } from '@services/error-helper';
})
export default class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy, CanLeave {
- @ViewChild('evaluateFormEl') formElement!: ElementRef;
+ readonly formElement = viewChild.required('evaluateFormEl');
assessment!: AddonModWorkshopSubmissionAssessmentWithFormData;
submission!: AddonModWorkshopSubmissionData;
@@ -184,7 +184,7 @@ export default class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy
// Show confirmation if some data has been modified.
await CoreAlerts.confirmLeaveWithChanges();
- CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId);
+ CoreForms.triggerFormCancelledEvent(this.formElement(), this.siteId);
return true;
}
@@ -434,7 +434,7 @@ export default class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy
grade,
);
- CoreForms.triggerFormSubmittedEvent(this.formElement, !!result, this.siteId);
+ CoreForms.triggerFormSubmittedEvent(this.formElement(), !!result, this.siteId);
const data: AddonModWorkshopAssessmentSavedChangedEventData = {
workshopId: this.workshopId,
diff --git a/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts b/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts
index 39ba14f5cb1..417469f7322 100644
--- a/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts
+++ b/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnInit, OnDestroy, ViewChild, ElementRef, inject } from '@angular/core';
+import { Component, OnInit, OnDestroy, ElementRef, inject, viewChild } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { CoreError } from '@classes/errors/error';
import { CoreCourseModuleData } from '@features/course/services/course-helper';
@@ -63,7 +63,7 @@ import { CorePromiseUtils } from '@singletons/promise-utils';
})
export default class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, CanLeave {
- @ViewChild('editFormEl') formElement!: ElementRef;
+ readonly formElement = viewChild.required('editFormEl');
module!: CoreCourseModuleData;
courseId!: number;
@@ -160,7 +160,7 @@ export default class AddonModWorkshopEditSubmissionPage implements OnInit, OnDes
CoreFileUploader.clearTmpFiles(this.submission.attachmentfiles);
}
- CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId);
+ CoreForms.triggerFormCancelledEvent(this.formElement(), this.siteId);
return true;
}
@@ -472,7 +472,7 @@ export default class AddonModWorkshopEditSubmissionPage implements OnInit, OnDes
}
}
- CoreForms.triggerFormSubmittedEvent(this.formElement, !!newSubmissionId, this.siteId);
+ CoreForms.triggerFormSubmittedEvent(this.formElement(), !!newSubmissionId, this.siteId);
const data: AddonModWorkshopSubmissionChangedEventData = {
workshopId: this.workshopId,
diff --git a/src/addons/mod/workshop/pages/index/index.html b/src/addons/mod/workshop/pages/index/index.html
index 5a2e086608f..9c2ddb04797 100644
--- a/src/addons/mod/workshop/pages/index/index.html
+++ b/src/addons/mod/workshop/pages/index/index.html
@@ -12,7 +12,7 @@
-
+
diff --git a/src/addons/mod/workshop/pages/index/index.ts b/src/addons/mod/workshop/pages/index/index.ts
index a57e39eab14..e3872edfebd 100644
--- a/src/addons/mod/workshop/pages/index/index.ts
+++ b/src/addons/mod/workshop/pages/index/index.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnInit, ViewChild } from '@angular/core';
+import { Component, OnInit, viewChild } from '@angular/core';
import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
import { CoreNavigator } from '@services/navigator';
import { AddonModWorkshopIndexComponent } from '../../components/index/index';
@@ -32,7 +32,7 @@ import { CoreSharedModule } from '@/core/shared.module';
export default class AddonModWorkshopIndexPage extends CoreCourseModuleMainActivityPage
implements OnInit {
- @ViewChild(AddonModWorkshopIndexComponent) activityComponent?: AddonModWorkshopIndexComponent;
+ readonly activityComponent = viewChild(AddonModWorkshopIndexComponent);
selectedGroup = 0;
diff --git a/src/addons/mod/workshop/pages/submission/submission.ts b/src/addons/mod/workshop/pages/submission/submission.ts
index 4d244958e35..f6105287142 100644
--- a/src/addons/mod/workshop/pages/submission/submission.ts
+++ b/src/addons/mod/workshop/pages/submission/submission.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnInit, OnDestroy, ViewChild, ElementRef, inject } from '@angular/core';
+import { Component, OnInit, OnDestroy, ElementRef, inject, viewChild } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { Params } from '@angular/router';
import { CoreCourse } from '@features/course/services/course';
@@ -79,9 +79,9 @@ import { CoreErrorHelper } from '@services/error-helper';
})
export default class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLeave {
- @ViewChild(AddonModWorkshopAssessmentStrategyComponent) assessmentStrategy?: AddonModWorkshopAssessmentStrategyComponent;
+ readonly assessmentStrategy = viewChild(AddonModWorkshopAssessmentStrategyComponent);
- @ViewChild('feedbackFormEl') formElement?: ElementRef;
+ readonly formElement = viewChild('feedbackFormEl');
module!: CoreCourseModuleData;
workshop!: AddonModWorkshopData;
@@ -191,7 +191,8 @@ export default class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy
* @returns Resolved if we can leave it, rejected if not.
*/
async canLeave(): Promise {
- const assessmentHasChanged = await this.assessmentStrategy?.hasDataChanged();
+ const assessmentStrategy = this.assessmentStrategy();
+ const assessmentHasChanged = await assessmentStrategy?.hasDataChanged();
if (this.forceLeave || (!this.hasEvaluationChanged() && !assessmentHasChanged)) {
return true;
}
@@ -199,8 +200,8 @@ export default class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy
// Show confirmation if some data has been modified.
await CoreAlerts.confirmLeaveWithChanges();
- CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId);
- CoreForms.triggerFormCancelledEvent(this.assessmentStrategy?.formElement, this.siteId);
+ CoreForms.triggerFormCancelledEvent(this.formElement(), this.siteId);
+ CoreForms.triggerFormCancelledEvent(assessmentStrategy?.formElement(), this.siteId);
return true;
}
@@ -520,10 +521,11 @@ export default class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy
* Save the assessment.
*/
async saveAssessment(): Promise {
- const assessmentHasChanged = await this.assessmentStrategy?.hasDataChanged();
+ const assessmentStrategy = this.assessmentStrategy();
+ const assessmentHasChanged = await assessmentStrategy?.hasDataChanged();
if (assessmentHasChanged) {
try {
- await this.assessmentStrategy?.saveAssessment();
+ await assessmentStrategy?.saveAssessment();
this.forceLeavePage();
} catch {
// Error, stay on the page.
@@ -576,7 +578,7 @@ export default class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy
inputData.published,
String(inputData.grade),
);
- CoreForms.triggerFormSubmittedEvent(this.formElement, !!result, this.siteId);
+ CoreForms.triggerFormSubmittedEvent(this.formElement(), !!result, this.siteId);
await AddonModWorkshop.invalidateSubmissionData(this.workshopId, this.submissionId).finally(() => {
const data: AddonModWorkshopSubmissionChangedEventData = {
diff --git a/src/addons/notes/components/add/add-modal.ts b/src/addons/notes/components/add/add-modal.ts
index 7c53c39d90e..8465fe8d0a9 100644
--- a/src/addons/notes/components/add/add-modal.ts
+++ b/src/addons/notes/components/add/add-modal.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import { AddonNotes, AddonNotesPublishState } from '@addons/notes/services/notes';
-import { Component, ViewChild, ElementRef, Input } from '@angular/core';
+import { Component, ElementRef, Input, viewChild } from '@angular/core';
import { CoreSites } from '@services/sites';
import { CoreForms } from '@singletons/form';
import { ModalController } from '@singletons';
@@ -34,7 +34,7 @@ import { CoreAlerts } from '@services/overlays/alerts';
})
export class AddonNotesAddComponent {
- @ViewChild('itemEdit') formElement?: ElementRef;
+ readonly formElement = viewChild('itemEdit');
@Input({ required: true }) courseId!: number;
@Input() userId?: number;
@@ -60,7 +60,7 @@ export class AddonNotesAddComponent {
this.userId = this.userId || CoreSites.getCurrentSiteUserId();
const sent = await AddonNotes.addNote(this.userId, this.courseId, this.type, this.text);
- CoreForms.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.getCurrentSiteId());
+ CoreForms.triggerFormSubmittedEvent(this.formElement(), sent, CoreSites.getCurrentSiteId());
ModalController.dismiss({ type: this.type, sent: true }).finally(() => {
CoreToasts.show({
@@ -81,7 +81,7 @@ export class AddonNotesAddComponent {
* Close modal.
*/
closeModal(): void {
- CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
+ CoreForms.triggerFormCancelledEvent(this.formElement(), CoreSites.getCurrentSiteId());
ModalController.dismiss({ type: this.type });
}
diff --git a/src/addons/notes/pages/list/list.ts b/src/addons/notes/pages/list/list.ts
index c7b48e54473..519748d270b 100644
--- a/src/addons/notes/pages/list/list.ts
+++ b/src/addons/notes/pages/list/list.ts
@@ -17,7 +17,7 @@ import { AddonNotesAddModalReturn } from '@addons/notes/components/add/add-modal
import { AddonNotes, AddonNotesNoteFormatted, AddonNotesPublishState } from '@addons/notes/services/notes';
import { AddonNotesOffline } from '@addons/notes/services/notes-offline';
import { AddonNotesSync } from '@addons/notes/services/notes-sync';
-import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
+import { Component, OnDestroy, OnInit, viewChild } from '@angular/core';
import { CoreAnimations } from '@components/animations';
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import { IonContent } from '@ionic/angular';
@@ -49,7 +49,7 @@ import { CoreSharedModule } from '@/core/shared.module';
})
export default class AddonNotesListPage implements OnInit, OnDestroy {
- @ViewChild(IonContent) content?: IonContent;
+ readonly content = viewChild(IonContent);
courseId!: number;
userId?: number;
@@ -92,7 +92,7 @@ export default class AddonNotesListPage implements OnInit, OnDestroy {
this.refreshIcon = CoreConstants.ICON_LOADING;
this.syncIcon = CoreConstants.ICON_LOADING;
- this.content?.scrollToTop();
+ this.content()?.scrollToTop();
this.fetchNotes(false);
}
}, CoreSites.getCurrentSiteId());
diff --git a/src/addons/notifications/pages/list/list.ts b/src/addons/notifications/pages/list/list.ts
index 5ddaae751ca..cce8e29da5f 100644
--- a/src/addons/notifications/pages/list/list.ts
+++ b/src/addons/notifications/pages/list/list.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
+import { AfterViewInit, Component, OnDestroy, viewChild } from '@angular/core';
import { Subscription } from 'rxjs';
import { CoreUtils } from '@singletons/utils';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
@@ -52,7 +52,7 @@ import { ADDONS_NOTIFICATIONS_READ_CHANGED_EVENT, ADDONS_NOTIFICATIONS_READ_CRON
})
export default class AddonNotificationsListPage implements AfterViewInit, OnDestroy {
- @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
+ readonly splitView = viewChild.required(CoreSplitViewComponent);
notifications!: CoreListItemsManager;
fetchMoreNotificationsFailed = false;
canMarkAllNotificationsAsRead = false;
@@ -96,7 +96,7 @@ export default class AddonNotificationsListPage implements AfterViewInit, OnDest
async ngAfterViewInit(): Promise {
await this.fetchInitialNotifications();
- this.notifications.start(this.splitView);
+ this.notifications.start(this.splitView());
this.cronObserver = CoreEvents.on(ADDONS_NOTIFICATIONS_READ_CRON_EVENT, () => {
if (!this.isCurrentView) {
diff --git a/src/addons/privatefiles/components/file-actions/file-actions.html b/src/addons/privatefiles/components/file-actions/file-actions.html
index c0999174853..966faab3300 100644
--- a/src/addons/privatefiles/components/file-actions/file-actions.html
+++ b/src/addons/privatefiles/components/file-actions/file-actions.html
@@ -1,10 +1,10 @@
-
+
- {{ filename }}
+ {{ filename() }}
@@ -16,7 +16,7 @@
- @if (isDownloaded) {
+ @if (isDownloaded()) {
{{ 'core.removedownloadeddata' | translate }}
@@ -25,6 +25,6 @@
- {{ 'core.delete' | translate }}
+ {{ 'core.delete' | translate }}
diff --git a/src/addons/privatefiles/components/file-actions/file-actions.ts b/src/addons/privatefiles/components/file-actions/file-actions.ts
index 677b9b067dd..42afc30f6d0 100644
--- a/src/addons/privatefiles/components/file-actions/file-actions.ts
+++ b/src/addons/privatefiles/components/file-actions/file-actions.ts
@@ -13,12 +13,12 @@
// limitations under the License.
import { CoreSharedModule } from '@/core/shared.module';
-import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { CoreModalComponent } from '@classes/modal-component';
@Component({
selector: 'addon-privatefiles-file-actions',
- styleUrl: './file-actions.scss',
+ styleUrl: 'file-actions.scss',
templateUrl: 'file-actions.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
@@ -27,9 +27,9 @@ import { CoreModalComponent } from '@classes/modal-component';
})
export class AddonPrivateFilesFileActionsComponent extends CoreModalComponent {
- @Input({ required: false }) isDownloaded = false;
- @Input({ required: true }) filename = '';
- @Input({ required: true }) icon = '';
+ readonly isDownloaded = input(false);
+ readonly filename = input.required();
+ readonly icon = input.required();
}
diff --git a/src/addons/qtype/ddmarker/component/ddmarker.ts b/src/addons/qtype/ddmarker/component/ddmarker.ts
index e3d77518950..cc8df8393cc 100644
--- a/src/addons/qtype/ddmarker/component/ddmarker.ts
+++ b/src/addons/qtype/ddmarker/component/ddmarker.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnDestroy, ElementRef, ViewChild } from '@angular/core';
+import { Component, OnDestroy, ElementRef, viewChild } from '@angular/core';
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
@@ -38,7 +38,7 @@ export class AddonQtypeDdMarkerComponent
extends CoreQuestionBaseComponent
implements OnDestroy {
- @ViewChild('questiontext') questionTextEl?: ElementRef;
+ readonly questionTextEl = viewChild('questiontext');
protected questionInstance?: AddonQtypeDdMarkerQuestion;
protected dropZones: unknown[] = []; // The drop zones received in the init object of the question.
@@ -166,8 +166,9 @@ export class AddonQtypeDdMarkerComponent
);
}
- if (this.questionTextEl) {
- await CoreWait.waitForImages(this.questionTextEl.nativeElement);
+ const questionTextEl = this.questionTextEl();
+ if (questionTextEl) {
+ await CoreWait.waitForImages(questionTextEl.nativeElement);
}
// Create the instance.
diff --git a/src/addons/qtype/ddwtos/component/ddwtos.ts b/src/addons/qtype/ddwtos/component/ddwtos.ts
index ece76bc3bca..e3983ca6b00 100644
--- a/src/addons/qtype/ddwtos/component/ddwtos.ts
+++ b/src/addons/qtype/ddwtos/component/ddwtos.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnDestroy, ElementRef, ViewChild } from '@angular/core';
+import { Component, OnDestroy, ElementRef, viewChild } from '@angular/core';
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
@@ -34,7 +34,7 @@ import { CoreSharedModule } from '@/core/shared.module';
})
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnDestroy {
- @ViewChild('questiontext') questionTextEl?: ElementRef;
+ readonly questionTextEl = viewChild('questiontext');
protected questionInstance?: AddonQtypeDdwtosQuestion;
protected inputIds: string[] = []; // Ids of the inputs of the question (where the answers will be stored).
@@ -134,8 +134,9 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent {
+ CoreSubscriptions.once(this.outlet().activateEvents, async () => {
await CorePlatform.ready();
this.logger.debug('Hide splash screen');
diff --git a/src/core/components/chart/chart.ts b/src/core/components/chart/chart.ts
index 51d1ca1e462..c1f918a2672 100644
--- a/src/core/components/chart/chart.ts
+++ b/src/core/components/chart/chart.ts
@@ -14,7 +14,7 @@
import { ContextLevel } from '@/core/constants';
import { toBoolean } from '@/core/transforms/boolean';
-import { Component, Input, OnDestroy, OnInit, ElementRef, OnChanges, ViewChild, SimpleChange } from '@angular/core';
+import { Component, Input, OnDestroy, OnInit, ElementRef, OnChanges, SimpleChange, viewChild } from '@angular/core';
import { CoreFilter } from '@features/filter/services/filter';
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
import { LegendOptions, ChartTypeRegistry, ChartType, type Chart, LegendItem } from 'chart.js';
@@ -62,7 +62,7 @@ export class CoreChartComponent implements OnDestroy, OnInit, OnChanges {
@Input() contextInstanceId?: number; // The instance ID related to the context.
@Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters.
@Input({ transform: toBoolean }) wsNotFiltered = false; // If true it means the WS didn't filter the labels for some reason.
- @ViewChild('canvas') canvas?: ElementRef;
+ readonly canvas = viewChild>('canvas');
chart?: ChartWithLegend;
legendItems: LegendItem[] = [];
@@ -99,7 +99,7 @@ export class CoreChartComponent implements OnDestroy, OnInit, OnChanges {
// Format labels if needed.
await this.formatLabels();
- const context = this.canvas?.nativeElement.getContext('2d');
+ const context = this.canvas()?.nativeElement.getContext('2d');
if (!context) {
return;
}
diff --git a/src/core/components/iframe/iframe.ts b/src/core/components/iframe/iframe.ts
index e49e1407e46..314aeb7e345 100644
--- a/src/core/components/iframe/iframe.ts
+++ b/src/core/components/iframe/iframe.ts
@@ -16,13 +16,15 @@ import {
Component,
Input,
Output,
- ViewChild,
ElementRef,
EventEmitter,
OnChanges,
SimpleChange,
OnDestroy,
inject,
+ viewChild,
+ computed,
+ effect,
} from '@angular/core';
import { SafeResourceUrl } from '@angular/platform-browser';
@@ -64,11 +66,8 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
static loadingTimeout = 15000;
- @ViewChild('iframe') set iframeElement(iframeRef: ElementRef | undefined) {
- this.iframe = iframeRef?.nativeElement;
-
- this.initIframeElement();
- }
+ readonly iframeRef = viewChild>('iframe');
+ readonly iframe = computed(() => this.iframeRef()?.nativeElement);
@Input() src?: string;
@Input() id: string | null = null;
@@ -78,7 +77,7 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
@Input({ transform: toBoolean }) showFullscreenOnToolbar = false;
@Input({ transform: toBoolean }) autoFullscreenOnRotate = false;
@Input({ transform: toBoolean }) allowAutoLogin = true;
- @Output() loaded: EventEmitter = new EventEmitter();
+ @Output() loaded = new EventEmitter();
loading?: boolean;
safeUrl?: SafeResourceUrl;
@@ -89,7 +88,6 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
initialized = false;
protected fullScreenInitialized = false;
- protected iframe?: HTMLIFrameElement;
protected style?: HTMLStyleElement;
protected orientationObs?: CoreEventObserver;
protected navSubscription?: Subscription;
@@ -98,10 +96,12 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
protected element: HTMLElement = inject(ElementRef).nativeElement;
constructor() {
- this.loaded = new EventEmitter();
-
// Listen for messages from the iframe.
window.addEventListener('message', this.messageListenerFunction = (event) => this.onIframeMessage(event));
+
+ effect(() => {
+ this.initIframeElement(this.iframe());
+ });
}
/**
@@ -208,20 +208,22 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
/**
* Initialize things related to the iframe element.
+ *
+ * @param iframe Iframe element.
*/
- protected initIframeElement(): void {
- if (!this.iframe) {
+ protected initIframeElement(iframe?: HTMLIFrameElement): void {
+ if (!iframe) {
return;
}
- CoreIframe.treatFrame(this.iframe, false);
+ CoreIframe.treatFrame(iframe, false);
- this.iframe.addEventListener('load', () => {
+ iframe.addEventListener('load', () => {
this.loading = false;
- this.loaded.emit(this.iframe); // Notify iframe was loaded.
+ this.loaded.emit(iframe); // Notify iframe was loaded.
});
- this.iframe.addEventListener('error', () => {
+ iframe.addEventListener('error', () => {
this.loading = false;
CoreAlerts.showError(Translate.instant('core.errorloadingcontent'));
});
@@ -332,6 +334,9 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
/**
* Toggle fullscreen mode.
+ *
+ * @param enable Whether to enable or disable fullscreen mode. If not set, it will toggle the current state.
+ * @param notifyIframe Whether to notify the iframe about the change. Defaults to true.
*/
toggleFullscreen(enable?: boolean, notifyIframe = true): void {
if (enable !== undefined) {
@@ -352,8 +357,9 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
document.body.classList.toggle('core-iframe-fullscreen', this.fullscreen);
- if (notifyIframe && this.iframe) {
- this.iframe.contentWindow?.postMessage(
+ const iframe = this.iframe();
+ if (notifyIframe && iframe) {
+ iframe.contentWindow?.postMessage(
this.fullscreen ? 'enterFullScreen' : 'exitFullScreen',
'*',
);
@@ -364,7 +370,6 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
* Treat an iframe message event.
*
* @param event Event.
- * @returns Promise resolved when done.
*/
protected async onIframeMessage(event: MessageEvent): Promise {
if (event.data == 'enterFullScreen' && this.showFullscreenOnToolbar && !this.fullscreen) {
diff --git a/src/core/components/input-errors/input-errors.ts b/src/core/components/input-errors/input-errors.ts
index 827e6b74582..e4ea0de39b3 100644
--- a/src/core/components/input-errors/input-errors.ts
+++ b/src/core/components/input-errors/input-errors.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, ElementRef, HostBinding, Input, OnChanges, OnInit, SimpleChange, inject } from '@angular/core';
+import { Component, ElementRef, Input, OnChanges, OnInit, SimpleChange, inject } from '@angular/core';
import { FormControl } from '@angular/forms';
import { CoreBaseModule } from '@/core/base.module';
import { CoreFaIconDirective } from '@directives/fa-icon';
@@ -43,6 +43,10 @@ import { CoreFaIconDirective } from '@directives/fa-icon';
CoreBaseModule,
CoreFaIconDirective,
],
+ host: {
+ '[class.has-errors]': '(control && control.dirty && !control.valid) || !!errorText',
+ '[role]': '"alert"',
+ },
})
export class CoreInputErrorsComponent implements OnInit, OnChanges {
@@ -53,13 +57,6 @@ export class CoreInputErrorsComponent implements OnInit, OnChanges {
protected hostElement: HTMLElement = inject(ElementRef).nativeElement;
- @HostBinding('class.has-errors')
- get hasErrors(): boolean {
- return (this.control && this.control.dirty && !this.control.valid) || !!this.errorText;
- }
-
- @HostBinding('role') role = 'alert';
-
/**
* Initialize some common errors if they aren't set.
*/
diff --git a/src/core/components/loading/core-loading.html b/src/core/components/loading/core-loading.html
index 5be19356648..a5e457b9868 100644
--- a/src/core/components/loading/core-loading.html
+++ b/src/core/components/loading/core-loading.html
@@ -1,16 +1,16 @@
-@if (!hideUntil) {
-
- @if (!placeholderType) {
+@if (!hideUntil()) {
+
+ @if (!placeholderType()) {
- @if (message) {
-
{{message}}
+ @if (message()) {
+
{{message()}}
}
} @else {
-
- @if (placeholderType === 'free') {
+
+ @if (placeholderType() === 'free') {
- } @else if (placeholderType === 'listwithicon' || placeholderType === 'listwithavatar') {
- @for (i of placeholderLimit|coreTimes; track i) {
+ } @else if (placeholderType() === 'listwithicon' || placeholderType() === 'listwithavatar') {
+ @for (i of placeholderLimit()|coreTimes; track i) {
@@ -25,22 +25,22 @@
}
- } @else if (placeholderType === 'imageandboxes') {
+ } @else if (placeholderType() === 'imageandboxes') {
- @for (i of placeholderLimit|coreTimes; track i) {
+ @for (i of placeholderLimit()|coreTimes; track i) {
+ [ngStyle]="{'width': placeholderWidth(), 'height': placeholderHeight()}" />
}
} @else {
- @for (i of placeholderLimit|coreTimes; track i) {
+ @for (i of placeholderLimit()|coreTimes; track i) {
+ [ngStyle]="{'width': placeholderWidth(), 'height': placeholderHeight()}" />
}
}
}
}
-@if (loaded) {
+@if (hideUntil()) {
}
diff --git a/src/core/components/loading/loading.ts b/src/core/components/loading/loading.ts
index 13005c276bb..c25db1abfac 100644
--- a/src/core/components/loading/loading.ts
+++ b/src/core/components/loading/loading.ts
@@ -14,15 +14,11 @@
import {
Component,
- Input,
- OnInit,
- OnChanges,
- SimpleChange,
ElementRef,
- AfterViewInit,
OnDestroy,
- HostBinding,
inject,
+ input,
+ effect,
} from '@angular/core';
import { CoreUtils } from '@singletons/utils';
import { CoreAnimations } from '@components/animations';
@@ -61,59 +57,46 @@ import { CoreTimesPipe } from '@pipes/times';
templateUrl: 'core-loading.html',
styleUrl: 'loading.scss',
animations: [CoreAnimations.SHOW_HIDE],
- imports: [CoreBaseModule, CoreTimesPipe],
+ imports: [
+ CoreBaseModule,
+ CoreTimesPipe,
+ ],
+ host: {
+ '[class.core-loading-inline]': '!fullscreen()',
+ '[class.core-loading-loaded]': 'hideUntil()',
+ '[attr.aria-busy]': '!hideUntil()',
+ '[attr.id]': 'uniqueId',
+ '[style.--loading-inline-min-height]': 'placeholderHeight()',
+ },
})
-export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, AsyncDirective, OnDestroy {
+export class CoreLoadingComponent implements AsyncDirective, OnDestroy {
- @Input({ transform: toBoolean }) hideUntil = false; // Determine when should the contents be shown.
- @Input() message?: string; // Message to show while loading.
- @Input({ transform: toBoolean }) fullscreen = true; // Use the whole screen.
- @Input() placeholderType?:
- 'row' | 'column' | 'rowwrap' | 'columnwrap' | 'listwithicon' | 'listwithavatar' | 'imageandboxes' | 'free';
+ readonly hideUntil = input(false, { transform: toBoolean }); // Determine when should the contents be shown.
+ readonly message = input
(Translate.instant('core.loading')); // Message to show while loading.
+ readonly fullscreen = input(true, { transform: toBoolean }); // Use the whole screen.
- @Input() placeholderWidth?: string;
- @Input() placeholderHeight?: string;
- @Input() placeholderLimit = 20;
-
- uniqueId: string;
- loaded = false;
+ readonly placeholderType = input();
+ readonly placeholderWidth = input();
+ readonly placeholderHeight = input();
+ readonly placeholderLimit = input(20);
protected element: HTMLElement = inject(ElementRef).nativeElement;
protected lastScrollPosition = Promise.resolve(undefined);
protected onReadyPromise = new CorePromisedValue();
protected mutationObserver: MutationObserver;
- @HostBinding('class.core-loading-inline')
- get inlineClass(): boolean {
- return !this.fullscreen;
- }
-
- @HostBinding('attr.aria-busy')
- get ariaBusy(): string {
- return this.loaded ? 'false' : 'true';
- }
-
- @HostBinding('style.--loading-inline-min-height')
- get minHeight(): string | undefined {
- return this.placeholderHeight;
- }
-
- @HostBinding('class.core-loading-loaded')
- get loadedClass(): boolean {
- return this.loaded;
- }
+ protected uniqueId: string;
constructor() {
CoreDirectivesRegistry.register(this.element, this);
// Calculate the unique ID.
this.uniqueId = `core-loading-content-${CoreUtils.getUniqueId('CoreLoadingComponent')}`;
- this.element.setAttribute('id', this.uniqueId);
// Throttle 20ms to let mutations resolve.
const throttleMutation = CoreUtils.throttle(async () => {
await CoreWait.nextTick();
- if (!this.loaded) {
+ if (!this.hideUntil()) {
return;
}
@@ -130,32 +113,19 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, A
throttleMutation();
}
});
- }
- /**
- * @inheritdoc
- */
- ngOnInit(): void {
- if (!this.message) {
- // Default loading message.
- this.message = Translate.instant('core.loading');
- }
- }
-
- /**
- * @inheritdoc
- */
- ngAfterViewInit(): void {
- this.changeState(this.hideUntil);
- }
-
- /**
- * @inheritdoc
- */
- ngOnChanges(changes: { [name: string]: SimpleChange }): void {
- if (changes.hideUntil) {
- this.changeState(this.hideUntil);
- }
+ effect(() => {
+ if (this.hideUntil()) {
+ this.onReadyPromise.resolve();
+ this.restoreScrollPosition();
+ if (CorePlatform.isIOS()) {
+ this.mutationObserver.observe(this.element, { childList: true });
+ }
+ } else {
+ this.lastScrollPosition = this.getScrollPosition();
+ this.mutationObserver.disconnect();
+ }
+ });
}
/**
@@ -165,38 +135,13 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, A
this.mutationObserver.disconnect();
}
- /**
- * Change loaded state.
- *
- * @param loaded True to load, false otherwise.
- */
- async changeState(loaded: boolean): Promise {
- if (this.loaded === loaded) {
- return;
- }
-
- this.loaded = loaded;
-
- if (loaded) {
- this.onReadyPromise.resolve();
- this.restoreScrollPosition();
- if (CorePlatform.isIOS()) {
- this.mutationObserver.observe(this.element, { childList: true });
- }
- } else {
- this.lastScrollPosition = this.getScrollPosition();
- this.mutationObserver.disconnect();
- }
- }
-
/**
* Gets current scroll position.
*
* @returns the scroll position or undefined if scroll not found.
*/
protected async getScrollPosition(): Promise {
- const content = this.element.closest('ion-content');
- const scrollElement = await content?.getScrollElement();
+ const scrollElement = await this.getScrollElement();
return scrollElement?.scrollTop;
}
@@ -211,12 +156,26 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, A
return;
}
- const content = this.element.closest('ion-content');
- const scrollElement = await content?.getScrollElement();
+ const scrollElement = await this.getScrollElement();
scrollElement?.scrollTo({ top: scrollPosition });
}
+ /**
+ * Gets the scroll element to use.
+ *
+ * @returns The scroll element or undefined if not found.
+ */
+ protected async getScrollElement(): Promise {
+ const content = this.element.closest('ion-content');
+
+ if (!content || 'getScrollElement' in content === false) {
+ return undefined;
+ }
+
+ return await content.getScrollElement();
+ }
+
/**
* @inheritdoc
*/
@@ -225,3 +184,6 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, A
}
}
+
+type CoreLoadingPlaceholderTypes =
+ 'row' | 'column' | 'rowwrap' | 'columnwrap' | 'listwithicon' | 'listwithavatar' | 'imageandboxes' | 'free';
diff --git a/src/core/components/local-file/local-file.ts b/src/core/components/local-file/local-file.ts
index 68a65d6123c..5301002ff46 100644
--- a/src/core/components/local-file/local-file.ts
+++ b/src/core/components/local-file/local-file.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, Input, Output, OnInit, EventEmitter, ViewChild, ElementRef } from '@angular/core';
+import { Component, Input, Output, OnInit, EventEmitter, ElementRef, viewChild } from '@angular/core';
import { FileEntry } from '@awesome-cordova-plugins/file/ngx';
import { CoreIonLoadingElement } from '@classes/ion-loading';
@@ -66,7 +66,7 @@ export class CoreLocalFileComponent implements OnInit {
@Output() onRename = new EventEmitter<{ file: FileEntry }>(); // Will notify when the file is renamed.
@Output() onClick = new EventEmitter(); // Will notify when the file is clicked. Only if overrideClick is true.
- @ViewChild('nameForm') formElement?: ElementRef;
+ readonly formElement = viewChild('nameForm');
fileName?: string;
fileIcon?: string;
@@ -194,7 +194,7 @@ export class CoreLocalFileComponent implements OnInit {
if (newName == this.file.name) {
// Name hasn't changed, stop.
this.editMode = false;
- CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
+ CoreForms.triggerFormCancelledEvent(this.formElement(), CoreSites.getCurrentSiteId());
return;
}
@@ -214,7 +214,7 @@ export class CoreLocalFileComponent implements OnInit {
// File doesn't exist, move it.
const fileEntry = await CoreFile.moveFile(this.relativePath, newPath);
- CoreForms.triggerFormSubmittedEvent(this.formElement, false, CoreSites.getCurrentSiteId());
+ CoreForms.triggerFormSubmittedEvent(this.formElement(), false, CoreSites.getCurrentSiteId());
this.editMode = false;
this.file = fileEntry;
diff --git a/src/core/components/message/message.ts b/src/core/components/message/message.ts
index 23f5bbd0fee..a65fadae026 100644
--- a/src/core/components/message/message.ts
+++ b/src/core/components/message/message.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import { ContextLevel } from '@/core/constants';
-import { Component, HostBinding, computed, input, output } from '@angular/core';
+import { Component, computed, input, output } from '@angular/core';
import { CoreAnimations } from '@components/animations';
import { CoreSites } from '@services/sites';
import { CoreText } from '@singletons/text';
@@ -46,6 +46,7 @@ import { CoreFormatDatePipe } from '@pipes/format-date';
host: {
'[@coreSlideInOut]': 'isMine() ? "" : "fromLeft"',
'[class.is-mine]': 'isMine()',
+ '[class.no-user]': '!message()?.showUserData',
},
})
export class CoreMessageComponent {
@@ -68,10 +69,6 @@ export class CoreMessageComponent {
protected readonly userId = computed(() => this.user()?.userid || this.user()?.id);
protected readonly isMine = computed(() => this.userId() === CoreSites.getCurrentSiteUserId());
- @HostBinding('class.no-user') get showUser(): boolean {
- return !this.message()?.showUserData;
- }
-
/**
* Emits the delete action.
*
diff --git a/src/core/components/navbar-buttons/navbar-buttons.ts b/src/core/components/navbar-buttons/navbar-buttons.ts
index f632e99a671..8baabcd0dfb 100644
--- a/src/core/components/navbar-buttons/navbar-buttons.ts
+++ b/src/core/components/navbar-buttons/navbar-buttons.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, Input, OnInit, OnDestroy, ElementRef, ViewContainerRef, ViewChild, inject } from '@angular/core';
+import { Component, Input, OnInit, OnDestroy, ElementRef, ViewContainerRef, inject, viewChild } from '@angular/core';
import { CoreLogger } from '@singletons/logger';
import { CoreContextMenuComponent } from '../context-menu/context-menu';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
@@ -48,7 +48,7 @@ const BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden';
})
export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
- @ViewChild('contextMenuContainer', { read: ViewContainerRef }) container!: ViewContainerRef;
+ readonly container = viewChild.required('contextMenuContainer', { read: ViewContainerRef });
// If the hidden input is true, hide all buttons.
// eslint-disable-next-line @angular-eslint/no-input-rename
@@ -175,7 +175,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
* @returns Created component.
*/
protected createMainContextMenu(): CoreContextMenuComponent {
- const componentRef = this.container.createComponent(CoreContextMenuComponent);
+ const componentRef = this.container().createComponent(CoreContextMenuComponent);
this.createdMainContextMenuElement = componentRef.location.nativeElement;
diff --git a/src/core/components/show-password/core-show-password.html b/src/core/components/show-password/core-show-password.html
index cb863156327..67f0640bf77 100644
--- a/src/core/components/show-password/core-show-password.html
+++ b/src/core/components/show-password/core-show-password.html
@@ -1,4 +1,4 @@
-@if (!this.ionInput) {
-
+@if (!ionInput()) {
+
}
diff --git a/src/core/components/show-password/show-password.ts b/src/core/components/show-password/show-password.ts
index f60092e7b20..64c6b3dec8e 100644
--- a/src/core/components/show-password/show-password.ts
+++ b/src/core/components/show-password/show-password.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, AfterViewInit, Input, ContentChild, ViewEncapsulation } from '@angular/core';
+import { Component, ViewEncapsulation, input, contentChild, effect } from '@angular/core';
import { IonInput } from '@ionic/angular';
import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
@@ -51,43 +51,42 @@ import { CoreBaseModule } from '@/core/base.module';
encapsulation: ViewEncapsulation.None,
imports: [CoreBaseModule],
})
-export class CoreShowPasswordComponent implements AfterViewInit {
+export class CoreShowPasswordComponent {
/**
* @deprecated since 4.5. Not used anymore.
*/
- @Input() initialShown = '';
+ readonly initialShown = input('');
/**
* @deprecated since 4.4. Not used anymore.
*/
- @Input() name = '';
+ readonly name = input('');
/**
* @deprecated since 4.4. Use slotted solution instead.
*/
- @ContentChild(IonInput) ionInput?: IonInput | HTMLIonInputElement;
+ readonly ionInput = contentChild(IonInput);
- /**
- * @inheritdoc
- */
- async ngAfterViewInit(): Promise {
+ constructor() {
CoreLogger.getInstance('CoreShowPasswordComponent')
.warn('Deprecated component, use instead.');
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- if (!this.ionInput) {
- return;
- }
+ effect(async () => {
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
+ const ionInput = this.ionInput();
+ if (!ionInput) {
+ return;
+ }
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- const input = await CorePromiseUtils.ignoreErrors(this.ionInput.getInputElement());
- if (!input) {
- return;
- }
+ const input = await CorePromiseUtils.ignoreErrors(ionInput.getInputElement());
+ if (!input) {
+ return;
+ }
- const toggle = convertTextToHTMLElement('');
- input.parentElement?.appendChild(toggle.children[0]);
+ const toggle = convertTextToHTMLElement('');
+ input.parentElement?.appendChild(toggle.children[0]);
+ });
}
}
diff --git a/src/core/components/split-view/split-view.ts b/src/core/components/split-view/split-view.ts
index 8eaf066b951..5ad1ac4439b 100644
--- a/src/core/components/split-view/split-view.ts
+++ b/src/core/components/split-view/split-view.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild, inject } from '@angular/core';
+import { AfterViewInit, Component, ElementRef, Input, OnDestroy, inject, viewChild } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { IonContent, IonRouterOutlet } from '@ionic/angular';
import { CoreScreen } from '@services/screen';
@@ -42,8 +42,8 @@ const disabledScrollClass = 'disable-scroll-y';
})
export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
- @ViewChild(IonContent) menuContent!: IonContent;
- @ViewChild(IonRouterOutlet) contentOutlet!: IonRouterOutlet;
+ readonly menuContent = viewChild.required(IonContent);
+ readonly contentOutlet = viewChild.required(IonRouterOutlet);
@Input() placeholderText = 'core.emptysplit';
@Input() mode?: CoreSplitViewMode;
isNested = false;
@@ -62,7 +62,7 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
}
get outletActivated(): boolean {
- return this.contentOutlet.isActivated;
+ return this.contentOutlet().isActivated;
}
get outletRouteObservable(): Observable {
@@ -82,8 +82,8 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
this.disableScrollOnParent();
this.subscriptions = [
- this.contentOutlet.activateEvents.subscribe(() => this.updateOutletRoute()),
- this.contentOutlet.deactivateEvents.subscribe(() => this.updateOutletRoute()),
+ this.contentOutlet().activateEvents.subscribe(() => this.updateOutletRoute()),
+ this.contentOutlet().deactivateEvents.subscribe(() => this.updateOutletRoute()),
CoreScreen.layoutObservable.subscribe(() => this.updateClasses()),
];
@@ -103,7 +103,8 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
* Update outlet status.
*/
private updateOutletRoute(): void {
- const outletRoute = this.contentOutlet.isActivated ? this.contentOutlet.activatedRoute.snapshot : null;
+ const contentOutlet = this.contentOutlet();
+ const outletRoute = contentOutlet.isActivated ? contentOutlet.activatedRoute.snapshot : null;
this.updateClasses();
@@ -116,7 +117,7 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
private updateClasses(): void {
const classes: string[] = [this.getCurrentMode()];
- if (this.contentOutlet.isActivated) {
+ if (this.contentOutlet().isActivated) {
classes.push('outlet-activated');
}
@@ -143,7 +144,7 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
}
if (CoreScreen.isMobile) {
- return this.contentOutlet.isActivated
+ return this.contentOutlet().isActivated
? CoreSplitViewMode.CONTENT_ONLY
: CoreSplitViewMode.MENU_ONLY;
}
diff --git a/src/core/components/tabs-outlet/tabs-outlet.ts b/src/core/components/tabs-outlet/tabs-outlet.ts
index c457635c2d9..0db1876c252 100644
--- a/src/core/components/tabs-outlet/tabs-outlet.ts
+++ b/src/core/components/tabs-outlet/tabs-outlet.ts
@@ -18,7 +18,7 @@ import {
OnChanges,
OnDestroy,
AfterViewInit,
- ViewChild,
+ viewChild,
SimpleChange,
CUSTOM_ELEMENTS_SCHEMA,
ComponentRef,
@@ -78,7 +78,7 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent CoreTabsOutletComponent.formatTab(tab));
} }) tabs: CoreTabsOutletTabWithId[] = [];
- @ViewChild(IonTabs) protected ionTabs!: IonTabs;
+ readonly ionTabs = viewChild.required(IonTabs);
protected lastActiveComponent?: Partial;
protected existsInNavigationStack = false;
@@ -109,7 +109,7 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent {
+ this.subscriptions.push(this.ionTabs().outlet.stackDidChange.subscribe(async (stackEvent: StackDidChangeEvent) => {
if (!this.isCurrentView) {
return;
}
@@ -130,8 +130,8 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent {
- this.lastActiveComponent = this.ionTabs.outlet.component;
+ this.subscriptions.push(this.ionTabs().outlet.activateEvents.subscribe(() => {
+ this.lastActiveComponent = this.ionTabs().outlet.component;
}));
}
@@ -155,8 +155,8 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent).ionViewDidEnter?.();
+ if (this.existsInNavigationStack && this.ionTabs().outlet.isActivated) {
+ (this.ionTabs().outlet.component as Partial).ionViewDidEnter?.();
}
// After the view has entered for the first time, we can assume that it'll always be in the navigation stack
@@ -204,7 +204,7 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent {
const instance = CoreDirectivesRegistry.resolve(element, CoreNavBarButtonsComponent);
diff --git a/src/core/components/tabs/tab.ts b/src/core/components/tabs/tab.ts
index 0f6101ef24c..c4fef732019 100644
--- a/src/core/components/tabs/tab.ts
+++ b/src/core/components/tabs/tab.ts
@@ -44,7 +44,7 @@ import { CoreBaseModule } from '@/core/base.module';
* Example usage:
*
*
- *
+ *
*
*
*
diff --git a/src/core/components/tabs/tabs.ts b/src/core/components/tabs/tabs.ts
index 84d02b864d6..6c520e17a37 100644
--- a/src/core/components/tabs/tabs.ts
+++ b/src/core/components/tabs/tabs.ts
@@ -35,7 +35,7 @@ import { CoreUpdateNonReactiveAttributesDirective } from '@directives/update-non
* Example usage:
*
*
- *
+ *
*
*
*
@@ -58,8 +58,7 @@ export class CoreTabsComponent extends CoreTabsBaseComponent i
@Input({ transform: toBoolean }) parentScrollable = false; // Determine if scroll should be in the parent content or the tab.
@Input() layout: 'icon-top' | 'icon-start' | 'icon-end' | 'icon-bottom' | 'icon-hide' | 'label-hide' = 'icon-hide';
- @ViewChild('originalTabs')
- set originalTabs(originalTabs: ElementRef) {
+ @ViewChild('originalTabs') set originalTabs(originalTabs: ElementRef) {
/**
* This setTimeout waits for Ionic's async initialization to complete.
* Otherwise, an outdated swiper reference will be used.
diff --git a/src/core/directives/aria-button.ts b/src/core/directives/aria-button.ts
index ae7a656f7c0..854fc1f959e 100644
--- a/src/core/directives/aria-button.ts
+++ b/src/core/directives/aria-button.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Directive, ElementRef, OnInit, Output, EventEmitter, OnChanges, SimpleChanges, Input, inject } from '@angular/core';
+import { Directive, ElementRef, OnInit, inject, input, effect, output } from '@angular/core';
import { CoreDom } from '@singletons/dom';
import { toBoolean } from '../transforms/boolean';
@@ -22,35 +22,31 @@ import { toBoolean } from '../transforms/boolean';
@Directive({
selector: '[ariaButtonClick]',
})
-export class CoreAriaButtonClickDirective implements OnInit, OnChanges {
+export class CoreAriaButtonClickDirective implements OnInit {
- @Input({ transform: toBoolean }) disabled = false;
- @Output() ariaButtonClick = new EventEmitter();
+ readonly disabled = input(false, { transform: toBoolean });
+ readonly ariaButtonClick = output();// Emit when the button is clicked.
protected element: HTMLElement = inject(ElementRef).nativeElement;
- /**
- * @inheritdoc
- */
- ngOnInit(): void {
- CoreDom.initializeClickableElementA11y(this.element, (event) => this.ariaButtonClick.emit(event));
+ constructor() {
+ effect(() => {
+ const disabled = this.disabled();
+ if (this.element.getAttribute('tabindex') === '0' && disabled) {
+ this.element.setAttribute('tabindex', '-1');
+ }
+
+ if (this.element.getAttribute('tabindex') === '-1' && !disabled) {
+ this.element.setAttribute('tabindex', '0');
+ }
+ });
}
/**
* @inheritdoc
*/
- ngOnChanges(changes: SimpleChanges): void {
- if (!changes.disabled) {
- return;
- }
-
- if (this.element.getAttribute('tabindex') === '0' && this.disabled) {
- this.element.setAttribute('tabindex', '-1');
- }
-
- if (this.element.getAttribute('tabindex') === '-1' && !this.disabled) {
- this.element.setAttribute('tabindex', '0');
- }
+ ngOnInit(): void {
+ CoreDom.initializeClickableElementA11y(this.element, (event) => this.ariaButtonClick.emit(event));
}
}
diff --git a/src/core/directives/auto-focus.ts b/src/core/directives/auto-focus.ts
index 7ef7147262f..e77555ac090 100644
--- a/src/core/directives/auto-focus.ts
+++ b/src/core/directives/auto-focus.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Directive, Input, ElementRef, AfterViewInit, inject } from '@angular/core';
+import { Directive, ElementRef, AfterViewInit, inject, input } from '@angular/core';
import { CoreDom } from '@singletons/dom';
import { CoreWait } from '@singletons/wait';
@@ -31,7 +31,7 @@ import { toBoolean } from '../transforms/boolean';
})
export class CoreAutoFocusDirective implements AfterViewInit {
- @Input({ alias: 'core-auto-focus', transform: toBoolean }) autoFocus = true;
+ readonly autoFocus = input(true, { alias: 'core-auto-focus', transform: toBoolean });
protected element: HTMLIonInputElement | HTMLIonTextareaElement | HTMLIonSearchbarElement | HTMLElement
= inject(ElementRef).nativeElement;
@@ -40,7 +40,7 @@ export class CoreAutoFocusDirective implements AfterViewInit {
* @inheritdoc
*/
async ngAfterViewInit(): Promise {
- if (!this.autoFocus) {
+ if (!this.autoFocus()) {
return;
}
diff --git a/src/core/directives/auto-rows.ts b/src/core/directives/auto-rows.ts
index 81d582c9ed1..042804a99d2 100644
--- a/src/core/directives/auto-rows.ts
+++ b/src/core/directives/auto-rows.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Directive, ElementRef, Output, EventEmitter, AfterViewInit, Input, OnChanges, inject } from '@angular/core';
+import { Directive, ElementRef, inject, input, effect, output } from '@angular/core';
/**
* Directive to adapt a textarea rows depending on the input text. It's based on Moodle's data-auto-rows.
@@ -24,45 +24,28 @@ import { Directive, ElementRef, Output, EventEmitter, AfterViewInit, Input, OnCh
@Directive({
selector: 'textarea[core-auto-rows], ion-textarea[core-auto-rows]',
})
-export class CoreAutoRowsDirective implements AfterViewInit, OnChanges {
+export class CoreAutoRowsDirective {
+
+ readonly value = input(undefined, { alias: 'core-auto-rows' });
+ readonly onResize = output();// Emit when resizing the textarea.
protected height = 0;
protected element: HTMLElement = inject(ElementRef).nativeElement;
- @Input('core-auto-rows') value?: string;
- @Output() onResize: EventEmitter; // Emit when resizing the textarea.
-
constructor() {
- this.onResize = new EventEmitter();
- }
+ effect(() => {
+ this.value();
- /**
- * Resize after initialized.
- */
- ngAfterViewInit(): void {
- // Wait for rendering of child views.
- setTimeout(() => {
+ // Resize the textarea when the value changes.
this.resize();
- }, 300);
- }
-
- /**
- * Resize when content changes.
- */
- ngOnChanges(): void {
- this.resize();
-
- if (this.value === '') {
- // Maybe the form was resetted. In that case it takes a bit to update the height.
- setTimeout(() => this.resize(), 300);
- }
+ });
}
/**
* Resize the textarea.
*/
protected resize(): void {
- if (this.element.tagName == 'ION-TEXTAREA') {
+ if (this.element.tagName === 'ION-TEXTAREA') {
// Search the actual textarea.
const textarea = this.element.querySelector('textarea');
if (!textarea) {
@@ -77,7 +60,7 @@ export class CoreAutoRowsDirective implements AfterViewInit, OnChanges {
this.element.style.height = `${this.element.scrollHeight}px`;
// Emit event when resizing.
- if (this.height != this.element.scrollHeight) {
+ if (this.height !== this.element.scrollHeight) {
this.height = this.element.scrollHeight;
this.onResize.emit();
}
diff --git a/src/core/directives/collapsible-footer.ts b/src/core/directives/collapsible-footer.ts
index 8ac2a7641f7..b9e5ec677cf 100644
--- a/src/core/directives/collapsible-footer.ts
+++ b/src/core/directives/collapsible-footer.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Directive, ElementRef, Input, OnDestroy, OnInit, inject } from '@angular/core';
+import { Directive, ElementRef, OnDestroy, OnInit, inject, input } from '@angular/core';
import { ScrollDetail } from '@ionic/core';
import { IonContent } from '@ionic/angular';
import { CoreUtils } from '@singletons/utils';
@@ -38,7 +38,7 @@ import { toBoolean } from '../transforms/boolean';
})
export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
- @Input({ transform: toBoolean }) appearOnBottom = false; // Whether footer should re-appear when reaching the bottom.
+ readonly appearOnBottom = input(false, { transform: toBoolean }); // Whether footer should re-appear when reaching the bottom.
protected id = '0';
protected element: HTMLElement = inject(ElementRef).nativeElement;
@@ -218,7 +218,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
protected onScroll(scrollDetail: ScrollDetail, scrollElement: HTMLElement): void {
const maxScroll = scrollElement.scrollHeight - scrollElement.offsetHeight;
const footerHasFocus = this.moduleNav?.contains(document.activeElement);
- if (scrollDetail.scrollTop <= 0 || (this.appearOnBottom && scrollDetail.scrollTop >= maxScroll) || footerHasFocus) {
+ if (scrollDetail.scrollTop <= 0 || (this.appearOnBottom() && scrollDetail.scrollTop >= maxScroll) || footerHasFocus) {
// Reset.
this.setBarHeight(this.initialHeight);
} else {
diff --git a/src/core/directives/collapsible-item.ts b/src/core/directives/collapsible-item.ts
index 0b705c6261c..4fc324b80b9 100644
--- a/src/core/directives/collapsible-item.ts
+++ b/src/core/directives/collapsible-item.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Directive, ElementRef, Input, OnDestroy, OnInit, inject } from '@angular/core';
+import { Directive, ElementRef, OnDestroy, OnInit, inject, input } from '@angular/core';
import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { CoreLoadingComponent } from '@components/loading/loading';
import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
@@ -46,7 +46,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
* Using this parameter will force display: block to calculate height better.
* If you want to avoid this use class="inline" at the same time to use display: inline-block.
*/
- @Input('collapsible-item') height: number | string = defaultMaxHeight;
+ readonly height = input(defaultMaxHeight, { alias: 'collapsible-item' });
protected element: HTMLElement = inject(ElementRef).nativeElement;
protected toggleExpandEnabled = false;
@@ -72,16 +72,17 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
* @inheritdoc
*/
async ngOnInit(): Promise {
- if (this.height === null) {
+ const height = this.height();
+ if (height === null) {
return;
}
- if (typeof this.height === 'string') {
- this.maxHeight = this.height === ''
+ if (typeof height === 'string') {
+ this.maxHeight = height === ''
? defaultMaxHeight
- : parseInt(this.height, 10);
+ : parseInt(height, 10);
} else {
- this.maxHeight = this.height;
+ this.maxHeight = height;
}
this.maxHeight = this.maxHeight < minMaxHeight ? defaultMaxHeight : this.maxHeight;
diff --git a/src/core/directives/download-file.ts b/src/core/directives/download-file.ts
index d668fa9c1c4..7815e8a0691 100644
--- a/src/core/directives/download-file.ts
+++ b/src/core/directives/download-file.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Directive, Input, OnInit, ElementRef, inject } from '@angular/core';
+import { Directive, OnInit, ElementRef, inject, input } from '@angular/core';
import { CoreFileHelper } from '@services/file-helper';
import { CoreAlerts } from '@services/overlays/alerts';
import { CoreLoadings } from '@services/overlays/loadings';
@@ -28,9 +28,9 @@ import { Translate } from '@singletons';
})
export class CoreDownloadFileDirective implements OnInit {
- @Input('core-download-file') file?: CoreWSFile; // The file to download.
- @Input() component?: string; // Component to link the file to.
- @Input() componentId?: string | number; // Component ID to use in conjunction with the component.
+ readonly file = input(undefined, { alias: 'core-download-file' }); // The file to download.
+ readonly component = input(); // Component to link the file to.
+ readonly componentId = input(); // Component ID to use in conjunction with the component.
protected element: HTMLElement = inject(ElementRef).nativeElement;
@@ -39,7 +39,8 @@ export class CoreDownloadFileDirective implements OnInit {
*/
ngOnInit(): void {
this.element.addEventListener('click', async (ev: Event) => {
- if (!this.file) {
+ const file = this.file();
+ if (!file) {
return;
}
@@ -49,7 +50,7 @@ export class CoreDownloadFileDirective implements OnInit {
const modal = await CoreLoadings.show();
try {
- await CoreFileHelper.downloadAndOpenFile(this.file, this.component, this.componentId);
+ await CoreFileHelper.downloadAndOpenFile(file, this.component(), this.componentId());
} catch (error) {
CoreAlerts.showError(error, { default: Translate.instant('core.errordownloading') });
} finally {
diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts
index 8f29f542fe1..cade417f378 100644
--- a/src/core/directives/format-text.ts
+++ b/src/core/directives/format-text.ts
@@ -21,10 +21,10 @@ import {
OnChanges,
SimpleChange,
ViewContainerRef,
- ViewChild,
OnDestroy,
ChangeDetectorRef,
inject,
+ viewChild,
} from '@angular/core';
import { CoreSites } from '@services/sites';
@@ -75,7 +75,7 @@ import { CoreBootstrap } from '@singletons/bootstrap';
})
export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirective {
- @ViewChild(CoreCollapsibleItemDirective) collapsible?: CoreCollapsibleItemDirective;
+ readonly collapsible = viewChild(CoreCollapsibleItemDirective);
@Input() text?: string; // The text to format.
@Input() siteId?: string; // Site ID to use.
@@ -339,7 +339,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
return;
}
- this.collapsible?.elementClicked(e);
+ this.collapsible()?.elementClicked(e);
}
/**
diff --git a/src/core/directives/supress-events.ts b/src/core/directives/supress-events.ts
index a74a7ceaa68..033a322a37a 100644
--- a/src/core/directives/supress-events.ts
+++ b/src/core/directives/supress-events.ts
@@ -14,7 +14,7 @@
// Based on http://roblouie.com/article/198/using-gestures-in-the-ionic-2-beta/
-import { Directive, ElementRef, OnInit, Input, Output, EventEmitter, inject } from '@angular/core';
+import { Directive, ElementRef, OnInit, Output, EventEmitter, inject, input } from '@angular/core';
import { CoreLogger } from '@singletons/logger';
/**
@@ -41,7 +41,9 @@ import { CoreLogger } from '@singletons/logger';
})
export class CoreSupressEventsDirective implements OnInit {
- @Input('core-suppress-events') suppressEvents?: string | string[];
+ readonly suppressEvents = input(undefined, { alias: 'core-suppress-events' });
+ // Not migrable yet, observed is not supported in Angular 20.
+ // https://github.com/angular/angular/issues/54837
@Output() onClick = new EventEmitter();
protected element: HTMLElement = inject(ElementRef).nativeElement;
@@ -59,17 +61,18 @@ export class CoreSupressEventsDirective implements OnInit {
let events: string[];
- if (this.suppressEvents == 'all' || this.suppressEvents === undefined || this.suppressEvents === null) {
+ const suppressEvents = this.suppressEvents();
+ if (suppressEvents == 'all' || suppressEvents === undefined || suppressEvents === null) {
// Suppress all events.
events = ['click', 'mousedown', 'touchdown', 'touchmove', 'touchstart'];
- } else if (typeof this.suppressEvents == 'string') {
+ } else if (typeof suppressEvents == 'string') {
// It's a string, just suppress this event.
- events = [this.suppressEvents];
+ events = [suppressEvents];
- } else if (Array.isArray(this.suppressEvents)) {
+ } else if (Array.isArray(suppressEvents)) {
// Array supplied.
- events = this.suppressEvents;
+ events = suppressEvents;
} else {
events = [];
}
diff --git a/src/core/directives/swipe-navigation.ts b/src/core/directives/swipe-navigation.ts
index 994d2fb44a1..963507e9332 100644
--- a/src/core/directives/swipe-navigation.ts
+++ b/src/core/directives/swipe-navigation.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import { CoreConstants } from '@/core/constants';
-import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, inject } from '@angular/core';
+import { AfterViewInit, Directive, ElementRef, OnDestroy, inject, input } from '@angular/core';
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreSwipeNavigationTourComponent } from '@components/swipe-navigation-tour/swipe-navigation-tour';
import { CoreUserTours } from '@features/usertours/services/user-tours';
@@ -36,7 +36,7 @@ const SWIPE_FRICTION = 0.6;
})
export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy {
- @Input('core-swipe-navigation') manager?: CoreSwipeNavigationItemsManager;
+ readonly manager = input(undefined, { alias: 'core-swipe-navigation' });
protected element: HTMLElement = inject(ElementRef).nativeElement;
protected swipeGesture?: Gesture;
@@ -49,7 +49,7 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy {
}
get enabled(): boolean {
- return !!this.manager;
+ return !!this.manager();
}
/**
@@ -77,7 +77,7 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy {
},
});
- const source = this.manager?.getSource();
+ const source = this.manager()?.getSource();
if (!source) {
return;
}
@@ -108,8 +108,8 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy {
}
CorePlatform.isRTL
- ? this.manager?.navigateToPreviousItem()
- : this.manager?.navigateToNextItem();
+ ? this.manager()?.navigateToPreviousItem()
+ : this.manager()?.navigateToNextItem();
}
/**
@@ -121,8 +121,8 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy {
}
CorePlatform.isRTL
- ? this.manager?.navigateToNextItem()
- : this.manager?.navigateToPreviousItem();
+ ? this.manager()?.navigateToNextItem()
+ : this.manager()?.navigateToPreviousItem();
}
/**
@@ -131,13 +131,14 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy {
* @returns If has an item to the right.
*/
protected async hasItemRight(): Promise {
- if (!this.manager) {
+ const manager = this.manager();
+ if (!manager) {
return false;
}
return CorePlatform.isRTL
- ? await this.manager.hasNextItem()
- : await this.manager.hasPreviousItem();
+ ? await manager.hasNextItem()
+ : await manager.hasPreviousItem();
}
/**
@@ -146,13 +147,14 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy {
* @returns If has an item to the left.
*/
protected async hasItemLeft(): Promise