MOBILE-2324 core: Create and apply core-dynamic-component
parent
48716ab545
commit
a0312278d8
|
@ -38,6 +38,7 @@ import { CoreTabsComponent } from './tabs/tabs';
|
||||||
import { CoreTabComponent } from './tabs/tab';
|
import { CoreTabComponent } from './tabs/tab';
|
||||||
import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor';
|
import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor';
|
||||||
import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
||||||
|
import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -61,7 +62,8 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
||||||
CoreTabsComponent,
|
CoreTabsComponent,
|
||||||
CoreTabComponent,
|
CoreTabComponent,
|
||||||
CoreRichTextEditorComponent,
|
CoreRichTextEditorComponent,
|
||||||
CoreNavBarButtonsComponent
|
CoreNavBarButtonsComponent,
|
||||||
|
CoreDynamicComponent
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
CoreContextMenuPopoverComponent,
|
CoreContextMenuPopoverComponent,
|
||||||
|
@ -92,7 +94,8 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
||||||
CoreTabsComponent,
|
CoreTabsComponent,
|
||||||
CoreTabComponent,
|
CoreTabComponent,
|
||||||
CoreRichTextEditorComponent,
|
CoreRichTextEditorComponent,
|
||||||
CoreNavBarButtonsComponent
|
CoreNavBarButtonsComponent,
|
||||||
|
CoreDynamicComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoreComponentsModule {}
|
export class CoreComponentsModule {}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<!-- Content to display if no dynamic component. -->
|
||||||
|
<ng-content *ngIf="!instance"></ng-content>
|
||||||
|
|
||||||
|
<!-- Container of the dynamic component -->
|
||||||
|
<ng-container #dynamicComponent></ng-container>
|
|
@ -0,0 +1,168 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 {
|
||||||
|
Component, Input, ViewChild, OnInit, OnChanges, DoCheck, ViewContainerRef, ComponentFactoryResolver,
|
||||||
|
KeyValueDiffers, SimpleChange
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CoreLoggerProvider } from '../../providers/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to create another component dynamically.
|
||||||
|
*
|
||||||
|
* You need to pass the class of the component to this component (the class, not the name), along with the input data.
|
||||||
|
*
|
||||||
|
* So you should do something like:
|
||||||
|
*
|
||||||
|
* import { MyComponent } from './component';
|
||||||
|
*
|
||||||
|
* ...
|
||||||
|
*
|
||||||
|
* this.component = MyComponent;
|
||||||
|
*
|
||||||
|
* And in the template:
|
||||||
|
*
|
||||||
|
* <core-dynamic-component [component]="component" [data]="data">
|
||||||
|
* <p>Cannot render the data.</p>
|
||||||
|
* </core-dynamic-component>
|
||||||
|
*
|
||||||
|
* Please notice that the component that you pass needs to be declared in entryComponents of the module to be created dynamically.
|
||||||
|
*
|
||||||
|
* 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.".
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-dynamic-component',
|
||||||
|
templateUrl: 'dynamic-component.html'
|
||||||
|
})
|
||||||
|
export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck {
|
||||||
|
|
||||||
|
@Input() component: any;
|
||||||
|
@Input() data: any;
|
||||||
|
|
||||||
|
// Get the container where to put the dynamic component.
|
||||||
|
@ViewChild('dynamicComponent', { read: ViewContainerRef }) set dynamicComponent(el: ViewContainerRef) {
|
||||||
|
this.container = el;
|
||||||
|
this.createComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
instance: any;
|
||||||
|
container: ViewContainerRef;
|
||||||
|
protected logger: any;
|
||||||
|
protected differ: any; // To detect changes in the data input.
|
||||||
|
|
||||||
|
constructor(logger: CoreLoggerProvider, private factoryResolver: ComponentFactoryResolver, differs: KeyValueDiffers) {
|
||||||
|
this.logger = logger.getInstance('CoreDynamicComponent');
|
||||||
|
this.differ = differs.find([]).create();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.createComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect changes on input properties.
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||||
|
if (!this.instance && changes.component) {
|
||||||
|
this.createComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays).
|
||||||
|
*/
|
||||||
|
ngDoCheck(): void {
|
||||||
|
if (this.instance) {
|
||||||
|
// Check if there's any change in the data object.
|
||||||
|
const changes = this.differ.diff(this.data);
|
||||||
|
if (changes) {
|
||||||
|
this.setInputData();
|
||||||
|
if (this.instance.ngOnChanges) {
|
||||||
|
this.instance.ngOnChanges(this.createChangesForComponent(changes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a component, add it to a container and set the input data.
|
||||||
|
*
|
||||||
|
* @return {boolean} Whether the component was successfully created.
|
||||||
|
*/
|
||||||
|
protected createComponent(): boolean {
|
||||||
|
if (!this.component || !this.container) {
|
||||||
|
// No component to instantiate or container doesn't exist right now.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.instance) {
|
||||||
|
// Component already instantiated.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create the component and add it to the container.
|
||||||
|
const factory = this.factoryResolver.resolveComponentFactory(this.component),
|
||||||
|
componentRef = this.container.createComponent(factory);
|
||||||
|
|
||||||
|
this.instance = componentRef.instance;
|
||||||
|
|
||||||
|
this.setInputData();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (ex) {
|
||||||
|
this.logger.error('Error creating component', ex);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the input data for the component.
|
||||||
|
*/
|
||||||
|
protected setInputData(): void {
|
||||||
|
for (const name in this.data) {
|
||||||
|
this.instance[name] = this.data[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the changes on the data input, create the changes object for the component.
|
||||||
|
*
|
||||||
|
* @param {any} changes Changes in the data input (detected by KeyValueDiffer).
|
||||||
|
* @return {{[name: string]: SimpleChange}} List of changes for the component.
|
||||||
|
*/
|
||||||
|
protected createChangesForComponent(changes: any): { [name: string]: SimpleChange } {
|
||||||
|
const newChanges: { [name: string]: SimpleChange } = {};
|
||||||
|
|
||||||
|
// Added items are considered first change.
|
||||||
|
changes.forEachAddedItem((item) => {
|
||||||
|
newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Changed or removed items aren't first change.
|
||||||
|
changes.forEachChangedItem((item) => {
|
||||||
|
newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, false);
|
||||||
|
});
|
||||||
|
changes.forEachRemovedItem((item) => {
|
||||||
|
newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return newChanges;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,45 +1,45 @@
|
||||||
<!-- Default course format. -->
|
<!-- Default course format. -->
|
||||||
<div *ngIf="!componentInstances.courseFormat">
|
<core-dynamic-component [component]="courseFormatComponent" [data]="data">
|
||||||
<!-- Course summary. By default we only display the course progress. -->
|
<!-- Course summary. By default we only display the course progress. -->
|
||||||
<ion-list no-lines *ngIf="!componentInstances.courseSummary">
|
<core-dynamic-component [component]="courseSummaryComponent" [data]="data">
|
||||||
<ion-item *ngIf="course.progress != null && course.progress >= 0">
|
<ion-list no-lines>
|
||||||
<core-progress-bar [progress]="course.progress"></core-progress-bar>
|
<ion-item *ngIf="course.progress != null && course.progress >= 0">
|
||||||
</ion-item>
|
<core-progress-bar [progress]="course.progress"></core-progress-bar>
|
||||||
</ion-list>
|
</ion-item>
|
||||||
<ng-template #courseSummary></ng-template>
|
</ion-list>
|
||||||
|
</core-dynamic-component>
|
||||||
|
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
<!-- Section selector. -->
|
<!-- Section selector. -->
|
||||||
<div *ngIf="!componentInstances.sectionSelector && displaySectionSelector && sections && sections.length" no-padding class="clearfix">
|
<core-dynamic-component [component]="sectionSelectorComponent" [data]="data">
|
||||||
<!-- @todo: How to display availabilityinfo and not visible messages? -->
|
<div *ngIf="displaySectionSelector && sections && sections.length" no-padding class="clearfix">
|
||||||
<ion-select [ngModel]="selectedSection" (ngModelChange)="sectionChanged($event)" [compareWith]="compareSections" [selectOptions]="selectOptions" float-start interface="popover">
|
<!-- @todo: How to display availabilityinfo and not visible messages? -->
|
||||||
<ion-option *ngFor="let section of sections" [value]="section">{{section.formattedName || section.name}}</ion-option>
|
<ion-select [ngModel]="selectedSection" (ngModelChange)="sectionChanged($event)" [compareWith]="compareSections" [selectOptions]="selectOptions" float-start interface="popover">
|
||||||
</ion-select>
|
<ion-option *ngFor="let section of sections" [value]="section">{{section.formattedName || section.name}}</ion-option>
|
||||||
<!-- Section download. -->
|
</ion-select>
|
||||||
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
|
<!-- Section download. -->
|
||||||
</div>
|
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
|
||||||
<ng-template #sectionSelector></ng-template>
|
</div>
|
||||||
|
</core-dynamic-component>
|
||||||
|
|
||||||
<!-- Single section. -->
|
<!-- Single section. -->
|
||||||
<div *ngIf="selectedSection && selectedSection.id != allSectionsId">
|
<div *ngIf="selectedSection && selectedSection.id != allSectionsId">
|
||||||
<ng-container *ngIf="!componentInstances.singleSection">
|
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
|
||||||
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection}"></ng-container>
|
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection}"></ng-container>
|
||||||
<core-empty-box *ngIf="!selectedSection.hasContent" icon="qr-scanner" [message]="'core.course.nocontentavailable' | translate"></core-empty-box>
|
<core-empty-box *ngIf="!selectedSection.hasContent" icon="qr-scanner" [message]="'core.course.nocontentavailable' | translate"></core-empty-box>
|
||||||
</ng-container>
|
</core-dynamic-component>
|
||||||
<ng-template #singleSection></ng-template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Multiple sections. -->
|
<!-- Multiple sections. -->
|
||||||
<div *ngIf="selectedSection && selectedSection.id == allSectionsId">
|
<div *ngIf="selectedSection && selectedSection.id == allSectionsId">
|
||||||
<ng-container *ngIf="!componentInstances.allSections">
|
<core-dynamic-component [component]="allSectionsComponent" [data]="data">
|
||||||
<ng-container *ngFor="let section of sections">
|
<ng-container *ngFor="let section of sections">
|
||||||
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}"></ng-container>
|
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}"></ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</core-dynamic-component>
|
||||||
<ng-template #allSections></ng-template>
|
|
||||||
</div>
|
</div>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
</div>
|
</core-dynamic-component>
|
||||||
|
|
||||||
<!-- Template to render a section. -->
|
<!-- Template to render a section. -->
|
||||||
<ng-template #sectionTemplate let-section="section">
|
<ng-template #sectionTemplate let-section="section">
|
||||||
|
@ -78,6 +78,3 @@
|
||||||
<ion-badge class="core-course-download-section-progress" *ngIf="section.isDownloading && section.total > 0 && section.count < section.total">{{section.count}} / {{section.total}}</ion-badge>
|
<ion-badge class="core-course-download-section-progress" *ngIf="section.isDownloading && section.total > 0 && section.count < section.total">{{section.count}} / {{section.total}}</ion-badge>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<!-- Custom course format that overrides the default one. -->
|
|
||||||
<ng-template #courseFormat></ng-template>
|
|
|
@ -12,13 +12,9 @@
|
||||||
// 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 {
|
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter } from '@angular/core';
|
||||||
Component, Input, OnInit, OnChanges, OnDestroy, ViewContainerRef, ComponentFactoryResolver, ViewChild, ChangeDetectorRef,
|
|
||||||
SimpleChange, Output, EventEmitter
|
|
||||||
} from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { CoreEventsProvider } from '../../../../providers/events';
|
import { CoreEventsProvider } from '../../../../providers/events';
|
||||||
import { CoreLoggerProvider } from '../../../../providers/logger';
|
|
||||||
import { CoreSitesProvider } from '../../../../providers/sites';
|
import { CoreSitesProvider } from '../../../../providers/sites';
|
||||||
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||||
import { CoreCourseProvider } from '../../../course/providers/course';
|
import { CoreCourseProvider } from '../../../course/providers/course';
|
||||||
|
@ -48,31 +44,15 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
@Input() initialSectionNumber?: number; // The section to load first (by number).
|
@Input() initialSectionNumber?: number; // The section to load first (by number).
|
||||||
@Output() completionChanged?: EventEmitter<void>; // Will emit an event when any module completion changes.
|
@Output() completionChanged?: EventEmitter<void>; // Will emit an event when any module completion changes.
|
||||||
|
|
||||||
// Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf.
|
// All the possible component classes.
|
||||||
@ViewChild('courseFormat', { read: ViewContainerRef }) set courseFormat(el: ViewContainerRef) {
|
courseFormatComponent: any;
|
||||||
if (this.course) {
|
courseSummaryComponent: any;
|
||||||
this.createComponent('courseFormat', this.cfDelegate.getCourseFormatComponent(this.course), el);
|
sectionSelectorComponent: any;
|
||||||
} else {
|
singleSectionComponent: any;
|
||||||
// The component hasn't been initialized yet. Store the container.
|
allSectionsComponent: any;
|
||||||
this.componentContainers['courseFormat'] = el;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ViewChild('courseSummary', { read: ViewContainerRef }) set courseSummary(el: ViewContainerRef) {
|
|
||||||
this.createComponent('courseSummary', this.cfDelegate.getCourseSummaryComponent(this.course), el);
|
|
||||||
}
|
|
||||||
@ViewChild('sectionSelector', { read: ViewContainerRef }) set sectionSelector(el: ViewContainerRef) {
|
|
||||||
this.createComponent('sectionSelector', this.cfDelegate.getSectionSelectorComponent(this.course), el);
|
|
||||||
}
|
|
||||||
@ViewChild('singleSection', { read: ViewContainerRef }) set singleSection(el: ViewContainerRef) {
|
|
||||||
this.createComponent('singleSection', this.cfDelegate.getSingleSectionComponent(this.course), el);
|
|
||||||
}
|
|
||||||
@ViewChild('allSections', { read: ViewContainerRef }) set allSections(el: ViewContainerRef) {
|
|
||||||
this.createComponent('allSections', this.cfDelegate.getAllSectionsComponent(this.course), el);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instances and containers of all the components that the handler could define.
|
// Data to pass to the components.
|
||||||
protected componentContainers: { [type: string]: ViewContainerRef } = {};
|
data: any = {};
|
||||||
componentInstances: { [type: string]: any } = {};
|
|
||||||
|
|
||||||
displaySectionSelector: boolean;
|
displaySectionSelector: boolean;
|
||||||
selectedSection: any;
|
selectedSection: any;
|
||||||
|
@ -80,23 +60,20 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
selectOptions: any = {};
|
selectOptions: any = {};
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
|
|
||||||
protected logger;
|
|
||||||
protected sectionStatusObserver;
|
protected sectionStatusObserver;
|
||||||
|
|
||||||
constructor(logger: CoreLoggerProvider, private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService,
|
constructor(private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService,
|
||||||
private factoryResolver: ComponentFactoryResolver, private cdr: ChangeDetectorRef,
|
|
||||||
private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider,
|
private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider,
|
||||||
eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider,
|
eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider,
|
||||||
prefetchDelegate: CoreCourseModulePrefetchDelegate) {
|
prefetchDelegate: CoreCourseModulePrefetchDelegate) {
|
||||||
|
|
||||||
this.logger = logger.getInstance('CoreCourseFormatComponent');
|
|
||||||
this.selectOptions.title = translate.instant('core.course.sections');
|
this.selectOptions.title = translate.instant('core.course.sections');
|
||||||
this.completionChanged = new EventEmitter();
|
this.completionChanged = new EventEmitter();
|
||||||
|
|
||||||
// Listen for section status changes.
|
// Listen for section status changes.
|
||||||
this.sectionStatusObserver = eventsProvider.on(CoreEventsProvider.SECTION_STATUS_CHANGED, (data) => {
|
this.sectionStatusObserver = eventsProvider.on(CoreEventsProvider.SECTION_STATUS_CHANGED, (data) => {
|
||||||
if (this.downloadEnabled && this.sections && this.sections.length && this.course && data.sectionId &&
|
if (this.downloadEnabled && this.sections && this.sections.length && this.course && data.sectionId &&
|
||||||
data.courseId == this.course.id) {
|
data.courseId == this.course.id) {
|
||||||
// Check if the affected section is being downloaded.
|
// Check if the affected section is being downloaded.
|
||||||
// If so, we don't update section status because it'll already be updated when the download finishes.
|
// If so, we don't update section status because it'll already be updated when the download finishes.
|
||||||
const downloadId = this.courseHelper.getSectionDownloadId({ id: data.sectionId });
|
const downloadId = this.courseHelper.getSectionDownloadId({ id: data.sectionId });
|
||||||
|
@ -135,15 +112,19 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.displaySectionSelector = this.cfDelegate.displaySectionSelector(this.course);
|
this.displaySectionSelector = this.cfDelegate.displaySectionSelector(this.course);
|
||||||
|
|
||||||
this.createComponent(
|
|
||||||
'courseFormat', this.cfDelegate.getCourseFormatComponent(this.course), this.componentContainers['courseFormat']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect changes on input properties.
|
* Detect changes on input properties.
|
||||||
*/
|
*/
|
||||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||||
|
this.setInputData();
|
||||||
|
|
||||||
|
if (changes.course) {
|
||||||
|
// Course has changed, try to get the components.
|
||||||
|
this.getComponents();
|
||||||
|
}
|
||||||
|
|
||||||
if (changes.sections && this.sections) {
|
if (changes.sections && this.sections) {
|
||||||
if (!this.selectedSection) {
|
if (!this.selectedSection) {
|
||||||
// There is no selected section yet, calculate which one to load.
|
// There is no selected section yet, calculate which one to load.
|
||||||
|
@ -186,62 +167,39 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
if (changes.downloadEnabled && this.downloadEnabled) {
|
if (changes.downloadEnabled && this.downloadEnabled) {
|
||||||
this.calculateSectionsStatus(false);
|
this.calculateSectionsStatus(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the changes to the components and call ngOnChanges if it exists.
|
|
||||||
for (const type in this.componentInstances) {
|
|
||||||
const instance = this.componentInstances[type];
|
|
||||||
|
|
||||||
for (const name in changes) {
|
|
||||||
instance[name] = changes[name].currentValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instance.ngOnChanges) {
|
|
||||||
instance.ngOnChanges(changes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a component, add it to a container and set the input data.
|
* Set the input data for components.
|
||||||
*
|
|
||||||
* @param {string} type The "type" of the component.
|
|
||||||
* @param {any} componentClass The class of the component to create.
|
|
||||||
* @param {ViewContainerRef} container The container to add the component to.
|
|
||||||
* @return {boolean} Whether the component was successfully created.
|
|
||||||
*/
|
*/
|
||||||
protected createComponent(type: string, componentClass: any, container: ViewContainerRef): boolean {
|
protected setInputData(): void {
|
||||||
if (!componentClass || !container) {
|
this.data.course = this.course;
|
||||||
// No component to instantiate or container doesn't exist right now.
|
this.data.sections = this.sections;
|
||||||
return false;
|
this.data.initialSectionId = this.initialSectionId;
|
||||||
}
|
this.data.initialSectionNumber = this.initialSectionNumber;
|
||||||
|
this.data.downloadEnabled = this.downloadEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.componentInstances[type] && container === this.componentContainers[type]) {
|
/**
|
||||||
// Component already instantiated and the component hasn't been destroyed, nothing to do.
|
* Get the components classes.
|
||||||
return true;
|
*/
|
||||||
}
|
protected getComponents(): void {
|
||||||
|
if (this.course) {
|
||||||
try {
|
if (!this.courseFormatComponent) {
|
||||||
// Create the component and add it to the container.
|
this.courseFormatComponent = this.cfDelegate.getCourseFormatComponent(this.course);
|
||||||
const factory = this.factoryResolver.resolveComponentFactory(componentClass),
|
}
|
||||||
componentRef = container.createComponent(factory);
|
if (!this.courseSummaryComponent) {
|
||||||
|
this.courseSummaryComponent = this.cfDelegate.getCourseSummaryComponent(this.course);
|
||||||
this.componentContainers[type] = container;
|
}
|
||||||
this.componentInstances[type] = componentRef.instance;
|
if (!this.sectionSelectorComponent) {
|
||||||
|
this.sectionSelectorComponent = this.cfDelegate.getSectionSelectorComponent(this.course);
|
||||||
// Set the Input data.
|
}
|
||||||
this.componentInstances[type].course = this.course;
|
if (!this.singleSectionComponent) {
|
||||||
this.componentInstances[type].sections = this.sections;
|
this.singleSectionComponent = this.cfDelegate.getSingleSectionComponent(this.course);
|
||||||
this.componentInstances[type].initialSectionId = this.initialSectionId;
|
}
|
||||||
this.componentInstances[type].initialSectionNumber = this.initialSectionNumber;
|
if (!this.allSectionsComponent) {
|
||||||
this.componentInstances[type].downloadEnabled = this.downloadEnabled;
|
this.allSectionsComponent = this.cfDelegate.getAllSectionsComponent(this.course);
|
||||||
|
}
|
||||||
this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed.
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (ex) {
|
|
||||||
this.logger.error('Error creating component', type, ex);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,16 +211,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
sectionChanged(newSection: any): void {
|
sectionChanged(newSection: any): void {
|
||||||
const previousValue = this.selectedSection;
|
const previousValue = this.selectedSection;
|
||||||
this.selectedSection = newSection;
|
this.selectedSection = newSection;
|
||||||
|
this.data.section = this.selectedSection;
|
||||||
// If there is a component to render the current section, update its section.
|
|
||||||
if (this.componentInstances.singleSection) {
|
|
||||||
this.componentInstances.singleSection.section = this.selectedSection;
|
|
||||||
if (this.componentInstances.singleSection.ngOnChanges) {
|
|
||||||
this.componentInstances.singleSection.ngOnChanges({
|
|
||||||
section: new SimpleChange(previousValue, newSection, typeof previousValue != 'undefined')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,117 +0,0 @@
|
||||||
// (C) Copyright 2015 Martin Dougiamas
|
|
||||||
//
|
|
||||||
// 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 { Component, Input, OnChanges, ViewContainerRef, ComponentFactoryResolver, SimpleChange } from '@angular/core';
|
|
||||||
import { CoreLoggerProvider } from '../../../../../providers/logger';
|
|
||||||
import { CoreCourseModuleDelegate } from '../../../providers/module-delegate';
|
|
||||||
import { CoreCourseUnsupportedModuleComponent } from '../../../components/unsupported-module/unsupported-module';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component to display single activity format. It will determine the right component to use and instantiate it.
|
|
||||||
*
|
|
||||||
* The instantiated component will receive the course and the module as inputs.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'core-course-format-single-activity',
|
|
||||||
template: ''
|
|
||||||
})
|
|
||||||
export class CoreCourseFormatSingleActivityComponent implements OnChanges {
|
|
||||||
@Input() course: any; // The course to render.
|
|
||||||
@Input() sections: any[]; // List of course sections.
|
|
||||||
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
|
|
||||||
|
|
||||||
protected logger: any;
|
|
||||||
protected module: any;
|
|
||||||
protected componentInstance: any;
|
|
||||||
|
|
||||||
constructor(logger: CoreLoggerProvider, private viewRef: ViewContainerRef, private factoryResolver: ComponentFactoryResolver,
|
|
||||||
private moduleDelegate: CoreCourseModuleDelegate) {
|
|
||||||
this.logger = logger.getInstance('CoreCourseFormatSingleActivityComponent');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect changes on input properties.
|
|
||||||
*/
|
|
||||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
|
||||||
if (this.course && this.sections && this.sections.length) {
|
|
||||||
// In single activity the module should only have 1 section and 1 module. Get the module.
|
|
||||||
const module = this.sections[0] && this.sections[0].modules && this.sections[0].modules[0];
|
|
||||||
if (module && !this.componentInstance) {
|
|
||||||
// We haven't created the component yet. Create it now.
|
|
||||||
this.createComponent(module);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.componentInstance && this.componentInstance.ngOnChanges) {
|
|
||||||
// Call ngOnChanges of the component.
|
|
||||||
const newChanges: { [name: string]: SimpleChange } = {};
|
|
||||||
|
|
||||||
// Check if course has changed.
|
|
||||||
if (changes.course) {
|
|
||||||
newChanges.course = changes.course;
|
|
||||||
this.componentInstance.course = this.course;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if module has changed.
|
|
||||||
if (changes.sections && module != this.module) {
|
|
||||||
newChanges.module = {
|
|
||||||
currentValue: module,
|
|
||||||
firstChange: changes.sections.firstChange,
|
|
||||||
previousValue: this.module,
|
|
||||||
isFirstChange: (): boolean => {
|
|
||||||
return newChanges.module.firstChange;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.componentInstance.module = module;
|
|
||||||
this.module = module;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(newChanges).length) {
|
|
||||||
this.componentInstance.ngOnChanges(newChanges);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the component, add it to the container and set the input data.
|
|
||||||
*
|
|
||||||
* @param {any} module The module.
|
|
||||||
* @return {boolean} Whether the component was successfully created.
|
|
||||||
*/
|
|
||||||
protected createComponent(module: any): boolean {
|
|
||||||
const componentClass = this.moduleDelegate.getMainComponent(this.course, module) || CoreCourseUnsupportedModuleComponent;
|
|
||||||
if (!componentClass) {
|
|
||||||
// No component to instantiate.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create the component and add it to the container.
|
|
||||||
const factory = this.factoryResolver.resolveComponentFactory(componentClass),
|
|
||||||
componentRef = this.viewRef.createComponent(factory);
|
|
||||||
|
|
||||||
this.componentInstance = componentRef.instance;
|
|
||||||
|
|
||||||
// Set the Input data.
|
|
||||||
this.componentInstance.courseId = this.course.id;
|
|
||||||
this.componentInstance.module = module;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (ex) {
|
|
||||||
this.logger.error('Error creating component', ex);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1 @@
|
||||||
|
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
|
|
@ -0,0 +1,55 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { Component, Input, OnChanges, SimpleChange } from '@angular/core';
|
||||||
|
import { CoreCourseModuleDelegate } from '../../../providers/module-delegate';
|
||||||
|
import { CoreCourseUnsupportedModuleComponent } from '../../../components/unsupported-module/unsupported-module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display single activity format. It will determine the right component to use and instantiate it.
|
||||||
|
*
|
||||||
|
* The instantiated component will receive the course and the module as inputs.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-course-format-single-activity',
|
||||||
|
templateUrl: 'singleactivity.html'
|
||||||
|
})
|
||||||
|
export class CoreCourseFormatSingleActivityComponent implements OnChanges {
|
||||||
|
@Input() course: any; // The course to render.
|
||||||
|
@Input() sections: any[]; // List of course sections.
|
||||||
|
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
|
||||||
|
|
||||||
|
componentClass: any; // The class of the component to render.
|
||||||
|
data: any = {}; // Data to pass to the component.
|
||||||
|
|
||||||
|
constructor(private moduleDelegate: CoreCourseModuleDelegate) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect changes on input properties.
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||||
|
if (this.course && this.sections && this.sections.length) {
|
||||||
|
// In single activity the module should only have 1 section and 1 module. Get the module.
|
||||||
|
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.data.courseId = this.course.id;
|
||||||
|
this.data.module = module;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CoreCourseFormatHandler } from '../../../providers/format-delegate';
|
import { CoreCourseFormatHandler } from '../../../providers/format-delegate';
|
||||||
import { CoreCourseFormatSingleActivityComponent } from '../components/format';
|
import { CoreCourseFormatSingleActivityComponent } from '../components/singleactivity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler to support singleactivity course format.
|
* Handler to support singleactivity course format.
|
||||||
|
|
|
@ -13,15 +13,17 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CoreCourseFormatSingleActivityComponent } from './components/format';
|
import { CoreCourseFormatSingleActivityComponent } from './components/singleactivity';
|
||||||
import { CoreCourseFormatSingleActivityHandler } from './providers/handler';
|
import { CoreCourseFormatSingleActivityHandler } from './providers/handler';
|
||||||
import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
|
import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
|
||||||
|
import { CoreComponentsModule } from '../../../../components/components.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
CoreCourseFormatSingleActivityComponent
|
CoreCourseFormatSingleActivityComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
|
CoreComponentsModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CoreCourseFormatSingleActivityHandler
|
CoreCourseFormatSingleActivityHandler
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { CommonModule } from '@angular/common';
|
||||||
import { IonicModule } from 'ionic-angular';
|
import { IonicModule } from 'ionic-angular';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { CoreUserProfileFieldComponent } from './user-profile-field/user-profile-field';
|
import { CoreUserProfileFieldComponent } from './user-profile-field/user-profile-field';
|
||||||
|
import { CoreComponentsModule } from '../../../components/components.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -26,6 +27,7 @@ import { CoreUserProfileFieldComponent } from './user-profile-field/user-profile
|
||||||
CommonModule,
|
CommonModule,
|
||||||
IonicModule,
|
IonicModule,
|
||||||
TranslateModule.forChild(),
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
<!-- User profile field that overrides the default one. -->
|
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
|
||||||
<ng-template #userProfileField></ng-template>
|
|
||||||
|
|
|
@ -12,8 +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 { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { CoreLoggerProvider } from '../../../../providers/logger';
|
|
||||||
import { CoreUserProfileFieldDelegate } from '../../providers/user-profile-field-delegate';
|
import { CoreUserProfileFieldDelegate } from '../../providers/user-profile-field-delegate';
|
||||||
import { CoreUtilsProvider } from '../../../../providers/utils/utils';
|
import { CoreUtilsProvider } from '../../../../providers/utils/utils';
|
||||||
|
|
||||||
|
@ -31,74 +30,23 @@ export class CoreUserProfileFieldComponent implements OnInit {
|
||||||
@Input() form?: any; // Form where to add the form control. Required if edit=true or signup=true.
|
@Input() form?: any; // Form where to add the form control. Required if edit=true or signup=true.
|
||||||
@Input() registerAuth?: string; // Register auth method. E.g. 'email'.
|
@Input() registerAuth?: string; // Register auth method. E.g. 'email'.
|
||||||
|
|
||||||
// Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf.
|
componentClass: any; // The class of the component to render.
|
||||||
@ViewChild('userProfileField', { read: ViewContainerRef }) set userProfileField(el: ViewContainerRef) {
|
data: any = {}; // Data to pass to the component.
|
||||||
if (this.field) {
|
|
||||||
this.createComponent(this.ufDelegate.getComponent(this.field, this.signup), el);
|
|
||||||
} else {
|
|
||||||
// The component hasn't been initialized yet. Store the container.
|
|
||||||
this.fieldContainer = el;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected logger;
|
constructor(private ufDelegate: CoreUserProfileFieldDelegate, private utilsProvider: CoreUtilsProvider) { }
|
||||||
|
|
||||||
// Instances and containers of all the components that the handler could define.
|
|
||||||
protected fieldContainer: ViewContainerRef;
|
|
||||||
protected fieldInstance: any;
|
|
||||||
|
|
||||||
constructor(logger: CoreLoggerProvider, private factoryResolver: ComponentFactoryResolver,
|
|
||||||
private ufDelegate: CoreUserProfileFieldDelegate, private utilsProvider: CoreUtilsProvider) {
|
|
||||||
this.logger = logger.getInstance('CoreUserProfileFieldComponent');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.createComponent(this.ufDelegate.getComponent(this.field, this.signup), this.fieldContainer);
|
this.componentClass = this.ufDelegate.getComponent(this.field, this.signup);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
this.data.field = this.field;
|
||||||
* Create a component, add it to a container and set the input data.
|
this.data.edit = this.utilsProvider.isTrueOrOne(this.edit);
|
||||||
*
|
if (this.edit) {
|
||||||
* @param {any} componentClass The class of the component to create.
|
this.data.signup = this.utilsProvider.isTrueOrOne(this.signup);
|
||||||
* @param {ViewContainerRef} container The container to add the component to.
|
this.data.disabled = this.utilsProvider.isTrueOrOne(this.field.locked);
|
||||||
* @return {boolean} Whether the component was successfully created.
|
this.data.form = this.form;
|
||||||
*/
|
|
||||||
protected createComponent(componentClass: any, container: ViewContainerRef): boolean {
|
|
||||||
if (!componentClass || !container) {
|
|
||||||
// No component to instantiate or container doesn't exist right now.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.fieldInstance && container === this.fieldContainer) {
|
|
||||||
// Component already instantiated and the component hasn't been destroyed, nothing to do.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create the component and add it to the container.
|
|
||||||
const factory = this.factoryResolver.resolveComponentFactory(componentClass),
|
|
||||||
componentRef = container.createComponent(factory);
|
|
||||||
|
|
||||||
this.fieldContainer = container;
|
|
||||||
this.fieldInstance = componentRef.instance;
|
|
||||||
|
|
||||||
// Set the Input data.
|
|
||||||
this.fieldInstance.field = this.field;
|
|
||||||
this.fieldInstance.edit = this.utilsProvider.isTrueOrOne(this.edit);
|
|
||||||
if (this.edit) {
|
|
||||||
this.fieldInstance.signup = this.utilsProvider.isTrueOrOne(this.signup);
|
|
||||||
this.fieldInstance.disabled = this.utilsProvider.isTrueOrOne(this.field.locked);
|
|
||||||
this.fieldInstance.form = this.form;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (ex) {
|
|
||||||
this.logger.error('Error creating user field component', ex, componentClass);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue