From 4053e2d7413fd44028a6f2373e53d93360a241d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 24 Nov 2023 14:06:59 +0100 Subject: [PATCH] MOBILE-3947 core: Move from ViewEngine to Ivy and fix plugins --- src/app/app.module.ts | 6 +- .../dynamic-component/dynamic-component.ts | 7 +- .../components/compile-html/compile-html.ts | 42 ++++++----- src/core/features/compile/pipes/translate.ts | 28 ++++++++ src/core/features/compile/services/compile.ts | 70 ++++++++----------- .../plugin-content/plugin-content.ts | 41 +++++++---- src/core/singletons/index.ts | 3 +- 7 files changed, 114 insertions(+), 83 deletions(-) create mode 100644 src/core/features/compile/pipes/translate.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d0a5dcfd4..3e81eed52 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, COMPILER_OPTIONS, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouteReuseStrategy } from '@angular/router'; @@ -28,7 +28,7 @@ import { AddonsModule } from '@addons/addons.module'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; -import { JitCompilerFactory } from '@angular/platform-browser-dynamic'; + import { CoreCronDelegate } from '@services/cron'; import { CoreSiteInfoCronHandler } from '@services/handlers/site-info-cron'; import { moodleTransitionAnimation } from '@classes/page-transition'; @@ -71,8 +71,6 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, - { provide: COMPILER_OPTIONS, useValue: {}, multi: true }, - { provide: JitCompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS] }, { provide: APP_INITIALIZER, multi: true, diff --git a/src/core/components/dynamic-component/dynamic-component.ts b/src/core/components/dynamic-component/dynamic-component.ts index 82a8d4310..52dbded55 100644 --- a/src/core/components/dynamic-component/dynamic-component.ts +++ b/src/core/components/dynamic-component/dynamic-component.ts @@ -69,7 +69,8 @@ export class CoreDynamicComponent implements OnChanges, DoCheck @Input() data?: Record; // Get the container where to put the dynamic component. - @ViewChild('dynamicComponent', { read: ViewContainerRef }) set dynamicComponent(el: ViewContainerRef) { + @ViewChild('dynamicComponent', { read: ViewContainerRef }) + set dynamicComponent(el: ViewContainerRef) { this.container = el; // Use a timeout to avoid ExpressionChangedAfterItHasBeenCheckedError. @@ -94,7 +95,7 @@ export class CoreDynamicComponent implements OnChanges, DoCheck } /** - * Detect changes on input properties. + * @inheritdoc */ ngOnChanges(changes: { [name: string]: SimpleChange }): void { if (changes.component && !this.component) { @@ -108,7 +109,7 @@ export class CoreDynamicComponent implements OnChanges, DoCheck } /** - * Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays). + * @inheritdoc */ ngDoCheck(): void { if (this.instance) { diff --git a/src/core/features/compile/components/compile-html/compile-html.ts b/src/core/features/compile/components/compile-html/compile-html.ts index 1a6fad10b..4ac7ea86b 100644 --- a/src/core/features/compile/components/compile-html/compile-html.ts +++ b/src/core/features/compile/components/compile-html/compile-html.ts @@ -56,7 +56,7 @@ import { CoreUtils } from '@services/utils/utils'; */ @Component({ selector: 'core-compile-html', - template: '', + template: '', styles: [':host { display: contents; }'], }) export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { @@ -66,16 +66,16 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { @Input() jsData?: Record; // Data to pass to the fake component. @Input() extraImports: unknown[] = []; // Extra import modules. @Input() extraProviders: Type[] = []; // Extra providers. - @Input() forceCompile?: boolean; // Set it to true to force compile even if the text/javascript hasn't changed. + @Input() forceCompile = false; // Set it to true to force compile even if the text/javascript hasn't changed. @Output() created = new EventEmitter(); // Will emit an event when the component is instantiated. @Output() compiling = new EventEmitter(); // Event that indicates whether the template is being compiled. + loaded = false; + componentInstance?: any; // eslint-disable-line @typescript-eslint/no-explicit-any + // Get the container where to put the content. @ViewChild('dynamicComponent', { read: ViewContainerRef }) container?: ViewContainerRef; - loaded?: boolean; - componentInstance?: any; // eslint-disable-line @typescript-eslint/no-explicit-any - protected componentRef?: ComponentRef; protected element: HTMLElement; protected differ: KeyValueDiffer; // To detect changes in the jsData input. @@ -114,6 +114,10 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { * @inheritdoc */ async ngOnChanges(changes: Record): Promise { + if (!this.container) { + return; + } + // Only compile if text/javascript has changed or the forceCompile flag has been set to true. if (this.text !== undefined && (changes.text || changes.javascript || (changes.forceCompile && CoreUtils.isTrueOrOne(this.forceCompile)))) { @@ -124,16 +128,18 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { try { const componentClass = await this.getComponentClass(); - const factory = await CoreCompile.createAndCompileComponent(this.text, componentClass, this.extraImports); // Destroy previous components. this.componentRef?.destroy(); - if (factory) { - // Create the component. - this.componentRef = this.container?.createComponent(factory); - this.componentRef && this.created.emit(this.componentRef.instance); - } + // Create the component. + this.componentRef = await CoreCompile.createAndCompileComponent( + this.text, + componentClass, + this.container, + this.extraImports, + ); + this.componentRef && this.created.emit(this.componentRef.instance); this.loaded = true; } catch (error) { @@ -192,7 +198,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { } /** - * Component being initialized. + * @inheritdoc */ ngOnInit(): void { // If there is some javascript to run, do it now. @@ -204,7 +210,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { for (const name in compileInstance.pendingCalls) { const pendingCall = compileInstance.pendingCalls[name]; - if (typeof this[name] == 'function') { + if (typeof this[name] === 'function') { // Call the function. Promise.resolve(this[name].apply(this, pendingCall.params)).then(pendingCall.defer.resolve) .catch(pendingCall.defer.reject); @@ -218,21 +224,21 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { } /** - * Content has been initialized. + * @inheritdoc */ ngAfterContentInit(): void { this.callLifecycleHookOverride('ngAfterContentInit'); } /** - * View has been initialized. + * @inheritdoc */ ngAfterViewInit(): void { this.callLifecycleHookOverride('ngAfterViewInit'); } /** - * Component destroyed. + * @inheritdoc */ ngOnDestroy(): void { this.callLifecycleHookOverride('ngOnDestroy'); @@ -283,9 +289,9 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { * once the component has been created. * @returns Result of the call. Undefined if no component instance or the function doesn't exist. */ - callComponentFunction(name: string, params?: unknown[], callWhenCreated: boolean = true): unknown { + callComponentFunction(name: string, params?: unknown[], callWhenCreated = true): unknown { if (this.componentInstance) { - if (typeof this.componentInstance[name] == 'function') { + if (typeof this.componentInstance[name] === 'function') { return this.componentInstance[name].apply(this.componentInstance, params); } } else if (callWhenCreated) { diff --git a/src/core/features/compile/pipes/translate.ts b/src/core/features/compile/pipes/translate.ts new file mode 100644 index 000000000..3cf15adbd --- /dev/null +++ b/src/core/features/compile/pipes/translate.ts @@ -0,0 +1,28 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable, Pipe } from '@angular/core'; +import { TranslatePipe } from '@ngx-translate/core'; + +/** + * Copy of translate pipe to use when compiling a dynamic component. + * For some reason, when compiling a dynamic component the original translate pipe isn't found so we use this copy instead. + */ +@Injectable() +@Pipe({ + name: 'translate', + pure: false, // required to update the value when the promise is resolved + standalone: true, +}) +export class TranslatePipeForCompile extends TranslatePipe {} diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 2192c499a..d609d5877 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -17,14 +17,13 @@ import { Injector, Component, NgModule, - Compiler, - ComponentFactory, ComponentRef, - NgModuleRef, NO_ERRORS_SCHEMA, Type, + Provider, + createNgModule, + ViewContainerRef, } from '@angular/core'; -import { JitCompilerFactory } from '@angular/platform-browser-dynamic'; import { ActionSheetController, AlertController, @@ -34,6 +33,7 @@ import { ToastController, } from '@ionic/angular'; import { TranslateService } from '@ngx-translate/core'; +import { TranslatePipeForCompile } from '../pipes/translate'; import { CoreLogger } from '@singletons/logger'; import { CoreEvents } from '@singletons/events'; @@ -160,6 +160,8 @@ import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; import { CoreAutoLogoutService } from '@features/autologout/services/autologout'; +import '@angular/compiler'; + /** * Service to provide functionalities regarding compiling dynamic HTML and Javascript. */ @@ -167,7 +169,6 @@ import { CoreAutoLogoutService } from '@features/autologout/services/autologout' export class CoreCompileProvider { protected logger: CoreLogger; - protected compiler: Compiler; // Other Ionic/Angular providers that don't depend on where they are injected. protected readonly OTHER_SERVICES: unknown[] = [ @@ -186,10 +187,8 @@ export class CoreCompileProvider { getWorkshopComponentModules, ]; - constructor(protected injector: Injector, compilerFactory: JitCompilerFactory) { + constructor(protected injector: Injector) { this.logger = CoreLogger.getInstance('CoreCompileProvider'); - - this.compiler = compilerFactory.createCompiler(); } /** @@ -197,14 +196,17 @@ export class CoreCompileProvider { * * @param template The template of the component. * @param componentClass The JS class of the component. + * @param viewContainerRef View container reference to inject the component. * @param extraImports Extra imported modules if needed and not imported by this class. - * @returns Promise resolved with the factory to instantiate the component. + * @returns Promise resolved with the component reference. */ async createAndCompileComponent( template: string, componentClass: Type, + viewContainerRef: ViewContainerRef, extraImports: any[] = [], // eslint-disable-line @typescript-eslint/no-explicit-any - ): Promise | undefined> { + ): Promise | undefined> { + // Create the component using the template and the class. const component = Component({ template })(componentClass); @@ -213,17 +215,24 @@ export class CoreCompileProvider { ...CoreArray.flatten(lazyImports), ...this.IMPORTS, ...extraImports, + TranslatePipeForCompile, ]; - // Now create the module containing the component. - const module = NgModule({ imports, declarations: [component], schemas: [NO_ERRORS_SCHEMA] })(class {}); - try { - // Compile the module and the component. - const factories = await this.compiler.compileModuleAndAllComponentsAsync(module); + viewContainerRef.clear(); - // Search and return the factory of the component we just created. - return factories.componentFactories.find(factory => factory.componentType == component); + // Now create the module containing the component. + const ngModuleRef = createNgModule( + NgModule({ imports, declarations: [component], schemas: [NO_ERRORS_SCHEMA] })(class {}), + this.injector, + ); + + return viewContainerRef.createComponent( + component, + { + environmentInjector: ngModuleRef, + }, + ); } catch (error) { this.logger.error('Error compiling template', template); this.logger.error(error); @@ -331,10 +340,10 @@ export class CoreCompileProvider { // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance. for (const i in providers) { const providerDef = providers[i]; - if (typeof providerDef == 'function' && providerDef.name) { + if (typeof providerDef === 'function' && providerDef.name) { try { // Inject the provider to the instance. We use the class name as the property name. - instance[providerDef.name.replace(/DelegateService$/, 'Delegate')] = this.injector.get(providerDef); + instance[providerDef.name.replace(/DelegateService$/, 'Delegate')] = this.injector.get(providerDef); } catch (ex) { this.logger.error('Error injecting provider', providerDef.name, ex); } @@ -407,29 +416,6 @@ export class CoreCompileProvider { ]; } - /** - * Instantiate a dynamic component. - * - * @param template The template of the component. - * @param componentClass The JS class of the component. - * @param injector The injector to use. It's recommended to pass it so NavController and similar can be injected. - * @returns Promise resolved with the component instance. - */ - async instantiateDynamicComponent( - template: string, - componentClass: Type, - injector?: Injector, - ): Promise | undefined> { - injector = injector || this.injector; - - const factory = await this.createAndCompileComponent(template, componentClass); - - if (factory) { - // Create and return the component. - return factory.create(injector, undefined, undefined, injector.get(NgModuleRef)); - } - } - } export const CoreCompile = makeSingleton(CoreCompileProvider); diff --git a/src/core/features/siteplugins/components/plugin-content/plugin-content.ts b/src/core/features/siteplugins/components/plugin-content/plugin-content.ts index eadce9a79..a4244dc25 100644 --- a/src/core/features/siteplugins/components/plugin-content/plugin-content.ts +++ b/src/core/features/siteplugins/components/plugin-content/plugin-content.ts @@ -48,18 +48,18 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck { // Get the compile element. Don't set the right type to prevent circular dependencies. @ViewChild('compile') compileComponent?: CoreCompileHtmlComponent; - @HostBinding('class') @Input() component!: string; + @HostBinding('class') @Input() component = ''; @Input() method!: string; @Input() args?: Record; @Input() initResult?: CoreSitePluginsContent | null; // Result of the init WS call of the handler. - @Input() data?: Record; // Data to pass to the component. + @Input() data: Record = {}; // Data to pass to the component. @Input() preSets?: CoreSiteWSPreSets; // The preSets for the WS call. @Input() pageTitle?: string; // Current page title. It can be used by the "new-content" directives. @Output() onContentLoaded = new EventEmitter(); // Emits event when content is loaded. @Output() onLoadingContent = new EventEmitter(); // Emits an event when starts to load the content. - content?: string; // Content. - javascript?: string; // Javascript to execute. + content = ''; // Content. + javascript = ''; // Javascript to execute. otherData?: Record; // Other data of the content. dataLoaded = false; invalidateObservable = new Subject(); // An observable to notify observers when to invalidate data. @@ -120,13 +120,27 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck { this.jsData = Object.assign(this.data, CoreSitePlugins.createDataForJS(this.initResult, result)); // Pass some methods as jsData so they can be called from the template too. - this.jsData.fetchContent = refresh => this.fetchContent(refresh); - this.jsData.openContent = (title, args, component, method, jsData, preSets, ptrEnabled) => - this.openContent(title, args, component, method, jsData, preSets, ptrEnabled); - this.jsData.refreshContent = showSpinner => this.refreshContent(showSpinner); - this.jsData.updateContent = (args, component, method, jsData, preSets) => - this.updateContent(args, component, method, jsData, preSets); - this.jsData.updateModuleCourseContent = (cmId, alreadyFetched) => this.updateModuleCourseContent(cmId, alreadyFetched); + this.jsData.fetchContent = (refresh?: boolean) => this.fetchContent(refresh); + this.jsData.openContent = ( + title: string, + args?: Record, + component?: string, + method?: string, + jsData?: Record | boolean, + preSets?: CoreSiteWSPreSets, + ptrEnabled?: boolean, + ) => this.openContent(title, args, component, method, jsData, preSets, ptrEnabled); + this.jsData.refreshContent = (showSpinner?: boolean) => this.refreshContent(showSpinner); + this.jsData.updateContent = ( + args?: Record, + component?: string, + method?: string, + jsData?: Record, + preSets?: CoreSiteWSPreSets, + ) => this.updateContent(args, component, method, jsData, preSets); + this.jsData.updateModuleCourseContent = (cmId: number, alreadyFetched?: boolean) => + this.updateModuleCourseContent(cmId, alreadyFetched); + this.jsData.updateCachedContent = () => this.updateCachedContent(); this.onContentLoaded.emit({ refresh: !!refresh, success: true }); } catch (error) { @@ -154,7 +168,7 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck { */ openContent( title: string, - args?: Record, + args: Record = {}, component?: string, method?: string, jsData?: Record | boolean, @@ -167,7 +181,6 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck { component = component || this.component; method = method || this.method; - args = args || {}; const hash = Md5.hashAsciiStr(JSON.stringify(args)); CoreNavigator.navigateToSitePath(`siteplugins/content/${component}/${method}/${hash}`, { @@ -187,7 +200,7 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck { * * @param showSpinner Whether to show spinner while refreshing. */ - async refreshContent(showSpinner: boolean = true): Promise { + async refreshContent(showSpinner = true): Promise { if (showSpinner) { this.dataLoaded = false; } diff --git a/src/core/singletons/index.ts b/src/core/singletons/index.ts index ae1a6f99e..108ce8acf 100644 --- a/src/core/singletons/index.ts +++ b/src/core/singletons/index.ts @@ -226,7 +226,6 @@ export const Translate: Omit, 'instant'> & // Async singletons. export const AngularFrameworkDelegate = asyncInstance(async () => { const injector = await singletonsInjector; - const environmentInjector = await injector.get(EnvironmentInjector); - return AngularDelegate.create(environmentInjector, injector); + return AngularDelegate.create(injector.get(EnvironmentInjector), injector); });