Por fin ha llegado la parte 3 de creando mi propio IDE donde empezaremos a conectar todo el Front a la parte Back que hicimos en nuestro tutorial anterior, pero si te lo perdiste, te dejo el enlace:
En esta parte trataremos de conectar algunos de nuestros ends, quizá haya cambios que en las publicaciones anteriores se me haya escapado así que … no me odies, hago esto «al vuelo» 😉
Lo primero es generar un servicio que podamos reusar para hacer llamadas a nuestro «API» que en el tutorial anterior creamos, lo nombraremos http.service.ts
y estará en el mismo nivel que app.component.ts
en realidad no importa ni su nombre ni ubicación tanto pero ahí lo pondremos:
import {Injectable} from '@angular/core';
import {HttpHeaders, HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {Router} from '@angular/router';
const API_URL = "http://localhost:3000";
@Injectable({
providedIn: 'root'
})
export class ApiHttpService {
public headers: any;
public apiUrl = API_URL;
constructor(
private http: HttpClient,
public router: Router
) {
this.headers = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
Accept: 'application/json'
})
};
}
public get(url: string): Observable<any> {
return this.http.get(
this.apiUrl + url,
this.headers
);
}
public post(url: string, body: any): Observable<any> {
return this.http.post(
this.apiUrl + url,
JSON.stringify(body),
this.headers
);
}
public put(url: string, body: any): Observable<any> {
return this.http.put(
this.apiUrl + url,
JSON.stringify(body),
this.headers
);
}
public delete(url: string): Observable<any> {
return this.http.delete(
this.apiUrl + url,
this.headers
);
}
public patch(url: string, body: any) {
return this.http.patch(
this.apiUrl + url,
JSON.stringify(body),
this.headers
);
}
}
Notemos que tenemos 1 función por cada método, y que en la parte superior agregamos http://localhost:3000
como la URL ya que nos estaríamos conectando al API en su base… cuando tenemos esto, podemos avanzar.
Ahora, para continuar lo primero que haremos será generar la conexión para obtener un directorio raíz apenas abramos nuestro IDE. Recordando, ya tenemos la base del usuario como directorio principal, así que haremos el pull a «/» y simplemente obtendremos lo que haya.
Hasta aquí antes de implementar esto haré un paréntesis, en el app.module
deben cargar el HttpClientModule
más o menos así:
// En la parte superior el import:
import {HttpClientModule} from "@angular/common/http";
// Y en los imports:
imports: [
...
HttpClientModule
...
]
Ahora sí necesitamos cargar nuestro pedacito de código dentro del constructor
de app.component
y como import para nuestros «proveedores» de la siguiente forma:
// "Hasta arriba" (o cerquita de arriba en los imports :P):
import {ApiHttpService} from "./http.service";
// En el @Component:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [ApiHttpService] //<--agregamos esta línea
})
...
// Y en el constructor
constructor(private apiService: ApiHttpService) {
...
}
Abramos otro paréntesis, si hasta aquí ejecutásemos la petición veríamos en consola que nos marca un problemirijillo de CORS, así que hay de 2, hacemos un PROXY para Angular o … abrimos CORS en NodeJS,en este caso haremos lo segundo PERO, te dejo un tutorial si quieres hacerlo desde Angular…
Así que vamos a nuestro proyecto de backend para el IDE e instalamos la siguiente dependencia https://www.npmjs.com/package/cors:
npm install cors
En nuestro archivo index.js
lo implementamos:
// Hasta arriba debajo de los require anteriores hacemos el import:
const cors = require('cors');
// Y abajo del app.use(bodyParser...) hacemos el uso:
app.use(cors());
Y eso es todo. Ahora sí podemos iniciar nuestro proyecto y veremos que funciona si hacemos el GET, en nuestro constructor de app.component
hacemos la prueba:
constructor(private apiService: ApiHttpService) {
this.apiService.get('/').subscribe((response) => {
console.log(response);
});
...
}
Como notita, recuerda, los ...
significa que hay algo por ahí, no hagas el copiar/pegar así de agresivo 😛 para que no te salgan errores.
Así que entonces veríamos algo así en la consola:

Ignora que tapé mis carpetas con cosas de «adultos» 😉😉(no es cierto son puras cosas de programación)… pero podemos ver que está el nombre y saber si es directorio… o no.
Para integrarlo lo haremos modificando casi en su totalidad el constructor de la siguiente manera:
constructor(private apiService: ApiHttpService) {
this.items = [];
this.apiService.get('/').subscribe((response) => {
const internalItems: any[] = [];
for(const item of response.datos) {
internalItems.push({
text: item.nombre,
value: item.nombre,
collapsed: true,
disabled: false,
})
}
this.items = [new TreeviewItem({
text: 'ide', value: 1, collapsed: true, disabled: false, children: internalItems
})];
});
}
Si hacemos click donde dice «IDE», veremos que se despliega nuestro listado:
Ahora, debido a que Ngx-TreeView tiene una forma peculiar de hacer un select de objetos, lamentable selección a veces de elementos, pero así es esto. Por concluyente, vamos a copiar algo de su código pero a nuestra manera y debido a esto concluiremos este tutorial y nos vamos hasta unos «infinitos» tutoriales (lo siento joven lector, la idea es hacerlos no tan larguísimos y enfadosos):
En nuestro app.component
vamos a agregar la siguiente lógica:
// Una variable llamada "value"
value = '';
// Vamos a borrar la variable config
// La siguiente función:
onValueChange(value: number): void {
console.log(value);
}
Y eso es todo :D, ha no te creas, bueno sí pero solo en ese archivo, lo siguiente es crear un montón de ficheros, te paso los nombres:
dropdown-treeview-select.component.html
dropdown-treeview-select.component.scss
dropdown-treeview-select.component.ts
dropdown-treeview-select-i18n.ts
Y ahora los contenidos, una vez que terminemos esta parte, te mostraré cómo luce y vamos a continuar en la siguiente parte del tutorial porque ya es demasiado por este y no quiero abrumarte…
Dentro del HTML agregamos:
<ng-template #itemTemplate let-item="item" let-onCollapseExpand="onCollapseExpand"
let-onCheckedChange="onCheckedChange">
<div class="text-nowrap row-item">
<i *ngIf="item.children" (click)="onCollapseExpand()" aria-hidden="true" [ngSwitch]="item.collapsed">
<svg *ngSwitchCase="true" width="0.8rem" height="0.8rem" viewBox="0 0 16 16" class="bi bi-caret-right-fill"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.14 8.753l-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z" />
</svg>
<svg *ngSwitchCase="false" width="0.8rem" height="0.8rem" viewBox="0 0 16 16" class="bi bi-caret-down-fill"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.247 11.14L2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z" />
</svg>
</i>
<label class="form-check-label" (click)="select(item)">{{ item.text }}</label>
</div>
</ng-template>
<ng-template #headerTemplate let-config="config" let-item="item" let-onCollapseExpand="onCollapseExpand"
let-onCheckedChange="onCheckedChange" let-onFilterTextChange="onFilterTextChange">
<div *ngIf="config.hasFilter" class="row row-filter">
<div class="col-12">
<input class="form-control" type="text" [placeholder]="i18n.getFilterPlaceholder()" [(ngModel)]="filterText"
(ngModelChange)="onFilterTextChange($event)" />
</div>
</div>
<div *ngIf="config.hasAllCheckBox || config.hasCollapseExpand" class="row">
<div class="col-12">
<label *ngIf="config.hasAllCheckBox" (click)="select(item)">
<strong>{{ i18n.getAllCheckboxText() }}</strong>
</label>
<label *ngIf="config.hasCollapseExpand" class="float-right" (click)="onCollapseExpand()">
<i [title]="i18n.getTooltipCollapseExpandText(item.collapsed)" aria-hidden="true" [ngSwitch]="item.collapsed">
<svg *ngSwitchCase="true" width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-arrows-angle-expand"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M1.5 10.036a.5.5 0 0 1 .5.5v3.5h3.5a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5v-4a.5.5 0 0 1 .5-.5z" />
<path fill-rule="evenodd"
d="M6.354 9.646a.5.5 0 0 1 0 .708l-4.5 4.5a.5.5 0 0 1-.708-.708l4.5-4.5a.5.5 0 0 1 .708 0zm8.5-8.5a.5.5 0 0 1 0 .708l-4.5 4.5a.5.5 0 0 1-.708-.708l4.5-4.5a.5.5 0 0 1 .708 0z" />
<path fill-rule="evenodd"
d="M10.036 1.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v4a.5.5 0 1 1-1 0V2h-3.5a.5.5 0 0 1-.5-.5z" />
</svg>
<svg *ngSwitchCase="false" width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-arrows-angle-contract"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9.5 2.036a.5.5 0 0 1 .5.5v3.5h3.5a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5v-4a.5.5 0 0 1 .5-.5z" />
<path fill-rule="evenodd"
d="M14.354 1.646a.5.5 0 0 1 0 .708l-4.5 4.5a.5.5 0 1 1-.708-.708l4.5-4.5a.5.5 0 0 1 .708 0zm-7.5 7.5a.5.5 0 0 1 0 .708l-4.5 4.5a.5.5 0 0 1-.708-.708l4.5-4.5a.5.5 0 0 1 .708 0z" />
<path fill-rule="evenodd"
d="M2.036 9.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0V10h-3.5a.5.5 0 0 1-.5-.5z" />
</svg>
</i>
</label>
</div>
</div>
<div *ngIf="config.hasDivider" class="dropdown-divider"></div>
</ng-template>
<ngx-dropdown-treeview [config]="config" [headerTemplate]="headerTemplate" [items]="items"
[itemTemplate]="itemTemplate">
</ngx-dropdown-treeview>
Dentro del archivo TS (que no es el que termina en i18n):
import { Component, Input, Output, EventEmitter, ViewChild, OnChanges } from '@angular/core';
import { TreeviewI18n, TreeviewItem, TreeviewConfig, DropdownTreeviewComponent, TreeviewHelper } from 'ngx-treeview';
import { DropdownTreeviewSelectI18n } from './dropdown-treeview-select-i18n';
@Component({
selector: 'ngx-dropdown-treeview-select',
templateUrl: './dropdown-treeview-select.component.html',
styleUrls: [
'./dropdown-treeview-select.component.scss'
],
providers: [
{ provide: TreeviewI18n, useClass: DropdownTreeviewSelectI18n }
]
})
export class DropdownTreeviewSelectComponent implements OnChanges {
@Input() config: TreeviewConfig;
@Input() items: TreeviewItem[] = [];
@Input() value: any;
@Output() valueChange = new EventEmitter<any>();
@ViewChild(DropdownTreeviewComponent, { static: false }) dropdownTreeviewComponent: DropdownTreeviewComponent | undefined;
filterText = '';
private dropdownTreeviewSelectI18n: DropdownTreeviewSelectI18n;
constructor(
public i18n: TreeviewI18n
) {
this.config = TreeviewConfig.create({
hasAllCheckBox: true,
hasFilter: true,
hasCollapseExpand: true,
decoupleChildFromParent: false,
maxHeight: 500,
});
this.dropdownTreeviewSelectI18n = i18n as DropdownTreeviewSelectI18n;
}
ngOnChanges(): void {
this.updateSelectedItem();
}
select(item: TreeviewItem): void {
if (!item.children) {
this.selectItem(item);
}
}
private updateSelectedItem(): void {
if (this.items) {
const selectedItem = TreeviewHelper.findItemInList(this.items, this.value);
this.selectItem(selectedItem);
}
}
private selectItem(item: TreeviewItem): void {
if (this.dropdownTreeviewSelectI18n.selectedItem !== item) {
this.dropdownTreeviewSelectI18n.selectedItem = item;
if (this.dropdownTreeviewComponent) {
this.dropdownTreeviewComponent.onSelectedChange([item]);
}
if (item) {
if (this.value !== item.value) {
this.value = item.value;
this.valueChange.emit(item.value);
}
}
}
}
}
Ahora sí en el archivo TS que es para i18n:
import { Injectable } from '@angular/core';
import { TreeviewItem, TreeviewSelection, DefaultTreeviewI18n } from 'ngx-treeview';
@Injectable()
export class DropdownTreeviewSelectI18n extends DefaultTreeviewI18n {
private internalSelectedItem: TreeviewItem | undefined;
set selectedItem(value: TreeviewItem) {
this.internalSelectedItem = value;
}
get selectedItem(): TreeviewItem {
return <TreeviewItem>this.internalSelectedItem;
}
override getText(selection: TreeviewSelection): string {
return this.internalSelectedItem ? this.internalSelectedItem.text : 'Please select';
}
}
Por último en el SCSS:
label {
margin-bottom: 0;
cursor: pointer;
}
.bi {
cursor: pointer;
margin-right: 0.3rem;
}
Y ya para finalizar, importamos el Componente en el app.module
:
// Como de costumbre el import arriba:
import {DropdownTreeviewSelectComponent} from "./dropdown-treeview-select.component";
// Y en las declarations:
declarations: [
AppComponent,
DropdownTreeviewSelectComponent
],
¡Listo!, si hicimos todo bien, deberíamos ver lo siguiente:

Y dirás, ¿aquí qué?, lo veo casi igual pues no, si cliqueamos el «ide» entonces veríamos la lista y podemos seleccionar cosas, por ejemplo:

Con esto, ya podemos ir empezando a abrir archivos, pero como dije, no quiero abrumarte, así que nos vemos en la cuarta entrega…