diff --git a/docs/card-view.component.md b/docs/card-view.component.md index 887eebd9a8..19ead327be 100644 --- a/docs/card-view.component.md +++ b/docs/card-view.component.md @@ -45,7 +45,7 @@ Displays a configurable property list renderer. ## Details -You define the property list, the CardViewComponent does the rest. Each property represents a card view item (a row) in the card view component. At the time of writing two different kind of card view item (property type) is supported out of the box ([text](#card-text-item) item and [date](#card-date-item) item) but you can define your own custom types as well. +You define the property list, the CardViewComponent does the rest. Each property represents a card view item (a row) in the card view component. At the time of writing three a few kind of card view item (property type) is supported out of the box (e.g: [text](#card-text-item) item and [date](#card-date-item) item) but you can define your own custom types as well. ### Editing @@ -171,13 +171,16 @@ Card item components are loaded dynamically, which makes you able to define your Let's consider you want to have a **stardate** type to display Captain Picard's birthday (47457.1). For this, you need to do the following steps. -#### 1. Define the model for the custom type +#### 1. Define the Model for the custom type -Your model has to extend the CardViewBaseItemModel and implement the CardViewItem interface. +Your model has to extend the **CardViewBaseItemModel** and implement the **CardViewItem** and **DynamicComponentModel** interface. *(You can check how the CardViewTextItemModel is implemented for further guidance.)* ```js -export class CardViewStarDateItemModel extends CardViewBaseItemModel implements CardViewItem { +import { CardViewBaseItemModel, CardViewItem, DynamicComponentModel } from 'ng2-alfresco-core'; + +export class CardViewStarDateItemModel extends CardViewBaseItemModel implements +CardViewItem, DynamicComponentModel { type: string = 'star-date'; get displayValue() { @@ -190,13 +193,9 @@ export class CardViewStarDateItemModel extends CardViewBaseItemModel implements } ``` -The most important part of this model is the value of the **type** attribute. This is how the Card View component will be able to recognise which component is needed to render it dynamically. +#### 2. Define the Component for the custom type -The type is a **hyphen-separated-lowercase-words** string (just like how I wrote it). This will be converted to a PascalCase (or UpperCamelCase) string to find the right component. In our case the Card View component will look for the CardView**StarDate**ItemComponent. - -#### 2. Define the component for the custom type - -As discussed in the previous step the only important thing here is the naming of your component class ( **CardViewStarDateItemComponent**). Since the selector is not used in this case, you can give any selector name to it, but it makes sense to follow the angular standards. +Create your custom card view item component. Defining the selector is not important, being it a dinamically loaded component, so you can give any selector name to it, but it makes sense to follow the angular standards. ```js @Component({ @@ -242,6 +241,27 @@ For Angular to be able to load your custom component dynamically, you have to re export class MyModule {} ``` +#### 4. Bind your custom component to the custom model type, enabling Angular's dynamic component loader to find it. + +For mapping each type to their Component, there is the **CardItemTypeService**. This service extends the **DynamicComponentMapper** abstract class. +This CardItemTypeService is responible for the type resolution, it contains all the default components (e.g.: text, date, etc...) also. In order to make your component available, you need to extend the list of possible components. + +You can extend this list the following way: + +```js +@Component({ + ... + providers: [ CardItemTypeService ] // If you don't want to pollute the main instance of the CardItemTypeService service + ... +}) +export class SomeParentComponent { + + constructor(private cardItemTypeService: CardItemTypeService) { + cardItemTypeService.setComponentTypeResolver('star-date', () => CardViewStarDateItemComponent); + } +} +``` + ## See also diff --git a/ng2-components/ng2-activiti-form/src/services/form-rendering.service.spec.ts b/ng2-components/ng2-activiti-form/src/services/form-rendering.service.spec.ts index 4b2c30a2d2..50446d3fb5 100644 --- a/ng2-components/ng2-activiti-form/src/services/form-rendering.service.spec.ts +++ b/ng2-components/ng2-activiti-form/src/services/form-rendering.service.spec.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { DynamicComponentResolver } from 'ng2-alfresco-core'; import { AttachWidgetComponent, FormFieldModel, @@ -22,7 +23,7 @@ import { UnknownWidgetComponent, UploadWidgetComponent } from './../components/widgets/index'; -import { DefaultTypeResolver, FormRenderingService } from './form-rendering.service'; +import { FormRenderingService } from './form-rendering.service'; describe('FormRenderingService', () => { @@ -88,9 +89,9 @@ describe('FormRenderingService', () => { expect( () => service.setComponentTypeResolver( null, - DefaultTypeResolver.fromType(UnknownWidgetComponent) + DynamicComponentResolver.fromType(UnknownWidgetComponent) ) - ).toThrowError('fieldType is null or not defined'); + ).toThrowError('type is null or not defined'); }); it('should require type resolver instance to set resolver for type', () => { @@ -106,13 +107,13 @@ describe('FormRenderingService', () => { expect( () => service.setComponentTypeResolver( FormFieldTypes.TEXT, - DefaultTypeResolver.fromType(UnknownWidgetComponent) + DynamicComponentResolver.fromType(UnknownWidgetComponent) ) ).toThrowError('already mapped, use override option if you intend replacing existing mapping.'); }); it('should override existing resolver with explicit flag', () => { - let customResolver = DefaultTypeResolver.fromType(UnknownWidgetComponent); + let customResolver = DynamicComponentResolver.fromType(UnknownWidgetComponent); service.setComponentTypeResolver(FormFieldTypes.TEXT, customResolver, true); expect(service.getComponentTypeResolver(FormFieldTypes.TEXT)).toBe(customResolver); }); diff --git a/ng2-components/ng2-activiti-form/src/services/form-rendering.service.ts b/ng2-components/ng2-activiti-form/src/services/form-rendering.service.ts index 8e64ad0877..c5dae3279d 100644 --- a/ng2-components/ng2-activiti-form/src/services/form-rendering.service.ts +++ b/ng2-components/ng2-activiti-form/src/services/form-rendering.service.ts @@ -16,6 +16,7 @@ */ import { Injectable, Type } from '@angular/core'; +import { DynamicComponentMapper, DynamicComponentResolveFunction, DynamicComponentResolver } from 'ng2-alfresco-core'; import { AmountWidgetComponent, @@ -41,30 +42,33 @@ import { } from './../components/widgets/index'; @Injectable() -export class FormRenderingService { +export class FormRenderingService extends DynamicComponentMapper { - private types: { [key: string]: ComponentTypeResolver } = { - 'text': DefaultTypeResolver.fromType(TextWidgetComponent), - 'string': DefaultTypeResolver.fromType(TextWidgetComponent), - 'integer': DefaultTypeResolver.fromType(NumberWidgetComponent), - 'multi-line-text': DefaultTypeResolver.fromType(MultilineTextWidgetComponentComponent), - 'boolean': DefaultTypeResolver.fromType(CheckboxWidgetComponent), - 'dropdown': DefaultTypeResolver.fromType(DropdownWidgetComponent), - 'date': DefaultTypeResolver.fromType(DateWidgetComponent), - 'amount': DefaultTypeResolver.fromType(AmountWidgetComponent), - 'radio-buttons': DefaultTypeResolver.fromType(RadioButtonsWidgetComponent), - 'hyperlink': DefaultTypeResolver.fromType(HyperlinkWidgetComponent), - 'readonly-text': DefaultTypeResolver.fromType(DisplayTextWidgetComponentComponent), - 'typeahead': DefaultTypeResolver.fromType(TypeaheadWidgetComponent), - 'people': DefaultTypeResolver.fromType(PeopleWidgetComponent), - 'functional-group': DefaultTypeResolver.fromType(FunctionalGroupWidgetComponent), - 'dynamic-table': DefaultTypeResolver.fromType(DynamicTableWidgetComponent), - 'container': DefaultTypeResolver.fromType(ContainerWidgetComponent), - 'group': DefaultTypeResolver.fromType(ContainerWidgetComponent), - 'document': DefaultTypeResolver.fromType(DocumentWidgetComponent) + protected defaultValue: Type<{}> = UnknownWidgetComponent; + protected types: { [key: string]: DynamicComponentResolveFunction } = { + 'text': DynamicComponentResolver.fromType(TextWidgetComponent), + 'string': DynamicComponentResolver.fromType(TextWidgetComponent), + 'integer': DynamicComponentResolver.fromType(NumberWidgetComponent), + 'multi-line-text': DynamicComponentResolver.fromType(MultilineTextWidgetComponentComponent), + 'boolean': DynamicComponentResolver.fromType(CheckboxWidgetComponent), + 'dropdown': DynamicComponentResolver.fromType(DropdownWidgetComponent), + 'date': DynamicComponentResolver.fromType(DateWidgetComponent), + 'amount': DynamicComponentResolver.fromType(AmountWidgetComponent), + 'radio-buttons': DynamicComponentResolver.fromType(RadioButtonsWidgetComponent), + 'hyperlink': DynamicComponentResolver.fromType(HyperlinkWidgetComponent), + 'readonly-text': DynamicComponentResolver.fromType(DisplayTextWidgetComponentComponent), + 'typeahead': DynamicComponentResolver.fromType(TypeaheadWidgetComponent), + 'people': DynamicComponentResolver.fromType(PeopleWidgetComponent), + 'functional-group': DynamicComponentResolver.fromType(FunctionalGroupWidgetComponent), + 'dynamic-table': DynamicComponentResolver.fromType(DynamicTableWidgetComponent), + 'container': DynamicComponentResolver.fromType(ContainerWidgetComponent), + 'group': DynamicComponentResolver.fromType(ContainerWidgetComponent), + 'document': DynamicComponentResolver.fromType(DocumentWidgetComponent) }; constructor() { + super(); + this.types['upload'] = (field: FormFieldModel): Type<{}> => { if (field) { let params = field.params; @@ -76,47 +80,4 @@ export class FormRenderingService { return UnknownWidgetComponent; }; } - - getComponentTypeResolver(fieldType: string, defaultValue: Type<{}> = UnknownWidgetComponent): ComponentTypeResolver { - if (fieldType) { - return this.types[fieldType] || DefaultTypeResolver.fromType(defaultValue); - } - return DefaultTypeResolver.fromType(defaultValue); - } - - setComponentTypeResolver(fieldType: string, resolver: ComponentTypeResolver, override: boolean = false) { - if (!fieldType) { - throw new Error(`fieldType is null or not defined`); - } - - if (!resolver) { - throw new Error(`resolver is null or not defined`); - } - - let existing = this.types[fieldType]; - if (existing && !override) { - throw new Error(`already mapped, use override option if you intend replacing existing mapping.`); - } - - this.types[fieldType] = resolver; - } - - resolveComponentType(field: FormFieldModel, defaultValue: Type<{}> = UnknownWidgetComponent): Type<{}> { - if (field) { - let resolver = this.getComponentTypeResolver(field.type, defaultValue); - return resolver(field); - } - return defaultValue; - } - -} - -export type ComponentTypeResolver = (field: FormFieldModel) => Type<{}>; - -export class DefaultTypeResolver { - static fromType(type: Type<{}>): ComponentTypeResolver { - return (field: FormFieldModel) => { - return type; - }; - } } diff --git a/ng2-components/ng2-alfresco-core/index.ts b/ng2-components/ng2-alfresco-core/index.ts index 29712306d3..ef7e7bcf25 100644 --- a/ng2-components/ng2-alfresco-core/index.ts +++ b/ng2-components/ng2-alfresco-core/index.ts @@ -39,6 +39,7 @@ import { AuthGuardBpm } from './src/services/auth-guard-bpm.service'; import { AuthGuardEcm } from './src/services/auth-guard-ecm.service'; import { AuthGuard } from './src/services/auth-guard.service'; import { AuthenticationService } from './src/services/authentication.service'; +import { CardItemTypeService } from './src/services/card-item-types.service'; import { CommentProcessService } from './src/services/comment-process.service'; import { ContentService } from './src/services/content.service'; import { CookieService } from './src/services/cookie.service'; @@ -98,6 +99,8 @@ export { AlfrescoTranslateLoader } from './src/services/translate-loader.service export { AppConfigService } from './src/services/app-config.service'; export { ThumbnailService } from './src/services/thumbnail.service'; export { UploadService } from './src/services/upload.service'; +export { DynamicComponentMapper, DynamicComponentResolveFunction, DynamicComponentResolver } from './src/services/dynamic-component-mapper.service'; +export { CardItemTypeService } from './src/services/card-item-types.service'; export { CardViewUpdateService } from './src/services/card-view-update.service'; export { UpdateNotification } from './src/services/card-view-update.service'; export { ClickNotification } from './src/services/card-view-update.service'; @@ -167,6 +170,7 @@ export * from './src/events/base-ui.event'; export * from './src/events/folder-created.event'; export * from './src/events/file.event'; +export * from './src/models/card-view-baseitem.model'; export * from './src/models/card-view-textitem.model'; export * from './src/models/card-view-mapitem.model'; export * from './src/models/card-view-dateitem.model'; @@ -219,6 +223,7 @@ export function providers() { PeopleProcessService, AppsProcessService, CommentProcessService, + CardItemTypeService, AppConfigService ]; } diff --git a/ng2-components/ng2-alfresco-core/src/components/view/card-view-item-dispatcher.component.spec.ts b/ng2-components/ng2-alfresco-core/src/components/view/card-view-item-dispatcher.component.spec.ts index 7ce4e271dd..f46fa4bb95 100644 --- a/ng2-components/ng2-alfresco-core/src/components/view/card-view-item-dispatcher.component.spec.ts +++ b/ng2-components/ng2-alfresco-core/src/components/view/card-view-item-dispatcher.component.spec.ts @@ -22,6 +22,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; import { CardViewItem } from '../../interface/card-view-item.interface'; +import { CardItemTypeService } from '../../services/card-item-types.service'; import { CardViewContentProxyDirective } from './card-view-content-proxy.directive'; import { CardViewItemDispatcherComponent } from './card-view-item-dispatcher.component'; @@ -37,9 +38,13 @@ export class CardViewShinyCustomElementItemComponent { describe('CardViewItemDispatcherComponent', () => { let fixture: ComponentFixture; + let cardItemTypeService: CardItemTypeService; let component: CardViewItemDispatcherComponent; beforeEach(async(() => { + cardItemTypeService = new CardItemTypeService(); + cardItemTypeService.setComponentTypeResolver('shiny-custom-element', () => CardViewShinyCustomElementItemComponent); + TestBed.configureTestingModule({ imports: [], declarations: [ @@ -47,7 +52,7 @@ describe('CardViewItemDispatcherComponent', () => { CardViewShinyCustomElementItemComponent, CardViewContentProxyDirective ], - providers: [] + providers: [ { provide: CardItemTypeService, useValue: cardItemTypeService } ] }); // entryComponents are not supported yet on TestBed, that is why this ugly workaround: diff --git a/ng2-components/ng2-alfresco-core/src/components/view/card-view-item-dispatcher.component.ts b/ng2-components/ng2-alfresco-core/src/components/view/card-view-item-dispatcher.component.ts index dbe47d371b..63821d6d94 100644 --- a/ng2-components/ng2-alfresco-core/src/components/view/card-view-item-dispatcher.component.ts +++ b/ng2-components/ng2-alfresco-core/src/components/view/card-view-item-dispatcher.component.ts @@ -20,10 +20,10 @@ import { ComponentFactoryResolver, Input, OnChanges, - Type, ViewChild } from '@angular/core'; import { CardViewItem } from '../../interface/card-view-item.interface'; +import { CardItemTypeService } from '../../services/card-item-types.service'; import { CardViewContentProxyDirective } from './card-view-content-proxy.directive'; @Component({ @@ -46,7 +46,8 @@ export class CardViewItemDispatcherComponent implements OnChanges { public ngOnInit; public ngDoCheck; - constructor(private resolver: ComponentFactoryResolver) { + constructor(private cardItemTypeService: CardItemTypeService, + private resolver: ComponentFactoryResolver) { const dynamicLifecycleMethods = [ 'ngOnInit', 'ngDoCheck', @@ -72,11 +73,8 @@ export class CardViewItemDispatcherComponent implements OnChanges { } private loadComponent() { - const upperCamelCasedType = this.getUpperCamelCase(this.property.type), - className = `CardView${upperCamelCasedType}ItemComponent`; + const factoryClass = this.cardItemTypeService.resolveComponentType(this.property); - const factories = Array.from(this.resolver['_factories'].keys()); - const factoryClass = > factories.find((x: any) => x.name === className); const factory = this.resolver.resolveComponentFactory(factoryClass); this.componentReference = this.content.viewContainerRef.createComponent(factory); @@ -84,11 +82,6 @@ export class CardViewItemDispatcherComponent implements OnChanges { this.componentReference.instance.property = this.property; } - private getUpperCamelCase(type: string): string { - const camelCasedType = type.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); - return camelCasedType[0].toUpperCase() + camelCasedType.substr(1); - } - private proxy(methodName, ...args) { if (this.componentReference.instance[methodName]) { this.componentReference.instance[methodName].apply(this.componentReference.instance, args); diff --git a/ng2-components/ng2-alfresco-core/src/models/card-view-dateitem.model.ts b/ng2-components/ng2-alfresco-core/src/models/card-view-dateitem.model.ts index 4ff4f6ef28..885049d28d 100644 --- a/ng2-components/ng2-alfresco-core/src/models/card-view-dateitem.model.ts +++ b/ng2-components/ng2-alfresco-core/src/models/card-view-dateitem.model.ts @@ -25,13 +25,14 @@ import * as moment from 'moment'; import { CardViewItem } from '../interface/card-view-item.interface'; +import { DynamicComponentModel } from '../services/dynamic-component-mapper.service'; import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model'; export interface CardViewDateItemProperties extends CardViewItemProperties { format?: string; } -export class CardViewDateItemModel extends CardViewBaseItemModel implements CardViewItem { +export class CardViewDateItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel { type: string = 'date'; format: string; diff --git a/ng2-components/ng2-alfresco-core/src/models/card-view-mapitem.model.ts b/ng2-components/ng2-alfresco-core/src/models/card-view-mapitem.model.ts index 47a21ca494..61fd04085d 100644 --- a/ng2-components/ng2-alfresco-core/src/models/card-view-mapitem.model.ts +++ b/ng2-components/ng2-alfresco-core/src/models/card-view-mapitem.model.ts @@ -24,9 +24,10 @@ */ import { CardViewItem } from '../interface/card-view-item.interface'; +import { DynamicComponentModel } from '../services/dynamic-component-mapper.service'; import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model'; -export class CardViewMapItemModel extends CardViewBaseItemModel implements CardViewItem { +export class CardViewMapItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel { type: string = 'map'; value: Map; diff --git a/ng2-components/ng2-alfresco-core/src/models/card-view-textitem.model.ts b/ng2-components/ng2-alfresco-core/src/models/card-view-textitem.model.ts index 46163c3222..f9d8f1156b 100644 --- a/ng2-components/ng2-alfresco-core/src/models/card-view-textitem.model.ts +++ b/ng2-components/ng2-alfresco-core/src/models/card-view-textitem.model.ts @@ -24,12 +24,13 @@ */ import { CardViewItem } from '../interface/card-view-item.interface'; +import { DynamicComponentModel } from '../services/dynamic-component-mapper.service'; import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model'; export interface CardViewTextItemProperties extends CardViewItemProperties { multiline?: boolean; } -export class CardViewTextItemModel extends CardViewBaseItemModel implements CardViewItem { +export class CardViewTextItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel { type: string = 'text'; multiline: boolean; diff --git a/ng2-components/ng2-alfresco-core/src/services/card-item-types.service.ts b/ng2-components/ng2-alfresco-core/src/services/card-item-types.service.ts new file mode 100644 index 0000000000..63fcf3ae46 --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/services/card-item-types.service.ts @@ -0,0 +1,34 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable, Type } from '@angular/core'; +import { CardViewDateItemComponent } from '../components/view/card-view-dateitem.component'; +import { CardViewMapItemComponent } from '../components/view/card-view-mapitem.component'; +import { CardViewTextItemComponent } from '../components/view/card-view-textitem.component'; +import { DynamicComponentMapper, DynamicComponentResolveFunction, DynamicComponentResolver } from '../services/dynamic-component-mapper.service'; + +@Injectable() +export class CardItemTypeService extends DynamicComponentMapper { + + protected defaultValue: Type<{}> = CardViewTextItemComponent; + + protected types: { [key: string]: DynamicComponentResolveFunction } = { + 'text': DynamicComponentResolver.fromType(CardViewTextItemComponent), + 'date': DynamicComponentResolver.fromType(CardViewDateItemComponent), + 'map': DynamicComponentResolver.fromType(CardViewMapItemComponent) + }; +} diff --git a/ng2-components/ng2-alfresco-core/src/services/dynamic-component-mapper.service.ts b/ng2-components/ng2-alfresco-core/src/services/dynamic-component-mapper.service.ts new file mode 100644 index 0000000000..c654dce9f1 --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/services/dynamic-component-mapper.service.ts @@ -0,0 +1,66 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Type } from '@angular/core'; + +export interface DynamicComponentModel { type: string; } +export type DynamicComponentResolveFunction = (model: DynamicComponentModel) => Type<{}>; +export class DynamicComponentResolver { + static fromType(type: Type<{}>): DynamicComponentResolveFunction { + return (model: DynamicComponentModel) => { + return type; + }; + } +} + +export abstract class DynamicComponentMapper { + + protected defaultValue: Type<{}> = undefined; + protected types: { [key: string]: DynamicComponentResolveFunction } = {}; + + getComponentTypeResolver(type: string, defaultValue: Type<{}> = this.defaultValue): DynamicComponentResolveFunction { + if (type) { + return this.types[type] || DynamicComponentResolver.fromType(defaultValue); + } + return DynamicComponentResolver.fromType(defaultValue); + } + + setComponentTypeResolver(type: string, resolver: DynamicComponentResolveFunction, override: boolean = false) { + if (!type) { + throw new Error(`type is null or not defined`); + } + + if (!resolver) { + throw new Error(`resolver is null or not defined`); + } + + let existing = this.types[type]; + if (existing && !override) { + throw new Error(`already mapped, use override option if you intend replacing existing mapping.`); + } + + this.types[type] = resolver; + } + + resolveComponentType(model: DynamicComponentModel, defaultValue: Type<{}> = this.defaultValue): Type<{}> { + if (model) { + let resolver = this.getComponentTypeResolver(model.type, defaultValue); + return resolver(model); + } + return defaultValue; + } +}