MOBILE-3947 core: Move from ViewEngine to Ivy and fix plugins

main
Pau Ferrer Ocaña 2023-11-24 14:06:59 +01:00 committed by Dani Palou
parent 22dbd6ad99
commit 4053e2d741
7 changed files with 114 additions and 83 deletions

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouteReuseStrategy } from '@angular/router'; import { RouteReuseStrategy } from '@angular/router';
@ -28,7 +28,7 @@ import { AddonsModule } from '@addons/addons.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { JitCompilerFactory } from '@angular/platform-browser-dynamic';
import { CoreCronDelegate } from '@services/cron'; import { CoreCronDelegate } from '@services/cron';
import { CoreSiteInfoCronHandler } from '@services/handlers/site-info-cron'; import { CoreSiteInfoCronHandler } from '@services/handlers/site-info-cron';
import { moodleTransitionAnimation } from '@classes/page-transition'; import { moodleTransitionAnimation } from '@classes/page-transition';
@ -71,8 +71,6 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
], ],
providers: [ providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{ provide: COMPILER_OPTIONS, useValue: {}, multi: true },
{ provide: JitCompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS] },
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
multi: true, multi: true,

View File

@ -69,7 +69,8 @@ export class CoreDynamicComponent<ComponentClass> implements OnChanges, DoCheck
@Input() data?: Record<string | number, unknown>; @Input() data?: Record<string | number, unknown>;
// Get the container where to put the dynamic component. // 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; this.container = el;
// Use a timeout to avoid ExpressionChangedAfterItHasBeenCheckedError. // Use a timeout to avoid ExpressionChangedAfterItHasBeenCheckedError.
@ -94,7 +95,7 @@ export class CoreDynamicComponent<ComponentClass> implements OnChanges, DoCheck
} }
/** /**
* Detect changes on input properties. * @inheritdoc
*/ */
ngOnChanges(changes: { [name: string]: SimpleChange }): void { ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if (changes.component && !this.component) { if (changes.component && !this.component) {
@ -108,7 +109,7 @@ export class CoreDynamicComponent<ComponentClass> implements OnChanges, DoCheck
} }
/** /**
* Detect and act upon changes that Angular cant or wont detect on its own (objects and arrays). * @inheritdoc
*/ */
ngDoCheck(): void { ngDoCheck(): void {
if (this.instance) { if (this.instance) {

View File

@ -56,7 +56,7 @@ import { CoreUtils } from '@services/utils/utils';
*/ */
@Component({ @Component({
selector: 'core-compile-html', selector: 'core-compile-html',
template: '<core-loading [hideUntil]="loaded"><ng-container #dynamicComponent></ng-container></core-loading>', template: '<core-loading [hideUntil]="loaded"><ng-container #dynamicComponent /></core-loading>',
styles: [':host { display: contents; }'], styles: [':host { display: contents; }'],
}) })
export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
@ -66,16 +66,16 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
@Input() jsData?: Record<string, unknown>; // Data to pass to the fake component. @Input() jsData?: Record<string, unknown>; // Data to pass to the fake component.
@Input() extraImports: unknown[] = []; // Extra import modules. @Input() extraImports: unknown[] = []; // Extra import modules.
@Input() extraProviders: Type<unknown>[] = []; // Extra providers. @Input() extraProviders: Type<unknown>[] = []; // 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<unknown>(); // Will emit an event when the component is instantiated. @Output() created = new EventEmitter<unknown>(); // Will emit an event when the component is instantiated.
@Output() compiling = new EventEmitter<boolean>(); // Event that indicates whether the template is being compiled. @Output() compiling = new EventEmitter<boolean>(); // 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. // Get the container where to put the content.
@ViewChild('dynamicComponent', { read: ViewContainerRef }) container?: ViewContainerRef; @ViewChild('dynamicComponent', { read: ViewContainerRef }) container?: ViewContainerRef;
loaded?: boolean;
componentInstance?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
protected componentRef?: ComponentRef<unknown>; protected componentRef?: ComponentRef<unknown>;
protected element: HTMLElement; protected element: HTMLElement;
protected differ: KeyValueDiffer<unknown, unknown>; // To detect changes in the jsData input. protected differ: KeyValueDiffer<unknown, unknown>; // To detect changes in the jsData input.
@ -114,6 +114,10 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
* @inheritdoc * @inheritdoc
*/ */
async ngOnChanges(changes: Record<string, SimpleChange>): Promise<void> { async ngOnChanges(changes: Record<string, SimpleChange>): Promise<void> {
if (!this.container) {
return;
}
// Only compile if text/javascript has changed or the forceCompile flag has been set to true. // Only compile if text/javascript has changed or the forceCompile flag has been set to true.
if (this.text !== undefined && (changes.text || changes.javascript || if (this.text !== undefined && (changes.text || changes.javascript ||
(changes.forceCompile && CoreUtils.isTrueOrOne(this.forceCompile)))) { (changes.forceCompile && CoreUtils.isTrueOrOne(this.forceCompile)))) {
@ -124,16 +128,18 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
try { try {
const componentClass = await this.getComponentClass(); const componentClass = await this.getComponentClass();
const factory = await CoreCompile.createAndCompileComponent(this.text, componentClass, this.extraImports);
// Destroy previous components. // Destroy previous components.
this.componentRef?.destroy(); this.componentRef?.destroy();
if (factory) { // Create the component.
// Create the component. this.componentRef = await CoreCompile.createAndCompileComponent(
this.componentRef = this.container?.createComponent(factory); this.text,
this.componentRef && this.created.emit(this.componentRef.instance); componentClass,
} this.container,
this.extraImports,
);
this.componentRef && this.created.emit(this.componentRef.instance);
this.loaded = true; this.loaded = true;
} catch (error) { } catch (error) {
@ -192,7 +198,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
} }
/** /**
* Component being initialized. * @inheritdoc
*/ */
ngOnInit(): void { ngOnInit(): void {
// If there is some javascript to run, do it now. // 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) { for (const name in compileInstance.pendingCalls) {
const pendingCall = compileInstance.pendingCalls[name]; const pendingCall = compileInstance.pendingCalls[name];
if (typeof this[name] == 'function') { if (typeof this[name] === 'function') {
// Call the function. // Call the function.
Promise.resolve(this[name].apply(this, pendingCall.params)).then(pendingCall.defer.resolve) Promise.resolve(this[name].apply(this, pendingCall.params)).then(pendingCall.defer.resolve)
.catch(pendingCall.defer.reject); .catch(pendingCall.defer.reject);
@ -218,21 +224,21 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
} }
/** /**
* Content has been initialized. * @inheritdoc
*/ */
ngAfterContentInit(): void { ngAfterContentInit(): void {
this.callLifecycleHookOverride('ngAfterContentInit'); this.callLifecycleHookOverride('ngAfterContentInit');
} }
/** /**
* View has been initialized. * @inheritdoc
*/ */
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.callLifecycleHookOverride('ngAfterViewInit'); this.callLifecycleHookOverride('ngAfterViewInit');
} }
/** /**
* Component destroyed. * @inheritdoc
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.callLifecycleHookOverride('ngOnDestroy'); this.callLifecycleHookOverride('ngOnDestroy');
@ -283,9 +289,9 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
* once the component has been created. * once the component has been created.
* @returns Result of the call. Undefined if no component instance or the function doesn't exist. * @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 (this.componentInstance) {
if (typeof this.componentInstance[name] == 'function') { if (typeof this.componentInstance[name] === 'function') {
return this.componentInstance[name].apply(this.componentInstance, params); return this.componentInstance[name].apply(this.componentInstance, params);
} }
} else if (callWhenCreated) { } else if (callWhenCreated) {

View File

@ -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 {}

View File

@ -17,14 +17,13 @@ import {
Injector, Injector,
Component, Component,
NgModule, NgModule,
Compiler,
ComponentFactory,
ComponentRef, ComponentRef,
NgModuleRef,
NO_ERRORS_SCHEMA, NO_ERRORS_SCHEMA,
Type, Type,
Provider,
createNgModule,
ViewContainerRef,
} from '@angular/core'; } from '@angular/core';
import { JitCompilerFactory } from '@angular/platform-browser-dynamic';
import { import {
ActionSheetController, ActionSheetController,
AlertController, AlertController,
@ -34,6 +33,7 @@ import {
ToastController, ToastController,
} from '@ionic/angular'; } from '@ionic/angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { TranslatePipeForCompile } from '../pipes/translate';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { CoreEvents } from '@singletons/events'; import { CoreEvents } from '@singletons/events';
@ -160,6 +160,8 @@ import { CorePromisedValue } from '@classes/promised-value';
import { CorePlatform } from '@services/platform'; import { CorePlatform } from '@services/platform';
import { CoreAutoLogoutService } from '@features/autologout/services/autologout'; import { CoreAutoLogoutService } from '@features/autologout/services/autologout';
import '@angular/compiler';
/** /**
* Service to provide functionalities regarding compiling dynamic HTML and Javascript. * Service to provide functionalities regarding compiling dynamic HTML and Javascript.
*/ */
@ -167,7 +169,6 @@ import { CoreAutoLogoutService } from '@features/autologout/services/autologout'
export class CoreCompileProvider { export class CoreCompileProvider {
protected logger: CoreLogger; protected logger: CoreLogger;
protected compiler: Compiler;
// Other Ionic/Angular providers that don't depend on where they are injected. // Other Ionic/Angular providers that don't depend on where they are injected.
protected readonly OTHER_SERVICES: unknown[] = [ protected readonly OTHER_SERVICES: unknown[] = [
@ -186,10 +187,8 @@ export class CoreCompileProvider {
getWorkshopComponentModules, getWorkshopComponentModules,
]; ];
constructor(protected injector: Injector, compilerFactory: JitCompilerFactory) { constructor(protected injector: Injector) {
this.logger = CoreLogger.getInstance('CoreCompileProvider'); this.logger = CoreLogger.getInstance('CoreCompileProvider');
this.compiler = compilerFactory.createCompiler();
} }
/** /**
@ -197,14 +196,17 @@ export class CoreCompileProvider {
* *
* @param template The template of the component. * @param template The template of the component.
* @param componentClass The JS class 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. * @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<T = unknown>( async createAndCompileComponent<T = unknown>(
template: string, template: string,
componentClass: Type<T>, componentClass: Type<T>,
viewContainerRef: ViewContainerRef,
extraImports: any[] = [], // eslint-disable-line @typescript-eslint/no-explicit-any extraImports: any[] = [], // eslint-disable-line @typescript-eslint/no-explicit-any
): Promise<ComponentFactory<T> | undefined> { ): Promise<ComponentRef<T> | undefined> {
// Create the component using the template and the class. // Create the component using the template and the class.
const component = Component({ template })(componentClass); const component = Component({ template })(componentClass);
@ -213,17 +215,24 @@ export class CoreCompileProvider {
...CoreArray.flatten(lazyImports), ...CoreArray.flatten(lazyImports),
...this.IMPORTS, ...this.IMPORTS,
...extraImports, ...extraImports,
TranslatePipeForCompile,
]; ];
// Now create the module containing the component.
const module = NgModule({ imports, declarations: [component], schemas: [NO_ERRORS_SCHEMA] })(class {});
try { try {
// Compile the module and the component. viewContainerRef.clear();
const factories = await this.compiler.compileModuleAndAllComponentsAsync(module);
// Search and return the factory of the component we just created. // Now create the module containing the component.
return factories.componentFactories.find(factory => factory.componentType == component); const ngModuleRef = createNgModule(
NgModule({ imports, declarations: [component], schemas: [NO_ERRORS_SCHEMA] })(class {}),
this.injector,
);
return viewContainerRef.createComponent(
component,
{
environmentInjector: ngModuleRef,
},
);
} catch (error) { } catch (error) {
this.logger.error('Error compiling template', template); this.logger.error('Error compiling template', template);
this.logger.error(error); 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. // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
for (const i in providers) { for (const i in providers) {
const providerDef = providers[i]; const providerDef = providers[i];
if (typeof providerDef == 'function' && providerDef.name) { if (typeof providerDef === 'function' && providerDef.name) {
try { try {
// Inject the provider to the instance. We use the class name as the property name. // 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<Provider>(providerDef);
} catch (ex) { } catch (ex) {
this.logger.error('Error injecting provider', providerDef.name, 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<T = unknown>(
template: string,
componentClass: Type<T>,
injector?: Injector,
): Promise<ComponentRef<T> | 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); export const CoreCompile = makeSingleton(CoreCompileProvider);

View File

@ -48,18 +48,18 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck {
// Get the compile element. Don't set the right type to prevent circular dependencies. // Get the compile element. Don't set the right type to prevent circular dependencies.
@ViewChild('compile') compileComponent?: CoreCompileHtmlComponent; @ViewChild('compile') compileComponent?: CoreCompileHtmlComponent;
@HostBinding('class') @Input() component!: string; @HostBinding('class') @Input() component = '';
@Input() method!: string; @Input() method!: string;
@Input() args?: Record<string, unknown>; @Input() args?: Record<string, unknown>;
@Input() initResult?: CoreSitePluginsContent | null; // Result of the init WS call of the handler. @Input() initResult?: CoreSitePluginsContent | null; // Result of the init WS call of the handler.
@Input() data?: Record<string, unknown>; // Data to pass to the component. @Input() data: Record<string, unknown> = {}; // Data to pass to the component.
@Input() preSets?: CoreSiteWSPreSets; // The preSets for the WS call. @Input() preSets?: CoreSiteWSPreSets; // The preSets for the WS call.
@Input() pageTitle?: string; // Current page title. It can be used by the "new-content" directives. @Input() pageTitle?: string; // Current page title. It can be used by the "new-content" directives.
@Output() onContentLoaded = new EventEmitter<CoreSitePluginsPluginContentLoadedData>(); // Emits event when content is loaded. @Output() onContentLoaded = new EventEmitter<CoreSitePluginsPluginContentLoadedData>(); // Emits event when content is loaded.
@Output() onLoadingContent = new EventEmitter<boolean>(); // Emits an event when starts to load the content. @Output() onLoadingContent = new EventEmitter<boolean>(); // Emits an event when starts to load the content.
content?: string; // Content. content = ''; // Content.
javascript?: string; // Javascript to execute. javascript = ''; // Javascript to execute.
otherData?: Record<string, unknown>; // Other data of the content. otherData?: Record<string, unknown>; // Other data of the content.
dataLoaded = false; dataLoaded = false;
invalidateObservable = new Subject<void>(); // An observable to notify observers when to invalidate data. invalidateObservable = new Subject<void>(); // 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)); 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. // Pass some methods as jsData so they can be called from the template too.
this.jsData.fetchContent = refresh => this.fetchContent(refresh); this.jsData.fetchContent = (refresh?: boolean) => this.fetchContent(refresh);
this.jsData.openContent = (title, args, component, method, jsData, preSets, ptrEnabled) => this.jsData.openContent = (
this.openContent(title, args, component, method, jsData, preSets, ptrEnabled); title: string,
this.jsData.refreshContent = showSpinner => this.refreshContent(showSpinner); args?: Record<string, unknown>,
this.jsData.updateContent = (args, component, method, jsData, preSets) => component?: string,
this.updateContent(args, component, method, jsData, preSets); method?: string,
this.jsData.updateModuleCourseContent = (cmId, alreadyFetched) => this.updateModuleCourseContent(cmId, alreadyFetched); jsData?: Record<string, unknown> | 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<string, unknown>,
component?: string,
method?: string,
jsData?: Record<string, unknown>,
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 }); this.onContentLoaded.emit({ refresh: !!refresh, success: true });
} catch (error) { } catch (error) {
@ -154,7 +168,7 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck {
*/ */
openContent( openContent(
title: string, title: string,
args?: Record<string, unknown>, args: Record<string, unknown> = {},
component?: string, component?: string,
method?: string, method?: string,
jsData?: Record<string, unknown> | boolean, jsData?: Record<string, unknown> | boolean,
@ -167,7 +181,6 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck {
component = component || this.component; component = component || this.component;
method = method || this.method; method = method || this.method;
args = args || {};
const hash = <string> Md5.hashAsciiStr(JSON.stringify(args)); const hash = <string> Md5.hashAsciiStr(JSON.stringify(args));
CoreNavigator.navigateToSitePath(`siteplugins/content/${component}/${method}/${hash}`, { 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. * @param showSpinner Whether to show spinner while refreshing.
*/ */
async refreshContent(showSpinner: boolean = true): Promise<void> { async refreshContent(showSpinner = true): Promise<void> {
if (showSpinner) { if (showSpinner) {
this.dataLoaded = false; this.dataLoaded = false;
} }

View File

@ -226,7 +226,6 @@ export const Translate: Omit<CoreSingletonProxy<TranslateService>, 'instant'> &
// Async singletons. // Async singletons.
export const AngularFrameworkDelegate = asyncInstance(async () => { export const AngularFrameworkDelegate = asyncInstance(async () => {
const injector = await singletonsInjector; const injector = await singletonsInjector;
const environmentInjector = await injector.get(EnvironmentInjector);
return AngularDelegate.create(environmentInjector, injector); return AngularDelegate.create(injector.get(EnvironmentInjector), injector);
}); });