Любой создаваемый проект не обходится без динамического создания элементов. Рано или поздно вам понадобится либо создать tooltip для элемента, показать модальное окно, или вовсе сформировать некоторые блоки динамически подгружая их с сервера. При решении таких задач я зачастую определяю зрелость фреймворка, который использую: насколько просто я могу в нем создавать динамический контент, и какие возможности он мне для этого предлагает. В этой статье мы поговорим о динамическом создании контента в новом Angular и рассмотрим различные подходы, которые он нам предоставляет.
Прежде чем перейти к созданию контента, нам необходимо рассмотреть ряд абстракций, которые есть в Angular — что они собой представляют и для чего используются. Так как ангуляр разработан как решение, которое может работать на различных платформах — в браузере, на мобильном устройстве и на сервере, — то прямая работа с DOM в нем не очень приветствуется, хотя и возможна. Например, следующий пример будет хорошо работать в браузере, но может перестать работать, если вы используете Web Worker или ваш код работает на мобильном устройстве.
import { Component, AfterComponentInit, ViewChild } from '@angular/core';
@Component({
selector: 'some-component',
templateUrl: '<input type="text" #input>'
})
export class SomeComponent implements AfterContentInit {
@ViewChild('input') input;
ngAfterContentInit() {
this.input.nativeElement.focus();
}
}
Вместо прямой работы с DOM-элементом Angular предоставляет нам следующие абстракции — Renderer, TemplateRef, ElementRef и ViewContainerRef. Давайте рассмотрим их по порядку и посмотрим, как с помощью них мы сможем создавать динамический контент.
Renderer
Я буду говорить о Renderer2 (далее просто Renderer), так как первая версия уже помечена как deprecated. Renderer используется в основном для манипуляций над уже существующими элементами, например для изменения стилей элемента, атрибутов и параметров элемента. Наиболее часто его использование можно встретить при создании директив. Но он также позволяет создавать новые элементы и вставлять их в DOM, что подходит для нашей задачи.
Давайте рассмотрим методы, которые нам предоставляет Renderer:
- createElement(name: string, namespace?: string): any
Позволяет создать элемент DOM и опционально указать для него пространство имен. Пространство имен используется, например, для вставки SVG-элементов. Элемент после создания не будет отображаться в DOM пока мы его туда не добавим.let inputElement = this.renderer.createElement('input');
-
appendChild(parent: any, newChild: any): void
insertBefore(parent: any, newChild: any, refChild: any): void
removeChild(parent: any, oldChild: any): voidИспользуются для вставки/удаления созданных или существующих элементов в DOM.
let inputElement = this.renderer.createElement('input'); this.renderer.appendChild(parent, inputElement);
-
setAttribute(el: any, name: string, value: string, namespace?: string): void
removeAttribute(el: any, name: string, namespace?: string): void
setProperty(el: any, name: string, value: any): voidИспользуются для изменения атрибутов или параметров DOM-элемента, например, для установки значения checkbox.
this.renderer.setAttribute(inputElement, 'value', 'Hello from renderer'); this.renderer.setProperty(inputElement, 'checked', true);
- createText(value: string): any
Создает текстовый DOM-элемент, который можно добавит как дочерний в нужный элемент.let buttonElement = this.renderer.createElement('button'); const text = this.renderer.createText('Text'); this.renderer.appendChild(buttonElement, text);
- addClass(el: any, name: string): void
removeClass(el: any, name: string): void
Устанавливает или удаляет класс для DOM-элемента.this.renderer.addClass(buttonElement, 'btn-large');
Это далеко не все, что предоставляет Renderer, но, даже используя указанные методы уже можно динамически создавать и изменять элементы DOM.
Но прежде чем воспользоваться возможностями Renderer, нам необходимо рассмотреть еще один момент — как в ангуляр находить DOM элементы-контейнеры, в которые мы будем добавлять динамический контент. Для этого у нас есть два способа — воспользоваться Dependency Injector или использовать ряд декораторов — @ViewChild/@ViewChildren и @ContentChild/@ContentChildren. Давайте рассмотрим оба варианта и начнем с самого простого.
Доступ к элементу через DI
Данный способ довольно часто используется при создании собственных директив. Для того чтобы получить доступ к элементу (контейнеру) директивы, надо добавить в конструктор директивы приватную переменную с типом ElementRef. Давайте рассмотрим, как будет выглядеть добавление элементов с помощью сервиса Renderer в данном случае:
input { Directive, Renderer2, ElementRef, Input} from '@angular/core';
@Directive({
selector: 'someDirective'
})
export class SomeDirective {
constructor(
private renderer: Renderer2,
private elementRef: ElementRef
) {}
@Input() set content(value: string) {
let buttonElement = this.renderer.createElement('button');
const text = this.renderer.createText('Text');
this.renderer.appendChild(buttonElement, text);
this.renderer.appendChild(this.elementRef.nativeElement, buttonElement);
}
}
В данном примере мы создаем новую кнопку и вставляем ее в DOM. Пример, конечно, надуманный, но позволяет нам увидеть основные моменты для работы с DOM. Ссылка ElementRef указывает на элемент, на который была применена наша директива. Все довольно просто, но, к сожалению, данный метод удобен только для директив и не очень удобен, когда вы создаете компоненты с динамическим содержимым. Давайте теперь рассмотрим более универсальный метод.
@ViewChild/@ViewChildren и @ContentChild/@ContentChildren
Для поиска элементов в DOM ангуляр предоставляет ряд декораторов — @ViewChild/@ViewChildren и @ContentChild/@ContentChildren. Директива @ViewChild отличается от @ViewChildren тем, что первая всегда вернет вам только один элемент, в то время как вторая позволяет вам находить несколько элементов, возвращая вам объект типа QueryList.
QueryList представляет из себя итерируемый интерфейс, а также позволяет подписываться на изменение элементов через механизм Observable. Декораторы @ViewChildren и @ContentChildren необходимо использовать в обработчике ngAfterViewInit жизненного цикла компонента, так как раньше QuryList просто будет не определен.
Пара директив @ContentChild/@ContentChildren ведет себя аналогичным образом и отличается от связки @ViewChild/@ViewChildren только тем, что @ContentChild ищет элементы просто в DOM-дереве, в то время как @ViewChild ищет элементы в ShadowDom. В данной статье для простоты мы не будет рассматривать связку @ContentChild/@ContentChildren, а также ограничимся только @ViewChild-декоратором, так как не будем использовать несколько элементов. Для поиска элементов мы воспользуемся следующим синтаксисом @ViewChild:
@ViewChild('[query params]', { read: [referenceType], descendants: boolean });
где
- query params – элемент который ищем. Может быть, как имя шаблона, html элемент или компонент/директива.
- descendants – определяет искать элемент только среди прямых потомков или смотреть глубже.
- read — указание типа возвращаемого элемента. Обычно указание данного параметра не является необходимым, так как ангуляр довольно сообразителен и, если вы ищете шаблон, он вернет вам TemplateRef, если вы ищете html элемент, ангуляр вернет вам ElementRef. Но в некоторых случая, например, когда вам надо получить ViewContainerRef, вам придётся указать тип возвращаемого элемента.
При поиске элементов указанные декораторы возвращают переменную типа ElementRef — верхнеуровневую абстракцию, которая содержит в себе ссылку на «нативный» DOM-элемент:
class ElementRef {
constructor(nativeElement: any)
nativeElement: any
}
Итак, давайте посмотрим, как нам найти элемент в компоненте и, используя Renderer, изменить его содержимое:
@Component({
selector: 'some-component',
template: '<div #elem>Element text</div>'
})
export class SomeComponent implements AfterViewInit {
@ViewChild('elem') _elem: ElementRef;
constructor(private _renderer: Renderer2) {}
ngAfterViewInit() {
const buttonElement = this._renderer.createElement('button');
const text = this._renderer.createText('Text');
this._renderer.appendChild(buttonElement, text);
this._renderer.appendChild(this._elem.nativeElement, buttonElement);
}
}
Как и в примере выше, мы создаем кнопку с заданным текстом и добавляем ее в DOM. Только на этот раз мы вставляем кнопку в нужный нам контейнер внутри компонента. Данный подход слишком низкоуровневый и используется довольно редко, поэтому пойдем дальше и рассмотрим, что же еще предоставляет нам Angular.
TemplateRef
Идея использования шаблонов для вставки новых элементов не нова и давно используется JS-разработчиками. При использовании template тега из HTML5 браузер создаст DOM-дерево для содержимого тега, но не будет вставлять его в DOM. Вот пример использования тега template в классическом, «нативном» JS:
<template id="some_template">
<div>Template contrent text</div>
</template>
<div id="container"></div>
<script>
let tpl = document.querySelector('#some_template');
let container = document.querySelector('#container');
insertAfter(container, tpl.content);
</script>
Ангуляр предоставляет свою нотацию описания шаблонов, а также позволяет манипулировать шаблоном и его содержимым. С этой абстракцией вы могли познакомиться, если создавали свои собственные структурные директивы наподобие ngIf и ngFor. Для доступа к шаблону мы воспользуемся типом TemplateRef — это ссылка на элемент ng-template в вашем компоненте или директиве. У вас есть два способа получить доступ к шаблону — используя тег ng-template и Dependency Injection или используя поиск элементов через Query-декораторы, о которых мы рассказывали выше. Давайте рассмотрим оба способа и начнем с самого простого:
@Directive({
selector: '[isAdmin]'
})
export class IsAdminDirective {
@Input() set isAdmin(value: boolean) {
if (value) {
this.viewContainerRef.createEmbeddedView(this.templateRef);
} else {
this.viewContainerRef.clear();
}
}
constructor(
private templateRef: TemplateRef<any>,
private viewContainerRef: ViewContainerRef
) {}
}
В примере выше мы использовали Dependency Injection, чтобы получить доступ к шаблону нашей директивы и динамически вставили ее в DOM, используя ViewContainerRef. О ViewContainerRef мы поговорим позднее, пока не обращайте на него внимания, а сейчас давайте рассмотрим, как мы можем динамически создавать DOM-элементы, используя декоратор @ViewChild:
@Component({
selector: 'some-component',
template: `
<ng-template #tpl1><span>Some template content 1</span></ng-template>
<ng-template #tpl2><span>Some template content 2</span></ng-template>
<div #container></div>
`
})
export class SomeComponent {
@Input() set isAdmin(value: boolean) {
if (value) {
this.view = this.viewContainerRef.createEmbeddedView(this._tpl);
} else {
this.view.destroy();
}
}
@ViewChild('tpl1') _tpl: TemplateRef;
private view: EmbeddedViewRef<Object>;
constructor(private viewContainerRef: ViewContainerRef) {}
}
В данном примере с помощью декоратора @ViewChild мы находим нужный нам шаблон в виде переменной типа TemplateRef и вставляем его в DOM аналогичным способом, как и в примере с конструктором.
Кстати, ангуляр удалит тег ng-template и его содержимое из DOM и вместо него разместит комментарий <!—ng-template bindings={}-->. Данный способ позволяет создавать простой динамический контент на основе готовых шаблонов. Но пойдем дальше и посмотрим, что еще нам доступно.
ViewContainerRef
Настало время поговорить о ViewContainerRef, который мы неоднократно видели в примерах выше. ViewContainerRef представляет собой ссылку на контейнер компонента или директивы и, кроме доступа к элементу, позволяет создавать два типа View — Host Views (View элементы, создаваемые на основе компонентов) и Embedded Views (View элементы, создаваемые на основе готовых шаблонов). Все создаваемые элементы имеют базовый тип View, который является основным строительным блоком для Angular приложений и представляет собой сгруппированные DOM-элементы, с которыми ангуляр работает как с единым целым и позволяет привязывать эту группу к Change Detection механизму. ViewContainerRef содержит в себе довольно много методов, давайте их рассмотрим:
-
createEmbeddedView(templateRef: TemplateRef, context?: C, index?: number): EmbeddedViewRef
Этим методом мы пользовались в наших примерах. Он позволяет создавать новые View-элементы на основе готовых шаблонов и вставляет результат в DOM-контейнер. В качестве параметров можно также передать контекст, данные из которого можно использовать в шаблоне, и индекс, по которому можно разместить создаваемый элемент. -
createComponent(componentFactory: ComponentFactory, index?: number, injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef): ComponentRef
Создает View элемент на основе экземпляра компонента и вставляет его в DOM, возвращая нам указатель на созданный компонент. Для создания элемента необходимо сначала получить фабрику компонента и инжектор. -
clear(): void
Удаляет все View элементы в контейнере -
insert(viewRef: ViewRef, index?: number): ViewRef
Вставляет View-элемент, в заданную позицию контейнера -
remove(index?: number): void
Удаляет View-элемент по указанному индексу. Если индекс не задан, будет удален последний View-элемент. - destroy (index?: number): ViewRef
Удаляет View-элемент из DOM
Как работает createEmbeddedView мы видели уже в примерах выше, давайте теперь рассмотрим, как создавать View элементы, используя метод createComponent. Этот метод позволяет динамически создавать элементы на основе готовых компонентов. Но для начала нам нужно научиться находить фабрику нужного нам компонента и поможет нам в этом ComponentFactoryResolver. Я не буду тут описывать весь код создания компонента целиком, а сделаю ряд допущений.
Во-первых, предположим, что у нас есть в проекте компонент Popover, который выглядит, например, так:
@Component({
selector: 'iw-popover',
template: `
<div class="popover popover-{{placement}}">
<h3 class="popover-title">{{title}}</h3>
<div class="popover-content">
<ng-content></ng-content>
</div>
</div>
`
})
export class Popover {
@Input() placement: string;
@Input() title: string;
}
и он добавлен в атрибут entryComponents нашего модуля. Последнее, кстати, важно, так как без добавления компонента в entryComponents ничего работать не будет, ангуляр просто не узнает о компоненте, потому что не встретит его в шаблонах.
Также предположим, что вызов нашей директивы будет выглядеть следующим образом:
<ng-template #popoverContent>
Popover content
</ng-template>
<button [popover]="popoverContent" title="Popover title" placement="right">
Show popover
</button>
То есть наша директива получает на вход три параметра – ссылку на шаблон типа TemplateRef, значение заголовка и позицию, где надо показать popover.
Исходя из указанных предположений, код нашей директивы, показывающей popover, будет выглядеть следующим образом:
@HostListener('mouseover')
show() {
if (this._componentRef) {
this._componentRef.destroy();
}
this._contentViewRef = this.popover.createEmbeddedView();
const componentFactory = this._cfResolver.resolveComponentFactory(Popover);
this._componentRef = this._vcRef.createComponent(
componentFactory,
this._injector,
0,
[this._contentViewRef.rootNodes]
);
this._componentRef.instance.placement = this.placement;
this._componentRef.instance.title = this.title;
this._contentViewRef.detectChanges();
}
@HostListener('mouseleave')
hide() {
if (this._componentRef) {
this._componentRef.destroy();
this._componentRef = null;
}
}
constructor(
private _injector: Injector,
private _cfResolver: ComponentFactoryResolver
) {}
Как вы видите, здесь есть два обработчика событий мышки на компоненте – один показывающий компонент и один удаляющий его из DOM. В коде, показывающем компонент, мы сначала создаем View на основе переданного нам шаблона, находим фабрику нашего компонента Popover и затем создаем его, передавая на вход фабрику компонента, инжектор, позицию и вложенный контент, который будет вставлен на место ng-content в компоненте Popover. Также, поскольку наш компонент динамический, то надо передать в него необходимые параметры и сказать Change Detector-механизму, что данные изменились.
Все довольно несложно и, разобравшись в этом один раз, вы легко будете создавать компоненты. Казалось бы, мы научились создавать динамический контент на основе шаблонов и компонентов. И вроде все у нас есть для решения наших задач, но есть пара моментов, на которые я хотел бы обратить внимание.
Во-первых, интересный факт в том, что ангуляр не вставляет View-элемент внутрь указанного контейнера, а добавляет его сразу после контейнера. Поэтому для вставки элементов в DOM удобно использовать ng-container элемент, который избавит нас от лишнего элемента в DOM. Лично для меня это было удивительным откровением, когда я начал отлаживать DOM-разметку и потратил кучу времени, чтобы понять, где ошибся, что мой элемент не вставляется внутрь.
Во-вторых, динамически добавляемые компоненты не поддерживают Input- и Output-декораторы и это самая печаль. Для нас это выливается в то, что ngOnChanges метод компонента не будет вызываться, когда мы присваиваем новое значение Input переменным компонента. Из этой ситуации есть два выхода — использовать setter-метод для переменной компонента или контролировать перерисовку компонента вручную из компонента родителя. Можно еще использовать ngDoCheck и в нем самим сравнивать атрибуты.
Но давайте усложним задачу и предположим, что мы хотим динамически создавать компонент, который находится в модуле, который лежит в отдельном файле на сервере. Прям «заяц в утке, утка в шоке». Но это еще не все, также мы хотим создавать наш компонент из сервиса, а не из существующего компонента. Итак, давайте разбираться по порядку как такое можно сделать.
Первое, что нам нужно, это достать фабрику нужного компонента из модуля и создать экземпляр компонента. Для загрузки js файла с сервера я буду использовать SystemJS загрузчик. Также пусть нужный модуль экспортируется в переменной module из нашего JS файла. Ниже представлен код, который решает первую часть нашей задачи:
(<any>window).System.import('module.js')
.then((module: any) => module.module)
.then((exportedModule: any) => this.compiler.compileModuleAndAllComponentsAsync(exportedModule))
.then((moduleWithFactories: ModuleWithComponentFactories<any>) => {
const factory = moduleWithFactories.componentFactories.find((component) => {
return component.componentType.name === componentName
});
this.componentRef = this.content.createComponent(this.componentFactory, 0, this.injector);
})
В данном коде мы использовали JIT-компилятор ангуляра для того, чтобы скомпилировать загруженный модуль, и у данного подхода есть одна особенность. Так, если вы используете AOT-сборку вашего проекта, то компилятор будет не доступен во время выполнения приложения и указанный код работать не будет. Для решения данной проблемы можно создать отдельный модуль с сервисом, куда нужно будет вручную добавить компилятор, например, следующим образом:
import {
NgModule,
ModuleWithProviders,
Compiler,
COMPILER_OPTIONS,
CompilerOptions,
Optional
} from '@angular/core';
import { JitCompilerFactory } from '@angular/compiler';
export function createJitCompiler(options?: CompilerOptions[]) {
options = options || [];
return new JitCompilerFactory([{ useDebug: false, useJit: true }]).createCompiler(options);
}
@NgModule({
...
})
export class DynamicComponentModule {
static forRoot(metadata: NgModule): ModuleWithProviders {
return {
ngModule: DynamicComponentModule,
providers: [
{
provide: Compiler, useFactory: createJitCompiler, deps: [Optional(), COMPILER_OPTIONS]
}
]
}
}
}
Теперь у нас есть компонент и нам осталось понять, как вставить его в DOM из сервиса. Ведь тут у нас нет ViewContainerRef, который мы использовали ранее. Для вставки компонента в DOM нам надо сделать две вещи — найти корневой элемент нашего приложения и вставить в него созданный View-компонент. И тут воспользуемся ссылкой на наше angular-приложение, которая доступна средствами DI через переменную с типом ApplicationRef. Для этого в конструктор нашего класса необходимо добавить ApplicationRef:
constructor(
private applicationRef: ApplicationRef,
private injector: Injector
) {}
Итак, для вставки нашего компонента, надо найти место для вставки View, а также не забыть скопировать в наш компонент нужные параметры как мы делали это раньше. Давайте напишем ряд вспомогательных функций для решения этих задач:
private getRootViewContainer(): ComponentRef<any> {
const rootComponents = this.applicationRef['_rootComponents'];
if (rootComponents.length) {
return rootComponents[0];
}
throw new Error('View Container not found!');
}
getComponentRootNode(componentRef: ComponentRef<any>): HTMLElement {
return (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
}
projectComponentInputs(component: ComponentRef<any>, options: any): ComponentRef<any> {
if (options) {
const props = Object.getOwnPropertyNames(options);
for (const prop of props) {
component.instance[prop] = options[prop];
}
}
return component;
}
Теперь соберем все вместе и получим следующий код, добавляющий наш компонент в DOM:
let location: Element = this.getComponentRootNode(this.getRootViewContainer());
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(SomeComponent);
let componentRef = componentFactory.create(this.injector);
let appRef: any = this.applicationRef;
let componentRootNode = this.getComponentRootNode(componentRef);
const injector = ReflectiveInjector.resolveAndCreate([{
provide: 'dialog', useValue: componentRef
}], this.injector);
this.projectComponentInputs(componentRef, {
options: {
...
},
injector: injector
});
appRef.attachView(componentRef.hostView);
componentRef.onDestroy(() => {
appRef.detachView(componentRef.hostView);
componentRef = null;
});
location.appendChild(componentRootNode);
В данном коде есть пара моментов, которые надо пояснить. Кроме создания нашего компонента и поиска места для вставки его в DOM, мы также создали собственный Injector и добавили наш View компонента к приложению angular, вызвав метод attachView, чтобы он о нас знал и запускал ChangeDetector. Кроме этого, мы повесили обработчик на уничтожение компонента, в котором удаляем компонент из приложения.
Итак, мы сделали код, который позволяет создавать динамически компонент, который лежит во внешнем файле, а также вставлять его в DOM из нашего сервиса. Данный код я использую в нашем продукте для создание модальных диалогов.
Кстати, необязательно тянуть извне весь модуль целиком. Данная техника применима и в ситуациях, когда вы хотите создавать компоненты, имея только файл с содержимым шаблона. Например, у вас на сервере может лежать содержимое шаблона, и вы хотите на основе его создать View. Для этого вам нужно будет сделать следующее:
- Создать модуль и добавить в него все используемые в шаблоне компоненты.
- Создать компонент и указать загруженный шаблон.
- Сделать то же, что мы делали в коде выше — скомпилировать полученный модуль, получить из него ваш компонент и вставить в DOM
Вот пример кода, который решает данную задачу:
private createComponentFromTemplateString(template: string) {
@Component({
selector: 'some-selector',
template: template
})
class RuntimeComponent {}
NgModule({
imports: [imports],
providers: [providers],
declarations: [RuntimeComponent]
})
class RuntimeComponentModule {}
this.compiler.compileModuleAndAllComponentsAsync(RuntimeComponentModule)
.then((moduleWithFactories: ModuleWithComponentFactories<any>) => {
...
})
}
Встроенные решения
Ангуляр в последних версиях предоставляет две директивы, которые упрощают создание динамического контента — ngTemplateOutlet и ngComponentOutlet. Первая директива позволяет вам создавать DOM-элементы на основе готовых шаблонов, а вторая директива используется при создании полноценных компонентов. Давайте рассмотрим два примера как это происходит. Начнем с директивы ngTemplateOutlet:
@Component({
selector: 'some-component',
template: `
<ng-container *ngTemplateOutlet="greet"></ng-container>
<ng-container *ngTemplateOutlet="eng; context: myContect"></ng-container>
<ng-container *ngTemplateOutlet="svk; context: myContect"></ng-container>
<hr>
<ng-template #greet><span>Hello</span></ng-template>
<ng-template #eng let-name><span>Hello {{name}}!</span></ng-template>
<ng-template #svk let-person="localSk"><span>Ahoj {{person}}!</span></ng-template>
`
})
class NgTemplateOutletExample {
myContext = {$implicit: 'World', localSk: 'Svet'}
}
В данном примере будет вставлено три шаблона, в которые подставится значения, взятые из контекста. Ключевое слово $implicit определяет дефолтное значение переменной в случае отсутствия ее в контексте. По сути, данная директива под капотом просто делает вызов createEmbeddedView, используя переданный ей шаблон. Теперь рассмотрим директиву ngComponentOutlet.
@Component({
selector: 'some-component',
template: `
Hello World!
`
})
class HelloWorldComponent {}
@Component({
selector: 'component-outlet-example',
template: `
<ng-container *ngComponentOutlet="HelloWorld"></ng-container>
`
})
class ComponentOutletExample {
HelloWorld = HelloWorldComponent
}
Данная директива под капотом работает аналогично рассмотренным нами вариантам создания компонентов и просто вызывает createComponent-метод, используя фабрику компонента.
Итак, давайте подведем итоги. В новом ангуляре можно создавать динамический контент в различных вариантах: от простого создания DOM-элементов до формирования контента на основе готовых компонентов. Да кто-то скажет, что для этого надо написать довольно много кода, но тут в защиту могу сказать, что такой код пишется один раз и дальше просто используется. Как показывает мой опыт, другие фреймворки тоже не всегда дают простой способ создания, например, модальных окон, так что решение этой задачи в ангуляре для меня не было шокирующим, хотя и потребовало много времени.
Автор: KyKyPy3uK