npm package: @jj-viewer/skinnable
Description: This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.1.0.
Author: Dong-il Park
Latest version: 0.0.27

# @jj-viewer/skinnable

This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.1.0.

## Features

* Angular 프로젝트에서 다음 사항을 컴파일 이후에도 동적으로 로드하여 설정할 수 있도록 한다.
    - HTML (template)
    - Css (style)
    - Json (Component의 속성 설정)

* 컴포넌트간 API를 호출할 수 있는 Mediator를 제공한다.

>(주의) Jit 컴파일 일때만 정상 동작합니다.   
>* AOT 컴파일을 지원되지 않습니다. (--aot=false)
>* `<router-outlet></router-outlet>` 구문은 테스트되지 않았습니다.

## Installation
```
npm install @jj-viewer/skinnable --save
```

### Typescript 환경 설정

**tsconfig.json**
```
{
  "compilerOptions": {
    ...
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "useDefineForClassFields": true, // 생성자에서 초기화하지 않은 member도 Object.keys 목록에 추가함
    }
  },
  
  "angularCompilerOptions": {
    ...
    "enableIvy": false
  },
  ...
}
```

### proxy 설정
개발환경에서 런타임에 사용할 `bundle` 폴더에 접근하기 위해 proxy 서버를 설정합니다.  
`bundle` 폴더에는 skin(template, css) 파일 및 이미지등의 asset 파일을 넣어둡니다.

workspace (root) 위치가 다음 주소로 서비스된다고 가정할때
```
http://localhost/dev/workspace
```

1. `proxy.conf.json` 파일 생성
    ```json
    {
      "/bundle/*": {
        "target": "http://localhost/dev/workspace",
        "secure": false,
        "logLevel": "debug"
      }
    }
    ```

2. `angular.json` 파일 수정
    ```
    "serve": {
      "options": {
        "proxyConfig": "proxy.conf.json",
      },
    }
    ```
    또는 실행시 옵션 설정
    ```
    ng serve --proxy-config=proxy.conf.json
    ```

3. `ng serve` 실행
`http://localhost/dev/workspace/bundle` 서비스를 `http://localhost:4200/bundle`로 접근 가능해짐.

## Usage
>`testApp` 프로젝트에서 보다 자세한 예시를 참고하실 수 있습니다.

### 동적으로 Skin (template, css) 이 설정되는 과정

1. 설정 파일을 정의합니다. (`bundle` 폴더)
2. `SkinApplicationComponent`에서 설정 파일이 로드됩니다. (json 파일)
3. 설정 파일 내용이 각 `SkinComponent`에 동적으로 적용됩니다. (template, css)

### `AppModule` 설정

```typescript
@NgModule({

    // 중요: Skinnable Component는 AppModule에 export 되어 있어야 함
    // AppModule에 export 되어있는 module은 추가할 필요 없음
    providers: [
        {provide: SKIN_INJECT_MODULES, useValue: [AppModule]}
    ],

    schemas: [
        NO_ERRORS_SCHEMA,
        CUSTOM_ELEMENTS_SCHEMA
    ],

    imports: [
        ...imports,
        SkinnableModule
    ],

    exports: [
        ...exports
    ],
    ...
})
export class AppModule {}
```

#### `SKIN_INJECT_MODULES`에 값 지정  
컴파일된 이후에 사용되는 (다른 모듈에서 정의된) 컴포넌트를 사용하려면

1. 컴포넌트는 `AppModule`에서 `export` 되어 있어야 하고,
2. `exporte`된 컴포넌트를 런타임에 사용하는 `SkinnableModule`에 `import` 되어야 하므로,
3. 필요한 module을 배열로 지정하는 것 보다는 모든 기능(요소)을 포함하는 `AppModule`을 대표로 `import` 시킵니다..
4. `AppModule` 대신 필요한 모듈만 `import` 시켜도 됩니다.
    ```typescript
    {provide: SKIN_INJECT_MODULES, useValue: [...modules]}
    ```

>(주의) AOT 컴파일을 지원되지 않습니다. (--aot=false)
>`SkinnableModule`에 런타임에 `import`되는 Module을 동적으로 인식할 수없습니다.

### Component에 적용
`SkinComponent`를 상속 받아 사용합니다.
```typescript
import {
    SkinApplicationComponent,
    SkinComponent,
    SKINNABLE_TEMPLATE,
    SkinnableParameter,
    skinnableParameterProvider
} from '@jj-viewer/skinnable';

@Component({
    selector: 'jj-sample',
    providers: [skinnableParameterProvider],

    // 설정파일 또는 Attribute에 설정된 경로의 템플릿이 동적 로딩됨
    template: SKINNABLE_TEMPLATE

    // 아래 설정도 여전히 사용 가능함 (이곳 템플릿은 컴파일 됨)
    // templateUrl: 'sample.html',
    // styleUrls: ['sample.css'],
})
export class SampleComponent extends SkinComponent {
    constructor(protected param: SkinnableParameter, protected viewContainerRef: ViewContainerRef) {
        super(param, viewContainerRef);
    }
}
```

### Application에 적용
`SkinApplicationComponent`를 상속 받아 사용합니다.
```typescript
import {
    SkinApplicationComponent,
    SKINNABLE_TEMPLATE,
    SkinnableParameter,
    skinnableParameterProvider
} from '@jj-viewer/skinnable';

@Component({
    selector: 'jj-application',
    providers: [skinnableParameterProvider],
    template: SKINNABLE_TEMPLATE
})
export class ApplicationComponent extends SkinApplicationComponent {
    constructor(protected param: SkinnableParameter, protected viewContainerRef: ViewContainerRef) {
        super(param, viewContainerRef);
    }
}
```

### 컴포넌트간 통신

>`testApp` 프로젝트에서 `SampleComponent`에서 발송한 이벤트를 `DefineComponent`에서 받아 처리하는 예제 입니다.

컴포넌트간 서로 통신을 하려면 반드시 `id`가 (attribute or class value) 설정되어 있어야 합니다.

* `SampleComponent`에서 이벤트 발송
```typescript
// SampleComponent
bookLoadedEvent = new EventEmitter<any>();

// @오버라이딩
// 모든 컴포넌트 설정이 완료된 applicationCompleteEvent 발생 후 emit 한다.
// 오버라이딩해서 사용하도록 applicationCompleteEvent 청취가 구현되어 있다.
protected onApplicationComplete() {

    setTimeout(() => {
        // 예 : bookLoaded 생성 완료를 알림
        this.bookLoadedEvent.emit(this.id);
    }, 1000);

}
```
* template에 `id` 설정
```html
<jj-sample id="3.app_skin_sample" ...>
```

* `DefineComponent`에서 이벤트 청취
```typescript
// @오버라이딩
// 모든 컴포넌트 설정이 완료된 applicationCompleteEvent 발생 후 청취 한다.
protected onApplicationComplete() {

    // Mediator를 통해 컴포넌트 instance에 접근
    const book = this.param.mediatorService.get('3.app_skin_sample') as SampleComponent;

    // 테스트 : bookLoaded 생성 완료 청취
    const subscription = book.bookLoadedEvent.subscribe((id: string) => {
        console.error('bookLoadedEvent : ', book.nodeName, id);
        subscription.unsubscribe();

        // book API 사용
        // book.goPage(10);
    });

}
```

## 설정 파일 정의
컴포넌트에 설정값을 전달하는 방법은 다음과 같습니다.  
다음 순서대로 (우선순위) 설정값이 적용됩니다.

1. Attribute 설정값이 있으면 우선적으로 적용됩니다.
2. 설정 파일에서 `[태그(selector)]` 항목에서 정의된 설정값
3. 설정 파일에서 `[global]` 항목에서 정의된 설정값

설정 파일에 대해 설명합니다.
```javascript
{
  "global": {
    /*
    컴포넌트들의 공통 속성중 기본값으로 설정할 값을 전역 설정합니다.
    이곳에서는 properties, skins 외 다른 속성은 정의할 수 없습니다.
    새로운 속성을 추가하려면 properties 항목을 이용합니다.
    */
    
    // 이곳에 속성을 추가해도 컴포넌트에 전달되지 않습니다.
    "can't add propertise": "전달되지 않는 속성값",

    "properties": {
      "can add propertise": "추가할 수 있는 속성 영역",
      // 예) a, b 속성을 모든 컴포넌트에 전달 
      "a": 1, "b": 2
    },

    "skins": {
      "useLoader": true,
      "urlTemplate": "",
      "urlCss": ""
    }
  },

  // 'jj-application' selector를 가진 컴포넌트에 전달되는 속성 설정
  "jj-application": {
    "properties": {
      // 예) jj-application 컴포넌트에 global 설정값까지 함께 전달됨 
      // "a": 1, "b": 10, "c": 20
      "b": 10, "c": 20
    },
    "skins": {
      "useLoader": false,
      "urlTemplate": "bundle/skins/application.html",
      "urlCss": "bundle/skins/application.css"
    }
  },

  // 'jj-sample' selector를 가진 컴포넌트에 전달되는 속성 설정
  "jj-sample": {
    // attribute에 값이 지정된 경우 값 지정 생략해도 됨.
    "skins": {}
  }
  ...
}
```

## Skin 파일 지정 방법
  - `url-configuration`에 지정된 설정파일에 정의
  - attribute에서 직접 지정하는 방법

#### Template (HTML) 작성 방법
attribute에 다음 속성을 활용합니다.
* `url-configuration`
  - 설정파일 경로를 지정 합니다.
  - `SkinApplicationComponent`에만 설정할 수 있습니다.
* `url-template`
  - 동적 로드되는 템플릿(HTML) 파일 경로입니다.
  - 파일 내부에 @import url() 구문 사용할 수 없습니다.
* `url-css`
  - 동적 로드되는 CSS 파일 경로입니다.
* `use-loader`
  - 파일 로드 방식을 설정
  - (false) angular 로더를 사용하여 파일 내용을 로드
  - (true) 새로 작성된 http 로더로 파일 내용을 로드
  - true 이면 같은 url의 파일은 요청을 한번만 호출함 (cache 사용과는 다름)

Attribute 속성으로 사용
```html
<!--문자열은 "'"로 감싸야 함.-->
<jj-book [use-loader]="false"
         [url-template]="'bundle/skins/book.component.html'"
         [url-css]="'bundle/skins/book.component.css'">
```

Attribute 값으로 사용
```html
<!--type을 유지하려면 속성 바인딩 구문 사용해야 함.-->
<!--그렇지 않으면 아래 use-loader 속성은 문자열 "false"로 인식됨.-->
<jj-sample use-loader="false"
         url-template="bundle/skins/book.component.html"
         url-css="bundle/skins/book.component.css">
```

Attribute 바인딩 사용
```html
<!--interpoliate 구문 사용-->
<jj-application url-configuration="{{urlConfiguration}}">
```

### 경로 설정
Template, Css에 사용되는 URL은 publishing 후 `index.html`을 기준으로한 상대경로로 작성해야 합니다.
* Template 내부에서 경로 지정
    ```html
    <jj-sample urlTemplate="./bundle/skins/sample.html"
               urlCss="./bundle/skins/sample.css">
    </jj-sample>
    ```

* Css 내부에서 경로 지정
    ```css
    @import url("./bundle/skins/test_import.css");
    /* styleUrl로 지정된 경우 파일 내부에 @import url() 구문 사용 불가함. */
    ```

## 디렉티브로 기능을 구현하지 않은 이유
* Directive Attribute으로 로드 기능을 구현하는 경우 클래스 상속을 활용할 수없다.
* 기능이 필요한 모든 노드에 직접 디렉티브를 명시해 주어야 한다.  
  (설정값까지 attribute에 작성하면 작업이 너무 번거로워 진다.)