Skip to main content

Directivas compartidas

src/app/shared/directives/ tiene 13 directivas (más LowercasePipe, que en realidad es una directiva y vive en shared/pipes, ver Pipes). Resumen general del módulo en Compartido; permisos en Auth y RBAC y Guards.

:::caution Solo dos están declaradas SharedModule declara y exporta únicamente ShowWithPermissionDirective y ShowForUserTypesDirective. Las otras once directivas (más LowercasePipe) no están declaradas en ningún NgModule ni son standalone, y ningún template las usa: son código muerto hasta que alguien las declare. Se documentan por completitud y para evitar reimplementarlas. :::

Tabla de selectores​

ClaseSelector (se escribe así en la plantilla)ArchivoDeclaradaUsada en plantillas
ShowWithPermissionDirective*appShowWithPermissionshow-with-permission.directive.tssi80 usos en 28 plantillas
ShowForUserTypesDirective*appHideForUserTypes (nombre inverso al de la clase)show-for-user-types.directive.tssi3 plantillas
ClickOutsideDirective[clickOutside]click-out-side.directive.tsnono
CustomBadgeDirective[badgeStyler]custom-badge.directive.tsnono
DecimalInputDirective[sharedDecimalInput]decimal-input.directive.tsnono
DisableKeyPressDirective[disableKeyPress]disable-key-press.directive.tsnono
MaxCharactersDirective[sharedMaxCharacters]max-characters.directive.tsnono
OnlyAlphabetsDirective[onlyAlphabets]only-alphabets.directive.tsnono
OnlyNumbersDirective[onlyNumbers]only-numbers.directive.tsnono
OnlyNumbersWhitTwoDecimalDirective[appOnlyNumbersWhitTwoDecimal]only-numbers-whit-two-decimal.directive.tsnono
RequiredAsteriskDirective[appRequiredAsterisk]required-asterisk.directive.tsnono
RestrictNegativeDirective[restrictNegative]restrict-negative.directive.tsnono
ValidationBorderDirective[border-validation]validation-border.directive.tsnono

Directivas estructurales de visibilidad​

*appShowWithPermission​

Muestra el contenido solo si el usuario tiene el permiso. Verifica contra MyPermissionsService (conjunto plano de acrónimos, caché de 10 s).

InputTipoEfecto
appShowWithPermissionstring o string[]Un acrónimo exige ese permiso (hasPermission); un array exige cualquiera (hasAnyPermission)
appShowWithPermissionElseTemplateRef o nullPlantilla alternativa cuando se deniega

Comportamiento:

  • Deny-by-default: la vista no se crea hasta que la comprobación resuelve. Valor vacío, null o array vacío se renderizan como denegado (se muestra solo el else, si existe).
  • En cada evaluación hace clear() y recrea la vista; no hay vistas duplicadas.
  • Las comprobaciones se completan tras una emisión: si el conjunto de permisos cambia después, una vista ya creada no se actualiza hasta que se reasigne el input.
  • No hay equivalente en ng-container sin permiso: las plantillas usan <ng-container *appShowWithPermission="...">.

Ejemplos reales:

<!-- contacts-table.component.html: un acrónimo -->
<ng-container *appShowWithPermission="'CONTACT_CREATE'">
<button class="btn btn-outline-secondary" type="button" (click)="openModal(BUTTON_ACTIONS.ADD)">
...
</ng-container>

<!-- labels-panel.component.html: cualquiera de dos -->
<ng-container *appShowWithPermission="['CONTACT_UPDATE', 'LABEL_UPDATE']">
<button class="lp__btn lp__btn--primary" type="button" (click)="save()">
...
</ng-container>

Con alternativa (soportado y cubierto por spec, pero no usado en ninguna plantilla):

<button *appShowWithPermission="'CONTACT_DELETE'; else sinPermiso">Borrar</button>
<ng-template #sinPermiso><span>Sin permiso</span></ng-template>

Dónde se usa (usos por área): whaticket 25, connections 14, access-control 13, internal-chat 5, contacts 4, settings 4, departments 3, labels 3, users 3, sidebar 3, quick-responses 2, locked-tickets 1. Solo labels-panel.component.html usa la forma de array. El sidebar (shared/components/sidebar/sidebar.component.html) la aplica a menuItem.permission, childrenItem.permission y childrenSubItem.permission (Menu.permission admite string o array, ver core/interfaces/ui/nav.interface.ts) solo cuando el ítem declara permission; sin permiso declarado no hay compuerta. La lista de acrónimos por pantalla está en Auth y RBAC.

Specs: show-with-permission.directive.spec.ts (5 casos: permitido, denegado, oculto mientras la comprobación está pendiente, else, array con cualquiera) y sidebar.component.permission-gating.spec.ts (usa la directiva real para filtrar el menú).

La directiva oculta UI pero no protege datos ni endpoints: el backend responde 403 (ver Interceptores).

*appHideForUserTypes​

Clase ShowForUserTypesDirective. Oculta el contenido a ciertos userType; admin siempre lo ve.

InputValorLista de denegados
appHideForUserTypessin valor, undefined, null o cadena vacíaDEFAULT_ROLES_TO_HIDE_LIST: agent, collaboratorAgent, collaboratorOperator
cadena no vacíasolo ese tipo
arrayexactamente esos tipos (un array vacío no oculta a nadie)

Comportamiento:

  • Se suscribe a GlobalService.profile, normaliza userType (trim y minúsculas) y compara contra la lista en minúsculas.
  • Si el tipo es admin siempre muestra, aunque estuviera en la lista.
  • Crea o destruye la vista de forma idempotente (ensureShown y ensureHidden); reacciona si el perfil cambia.
  • A diferencia de *appShowWithPermission, muestra por defecto: antes de que el perfil emita, currentUserType es '' (no está en la lista) y el contenido se renderiza. En ese intervalo un agente podría ver un instante el contenido restringido.
<!-- new-chat-modal.component.html: crear grupo solo para tipos no restringidos -->
<button *appHideForUserTypes class="btn btn-link" type="button" (click)="openCreateGroup()">

<!-- sidebar.component.html: ítem de menú -->
<ng-container *appHideForUserTypes="menuItem.hideForUserTypes ?? []">

Dónde se usa: internal-chat/components/conversation-list (botón de la cabecera), internal-chat/components/new-chat-modal (crear grupo) y el sidebar.

Gotcha del sidebar: pasa menuItem.hideForUserTypes ?? []. Como [] significa "no ocultar a nadie", nunca se aplica la lista por defecto en el menú. Además ningún ítem de nav.constant.ts define hideForUserTypes, así que el filtro por tipo en el menú está inactivo; el filtrado real es por permission.

Specs: show-for-user-types.directive.spec.ts (6 casos: oculta a denegados, muestra a no denegados, admin siempre visible, lista vacía, lista por defecto, reactividad al cambiar el perfil).

Restricción de entrada de teclado​

Todas se aplican sobre un input. Ninguna está declarada hoy.

[onlyNumbers] (OnlyNumbersDirective)​

  • keypress: devuelve true solo para keyCode 8 y 48-57 (dígitos); cualquier otra tecla (incluido Enter) se cancela.
  • paste: cancela el pegado si el texto no cumple ^\d+$.
  • Fija autocomplete="off" con HostBinding.
  • Usa keyCode, API deprecada.

[onlyAlphabets] (OnlyAlphabetsDirective)​

  • keypress: acepta keyCode 8, 65-90 y 97-122 (letras ASCII). Bloquea espacios, acentos y ñ, lo que la hace poco apta para nombres en español.
  • No controla el pegado. Fija autocomplete="off".

[sharedDecimalInput] (DecimalInputDirective)​

Input sharedDecimalInput: { intDigs: number; decDigs: number }, por defecto { intDigs: 2, decDigs: 2 }. Ejemplo: sharedDecimalInput con enteros 6 y decimales 2.

  • keydown: solo deja pasar dígitos, Backspace, Delete, flechas, Tab, un único . y un - si el valor no empieza ya por -. Todo lo demás se cancela, incluidos Ctrl+C, Ctrl+V, Ctrl+A y Enter.
  • input y paste (diferido con setTimeout): valida con la regex ^-?\d{0,intDigs}(\.\d{0,decDigs})?$; si falla, recorta el último carácter y redispara input.

[appOnlyNumbersWhitTwoDecimal] (OnlyNumbersWhitTwoDecimalDirective)​

  • Requiere NgControl en el mismo elemento (formControlName, formControl o ngModel), porque lee ngControl.control.value.
  • keydown: ignora teclas especiales (Backspace, Tab, End, Home, flechas, Delete). Calcula valor actual + tecla y bloquea si no cumple ^(-)?\d+(\.\d{0,2})?$. Un - se admite solo en campo vacío. No tiene en cuenta la posición del cursor.
  • Un . como primer carácter se rechaza (debe haber un dígito antes). Ctrl+V por teclado también queda bloqueado; el pegado con menú contextual pasa por el handler paste, que aplica la misma regex.
  • current.concat(...) supone que el valor es string: con un control numérico lanzaría TypeError.

[restrictNegative] (RestrictNegativeDirective)​

  • keydown: bloquea -, e y E.
  • input: si el valor empieza por -, es menor que 0 o no es numérico, vacía el campo y redispara input. Pensada para type="number".

[sharedMaxCharacters] (MaxCharactersDirective)​

Input sharedMaxCharacters: number (por defecto 0).

  • keydown: con la longitud en el límite, cancela teclas imprimibles (key.length === 1 sin Ctrl, Meta ni Alt); Backspace, flechas y atajos siguen funcionando.
  • input y paste (diferido): recorta con slice(0, max).
  • Con el valor por defecto 0 bloquea toda escritura: siempre hay que pasar el número. El recorte asigna input.value sin redisparar input, por lo que el modelo del formulario puede conservar la versión larga hasta el siguiente evento.

[disableKeyPress] (DisableKeyPressDirective)​

Cancela todo keypress y fija autocomplete="off". Convierte el input en no editable por teclado (no por pegado ni por arrastre).

Utilidades de UI y formularios​

(clickOutside) (ClickOutsideDirective)​

Output clickOutside: EventEmitter<any>. Escucha document:click y emite (sin payload) cuando el event.target no está contenido en el elemento anfitrión. Cada instancia registra su propio listener global.

<div (clickOutside)="cerrarMenu()"> ... </div>

Gotcha: si el panel se abre con un clic que ocurre fuera del anfitrión, ese mismo clic puede emitir clickOutside al propagarse hasta document.

[badgeStyler] (CustomBadgeDirective)​

Inputs obligatorios (required: true): badgeValue: string, badgeEnum: { [key: string]: string } y badgeStyles: Record<string, BadgeStyleConfig> (BadgeStyleConfig extiende TranslationsPerLanguage y añade color). En ngOnChanges busca la clave del enum cuyo valor coincide con badgeValue, toma badgeStyles[valor], aplica color y un backgroundColor rgba con opacidad 0.1 (convierte #abc y #aabbcc). Si no hay coincidencia o el color no es hexadecimal válido, no pinta (con hex inválido el fondo queda en cadena vacía).

<span badgeStyler [badgeValue]="row.status" [badgeEnum]="STATUS_ENUM" [badgeStyles]="STATUS_STYLES"></span>

[appRequiredAsterisk] (RequiredAsteriskDirective)​

Pensada para el contenedor de un campo (div con label y control). Requiere un ControlContainer padre (formGroup).

  1. Busca dentro del anfitrión el primer input, select, textarea, ng-select y lee su atributo formControlName para obtener el AbstractControl.
  2. Añade al label un span.required-asterisk con *.
  3. Añade un div.required-error-message (oculto) con words.thisFieldIsRequired (traducido una vez con instant).
  4. Al blur del control marca touched y revalida; con cada statusChanges muestra el mensaje si invalid && touched.

Los estilos .required-asterisk y .required-error-message están en assets/scss/components/_form-input.scss.

Gotchas: el asterisco aparece siempre que se encuentre un control, aunque no tenga Validators.required; el mensaje se muestra para cualquier error, no solo required; usa formControlName literal (no sirve con [formControl]); la suscripción a statusChanges no se cancela.

[border-validation] (ValidationBorderDirective)​

Selector con guion, sin camelCase. Requiere NgControl en el mismo elemento. Se suscribe a statusChanges y valueChanges del control (con ngModel, espera un tick con setTimeout a que exista control) y delega en ValidationService (core/services/ui/validation.service.ts):

  • Borde y sombra rojos (#dc3545) si isInvalid && isTouched (dirty || touched).
  • Borde y sombra verdes (#28a745) si es válido y no está vacío.
  • Si no, limpia border y box-shadow.

ValidationService.getValidationState considera inválido, además de control.invalid, un campo con atributo HTML required vacío o un valor menor al atributo min. "Vacío" incluye valores falsos o <= 0, por lo que un 0 legítimo se trata como vacío.

Resumen de uso real​

Solo las dos directivas estructurales están en producción. Para activar cualquiera de las demás habría que declararla (y exportarla) en SharedModule, o convertirla a standalone.