Autor: GitHub Copilot
Fecha: 27 de octubre de 2025
Proyecto: devseniorInventory - Sistema de Inventarios
Esta guía documenta paso a paso cómo implementar un sistema de autenticación completo en Angular con componentes standalone, formularios reactivos y rutas protegidas.
- Vista Previa del Login
- Arquitectura del Proyecto
- Estructura del Proyecto
- Configuración Inicial
- Componentes de Autenticación
- Rutas y Navegación
- Estilos y Diseño
- Flujo de Autenticación
- Comandos y Ejecución
- Próximos Pasos
Así es como se verá tu pantalla de login una vez implementado:
Nota: Para agregar esta imagen al proyecto, toma una captura de pantalla de tu login en http://localhost:4200/login y guárdala como
docs/login-preview.png
✨ Diseño Moderno:
- Fondo oscuro con patrón topográfico sutil
- Logo de calabaza (pumpkin) centrado
- Formulario con estilo minimalista
🎨 Paleta de Colores:
- Primario:
#ff6a00(Naranja) - Oscuro:
#e65a00(Naranja más oscuro) - Fondo:
#111111/#151515(Negro/Gris oscuro) - Texto:
#ffffff/#dfdfdf(Blanco/Gris claro)
🎯 Elementos Clave:
- Logo: Calabaza decorativa (120×120px)
- Título: "Software de Inventarios"
- Subtítulo: "Ingresa tus credenciales para acceder"
- Campos:
- Username (con placeholder "LuneskaDev")
- Password (con asteriscos enmascarados)
- Botón: "Ingresar" (color naranja, deshabilitado si el form es inválido)
- Link: "¿No tienes una cuenta? Regístrate aquí" (en naranja)
📱 Responsive:
- Se adapta a pantallas móviles (max-width: 420px)
- El formulario mantiene su legibilidad en todos los tamaños
Esta implementación sigue las mejores prácticas de Angular moderno:
Nota: Guarda la imagen de referencia de la arquitectura en
docs/angular-architecture.pngpara visualizar la estructura completa del proyecto.
📦 ANGULAR 20 PROJECT STRUCTURE
├── Clean, Scalable & Future-Ready
│
├── 📁 my-angular20-app/
│ ├── 📁 src/
│ │ ├── 📁 app/
│ │ │ ├── 📁 core/ ← Singleton services, interceptors, guards
│ │ │ │ ├── 📁 shared/ ← Reusable components, directives, pipes
│ │ │ │ ├── 📁 features/ ← Feature modules
│ │ │ │ │ ├── 📁 auth/ ← Módulo de autenticación
│ │ │ │ │ │ ├── 📁 login/
│ │ │ │ │ │ │ ├── login-auth.component.ts
│ │ │ │ │ │ │ ├── login-auth.component.html
│ │ │ │ │ │ │ ├── login-auth.component.css
│ │ │ │ │ │ │ └── 📁 assets/
│ │ │ │ │ │ │ └── 📁 images/
│ │ │ │ │ │ ├── 📁 register/
│ │ │ │ │ │ │ ├── register-auth.component.ts
│ │ │ │ │ │ │ ├── register-auth.component.html
│ │ │ │ │ │ │ └── register-auth.component.css
│ │ │ │ │ │ └── 📁 services/ ← (Futuro) AuthService
│ │ │ │ │ └── 📁 dashboard/ ← (Futuro) Área autenticada
│ │ │ │ └── 📁 layouts/ ← Feature modules (dashboard, auth, etc.)
│ │ │ │ ├── 📁 auth-layout/
│ │ │ │ │ ├── auth-layout.component.ts
│ │ │ │ │ ├── auth-layout.component.html
│ │ │ │ │ └── auth-layout.component.css
│ │ │ │ └── 📁 main-layout/
│ │ │ │ ├── main-layout.component.ts
│ │ │ │ ├── main-layout.component.html
│ │ │ │ └── main-layout.component.css
│ │ │ ├── 📁 state/ ← State management (opcional)
│ │ │ ├── 📄 app.config.ts ← Angular 20 application config
│ │ │ ├── 📄 app.routes.ts ← Route definitions
│ │ │ └── 📄 app.component.ts
│ │ ├── 📁 assets/ ← Images, fonts, JSON, icons
│ │ │ └── 📁 images/
│ │ │ └── 3dicons-pumpkin-dynamic-color.png
│ │ ├── 📁 environments/ ← Environment configs (dev, prod, staging)
│ │ ├── 📄 main.ts ← Entry point with bootstrapApplication()
│ │ └── 📄 index.html
│ ├── 📄 angular.json
│ ├── 📄 package.json
│ └── 📄 tsconfig.json
core/
├── features/ → Funcionalidades específicas (auth, dashboard, etc.)
├── layouts/ → Layouts reutilizables (shell components)
├── shared/ → Componentes, pipes, directives compartidos
└── state/ → Gestión de estado global (opcional)
Beneficios:
- Separación clara de responsabilidades
- Facilita el escalado del proyecto
- Código más mantenible
features/auth/
├── login/ → Componente de login
├── register/ → Componente de registro
└── services/ → AuthService (próximo paso)
Beneficios:
- Todo lo relacionado con autenticación en un solo lugar
- Facilita el lazy loading
- Mejor colaboración en equipos
@Component({
standalone: true, // ← No necesita NgModule
imports: [ReactiveFormsModule] // ← Imports directos
})Beneficios:
- Menos boilerplate
- Mejor tree-shaking (bundle más pequeño)
- Componentes más portables
AuthLayoutComponent
└── <router-outlet> ← Renderiza Login o Register
MainLayoutComponent
└── <router-outlet> ← Renderiza Dashboard, Home, etc.
Beneficios:
- Reutilización de estructura visual
- Headers/footers/sidebars compartidos
- Fácil cambio de diseño por sección
Usuario visita app
↓
app.routes.ts
↓
┌─────────────────┐
│ AuthLayoutComponent │ (Path: '')
│ (Wrapper oscuro) │
└────────┬──────────┘
│
<router-outlet>
│
┌────┴────┐
│ │
┌───▼────┐ ┌─▼─────────┐
│ /login │ │ /register │
│ (Login)│ │ (Register)│
└───┬────┘ └─┬─────────┘
│ │
└────┬───┘
│ Autenticación exitosa
▼
┌─────────────────┐
│ MainLayoutComponent │ (Path: 'home')
│ (Área privada) │
└─────────────────┘
| Aspecto | ❌ Antes (Módulos) | ✅ Ahora (Standalone) |
|---|---|---|
| Configuración | NgModule + imports | Directo en component |
| Boilerplate | Alto | Mínimo |
| Bundle size | Más grande | Optimizado |
| Lazy loading | Complejo | Simplificado |
| Portabilidad | Baja | Alta |
devseniorInventory/
├── src/
│ ├── app/
│ │ ├── core/
│ │ │ ├── features/
│ │ │ │ └── auth/
│ │ │ │ ├── login/
│ │ │ │ │ ├── login-auth.component.ts
│ │ │ │ │ ├── login-auth.component.html
│ │ │ │ │ ├── login-auth.component.css
│ │ │ │ │ └── assets/
│ │ │ │ │ └── images/
│ │ │ │ │ └── 3dicons-pumpkin-dynamic-color.png
│ │ │ │ └── register/
│ │ │ │ ├── register-auth.component.ts
│ │ │ │ ├── register-auth.component.html
│ │ │ │ └── register-auth.component.css
│ │ │ └── layouts/
│ │ │ ├── auth-layout/
│ │ │ │ ├── auth-layout.component.ts
│ │ │ │ ├── auth-layout.component.html
│ │ │ │ └── auth-layout.component.css
│ │ │ └── main-layout/
│ │ │ ├── main-layout.component.ts
│ │ │ ├── main-layout.component.html
│ │ │ └── main-layout.component.css
│ │ ├── app.routes.ts
│ │ ├── app.config.ts
│ │ ├── app.ts
│ │ ├── app.html
│ │ └── app.css
│ ├── assets/
│ │ └── images/
│ │ └── 3dicons-pumpkin-dynamic-color.png
│ ├── index.html
│ ├── main.ts
│ └── styles.css
├── angular.json
├── package.json
├── tsconfig.json
└── README.md
Archivo: angular.json
¿Por qué?: Para que Angular copie las imágenes y archivos estáticos al build output.
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"devseniorInventory": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*",
"input": "src/app/core/features/auth/login/assets",
"output": "assets"
},
{
"glob": "**/*",
"input": "public",
"output": ""
}
],
"styles": ["src/styles.css"]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "devseniorInventory:build:production"
},
"development": {
"buildTarget": "devseniorInventory:build:development"
}
},
"defaultConfiguration": "development"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "tsconfig.spec.json",
"assets": [
"src/favicon.ico",
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*",
"input": "src/app/core/features/auth/login/assets",
"output": "assets"
},
{
"glob": "**/*",
"input": "public",
"output": ""
}
],
"styles": ["src/styles.css"]
}
}
}
}
}
}Puntos clave:
src/assets→ se copia adist/assetssrc/app/core/features/auth/login/assets→ también se copia adist/assets- Esto permite usar rutas como
assets/images/logo.pngen las plantillas
Archivo: src/app/app.routes.ts
¿Por qué?: Define las rutas de la aplicación y la navegación entre componentes.
import { Routes } from '@angular/router';
import { AuthLayoutComponent } from './core/layouts/auth-layout/auth-layout.component';
import { Login } from './core/features/auth/login/login-auth.component';
import { Register } from './core/features/auth/register/register-auth.component';
import { MainLayoutComponent } from './core/layouts/main-layout/main-layout.component';
export const routes: Routes = [
{
path: '',
component: AuthLayoutComponent,
children: [
{
path: '',
redirectTo: 'login',
pathMatch: 'full',
},
{
path: 'login',
component: Login,
},
{
path: 'register',
component: Register,
},
],
},
{
path: '',
component: MainLayoutComponent,
children: [{ path: 'home', component: MainLayoutComponent }],
},
];Explicación:
- Ruta raíz (
'') usaAuthLayoutComponentcomo layout - Redirige automáticamente a
/login - Rutas hijas:
/loginy/register /homeusaMainLayoutComponent(para área autenticada)
Archivo: src/app/app.config.ts
¿Por qué?: Configura los providers de Angular (router, formularios, etc.)
import {
ApplicationConfig,
provideBrowserGlobalErrorListeners,
provideZonelessChangeDetection,
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { importProvidersFrom } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { routes } from './app.routes';
import { provideAnimations } from '@angular/platform-browser/animations';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideRouter(routes),
// Habilita animaciones Angular
importProvidersFrom(ReactiveFormsModule), // Permite usar formularios reactivos
],
};Puntos clave:
provideRouter(routes): habilita el sistema de rutasReactiveFormsModule: necesario para formularios reactivosprovideZonelessChangeDetection(): mejora el rendimiento (Angular 18+)
Archivo: src/app/core/layouts/auth-layout/auth-layout.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-layout-auth',
standalone: true,
imports: [RouterOutlet],
templateUrl: './auth-layout.component.html',
styleUrls: ['./auth-layout.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AuthLayoutComponent {}Archivo: src/app/core/layouts/auth-layout/auth-layout.component.html
<div class="auth-layout">
<div class="auth-container">
<p>Welcome to the authentication page</p>
<router-outlet></router-outlet>
</div>
</div>Archivo: src/app/core/layouts/auth-layout/auth-layout.component.css
.auth-layout {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
}
.auth-container {
width: 100%;
max-width: 450px;
padding: 1rem;
}
.auth-container p {
text-align: center;
color: #dfdfdf;
margin-bottom: 1rem;
}Explicación:
RouterOutlet: marca donde se renderizan los componentes hijos (login/register)- Diseño centrado verticalmente y horizontalmente
- Fondo degradado oscuro
Archivo: src/app/core/features/auth/login/login-auth.component.ts
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormBuilder, Validators } from '@angular/forms';
import { RouterLink, Router } from '@angular/router';
@Component({
selector: 'app-login',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './login-auth.component.html',
styleUrls: ['./login-auth.component.css'],
})
export class Login {
loginForm: FormGroup;
constructor(private fb: FormBuilder, private router: Router) {
this.loginForm = this.fb.group({
username: ['', [Validators.required]],
password: ['', [Validators.required]],
});
}
onSubmit() {
if (this.loginForm.valid) {
const { username, password } = this.loginForm.value;
const userData = localStorage.getItem('user');
if (userData) {
const user = JSON.parse(userData);
// Allow login by username OR by email (user may have registered with email)
const input = (username || '').toString().trim();
const matchesUser =
(user.username && user.username === input) ||
(user.email && user.email.toLowerCase() === input.toLowerCase());
if (matchesUser && user.password === password) {
alert('Inicio de sesión exitoso 🎉');
this.router.navigate(['/home']);
} else {
alert('Credenciales incorrectas');
}
} else {
alert('No existe ningún usuario registrado.');
}
}
}
goToRegister() {
this.router.navigate(['/register']);
}
}Explicación del código:
-
Imports necesarios:
ReactiveFormsModule: para usar formularios reactivosFormGroup,FormBuilder,Validators: construcción y validación de formulariosRouter: navegación entre rutas
-
FormGroup:
- Define los campos del formulario:
usernameypassword - Ambos son requeridos (
Validators.required)
- Define los campos del formulario:
-
Lógica de login:
- Lee el usuario de
localStorage(guardado en el registro) - Compara el input con
user.usernameouser.email(case-insensitive para email) - Valida la contraseña
- Redirige a
/homesi es exitoso
- Lee el usuario de
Archivo: src/app/core/features/auth/login/login-auth.component.html
<div class="login-header">
<img
src="assets/images/3dicons-pumpkin-dynamic-color.png"
alt="Pumpkin logo"
width="120"
height="120"
loading="lazy"
/>
<h1>Software de Inventarios</h1>
<p>Ingresa tus credenciales para acceder</p>
</div>
<section class="login-auth-form">
<form class="login-auth" [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<label for="username">Username:</label>
<input type="text" id="username" formControlName="username" placeholder="LuneskaDev" />
<label for="password">Password:</label>
<input type="password" id="password" formControlName="password" placeholder="********" />
<button type="submit" [disabled]="loginForm.invalid">Ingresar</button>
</form>
<p>¿No tienes una cuenta? <a (click)="goToRegister()">Regístrate aquí</a></p>
</section>Puntos clave del HTML:
[formGroup]="loginForm": vincula el formulario con el FormGroup del componenteformControlName="username": vincula el input con el control del FormGroup(ngSubmit)="onSubmit()": ejecuta la función al enviar el formulario[disabled]="loginForm.invalid": deshabilita el botón si el formulario es inválido(click)="goToRegister()": navega al registro al hacer clic
Archivo: src/app/core/features/auth/login/login-auth.component.css
/* ========================== */
/* login-auth.component.css */
/* Estructura: base, layout, formulario, encabezado, utilidades */
/* Archivo de estilos para el componente de login. Comentarios en español. */
/* ========================== */
/* --------------------------
Base (ámbito del componente)
Usamos :host para que las fuentes y variables afecten sólo a este componente
y evitar que los estilos se propaguen fuera de su alcance.
-------------------------- */
:host {
display: block;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Open Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Brand color palette (oranges) - tweak these to tune the look */
--brand: #ff6a00; /* primary orange */
--brand-600: #e65a00; /* darker */
--brand-700: #cc4a00; /* darkest */
--bg-dark: #111111;
--panel-bg: #151515;
--muted: #dfdfdf;
}
:host *,
:host *::before,
:host *::after {
box-sizing: border-box;
}
/* --------------------------
Contenedor principal (layout)
Centra el formulario y controla la anchura máxima. Define el fondo
del panel, bordes y sombras para la tarjeta de autenticación.
-------------------------- */
.login-auth-form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
max-width: 360px; /* allows better display on larger screens */
border: 1px solid #1e1e1e;
padding: 1rem;
border-radius: 8px;
margin: 0.5rem auto; /* horizontally center */
background: var(--panel-bg);
box-shadow: 0 1px 4px rgba(16, 24, 40, 0.04);
}
.login-auth-form p {
font-weight: 500;
font-size: 0.9rem;
color: #ffffff;
text-align: center;
}
.login-auth-form p a {
color: var(--brand);
text-decoration: none;
}
.login-auth-form p a:hover {
text-decoration: underline;
}
/* --------------------------
Controles del formulario (inputs, selects, textarea)
Ocupan el 100% del ancho y tienen estados de foco accesibles
(borde + halo naranja para visibilidad). */
.login-auth-form input[type='text'],
.login-auth-form input[type='password'],
.login-auth-form input,
.login-auth-form textarea,
.login-auth-form select {
padding: 0.8rem;
margin-top: 5px;
font-size: 1rem;
width: 100%;
border: 1px solid #303030;
border-radius: 4px;
background: #242424;
transition: border-color 120ms ease-in-out, box-shadow 120ms ease-in-out;
}
.login-auth-form input:focus,
.login-auth-form textarea:focus,
.login-auth-form select:focus {
outline: none;
border-color: var(--brand-700);
/* using rgb of --brand (255,106,0) for subtle glow */
box-shadow: 0 0 0 3px rgba(255, 106, 0, 0.12);
}
/* --------------------------
Botón principal: estilos, hover y foco accesible
Usa la paleta de naranja definida en las variables CSS.
-------------------------- */
.login-auth-form button {
padding: 0.6rem 0.75rem;
font-size: 1rem;
background-color: var(--brand);
color: #ffffff;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 120ms ease, transform 60ms ease;
margin: 0 auto;
display: block;
}
.login-auth-form button:hover {
background-color: var(--brand-600);
}
.login-auth-form button:active {
transform: translateY(1px);
}
.login-auth-form button:focus {
outline: 3px solid rgba(255, 106, 0, 0.18);
outline-offset: 2px;
}
/* --------------------------
Encabezado (logo y títulos)
Contiene el logo de la aplicación y los textos centrados.
-------------------------- */
.login-header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 0.75rem;
text-align: center;
}
.login-header img {
width: 100px;
height: 100px;
margin: 1.25rem 0 0.5rem 0;
object-fit: contain;
display: block;
}
.login-header h1 {
font-size: 1.5rem;
margin: 0.125rem 0 0 0;
color: #fff;
}
.login-header p {
margin: 0.4rem 0 0 0;
color: var(--muted);
font-size: 0.95rem;
}
.login-auth label {
font-weight: 600;
margin-bottom: 0.25rem;
display: block;
color: #fff;
}
/* --------------------------
Utilidades y ajustes responsivos
Reglas para pantallas pequeñas y pequeños helpers de estilo.
-------------------------- */
@media (max-width: 420px) {
.login-auth-form {
padding: 0.75rem;
margin: 1rem;
}
.login-header img {
width: 80px;
height: 80px;
margin-top: 1rem;
}
}
/* Helper: estilos para enlaces dentro del formulario (coherencia de color) */
.login-auth-form a {
color: var(--brand);
text-decoration: none;
}
.login-auth-form a:hover {
text-decoration: underline;
}
/* Fin del archivo */Explicación de los estilos:
- Variables CSS (
--brand,--brand-600, etc.): paleta de colores reutilizable - Layout flexbox: centra el formulario
- Estados de foco: mejora accesibilidad para usuarios de teclado
- Responsive: ajustes para pantallas pequeñas
- Tema oscuro: fondo oscuro con contraste para mejor lectura
Archivo: src/app/core/features/auth/register/register-auth.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-register',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './register-auth.component.html',
styleUrls: ['./register-auth.component.css'],
})
export class Register {
registerForm: FormGroup;
constructor(private fb: FormBuilder, private router: Router) {
this.registerForm = this.fb.group({
fullName: ['', [Validators.required]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required]],
confirmPassword: ['', [Validators.required]],
});
}
onSubmit() {
if (this.registerForm.valid) {
const { fullName, email, password, confirmPassword } = this.registerForm.value;
// Validar que las contraseñas coincidan
if (password !== confirmPassword) {
alert('Las contraseñas no coinciden');
return;
}
// Guardar solo los campos necesarios (sin confirmPassword)
const userToStore = {
fullName,
email,
password,
};
localStorage.setItem('user', JSON.stringify(userToStore));
alert('Registro exitoso 🎉');
this.router.navigate(['/login']);
}
}
goToLogin() {
this.router.navigate(['/login']);
}
}Explicación del código:
-
Campos del formulario:
fullName: nombre completo (requerido)email: correo electrónico (requerido + validación de email)password: contraseña (requerido)confirmPassword: confirmación de contraseña (requerido)
-
Validación de contraseñas:
- Compara
passwordconconfirmPassword - Si no coinciden, muestra alerta y detiene el proceso
- Compara
-
Almacenamiento:
- Crea objeto
userToStoresolo con los campos necesarios - NO guarda
confirmPassword(no es necesario almacenarlo) - Guarda en
localStoragecon la clave'user'
- Crea objeto
-
Navegación:
- Redirige a
/logindespués del registro exitoso
- Redirige a
Archivo: src/app/core/features/auth/register/register-auth.component.html
<div class="register-header">
<img
src="assets/images/3dicons-pumpkin-dynamic-color.png"
alt="Pumpkin logo"
width="120"
height="120"
loading="lazy"
/>
<h1>Software de Inventarios</h1>
<p>Regístrate para crear una cuenta</p>
</div>
<section class="register-auth-form">
<form class="register-auth" [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<label for="fullName">Nombre completo:</label>
<input type="text" id="fullName" formControlName="fullName" placeholder="Luneska Dev" />
<label for="email">Email:</label>
<input type="email" id="email" formControlName="email" placeholder="[email protected]" />
<label for="password">Password:</label>
<input type="password" id="password" formControlName="password" placeholder="********" />
<label for="confirmPassword">Confirmar contraseña:</label>
<input
type="password"
id="confirmPassword"
formControlName="confirmPassword"
placeholder="********"
/>
<button type="submit" [disabled]="registerForm.invalid">Registrarse</button>
</form>
<p>¿Ya tienes una cuenta? <a (click)="goToLogin()">Inicia sesión aquí</a></p>
</section>Puntos importantes:
- Cada input usa
formControlNameque coincide exactamente con el FormGroup - El botón está deshabilitado mientras el formulario sea inválido
- Enlace para navegar de vuelta a login
Archivo: src/app/core/features/auth/register/register-auth.component.css
/* Puedes reutilizar los mismos estilos del login, solo cambia las clases */
/* Copia el contenido de login-auth.component.css y reemplaza:
- .login-auth-form → .register-auth-form
- .login-header → .register-header
- .login-auth → .register-auth
*/
:host {
display: block;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Open Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--brand: #ff6a00;
--brand-600: #e65a00;
--brand-700: #cc4a00;
--bg-dark: #111111;
--panel-bg: #151515;
--muted: #dfdfdf;
}
:host *,
:host *::before,
:host *::after {
box-sizing: border-box;
}
.register-auth-form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
max-width: 360px;
border: 1px solid #1e1e1e;
padding: 1rem;
border-radius: 8px;
margin: 0.5rem auto;
background: var(--panel-bg);
box-shadow: 0 1px 4px rgba(16, 24, 40, 0.04);
}
.register-auth-form p {
font-weight: 500;
font-size: 0.9rem;
color: #ffffff;
text-align: center;
}
.register-auth-form p a {
color: var(--brand);
text-decoration: none;
cursor: pointer;
}
.register-auth-form p a:hover {
text-decoration: underline;
}
.register-auth-form input[type='text'],
.register-auth-form input[type='email'],
.register-auth-form input[type='password'],
.register-auth-form input,
.register-auth-form textarea,
.register-auth-form select {
padding: 0.8rem;
margin-top: 5px;
font-size: 1rem;
width: 100%;
border: 1px solid #303030;
border-radius: 4px;
background: #242424;
transition: border-color 120ms ease-in-out, box-shadow 120ms ease-in-out;
color: #fff;
}
.register-auth-form input:focus,
.register-auth-form textarea:focus,
.register-auth-form select:focus {
outline: none;
border-color: var(--brand-700);
box-shadow: 0 0 0 3px rgba(255, 106, 0, 0.12);
}
.register-auth-form button {
padding: 0.6rem 0.75rem;
font-size: 1rem;
background-color: var(--brand);
color: #ffffff;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 120ms ease, transform 60ms ease;
margin: 0 auto;
display: block;
}
.register-auth-form button:hover:not(:disabled) {
background-color: var(--brand-600);
}
.register-auth-form button:active:not(:disabled) {
transform: translateY(1px);
}
.register-auth-form button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.register-auth-form button:focus {
outline: 3px solid rgba(255, 106, 0, 0.18);
outline-offset: 2px;
}
.register-header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 0.75rem;
text-align: center;
}
.register-header img {
width: 100px;
height: 100px;
margin: 1.25rem 0 0.5rem 0;
object-fit: contain;
display: block;
}
.register-header h1 {
font-size: 1.5rem;
margin: 0.125rem 0 0 0;
color: #fff;
}
.register-header p {
margin: 0.4rem 0 0 0;
color: var(--muted);
font-size: 0.95rem;
}
.register-auth label {
font-weight: 600;
margin-bottom: 0.25rem;
display: block;
color: #fff;
}
@media (max-width: 420px) {
.register-auth-form {
padding: 0.75rem;
margin: 1rem;
}
.register-header img {
width: 80px;
height: 80px;
margin-top: 1rem;
}
}┌─────────────────────────────────────────────────────────────────┐
│ INICIO DE LA APLICACIÓN │
└──────────────────────────┬──────────────────────────────────────┘
│
▼
┌──────────────┐
│ app.routes │
│ (Route '') │
└──────┬───────┘
│
▼
┌────────────────────────┐
│ AuthLayoutComponent │
│ (Wrapper con <router-outlet>) │
└────────┬───────────────┘
│
┌─────────────┴──────────────┐
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ /login │ │ /register │
│ (Login) │◄─────────────┤ (Register) │
└────┬─────┘ └──────┬───────┘
│ │
│ Usuario existe │ Nuevo usuario
│ Credenciales OK │ Llena formulario
│ │
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ Valida con │ │ Valida passwords │
│ localStorage │ │ coincidan │
└──────┬───────┘ └──────┬───────────┘
│ │
│ ✅ Correcto │ ✅ Correcto
│ │
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ router. │ │ Guarda en │
│ navigate │ │ localStorage │
│ (['/home']) │ └──────┬───────────┘
└──────┬───────┘ │
│ │
│ ▼
│ ┌──────────────┐
│ │ router. │
│ │ navigate │
│ │ (['/login']) │
│ └──────┬───────┘
│ │
└────────────┬───────────┘
│
▼
┌────────────────┐
│ MainLayout │
│ /home │
│ (Área privada)│
└────────────────┘
Usuario → Formulario de Registro
↓
Llena: fullName, email, password, confirmPassword
↓
Click "Registrarse"
↓
Validación:
- Todos los campos llenos? ✅
- Email válido? ✅
- password === confirmPassword? ✅
↓
Crea objeto: { fullName, email, password }
↓
localStorage.setItem('user', JSON.stringify(userToStore))
↓
Alert: "Registro exitoso 🎉"
↓
router.navigate(['/login'])
Usuario → Formulario de Login
↓
Ingresa: username (puede ser email), password
↓
Click "Ingresar"
↓
Validación:
- Todos los campos llenos? ✅
↓
Lee localStorage.getItem('user')
↓
¿Existe usuario guardado?
│
├─ NO → Alert: "No existe ningún usuario registrado"
│
└─ SÍ → Parsea JSON: user = JSON.parse(userData)
↓
Compara:
- input === user.email (case-insensitive) O
- input === user.username
- password === user.password
↓
¿Coinciden?
│
├─ NO → Alert: "Credenciales incorrectas"
│
└─ SÍ → Alert: "Inicio de sesión exitoso 🎉"
↓
router.navigate(['/home'])
@Component({
selector: 'app-login',
standalone: true, // ← Componente independiente (no necesita módulo)
imports: [ReactiveFormsModule], // ← Importa directamente lo que necesita
templateUrl: './login-auth.component.html',
styleUrls: ['./login-auth.component.css'],
})¿Por qué standalone?
- No necesitas crear
NgModule - Imports directos en el componente
- Mejor tree-shaking (bundle más pequeño)
- Patrón recomendado en Angular 14+
// 1. Crear FormGroup en el constructor
this.loginForm = this.fb.group({
username: ['', [Validators.required]], // valor inicial, validadores
password: ['', [Validators.required]]
});
// 2. Vincular en el template
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<input formControlName="username" />
<input formControlName="password" />
</form>
// 3. Acceder a valores
const { username, password } = this.loginForm.value;Ventajas:
- Validación en el componente (TypeScript)
- Estado del formulario reactivo
- Testing más fácil
- Mejor control de cambios
// En el componente
constructor(private router: Router) {}
// Navegar programáticamente
this.router.navigate(['/home']);
// En el template
<a [routerLink]="['/register']">Regístrate aquí</a>
// Con evento click
<a (click)="goToRegister()">Regístrate aquí</a>router-outlet:
<!-- El componente que coincida con la ruta se renderiza aquí -->
<router-outlet></router-outlet>// Guardar
const user = { fullName: 'Juan', email: '[email protected]', password: '1234' };
localStorage.setItem('user', JSON.stringify(user));
// Leer
const userData = localStorage.getItem('user');
if (userData) {
const user = JSON.parse(userData);
console.log(user.email); // '[email protected]'
}
// Eliminar
localStorage.removeItem('user');
// Limpiar todo
localStorage.clear();- LocalStorage NO es seguro para producción
- Las contraseñas deben ir cifradas en un backend
- Solo para demos/prototipos
# Si acabas de clonar el proyecto
npm install# Opción 1: usando npm script
npm start
# Opción 2: Angular CLI directo
ng serve
# Opción 3: con configuración específica
ng serve --configuration developmentLa aplicación estará disponible en: http://localhost:4200
# Build optimizado
npm run build
# O con Angular CLI
ng build --configuration productionLos archivos compilados estarán en: dist/devseniorInventory/browser/
# Abrir DevTools en el navegador (F12)
# → Application → Local Storage → http://localhost:4200
# → Click derecho en 'user' → DeleteO en la consola del navegador:
localStorage.removeItem('user');Usa este checklist para replicar el proyecto desde cero:
- Crear proyecto Angular:
ng new devseniorInventory - Configurar
angular.jsoncon rutas de assets - Crear estructura de carpetas:
core/features/auth/,core/layouts/
- Crear
AuthLayoutComponent- TypeScript con
RouterOutlet - HTML con contenedor y
<router-outlet> - CSS con diseño centrado y fondo oscuro
- TypeScript con
- Crear
MainLayoutComponent(placeholder para área autenticada)
- Crear
LoginComponent - Agregar imports:
ReactiveFormsModule,FormBuilder,Router,Validators - Crear
loginFormcon camposusernameypassword - Implementar lógica
onSubmit():- Validar formulario
- Leer de localStorage
- Comparar credenciales
- Navegar a
/homesi es correcto
- Crear template HTML:
- Header con logo y títulos
- Form con
[formGroup]yformControlName - Botón deshabilitado si es inválido
- Link a registro
- Crear CSS con:
- Variables de color (--brand, etc.)
- Estilos de formulario
- Estados de foco
- Responsive
- Crear
RegisterComponent - Agregar imports necesarios
- Crear
registerFormcon:fullName,email,password,confirmPassword - Implementar lógica
onSubmit():- Validar contraseñas coincidan
- Guardar en localStorage (sin confirmPassword)
- Navegar a
/login
- Crear template HTML con todos los campos
- Crear CSS (reutilizar/adaptar del login)
- Configurar
app.routes.ts:- Ruta raíz con
AuthLayoutComponent - Redirigir a
/login - Rutas hijas:
/loginy/register - Ruta
/homeconMainLayoutComponent
- Ruta raíz con
- Configurar
app.config.ts:-
provideRouter(routes) -
ReactiveFormsModule
-
- Colocar imagen del logo en
src/assets/images/ - Verificar que
angular.jsoncopie los assets
- Ejecutar
npm start - Probar registro completo
- Probar login con usuario registrado
- Verificar navegación entre rutas
- Verificar localStorage en DevTools
-
Contraseñas en texto plano:
- Se guardan sin cifrar en localStorage
- Cualquiera con acceso a DevTools puede verlas
-
Sin backend:
- Todo está en el cliente (navegador)
- No hay validación en servidor
-
Sin JWT/tokens:
- No hay gestión de sesiones real
- No hay expiración de login
-
Usar un backend real:
Frontend (Angular) → API REST (Node.js/Spring/etc.) → Base de Datos -
Cifrar contraseñas:
- Backend: bcrypt, argon2
- Nunca enviar/guardar contraseñas en texto plano
-
Implementar JWT:
// Login exitoso → servidor devuelve token localStorage.setItem('token', response.token); // En cada petición headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
-
Proteger rutas:
// AuthGuard export const authGuard: CanActivateFn = () => { const token = localStorage.getItem('token'); if (!token) { inject(Router).navigate(['/login']); return false; } return true; }; // En routes { path: 'home', component: HomeComponent, canActivate: [authGuard] }
-
Mensajes de validación visuales:
<input formControlName="email" /> <small *ngIf="registerForm.get('email')?.invalid && registerForm.get('email')?.touched"> Email inválido </small>
-
Loading spinner en botones:
isLoading = false; onSubmit() { this.isLoading = true; // ... lógica this.isLoading = false; }
<button [disabled]="loginForm.invalid || isLoading"> {{ isLoading ? 'Cargando...' : 'Ingresar' }} </button>
-
Toast notifications (en lugar de alert):
- Instalar librería como
ngx-toastr - Mensajes más profesionales
- Instalar librería como
-
Recuperar contraseña:
- Componente
/forgot-password - Enviar email con token de reset
- Componente
-
Validación de contraseña fuerte:
password: [ '', [ Validators.required, Validators.minLength(8), Validators.pattern(/^(?=.*[A-Za-z])(?=.*\d)/), // Letras y números ], ];
-
Mostrar/ocultar contraseña:
<input [type]="showPassword ? 'text' : 'password'" /> <button (click)="showPassword = !showPassword">👁️</button>
-
Remember me:
if (rememberMe) { localStorage.setItem('remember', username); }
-
Crear AuthService:
@Injectable({ providedIn: 'root' }) export class AuthService { login(username: string, password: string): Observable<User> {} register(user: RegisterData): Observable<void> {} logout(): void {} isAuthenticated(): boolean {} }
-
Crear modelos:
// models/user.model.ts export interface User { fullName: string; email: string; } export interface LoginCredentials { username: string; password: string; }
-
Implementar interceptores HTTP:
// Añadir token automáticamente export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = localStorage.getItem('token'); if (token) { req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` }, }); } return next(req); };
- Angular: https://angular.dev/
- Reactive Forms: https://angular.dev/guide/forms/reactive-forms
- Router: https://angular.dev/guide/routing
- Standalone Components: https://angular.dev/guide/components/importing
- Angular Tour of Heroes: https://angular.dev/tutorials/first-app
- RxJS Learn: https://www.learnrxjs.io/
- TypeScript Handbook: https://www.typescriptlang.org/docs/
- Angular DevTools (extensión Chrome/Edge): inspeccionar componentes
- JSON Formatter: visualizar localStorage
- Postman/Insomnia: probar APIs (cuando agregues backend)
npm install @angular/formsAsegúrate de importar ReactiveFormsModule en el componente:
@Component({
imports: [ReactiveFormsModule], // ← Aquí
})Verifica que:
[formGroup]="loginForm"está en el<form>formControlNamecoincide con el nombre en el FormGroup- Los imports están correctos
- Verifica que la ruta sea:
assets/images/nombre.png - Confirma que
angular.jsonincluye el directorio enassets - Reinicia el servidor:
Ctrl+Cynpm start
El FormGroup no está inicializado. Verifica el constructor:
constructor(private fb: FormBuilder) {
this.loginForm = this.fb.group({ ... }); // ← Debe estar aquí
}Has implementado:
✅ Sistema completo de autenticación (login + registro)
✅ Formularios reactivos con validación
✅ Navegación entre rutas con Angular Router
✅ Layouts reutilizables (AuthLayout, MainLayout)
✅ Diseño responsive con CSS moderno
✅ Paleta de colores consistente (variables CSS)
✅ Componentes standalone (patrón moderno Angular)
✅ Persistencia local con localStorage
✅ Validación de contraseñas coincidentes
✅ Estados de formulario (disabled, invalid, touched)
Configuración:
angular.json- Assets y build configsrc/app/app.routes.ts- Definición de rutassrc/app/app.config.ts- Providers globales
Layouts:
src/app/core/layouts/auth-layout/(3 archivos)src/app/core/layouts/main-layout/(3 archivos)
Componentes Auth:
src/app/core/features/auth/login/(3 archivos + assets)src/app/core/features/auth/register/(3 archivos)
Assets:
src/assets/images/3dicons-pumpkin-dynamic-color.png
Total: ~15 archivos modificados/creados
Ahora tienes un sistema de autenticación completo y funcional. Este código es una base sólida para:
- Proyectos personales
- Prototipos rápidos
- Aprender Angular y formularios reactivos
- Base para implementar backend real
Siguiente objetivo: Implementar backend con Node.js/Express o Spring Boot y conectar la autenticación real con JWT.
Happy Coding! 🚀
Documento generado el 27 de octubre de 2025
Versión Angular: 18+
Proyecto: devseniorInventory

