diff --git a/src/addon/mod/book/components/index/index.ts b/src/addon/mod/book/components/index/index.ts index 8e2b4bb92..1183a04e4 100644 --- a/src/addon/mod/book/components/index/index.ts +++ b/src/addon/mod/book/components/index/index.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, Optional } from '@angular/core'; -import { NavParams, NavController, Content, PopoverController } from 'ionic-angular'; +import { Content, PopoverController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; diff --git a/src/addon/mod/book/providers/module-handler.ts b/src/addon/mod/book/providers/module-handler.ts index d88e95a24..e440fcf33 100644 --- a/src/addon/mod/book/providers/module-handler.ts +++ b/src/addon/mod/book/providers/module-handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { NavController, NavOptions } from 'ionic-angular'; import { AddonModBookProvider } from './book'; import { AddonModBookIndexComponent } from '../components/index/index'; @@ -60,12 +60,14 @@ export class AddonModBookModuleHandler implements CoreCourseModuleHandler { /** * Get the component to render the module. This is needed to support singleactivity course format. * The component returned must implement CoreCourseModuleMainComponent. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course object. * @param {any} module The module object. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getMainComponent(course: any, module: any): any { + getMainComponent(injector: Injector, course: any, module: any): any | Promise { return AddonModBookIndexComponent; } } diff --git a/src/addon/mod/label/providers/module-handler.ts b/src/addon/mod/label/providers/module-handler.ts index 90cfdd780..06ab1e7f7 100644 --- a/src/addon/mod/label/providers/module-handler.ts +++ b/src/addon/mod/label/providers/module-handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; /** @@ -58,12 +58,14 @@ export class AddonModLabelModuleHandler implements CoreCourseModuleHandler { /** * Get the component to render the module. This is needed to support singleactivity course format. * The component returned must implement CoreCourseModuleMainComponent. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course object. * @param {any} module The module object. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getMainComponent(course: any, module: any): any { + getMainComponent(injector: Injector, course: any, module: any): any | Promise { // There's no need to implement this because label cannot be used in singleactivity course format. } } diff --git a/src/addon/userprofilefield/checkbox/providers/handler.ts b/src/addon/userprofilefield/checkbox/providers/handler.ts index a3dbb51e3..b160a08bd 100644 --- a/src/addon/userprofilefield/checkbox/providers/handler.ts +++ b/src/addon/userprofilefield/checkbox/providers/handler.ts @@ -12,7 +12,7 @@ // 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 } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '../../../../core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldCheckboxComponent } from '../component/checkbox'; @@ -60,10 +60,12 @@ export class AddonUserProfileFieldCheckboxHandler implements CoreUserProfileFiel /** * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. * - * @return {any} The component to use, undefined if not found. + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonUserProfileFieldCheckboxComponent; } } diff --git a/src/addon/userprofilefield/datetime/providers/handler.ts b/src/addon/userprofilefield/datetime/providers/handler.ts index d8135b94d..819d75f4c 100644 --- a/src/addon/userprofilefield/datetime/providers/handler.ts +++ b/src/addon/userprofilefield/datetime/providers/handler.ts @@ -12,7 +12,7 @@ // 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 } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '../../../../core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldDatetimeComponent } from '../component/datetime'; @@ -62,10 +62,12 @@ export class AddonUserProfileFieldDatetimeHandler implements CoreUserProfileFiel /** * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. * - * @return {any} The component to use, undefined if not found. + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonUserProfileFieldDatetimeComponent; } } diff --git a/src/addon/userprofilefield/menu/providers/handler.ts b/src/addon/userprofilefield/menu/providers/handler.ts index 931cef566..c02edb812 100644 --- a/src/addon/userprofilefield/menu/providers/handler.ts +++ b/src/addon/userprofilefield/menu/providers/handler.ts @@ -12,7 +12,7 @@ // 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 } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '../../../../core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldMenuComponent } from '../component/menu'; @@ -60,10 +60,12 @@ export class AddonUserProfileFieldMenuHandler implements CoreUserProfileFieldHan /** * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. * - * @return {any} The component to use, undefined if not found. + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonUserProfileFieldMenuComponent; } } diff --git a/src/addon/userprofilefield/text/providers/handler.ts b/src/addon/userprofilefield/text/providers/handler.ts index 1953660ce..7f9c3df5e 100644 --- a/src/addon/userprofilefield/text/providers/handler.ts +++ b/src/addon/userprofilefield/text/providers/handler.ts @@ -12,7 +12,7 @@ // 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 } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '../../../../core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldTextComponent } from '../component/text'; @@ -57,10 +57,12 @@ export class AddonUserProfileFieldTextHandler implements CoreUserProfileFieldHan /** * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. * - * @return {any} The component to use, undefined if not found. + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonUserProfileFieldTextComponent; } } diff --git a/src/addon/userprofilefield/textarea/providers/handler.ts b/src/addon/userprofilefield/textarea/providers/handler.ts index 1c07057db..c945e07aa 100644 --- a/src/addon/userprofilefield/textarea/providers/handler.ts +++ b/src/addon/userprofilefield/textarea/providers/handler.ts @@ -12,7 +12,7 @@ // 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 } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '../../../../core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldTextareaComponent } from '../component/textarea'; @@ -66,10 +66,12 @@ export class AddonUserProfileFieldTextareaHandler implements CoreUserProfileFiel /** * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. * - * @return {any} The component to use, undefined if not found. + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonUserProfileFieldTextareaComponent; } } diff --git a/src/components/dynamic-component/dynamic-component.ts b/src/components/dynamic-component/dynamic-component.ts index f2a045043..b8dd46fa2 100644 --- a/src/components/dynamic-component/dynamic-component.ts +++ b/src/components/dynamic-component/dynamic-component.ts @@ -13,9 +13,10 @@ // limitations under the License. import { - Component, Input, ViewChild, OnInit, OnChanges, DoCheck, ViewContainerRef, ComponentFactoryResolver, - KeyValueDiffers, SimpleChange + Component, Input, ViewChild, OnInit, OnChanges, DoCheck, ViewContainerRef, ComponentFactoryResolver, ComponentRef, + KeyValueDiffers, SimpleChange, ChangeDetectorRef, Optional, ElementRef } from '@angular/core'; +import { NavController } from 'ionic-angular'; import { CoreLoggerProvider } from '@providers/logger'; /** @@ -39,6 +40,10 @@ import { CoreLoggerProvider } from '@providers/logger'; * * Please notice that the component that you pass needs to be declared in entryComponents of the module to be created dynamically. * + * Alternatively, you can also supply a ComponentRef instead of the class of the component. In this case, the component won't + * be instantiated because it already is, it will be attached to the view and the right data will be passed to it. + * Passing ComponentRef is meant for site addons, so we'll inject a NavController instance to the component. + * * The contents of this component will be displayed if no component is supplied or it cannot be created. In the example above, * if no component is supplied then the template will show the message "Cannot render the data.". */ @@ -62,7 +67,8 @@ export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck { protected logger: any; protected differ: any; // To detect changes in the data input. - constructor(logger: CoreLoggerProvider, private factoryResolver: ComponentFactoryResolver, differs: KeyValueDiffers) { + constructor(logger: CoreLoggerProvider, protected factoryResolver: ComponentFactoryResolver, differs: KeyValueDiffers, + @Optional() protected navCtrl: NavController, protected cdr: ChangeDetectorRef, protected element: ElementRef) { this.logger = logger.getInstance('CoreDynamicComponent'); this.differ = differs.find([]).create(); } @@ -128,21 +134,32 @@ export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck { return true; } - try { - // Create the component and add it to the container. - const factory = this.factoryResolver.resolveComponentFactory(this.component), - componentRef = this.container.createComponent(factory); + if (this.component instanceof ComponentRef) { + // A ComponentRef was supplied instead of the component class. Add it to the view. + this.container.insert(this.component.hostView); + this.instance = this.component.instance; - this.instance = componentRef.instance; + // This feature is usually meant for site addons. Inject some properties. + this.instance['ChangeDetectorRef'] = this.cdr; + this.instance['NavController'] = this.navCtrl; + this.instance['componentContainer'] = this.element.nativeElement; + } else { + try { + // Create the component and add it to the container. + const factory = this.factoryResolver.resolveComponentFactory(this.component), + componentRef = this.container.createComponent(factory); - this.setInputData(); + this.instance = componentRef.instance; + } catch (ex) { + this.logger.error('Error creating component', ex); - return true; - } catch (ex) { - this.logger.error('Error creating component', ex); - - return false; + return false; + } } + + this.setInputData(); + + return true; } /** diff --git a/src/components/split-view/split-view.ts b/src/components/split-view/split-view.ts index 0ab53cca2..7d5a936b7 100644 --- a/src/components/split-view/split-view.ts +++ b/src/components/split-view/split-view.ts @@ -14,7 +14,7 @@ // Code based on https://github.com/martinpritchardelevate/ionic-split-pane-demo -import { Component, ViewChild, Input, ElementRef, OnInit } from '@angular/core'; +import { Component, ViewChild, Input, ElementRef, OnInit, Optional } from '@angular/core'; import { NavController, Nav } from 'ionic-angular'; /** @@ -54,7 +54,7 @@ export class CoreSplitViewComponent implements OnInit { // Empty placeholder for the 'detail' page. detailPage: any = null; - constructor(private masterNav: NavController, element: ElementRef) { + constructor(@Optional() private masterNav: NavController, element: ElementRef) { this.element = element.nativeElement; } diff --git a/src/core/compile/components/compile-html/compile-html.ts b/src/core/compile/components/compile-html/compile-html.ts index 8253e2a92..a7773a965 100644 --- a/src/core/compile/components/compile-html/compile-html.ts +++ b/src/core/compile/components/compile-html/compile-html.ts @@ -13,24 +13,12 @@ // limitations under the License. import { - Component, NgModule, Input, OnInit, OnChanges, OnDestroy, ViewContainerRef, Compiler, ViewChild, ComponentRef, - SimpleChange, ChangeDetectorRef + Component, Input, OnInit, OnChanges, OnDestroy, ViewContainerRef, ViewChild, ComponentRef, SimpleChange, ChangeDetectorRef, + ElementRef, Optional } from '@angular/core'; -import { IonicModule, NavController } from 'ionic-angular'; -import { TranslateModule } from '@ngx-translate/core'; +import { NavController } from 'ionic-angular'; import { CoreCompileProvider } from '../../../compile/providers/compile'; -// Import all modules that define components, directives and pipes. -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; -import { CorePipesModule } from '../../../../pipes/pipes.module'; -import { CoreCourseComponentsModule } from '../../../course/components/components.module'; -import { CoreCourseDirectivesModule } from '../../../course/directives/directives.module'; -import { CoreCoursesComponentsModule } from '../../../courses/components/components.module'; -import { CoreSiteAddonsDirectivesModule } from '../../../siteaddons/directives/directives.module'; -import { CoreSiteHomeComponentsModule } from '../../../sitehome/components/components.module'; -import { CoreUserComponentsModule } from '../../../user/components/components.module'; - /** * This component has a behaviour similar to $compile for AngularJS. Given an HTML code, it will compile it so all its * components and directives are instantiated. @@ -51,13 +39,6 @@ import { CoreUserComponentsModule } from '../../../user/components/components.mo template: '' }) export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { - // List of imports for dynamic module. Since the template can have any component we need to import all core components modules. - protected IMPORTS = [ - IonicModule, TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, CorePipesModule, - CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule, - CoreCourseDirectivesModule, CoreSiteAddonsDirectivesModule - ]; - @Input() text: string; // The HTML text to display. @Input() javascript: string; // The Javascript to execute in the component. @Input() jsData; // Data to pass to the fake component. @@ -66,9 +47,12 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { @ViewChild('dynamicComponent', { read: ViewContainerRef }) container: ViewContainerRef; protected componentRef: ComponentRef; + protected element; - constructor(protected compileProvider: CoreCompileProvider, protected compiler: Compiler, - protected cdr: ChangeDetectorRef, protected navCtrl: NavController) { } + constructor(protected compileProvider: CoreCompileProvider, protected cdr: ChangeDetectorRef, element: ElementRef, + @Optional() protected navCtrl: NavController) { + this.element = element.nativeElement; + } /** * Detect changes on input properties. @@ -76,26 +60,14 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { ngOnChanges(changes: { [name: string]: SimpleChange }): void { if ((changes.text || changes.javascript) && this.text) { // Create a new component and a new module. - const component = this.createComponent(), - module = NgModule({imports: this.IMPORTS, declarations: [component]})(class {}); - - // Compile the module and the component. - this.compiler.compileModuleAndAllComponentsAsync(module).then((factories) => { - // Search the factory of the component we just created. - let componentFactory; - for (const i in factories.componentFactories) { - const factory = factories.componentFactories[i]; - if (factory.componentType == component) { - componentFactory = factory; - break; - } - } - + this.compileProvider.createAndCompileComponent(this.text, this.getComponentClass()).then((factory) => { // Destroy previous components. this.componentRef && this.componentRef.destroy(); - // Create the component. - this.componentRef = this.container.createComponent(componentFactory); + if (factory) { + // Create the component. + this.componentRef = this.container.createComponent(factory); + } }); } } @@ -108,20 +80,16 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { } /** - * Create a dynamic component to compile the HTML and run the javascript. + * Get a class that defines the dynamic component. * * @return {any} The component class. */ - protected createComponent(): any { + protected getComponentClass(): any { // tslint:disable: no-this-assignment const compileInstance = this; // Create the component, using the text as the template. - return Component({ - template: this.text - }) - (class CoreCompileHtmlFakeComponent implements OnInit { - + return class CoreCompileHtmlFakeComponent implements OnInit { constructor() { // If there is some javascript to run, prepare the instance. if (compileInstance.javascript) { @@ -130,7 +98,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { // Add some more components and classes. this['ChangeDetectorRef'] = compileInstance.cdr; this['NavController'] = compileInstance.navCtrl; - this['componentContainer'] = compileInstance.container; + this['componentContainer'] = compileInstance.element; // Add the data passed to the component. for (const name in compileInstance.jsData) { @@ -145,6 +113,6 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { compileInstance.compileProvider.executeJavascript(this, compileInstance.javascript); } } - }); + }; } } diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index 25738aa15..1a7e10015 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable, Injector } from '@angular/core'; +import { Injectable, Injector, Component, NgModule, Compiler, ComponentFactory, ComponentRef, NgModuleRef } from '@angular/core'; import { - Platform, ActionSheetController, AlertController, LoadingController, ModalController, PopoverController, ToastController + Platform, ActionSheetController, AlertController, LoadingController, ModalController, PopoverController, ToastController, + IonicModule } from 'ionic-angular'; -import { TranslateService } from '@ngx-translate/core'; +import { TranslateService, TranslateModule } from '@ngx-translate/core'; import { CoreLoggerProvider } from '../../../providers/logger'; // Import core providers. @@ -55,6 +56,24 @@ import { CoreContentLinksModuleGradeHandler } from '../../contentlinks/classes/m import { CoreContentLinksModuleIndexHandler } from '../../contentlinks/classes/module-index-handler'; import { CoreCourseModulePrefetchHandlerBase } from '../../course/classes/module-prefetch-handler'; +// Import all modules that define components, directives and pipes. +import { CoreComponentsModule } from '../../../components/components.module'; +import { CoreDirectivesModule } from '../../../directives/directives.module'; +import { CorePipesModule } from '../../../pipes/pipes.module'; +import { CoreCourseComponentsModule } from '../../course/components/components.module'; +import { CoreCourseDirectivesModule } from '../../course/directives/directives.module'; +import { CoreCoursesComponentsModule } from '../../courses/components/components.module'; +import { CoreSiteAddonsDirectivesModule } from '../../siteaddons/directives/directives.module'; +import { CoreSiteHomeComponentsModule } from '../../sitehome/components/components.module'; +import { CoreUserComponentsModule } from '../../user/components/components.module'; + +// Import some components listed in entryComponents so they can be injected dynamically. +import { CoreCourseUnsupportedModuleComponent } from '../../course/components/unsupported-module/unsupported-module'; +import { CoreCourseFormatSingleActivityComponent } from '../../course/formats/singleactivity/components/singleactivity'; +import { CoreSiteAddonsModuleIndexComponent } from '../../siteaddons/components/module-index/module-index'; +import { CoreSiteAddonsCourseOptionComponent } from '../../siteaddons/components/course-option/course-option'; +import { CoreSiteAddonsCourseFormatComponent } from '../../siteaddons/components/course-format/course-format'; + /** * Service to provide functionalities regarding compiling dynamic HTML and Javascript. */ @@ -69,10 +88,46 @@ export class CoreCompileProvider { ModalController, PopoverController, ToastController, FormBuilder ]; - constructor(protected injector: Injector, logger: CoreLoggerProvider) { + // List of imports for dynamic module. Since the template can have any component we need to import all core components modules. + protected IMPORTS = [ + IonicModule, TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, CorePipesModule, + CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule, + CoreCourseDirectivesModule, CoreSiteAddonsDirectivesModule + ]; + + constructor(protected injector: Injector, logger: CoreLoggerProvider, protected compiler: Compiler) { this.logger = logger.getInstance('CoreCompileProvider'); } + /** + * Create and compile a dynamic component. + * + * @param {string} template The template of the component. + * @param {any} componentClass The JS class of the component. + * @return {Promise>} Promise resolved with the factory to instantiate the component. + */ + createAndCompileComponent(template: string, componentClass: any): Promise> { + // Create the component using the template and the class. + const component = Component({ + template: template + }) + (componentClass); + + // Now create the module containing the component. + const module = NgModule({imports: this.IMPORTS, declarations: [component]})(class {}); + + // Compile the module and the component. + return this.compiler.compileModuleAndAllComponentsAsync(module).then((factories) => { + // Search and return the factory of the component we just created. + for (const i in factories.componentFactories) { + const factory = factories.componentFactories[i]; + if (factory.componentType == component) { + return factory; + } + } + }); + } + /** * Eval some javascript using the context of the function. * @@ -124,6 +179,9 @@ export class CoreCompileProvider { } } + // Inject current service. + instance['CoreCompileProvider'] = this; + // Add some final classes. instance['injector'] = this.injector; instance['Validators'] = Validators; @@ -138,5 +196,29 @@ export class CoreCompileProvider { instance['CoreContentLinksModuleGradeHandler'] = CoreContentLinksModuleGradeHandler; instance['CoreContentLinksModuleIndexHandler'] = CoreContentLinksModuleIndexHandler; instance['CoreCourseModulePrefetchHandlerBase'] = CoreCourseModulePrefetchHandlerBase; + instance['CoreCourseUnsupportedModuleComponent'] = CoreCourseUnsupportedModuleComponent; + instance['CoreCourseFormatSingleActivityComponent'] = CoreCourseFormatSingleActivityComponent; + instance['CoreSiteAddonsModuleIndexComponent'] = CoreSiteAddonsModuleIndexComponent; + instance['CoreSiteAddonsCourseOptionComponent'] = CoreSiteAddonsCourseOptionComponent; + instance['CoreSiteAddonsCourseFormatComponent'] = CoreSiteAddonsCourseFormatComponent; + } + + /** + * Instantiate a dynamic component. + * + * @param {string} template The template of the component. + * @param {any} componentClass The JS class of the component. + * @param {Injector} [injector] The injector to use. It's recommended to pass it so NavController and similar can be injected. + * @return {Promise>} Promise resolved with the component instance. + */ + instantiateDynamicComponent(template: string, componentClass: any, injector?: Injector): Promise> { + injector = injector || this.injector; + + return this.createAndCompileComponent(template, componentClass).then((factory) => { + if (factory) { + // Create and return the component. + return factory.create(injector, undefined, undefined, injector.get(NgModuleRef)); + } + }); } } diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index b4a2985a9..51cd9555c 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -13,7 +13,7 @@ // limitations under the License. import { - Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter, ViewChildren, QueryList + Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter, ViewChildren, QueryList, Injector } from '@angular/core'; import { Content } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; @@ -69,7 +69,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { protected sectionStatusObserver; - constructor(private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, + constructor(private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, private injector: Injector, private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider, eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider, private content: Content, prefetchDelegate: CoreCourseModulePrefetchDelegate) { @@ -194,19 +194,29 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { protected getComponents(): void { if (this.course) { if (!this.courseFormatComponent) { - this.courseFormatComponent = this.cfDelegate.getCourseFormatComponent(this.course); + this.cfDelegate.getCourseFormatComponent(this.injector, this.course).then((component) => { + this.courseFormatComponent = component; + }); } if (!this.courseSummaryComponent) { - this.courseSummaryComponent = this.cfDelegate.getCourseSummaryComponent(this.course); + this.cfDelegate.getCourseSummaryComponent(this.injector, this.course).then((component) => { + this.courseSummaryComponent = component; + }); } if (!this.sectionSelectorComponent) { - this.sectionSelectorComponent = this.cfDelegate.getSectionSelectorComponent(this.course); + this.cfDelegate.getSectionSelectorComponent(this.injector, this.course).then((component) => { + this.sectionSelectorComponent = component; + }); } if (!this.singleSectionComponent) { - this.singleSectionComponent = this.cfDelegate.getSingleSectionComponent(this.course); + this.cfDelegate.getSingleSectionComponent(this.injector, this.course).then((component) => { + this.singleSectionComponent = component; + }); } if (!this.allSectionsComponent) { - this.allSectionsComponent = this.cfDelegate.getAllSectionsComponent(this.course); + this.cfDelegate.getAllSectionsComponent(this.injector, this.course).then((component) => { + this.allSectionsComponent = component; + }); } } } diff --git a/src/core/course/components/module/module.ts b/src/core/course/components/module/module.ts index 00d0bbf1f..fef73a640 100644 --- a/src/core/course/components/module/module.ts +++ b/src/core/course/components/module/module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'; +import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; @@ -67,7 +67,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { protected prefetchHandler: CoreCourseModulePrefetchHandler; protected statusObserver; - constructor(protected navCtrl: NavController, protected prefetchDelegate: CoreCourseModulePrefetchDelegate, + constructor(@Optional() protected navCtrl: NavController, protected prefetchDelegate: CoreCourseModulePrefetchDelegate, protected domUtils: CoreDomUtilsProvider, protected courseHelper: CoreCourseHelperProvider, protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider) { this.completionChanged = new EventEmitter(); diff --git a/src/core/course/formats/singleactivity/components/singleactivity.ts b/src/core/course/formats/singleactivity/components/singleactivity.ts index a94f8730f..a68a60fda 100644 --- a/src/core/course/formats/singleactivity/components/singleactivity.ts +++ b/src/core/course/formats/singleactivity/components/singleactivity.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnChanges, SimpleChange, ViewChild } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChange, ViewChild, Injector } from '@angular/core'; import { CoreCourseModuleDelegate } from '../../../providers/module-delegate'; import { CoreCourseUnsupportedModuleComponent } from '../../../components/unsupported-module/unsupported-module'; import { CoreDynamicComponent } from '../../../../../components/dynamic-component/dynamic-component'; @@ -36,7 +36,7 @@ export class CoreCourseFormatSingleActivityComponent implements OnChanges { componentClass: any; // The class of the component to render. data: any = {}; // Data to pass to the component. - constructor(private moduleDelegate: CoreCourseModuleDelegate) { } + constructor(private moduleDelegate: CoreCourseModuleDelegate, private injector: Injector) { } /** * Detect changes on input properties. @@ -47,8 +47,9 @@ export class CoreCourseFormatSingleActivityComponent implements OnChanges { const module = this.sections[0] && this.sections[0].modules && this.sections[0].modules[0]; if (module && !this.componentClass) { // We haven't obtained the class yet. Get it now. - this.componentClass = this.moduleDelegate.getMainComponent(this.course, module) || - CoreCourseUnsupportedModuleComponent; + this.moduleDelegate.getMainComponent(this.injector, this.course, module).then((component) => { + this.componentClass = component || CoreCourseUnsupportedModuleComponent; + }); } this.data.courseId = this.course.id; diff --git a/src/core/course/formats/singleactivity/providers/handler.ts b/src/core/course/formats/singleactivity/providers/handler.ts index ed7d44eef..9dd528021 100644 --- a/src/core/course/formats/singleactivity/providers/handler.ts +++ b/src/core/course/formats/singleactivity/providers/handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreCourseFormatHandler } from '../../../providers/format-delegate'; import { CoreCourseFormatSingleActivityComponent } from '../components/singleactivity'; @@ -86,11 +86,13 @@ export class CoreCourseFormatSingleActivityHandler implements CoreCourseFormatHa * Return the Component to use to display the course format instead of using the default one. * Use it if you want to display a format completely different from the default one. * If you want to customize the default format there are several methods to customize parts of it. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getCourseFormatComponent(course: any): any { + getCourseFormatComponent(injector: Injector, course: any): any | Promise { return CoreCourseFormatSingleActivityComponent; } } diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 093e0a29f..ac47adf2b 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChild, OnDestroy } from '@angular/core'; +import { Component, ViewChild, OnDestroy, Injector } from '@angular/core'; import { IonicPage, NavParams, Content, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; @@ -63,7 +63,7 @@ export class CoreCourseSectionPage implements OnDestroy { private courseFormatDelegate: CoreCourseFormatDelegate, private courseOptionsDelegate: CoreCourseOptionsDelegate, private translate: TranslateService, private courseHelper: CoreCourseHelperProvider, eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider, private coursesProvider: CoreCoursesProvider, - sitesProvider: CoreSitesProvider, private navCtrl: NavController, + sitesProvider: CoreSitesProvider, private navCtrl: NavController, private injector: Injector, private prefetchDelegate: CoreCourseModulePrefetchDelegate) { this.course = navParams.get('course'); this.sectionId = navParams.get('sectionId'); @@ -191,7 +191,8 @@ export class CoreCourseSectionPage implements OnDestroy { })); // Load the course handlers. - promises.push(this.courseOptionsDelegate.getHandlersToDisplay(this.course, refresh, false).then((handlers) => { + promises.push(this.courseOptionsDelegate.getHandlersToDisplay(this.injector, this.course, refresh, false) + .then((handlers) => { // Add the courseId to the handler component data. handlers.forEach((handler) => { handler.data.componentData = handler.data.componentData || {}; diff --git a/src/core/course/providers/format-delegate.ts b/src/core/course/providers/format-delegate.ts index 2398f4192..55f064f2a 100644 --- a/src/core/course/providers/format-delegate.ts +++ b/src/core/course/providers/format-delegate.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; @@ -85,44 +85,54 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler { * Return the Component to use to display the course format instead of using the default one. * Use it if you want to display a format completely different from the default one. * If you want to customize the default format there are several methods to customize parts of it. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getCourseFormatComponent?(course: any): any; + getCourseFormatComponent?(injector: Injector, course: any): any | Promise; /** * Return the Component to use to display the course summary inside the default course format. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getCourseSummaryComponent?(course: any): any; + getCourseSummaryComponent?(injector: Injector, course: any): any | Promise; /** * Return the Component to use to display the section selector inside the default course format. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getSectionSelectorComponent?(course: any): any; + getSectionSelectorComponent?(injector: Injector, course: any): any | Promise; /** * Return the Component to use to display a single section. This component will only be used if the user is viewing a * single section. If all the sections are displayed at once then it won't be used. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getSingleSectionComponent?(course: any): any; + getSingleSectionComponent?(injector: Injector, course: any): any | Promise; /** * Return the Component to use to display all sections in a course. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getAllSectionsComponent?(course: any): any; + getAllSectionsComponent?(injector: Injector, course: any): any | Promise; /** * Invalidate the data required to load the course format. @@ -199,31 +209,40 @@ export class CoreCourseFormatDelegate extends CoreDelegate { /** * Get the component to use to display all sections in a course. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getAllSectionsComponent(course: any): any { - return this.executeFunction(course.format, 'getAllSectionsComponent', [course]); + getAllSectionsComponent(injector: Injector, course: any): Promise { + return Promise.resolve(this.executeFunction(course.format, 'getAllSectionsComponent', [injector, course])).catch((e) => { + this.logger.error('Error getting all sections component', e); + }); } /** * Get the component to use to display a course format. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getCourseFormatComponent(course: any): any { - return this.executeFunction(course.format, 'getCourseFormatComponent', [course]); + getCourseFormatComponent(injector: Injector, course: any): Promise { + return Promise.resolve(this.executeFunction(course.format, 'getCourseFormatComponent', [injector, course])).catch((e) => { + this.logger.error('Error getting course format component', e); + }); } /** * Get the component to use to display the course summary in the default course format. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getCourseSummaryComponent(course: any): any { - return this.executeFunction(course.format, 'getCourseSummaryComponent', [course]); + getCourseSummaryComponent(injector: Injector, course: any): Promise { + return Promise.resolve(this.executeFunction(course.format, 'getCourseSummaryComponent', [injector, course])).catch((e) => { + this.logger.error('Error getting course summary component', e); + }); } /** @@ -259,22 +278,29 @@ export class CoreCourseFormatDelegate extends CoreDelegate { /** * Get the component to use to display the section selector inside the default course format. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getSectionSelectorComponent(course: any): any { - return this.executeFunction(course.format, 'getSectionSelectorComponent', [course]); + getSectionSelectorComponent(injector: Injector, course: any): Promise { + return Promise.resolve(this.executeFunction(course.format, 'getSectionSelectorComponent', [injector, course])) + .catch((e) => { + this.logger.error('Error getting section selector component', e); + }); } /** * Get the component to use to display a single section. This component will only be used if the user is viewing * a single section. If all the sections are displayed at once then it won't be used. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getSingleSectionComponent(course: any): any { - return this.executeFunction(course.format, 'getSingleSectionComponent', [course]); + getSingleSectionComponent(injector: Injector, course: any): Promise { + return Promise.resolve(this.executeFunction(course.format, 'getSingleSectionComponent', [injector, course])).catch((e) => { + this.logger.error('Error getting single section component', e); + }); } /** diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 3c015a056..c883ed13b 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; @@ -120,7 +120,7 @@ export class CoreCourseHelperProvider { private utils: CoreUtilsProvider, private translate: TranslateService, private loginHelper: CoreLoginHelperProvider, private courseOptionsDelegate: CoreCourseOptionsDelegate, private siteHomeProvider: CoreSiteHomeProvider, private eventsProvider: CoreEventsProvider, private fileHelper: CoreFileHelperProvider, - private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider) { } + private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider, private injector: Injector) { } /** * This function treats every module on the sections provided to load the handler data, treat completion @@ -275,7 +275,7 @@ export class CoreCourseHelperProvider { if (courseHandlers) { promise = Promise.resolve(courseHandlers); } else { - promise = this.courseOptionsDelegate.getHandlersToDisplay(course); + promise = this.courseOptionsDelegate.getHandlersToDisplay(this.injector, course); } return promise.then((handlers: CoreCourseOptionsHandlerToDisplay[]) => { @@ -323,7 +323,7 @@ export class CoreCourseHelperProvider { subPromises.push(this.courseProvider.getSections(course.id, false, true).then((courseSections) => { sections = courseSections; })); - subPromises.push(this.courseOptionsDelegate.getHandlersToDisplay(course).then((cHandlers) => { + subPromises.push(this.courseOptionsDelegate.getHandlersToDisplay(this.injector, course).then((cHandlers) => { handlers = cHandlers; })); diff --git a/src/core/course/providers/module-delegate.ts b/src/core/course/providers/module-delegate.ts index 53f910b08..27cc1eab8 100644 --- a/src/core/course/providers/module-delegate.ts +++ b/src/core/course/providers/module-delegate.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { NavController, NavOptions } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; @@ -38,12 +38,14 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler { /** * Get the component to render the module. This is needed to support singleactivity course format. * The component returned must implement CoreCourseModuleMainComponent. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course object. * @param {any} module The module object. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getMainComponent(course: any, module: any): any; + getMainComponent(injector: Injector, course: any, module: any): any | Promise; } /** @@ -176,17 +178,17 @@ export class CoreCourseModuleDelegate extends CoreDelegate { /** * Get the component to render the module. * + * @param {Injector} injector Injector. * @param {any} course The course object. * @param {any} module The module object. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getMainComponent?(course: any, module: any): any { + getMainComponent(injector: Injector, course: any, module: any): Promise { const handler = this.enabledHandlers[module.modname]; if (handler && handler.getMainComponent) { - const component = handler.getMainComponent(course, module); - if (component) { - return component; - } + return Promise.resolve(handler.getMainComponent(injector, course, module)).catch((err) => { + this.logger.error('Error getting main component', err); + }); } } diff --git a/src/core/course/providers/options-delegate.ts b/src/core/course/providers/options-delegate.ts index e73a4b6ab..93fe43a1e 100644 --- a/src/core/course/providers/options-delegate.ts +++ b/src/core/course/providers/options-delegate.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; @@ -45,10 +45,11 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler { /** * Returns the data needed to render the handler. * + * @param {Injector} injector Injector. * @param {number} courseId The course ID. - * @return {CoreCourseOptionsHandlerData} Data. + * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData?(courseId: number): CoreCourseOptionsHandlerData; + getDisplayData?(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise; /** * Should invalidate the data to determine if the handler is enabled for a certain course. @@ -243,6 +244,7 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { * Get the list of handlers that should be displayed for a course. * This function should be called only when the handlers need to be displayed, since it can call several WebServices. * + * @param {Injector} injector Injector. * @param {any} course The course object. * @param {boolean} [refresh] True if it should refresh the list. * @param {boolean} [isGuest] Whether it's guest. @@ -250,7 +252,7 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. * @return {Promise} Promise resolved with array of handlers. */ - getHandlersToDisplay(course: any, refresh?: boolean, isGuest?: boolean, navOptions?: any, admOptions?: any): + getHandlersToDisplay(injector: Injector, course: any, refresh?: boolean, isGuest?: boolean, navOptions?: any, admOptions?: any): Promise { course.id = parseInt(course.id, 10); @@ -269,14 +271,19 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { // Call getHandlersForAccess to make sure the handlers have been loaded. return this.getHandlersForAccess(course.id, refresh, accessData, course.navOptions, course.admOptions); }).then(() => { - const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] = []; + const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] = [], + promises = []; this.coursesHandlers[course.id].enabledHandlers.forEach((handler) => { - handlersToDisplay.push({ - data: handler.getDisplayData(course), - priority: handler.priority, - prefetch: handler.prefetch - }); + promises.push(Promise.resolve(handler.getDisplayData(injector, course)).then((data) => { + handlersToDisplay.push({ + data: data, + priority: handler.priority, + prefetch: handler.prefetch + }); + }).catch((err) => { + this.logger.error('Error getting data for handler', handler.name, err); + })); }); // Sort them by priority. diff --git a/src/core/courses/components/course-list-item/course-list-item.ts b/src/core/courses/components/course-list-item/course-list-item.ts index d3805a1f0..3a6e47343 100644 --- a/src/core/courses/components/course-list-item/course-list-item.ts +++ b/src/core/courses/components/course-list-item/course-list-item.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreCoursesProvider } from '../../providers/courses'; @@ -31,7 +31,8 @@ import { CoreCoursesProvider } from '../../providers/courses'; export class CoreCoursesCourseListItemComponent implements OnInit { @Input() course: any; // The course to render. - constructor(private navCtrl: NavController, private translate: TranslateService, private coursesProvider: CoreCoursesProvider) { + constructor(@Optional() private navCtrl: NavController, private translate: TranslateService, + private coursesProvider: CoreCoursesProvider) { } /** diff --git a/src/core/courses/components/course-progress/course-progress.ts b/src/core/courses/components/course-progress/course-progress.ts index f5152e3ff..77c81a975 100644 --- a/src/core/courses/components/course-progress/course-progress.ts +++ b/src/core/courses/components/course-progress/course-progress.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit, OnDestroy } from '@angular/core'; +import { Component, Input, OnInit, OnDestroy, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; @@ -44,7 +44,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { protected isDestroyed = false; protected courseStatusObserver; - constructor(private navCtrl: NavController, private courseHelper: CoreCourseHelperProvider, + constructor(@Optional() private navCtrl: NavController, private courseHelper: CoreCourseHelperProvider, private courseFormatDelegate: CoreCourseFormatDelegate, private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider, eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider) { // Listen for status change in course. diff --git a/src/core/courses/components/overview-events/overview-events.ts b/src/core/courses/components/overview-events/overview-events.ts index 4ffc4e3fb..50b866569 100644 --- a/src/core/courses/components/overview-events/overview-events.ts +++ b/src/core/courses/components/overview-events/overview-events.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, OnChanges, EventEmitter, SimpleChange } from '@angular/core'; +import { Component, Input, Output, OnChanges, EventEmitter, SimpleChange, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -43,9 +43,10 @@ export class CoreCoursesOverviewEventsComponent implements OnChanges { next30Days: any[] = []; future: any[] = []; - constructor(private navCtrl: NavController, private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider, - private domUtils: CoreDomUtilsProvider, private sitesProvider: CoreSitesProvider, - private courseProvider: CoreCourseProvider, private contentLinksHelper: CoreContentLinksHelperProvider) { + constructor(@Optional() private navCtrl: NavController, private utils: CoreUtilsProvider, + private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider, + private sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider, + private contentLinksHelper: CoreContentLinksHelperProvider) { this.loadMore = new EventEmitter(); } diff --git a/src/core/grades/components/course/course.ts b/src/core/grades/components/course/course.ts index 181080f4d..4dc6e1ba3 100644 --- a/src/core/grades/components/course/course.ts +++ b/src/core/grades/components/course/course.ts @@ -39,8 +39,9 @@ export class CoreGradesCourseComponent { gradesTable: any; constructor(private gradesProvider: CoreGradesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams, - private gradesHelper: CoreGradesHelperProvider, private sitesProvider: CoreSitesProvider, private navCtrl: NavController, - private appProvider: CoreAppProvider, @Optional() private svComponent: CoreSplitViewComponent) { + private gradesHelper: CoreGradesHelperProvider, private sitesProvider: CoreSitesProvider, + @Optional() private navCtrl: NavController, private appProvider: CoreAppProvider, + @Optional() private svComponent: CoreSplitViewComponent) { } /** diff --git a/src/core/grades/providers/course-option-handler.ts b/src/core/grades/providers/course-option-handler.ts index 54f9396e7..87ac8540f 100644 --- a/src/core/grades/providers/course-option-handler.ts +++ b/src/core/grades/providers/course-option-handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '../../course/providers/options-delegate'; import { CoreCourseProvider } from '../../course/providers/course'; @@ -80,9 +80,11 @@ export class CoreGradesCourseOptionHandler implements CoreCourseOptionsHandler { /** * Returns the data needed to render the handler. * - * @return {CoreCourseOptionsHandlerData} Data needed to render the handler. + * @param {Injector} injector Injector. + * @param {number} courseId The course ID. + * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData(): CoreCourseOptionsHandlerData { + getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise { return { title: 'core.grades.grades', class: 'core-grades-course-handler', diff --git a/src/core/siteaddons/directives/call-ws-new-content.ts b/src/core/siteaddons/directives/call-ws-new-content.ts index cf23d59f0..f4d0666e9 100644 --- a/src/core/siteaddons/directives/call-ws-new-content.ts +++ b/src/core/siteaddons/directives/call-ws-new-content.ts @@ -62,7 +62,7 @@ export class CoreSiteAddonsCallWSNewContentDirective extends CoreSiteAddonsCallW constructor(element: ElementRef, translate: TranslateService, domUtils: CoreDomUtilsProvider, siteAddonsProvider: CoreSiteAddonsProvider, @Optional() parentContent: CoreSiteAddonsAddonContentComponent, - protected utils: CoreUtilsProvider, protected navCtrl: NavController) { + protected utils: CoreUtilsProvider, @Optional() protected navCtrl: NavController) { super(element, translate, domUtils, siteAddonsProvider, parentContent); } diff --git a/src/core/siteaddons/directives/call-ws-on-load.ts b/src/core/siteaddons/directives/call-ws-on-load.ts index 9a2a1a8c2..bca0ab323 100644 --- a/src/core/siteaddons/directives/call-ws-on-load.ts +++ b/src/core/siteaddons/directives/call-ws-on-load.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; -import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; import { CoreUtilsProvider } from '../../../providers/utils/utils'; diff --git a/src/core/siteaddons/directives/new-content.ts b/src/core/siteaddons/directives/new-content.ts index a31920aa5..6e11a17ac 100644 --- a/src/core/siteaddons/directives/new-content.ts +++ b/src/core/siteaddons/directives/new-content.ts @@ -54,7 +54,7 @@ export class CoreSiteAddonsNewContentDirective implements OnInit { protected element: HTMLElement; - constructor(element: ElementRef, protected utils: CoreUtilsProvider, protected navCtrl: NavController, + constructor(element: ElementRef, protected utils: CoreUtilsProvider, @Optional() protected navCtrl: NavController, @Optional() protected parentContent: CoreSiteAddonsAddonContentComponent, protected domUtils: CoreDomUtilsProvider, protected siteAddonsProvider: CoreSiteAddonsProvider) { this.element = element.nativeElement || element; diff --git a/src/core/siteaddons/providers/helper.ts b/src/core/siteaddons/providers/helper.ts index c8e0b6c18..1bfa8336a 100644 --- a/src/core/siteaddons/providers/helper.ts +++ b/src/core/siteaddons/providers/helper.ts @@ -43,6 +43,9 @@ import { CoreCoursesProvider } from '../../courses/providers/courses'; * addons. * * This code is split from CoreSiteAddonsProvider to prevent circular dependencies. + * + * @todo: Support ViewChild and similar in site addons. Possible solution: make components and directives inject the instance + * inside the host DOM element? */ @Injectable() export class CoreSiteAddonsHelperProvider { @@ -337,7 +340,7 @@ export class CoreSiteAddonsHelperProvider { displaySectionSelector: (course: any): boolean => { return typeof handlerSchema.displaysectionselector != 'undefined' ? handlerSchema.displaysectionselector : true; }, - getCourseFormatComponent: (course: any): any => { + getCourseFormatComponent: (injector: Injector, course: any): any | Promise => { if (handlerSchema.method) { return CoreSiteAddonsCourseFormatComponent; } @@ -377,7 +380,8 @@ export class CoreSiteAddonsHelperProvider { : boolean | Promise => { return this.isHandlerEnabledForCourse(courseId, handlerSchema.restricttoenrolledcourses, bootstrapResult.restrict); }, - getDisplayData: (courseId: number): CoreCourseOptionsHandlerData => { + getDisplayData: (injector: Injector, courseId: number): + CoreCourseOptionsHandlerData | Promise => { return { title: prefixedTitle, class: handlerSchema.displaydata.class, @@ -488,7 +492,7 @@ export class CoreSiteAddonsHelperProvider { } }; }, - getMainComponent: (course: any, module: any): any => { + getMainComponent: (injector: Injector, course: any, module: any): any | Promise => { return CoreSiteAddonsModuleIndexComponent; } }); diff --git a/src/core/siteaddons/providers/siteaddons.ts b/src/core/siteaddons/providers/siteaddons.ts index f6051d5d4..777973182 100644 --- a/src/core/siteaddons/providers/siteaddons.ts +++ b/src/core/siteaddons/providers/siteaddons.ts @@ -144,7 +144,10 @@ export class CoreSiteAddonsProvider { */ createDataForJS(bootstrapResult: any, contentResult?: any): any { // First of all, add the data returned by the bootstrap JS (if any). - const data = this.utils.clone(bootstrapResult.jsResult || {}); + let data = this.utils.clone(bootstrapResult.jsResult || {}); + if (typeof data == 'boolean') { + data = {}; + } // Now add some data returned by the bootstrap WS call. data.BOOTSTRAP_TEMPLATES = this.utils.objectToKeyValueMap(bootstrapResult.templates, 'id', 'html'); diff --git a/src/core/user/components/user-profile-field/user-profile-field.ts b/src/core/user/components/user-profile-field/user-profile-field.ts index 65b401a32..7d410d3c3 100644 --- a/src/core/user/components/user-profile-field/user-profile-field.ts +++ b/src/core/user/components/user-profile-field/user-profile-field.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, Injector } from '@angular/core'; import { CoreUserProfileFieldDelegate } from '../../providers/user-profile-field-delegate'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -33,13 +33,16 @@ export class CoreUserProfileFieldComponent implements OnInit { componentClass: any; // The class of the component to render. data: any = {}; // Data to pass to the component. - constructor(private ufDelegate: CoreUserProfileFieldDelegate, private utilsProvider: CoreUtilsProvider) { } + constructor(private ufDelegate: CoreUserProfileFieldDelegate, private utilsProvider: CoreUtilsProvider, + private injector: Injector) { } /** * Component being initialized. */ ngOnInit(): void { - this.componentClass = this.ufDelegate.getComponent(this.field, this.signup); + this.ufDelegate.getComponent(this.injector, this.field, this.signup).then((component) => { + this.componentClass = component; + }); this.data.field = this.field; this.data.edit = this.utilsProvider.isTrueOrOne(this.edit); diff --git a/src/core/user/providers/course-option-handler.ts b/src/core/user/providers/course-option-handler.ts index c595341b8..8266e95af 100644 --- a/src/core/user/providers/course-option-handler.ts +++ b/src/core/user/providers/course-option-handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '../../course/providers/options-delegate'; import { CoreCourseProvider } from '../../course/providers/course'; @@ -79,9 +79,11 @@ export class CoreUserParticipantsCourseOptionHandler implements CoreCourseOption /** * Returns the data needed to render the handler. * - * @return {CoreCourseOptionsHandlerData} Data needed to render the handler. + * @param {Injector} injector Injector. + * @param {number} courseId The course ID. + * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData(): CoreCourseOptionsHandlerData { + getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise { return { title: 'core.user.participants', class: 'core-user-participants-handler', diff --git a/src/core/user/providers/user-profile-field-delegate.ts b/src/core/user/providers/user-profile-field-delegate.ts index 41c4b3d47..b8ba951c8 100644 --- a/src/core/user/providers/user-profile-field-delegate.ts +++ b/src/core/user/providers/user-profile-field-delegate.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; @@ -22,10 +22,12 @@ export interface CoreUserProfileFieldHandler extends CoreDelegateHandler { /** * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. * - * @return {any} The component to use, undefined if not found. + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any; + getComponent(injector: Injector): any | Promise; /** * Get the data to send for the field based on the input data. @@ -75,17 +77,23 @@ export class CoreUserProfileFieldDelegate extends CoreDelegate { /** * Get the component to use to display an user field. * + * @param {Injector} injector Injector. * @param {any} field User field to get the directive for. * @param {boolean} signup True if user is in signup page. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getComponent(field: any, signup: boolean): any { + getComponent(injector: Injector, field: any, signup: boolean): Promise { const type = field.type || field.datatype; + let result; if (signup) { - return this.executeFunction(type, 'getComponent'); + result = this.executeFunction(type, 'getComponent', [injector]); } else { - return this.executeFunctionOnEnabled(type, 'getComponent'); + result = this.executeFunctionOnEnabled(type, 'getComponent', [injector]); } + + return Promise.resolve(result).catch((err) => { + this.logger.error('Error getting component for field', type, err); + }); } /** diff --git a/src/directives/auto-focus.ts b/src/directives/auto-focus.ts index ce2aa6608..c5f0db88e 100644 --- a/src/directives/auto-focus.ts +++ b/src/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, OnInit, ElementRef } from '@angular/core'; +import { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -31,7 +31,7 @@ export class CoreAutoFocusDirective implements OnInit { protected element: HTMLElement; constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider, - private navCtrl: NavController) { + @Optional() private navCtrl: NavController) { this.element = element.nativeElement || element; } diff --git a/src/directives/download-file.ts b/src/directives/download-file.ts index f715d15ee..3418d87d1 100644 --- a/src/directives/download-file.ts +++ b/src/directives/download-file.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Directive, Input, OnInit, ElementRef } from '@angular/core'; -import { NavController } from 'ionic-angular'; import { CoreFileHelperProvider } from '../providers/file-helper'; import { CoreDomUtilsProvider } from '../providers/utils/dom'; import { CoreUtilsProvider } from '../providers/utils/utils'; diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index b65a5107d..3261db59b 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -62,7 +62,7 @@ export class CoreFormatTextDirective implements OnChanges { private textUtils: CoreTextUtilsProvider, private translate: TranslateService, private platform: Platform, private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private loggerProvider: CoreLoggerProvider, private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider, - private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController, + private contentLinksHelper: CoreContentLinksHelperProvider, @Optional() private navCtrl: NavController, @Optional() private content: Content) { this.element = element.nativeElement; this.element.classList.add('opacity-hide'); // Hide contents until they're treated. diff --git a/src/directives/link.ts b/src/directives/link.ts index d37179cf0..75c50b186 100644 --- a/src/directives/link.ts +++ b/src/directives/link.ts @@ -39,7 +39,7 @@ export class CoreLinkDirective implements OnInit { constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider, private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider, - private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController, + private contentLinksHelper: CoreContentLinksHelperProvider, @Optional() private navCtrl: NavController, @Optional() private content: Content) { // This directive can be added dynamically. In that case, the first param is the anchor HTMLElement. this.element = element.nativeElement || element; diff --git a/src/directives/user-link.ts b/src/directives/user-link.ts index c24adb892..d1714bacb 100644 --- a/src/directives/user-link.ts +++ b/src/directives/user-link.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Directive, Input, OnInit, ElementRef } from '@angular/core'; +import { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; /** @@ -27,7 +27,7 @@ export class CoreUserLinkDirective implements OnInit { protected element: HTMLElement; - constructor(element: ElementRef, private navCtrl: NavController) { + constructor(element: ElementRef, @Optional() private navCtrl: NavController) { // This directive can be added dynamically. In that case, the first param is the anchor HTMLElement. this.element = element.nativeElement || element; }