🏠 Home 

Angular Component Modifier

Permite modificar propiedades de componentes en Angular en un servidor de desarrollo con funcionalidad para guardar y restaurar valores y navegar a componentes padre, incluye soporte para signals


Installer dette script?
// ==UserScript==
// @name         Angular Component Modifier
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Permite modificar propiedades de componentes en Angular en un servidor de desarrollo con funcionalidad para guardar y restaurar valores y navegar a componentes padre, incluye soporte para signals
// @author       Blas Santomé Ocampo
// @match        http://localhost:*/*
// @grant        none
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const IGNORED_PROPERTIES = ["__ngContext__"];
const STORAGE_KEY = "angularModifier_savedStates";
let savedStates = {};
let currentElement = null;
try {
const storedStates = localStorage.getItem(STORAGE_KEY);
if (storedStates) {
savedStates = JSON.parse(storedStates);
}
} catch (err) {
console.warn("[Angular Modifier] Error al cargar estados guardados:", err);
}
console.log(
"[Angular Modifier] UserScript cargado. Usa OPTION (⌥) + Click en un componente app-*."
);
document.addEventListener(
"click",
function (event) {
if (!event.altKey) return;
event.preventDefault();
let ng = window.ng;
if (!ng) {
alert(
"⚠️ Angular DevTools no está disponible. Asegúrate de estar en un servidor de desarrollo."
);
return;
}
let el = event.target;
let component = null;
let componentName = "Componente Desconocido";
let componentId = "";
while (el) {
component = ng.getComponent(el);
if (component && el.tagName.toLowerCase().startsWith("app-")) {
componentName = el.tagName.toLowerCase();
componentId = generateComponentId(el, componentName);
currentElement = el;
break;
}
el = el.parentElement;
}
if (!component) {
alert(
"⚠️ No se encontró un componente Angular válido (app-*) en la jerarquía."
);
return;
}
console.log(
`[Angular Modifier] Componente seleccionado: ${componentName} (ID: ${componentId})`,
component
);
showComponentEditor(component, componentName, componentId);
},
true
);
function generateComponentId(element, componentName) {
let path = [];
let current = element;
while (current && current !== document.body) {
let index = 0;
let sibling = current;
while ((sibling = sibling.previousElementSibling)) {
index++;
}
path.unshift(index);
current = current.parentElement;
}
return `${componentName}_${path.join("_")}`;
}
function navigateToParentComponent(currentEl) {
let ng = window.ng;
if (!ng) {
alert(
"⚠️ Angular DevTools no está disponible. Asegúrate de estar en un servidor de desarrollo."
);
return false;
}
if (!currentEl) {
alert("⚠️ No hay ningún componente seleccionado actualmente.");
return false;
}
let parentEl = currentEl.parentElement;
let found = false;
while (parentEl) {
if (
parentEl.tagName &&
parentEl.tagName.toLowerCase().startsWith("app-") &&
ng.getComponent(parentEl)
) {
currentElement = parentEl;
const component = ng.getComponent(parentEl);
const componentName = parentEl.tagName.toLowerCase();
const componentId = generateComponentId(parentEl, componentName);
console.log(
`[Angular Modifier] Navegando al componente padre: ${componentName} (ID: ${componentId})`,
component
);
const existingModal = document.querySelector(".angular-modifier-modal");
if (existingModal) {
document.body.removeChild(existingModal);
}
showComponentEditor(component, componentName, componentId);
found = true;
break;
}
parentEl = parentEl.parentElement;
}
if (!found) {
alert("⚠️ No se encontró un componente padre que comience con 'app-'.");
}
return found;
}
function showComponentEditor(component, componentName, componentId) {
let modal = document.createElement("div");
modal.className = "angular-modifier-modal";
modal.style.position = "fixed";
modal.style.top = "50%";
modal.style.left = "50%";
modal.style.transform = "translate(-50%, -50%)";
modal.style.background = "white";
modal.style.padding = "20px";
modal.style.boxShadow = "0px 0px 10px rgba(0,0,0,0.2)";
modal.style.zIndex = "10000";
modal.style.borderRadius = "8px";
modal.style.width = "400px";
modal.style.maxHeight = "500px";
modal.style.overflowY = "auto";
let title = document.createElement("h3");
title.innerText = componentName;
title.style.marginTop = "0";
modal.appendChild(title);
let form = document.createElement("form");
let formGroups = {};
let editableProps = {};
let signals = {};
// Add signals section title
let signalsTitle = document.createElement("h4");
signalsTitle.innerText = "Signals";
signalsTitle.style.marginTop = "15px";
signalsTitle.style.color = "#007bff";
signalsTitle.style.display = "none"; // Hide initially, show only if signals are found
form.appendChild(signalsTitle);
// Separate div for signals
let signalsDiv = document.createElement("div");
signalsDiv.style.marginLeft = "10px";
form.appendChild(signalsDiv);
Object.keys(component).forEach((prop) => {
if (IGNORED_PROPERTIES.includes(prop)) return;
let value = component[prop];
if (typeof value === "function") {
// Check if this is a signal (signals are functions with specific properties)
if (isSignal(value)) {
signalsTitle.style.display = "block"; // Show signals section title
appendSignalField(signalsDiv, component, prop, value);
signals[prop] = value;
return;
}
return;
}
if (
value &&
typeof value === "object" &&
value.constructor.name === "FormGroup"
) {
formGroups[prop] = value;
appendFormGroupFields(form, value, prop);
return;
}
if (value !== null && typeof value === "object") return;
let input = appendEditableField(form, component, prop, value);
if (input) {
editableProps[prop] = {
type: typeof value,
input: input,
};
}
});
modal.appendChild(form);
let parentComponentButton = document.createElement("button");
parentComponentButton.innerText = "Ir al Componente Padre";
parentComponentButton.style.marginTop = "15px";
parentComponentButton.style.width = "100%";
parentComponentButton.style.padding = "8px";
parentComponentButton.style.background = "#ffc107";
parentComponentButton.style.color = "black";
parentComponentButton.style.border = "none";
parentComponentButton.style.borderRadius = "5px";
parentComponentButton.style.cursor = "pointer";
parentComponentButton.style.fontWeight = "bold";
parentComponentButton.addEventListener("click", (e) => {
e.preventDefault();
navigateToParentComponent(currentElement);
});
modal.appendChild(parentComponentButton);
let stateManagementDiv = document.createElement("div");
stateManagementDiv.style.marginTop = "15px";
stateManagementDiv.style.borderTop = "1px solid #eee";
stateManagementDiv.style.paddingTop = "10px";
let saveStateButton = document.createElement("button");
saveStateButton.innerText = "Guardar Estado Actual";
saveStateButton.style.padding = "5px 10px";
saveStateButton.style.marginRight = "10px";
saveStateButton.style.background = "#28a745";
saveStateButton.style.color = "white";
saveStateButton.style.border = "none";
saveStateButton.style.borderRadius = "5px";
saveStateButton.style.cursor = "pointer";
saveStateButton.addEventListener("click", (e) => {
e.preventDefault();
saveCurrentState(component, componentId, formGroups, editableProps, signals);
});
stateManagementDiv.appendChild(saveStateButton);
let restoreStateButton = document.createElement("button");
restoreStateButton.innerText = "Restaurar Estado";
restoreStateButton.style.padding = "5px 10px";
restoreStateButton.style.background = "#007bff";
restoreStateButton.style.color = "white";
restoreStateButton.style.border = "none";
restoreStateButton.style.borderRadius = "5px";
restoreStateButton.style.cursor = "pointer";
if (!savedStates[componentId]) {
restoreStateButton.disabled = true;
restoreStateButton.style.opacity = "0.5";
restoreStateButton.style.cursor = "not-allowed";
}
restoreStateButton.addEventListener("click", (e) => {
e.preventDefault();
restoreSavedState(component, componentId, formGroups, editableProps, signals);
});
stateManagementDiv.appendChild(restoreStateButton);
modal.appendChild(stateManagementDiv);
let fileLabel = document.createElement("label");
fileLabel.innerText = "Cargar JSON:";
fileLabel.style.display = "block";
fileLabel.style.marginTop = "15px";
modal.appendChild(fileLabel);
let fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = "application/json";
fileInput.style.marginTop = "5px";
fileInput.style.width = "100%";
fileInput.addEventListener("change", (event) =>
handleFileUpload(event, formGroups, signals)
);
modal.appendChild(fileInput);
let exportButton = document.createElement("button");
exportButton.innerText = "Exportar a JSON";
exportButton.style.marginTop = "10px";
exportButton.style.width = "100%";
exportButton.style.padding = "5px";
exportButton.style.background = "#17a2b8";
exportButton.style.color = "white";
exportButton.style.border = "none";
exportButton.style.borderRadius = "5px";
exportButton.style.cursor = "pointer";
exportButton.addEventListener("click", (e) => {
e.preventDefault();
exportToJson(component, formGroups, signals);
});
modal.appendChild(exportButton);
let closeButton = document.createElement("button");
closeButton.innerText = "Cerrar";
closeButton.style.marginTop = "10px";
closeButton.style.width = "100%";
closeButton.style.padding = "5px";
closeButton.style.background = "#d9534f";
closeButton.style.color = "white";
closeButton.style.border = "none";
closeButton.style.borderRadius = "5px";
closeButton.style.cursor = "pointer";
closeButton.addEventListener("click", () => {
document.body.removeChild(modal);
});
modal.appendChild(closeButton);
document.body.appendChild(modal);
}
// Function to check if a property is an Angular signal
function isSignal(value) {
return typeof value === 'function' &&
(value.name === 'Signal' ||
(typeof value() !== 'undefined' &&
typeof value.set === 'function'));
}
// Function to append a signal field to the form
function appendSignalField(container, component, prop, signal) {
try {
// Get current value
const currentValue = signal();
let signalLabel = document.createElement("label");
signalLabel.innerText = `${prop} (signal)`;
signalLabel.style.display = "block";
signalLabel.style.marginTop = "5px";
signalLabel.style.fontWeight = "bold";
signalLabel.style.color = "#007bff";
let signalInput = document.createElement("input");
signalInput.style.width = "100%";
signalInput.style.marginTop = "2px";
signalInput.dataset.signalName = prop;
if (typeof currentValue === "boolean") {
signalInput.type = "checkbox";
signalInput.checked = currentValue;
} else if (typeof currentValue === "number") {
signalInput.type = "number";
signalInput.value = currentValue;
} else if (typeof currentValue === "string") {
signalInput.type = "text";
signalInput.value = currentValue;
} else if (currentValue === null || currentValue === undefined) {
signalInput.type = "text";
signalInput.value = "";
signalInput.placeholder = "undefined/null";
} else {
// Complex object - not directly editable
let valueInfo = document.createElement("div");
valueInfo.innerText = `Valor complejo (${typeof currentValue}): ${JSON.stringify(currentValue).substring(0, 50)}${JSON.stringify(currentValue).length > 50 ? '...' : ''}`;
valueInfo.style.fontSize = "12px";
valueInfo.style.marginBottom = "10px";
valueInfo.style.color = "#666";
container.appendChild(signalLabel);
container.appendChild(valueInfo);
return;
}
signalInput.addEventListener("change", () => {
try {
let newValue;
if (signalInput.type === "checkbox") {
newValue = signalInput.checked;
} else if (signalInput.type === "number") {
newValue = parseFloat(signalInput.value);
} else {
newValue = signalInput.value;
}
// Update the signal value using set() method
signal.set(newValue);
console.log(`[Angular Modifier] Se actualizó signal '${prop}' a ${newValue}`);
} catch (err) {
alert(`⚠️ Error al actualizar signal '${prop}': ${err.message}`);
}
});
container.appendChild(signalLabel);
container.appendChild(signalInput);
return signalInput;
} catch (err) {
console.warn(`[Angular Modifier] Error al procesar signal '${prop}':`, err);
return null;
}
}
function appendEditableField(form, component, prop, value) {
let label = document.createElement("label");
label.innerText = prop;
label.style.display = "block";
label.style.marginTop = "5px";
let input = document.createElement("input");
input.style.width = "100%";
input.style.marginTop = "2px";
input.dataset.propName = prop;
if (typeof value === "boolean") {
input.type = "checkbox";
input.checked = value;
} else if (typeof value === "number") {
input.type = "number";
input.value = value;
} else if (typeof value === "string") {
input.type = "text";
input.value = value;
} else {
return null;
}
input.addEventListener("change", () => {
try {
if (input.type === "checkbox") {
component[prop] = input.checked;
} else if (input.type === "number") {
component[prop] = parseFloat(input.value);
} else {
component[prop] = input.value;
}
if (typeof ng.applyChanges === "function") {
ng.applyChanges(component);
console.log(`[Angular Modifier] Se aplicaron cambios en ${prop}`);
}
} catch (err) {
alert(`⚠️ Error al actualizar '${prop}': ${err.message}`);
}
});
form.appendChild(label);
form.appendChild(input);
return input;
}
function appendFormGroupFields(form, formGroup, formGroupName) {
let formGroupTitle = document.createElement("h4");
formGroupTitle.innerText = `Formulario: ${formGroupName}`;
formGroupTitle.style.marginTop = "10px";
formGroupTitle.style.color = "#007bff";
form.appendChild(formGroupTitle);
if (formGroup.controls) {
Object.keys(formGroup.controls).forEach((controlName) => {
try {
const control = formGroup.controls[controlName];
const currentValue = control.value;
let controlLabel = document.createElement("label");
controlLabel.innerText = controlName;
controlLabel.style.display = "block";
controlLabel.style.marginTop = "5px";
controlLabel.style.marginLeft = "10px";
let controlInput = document.createElement("input");
controlInput.style.width = "95%";
controlInput.style.marginTop = "2px";
controlInput.style.marginLeft = "10px";
controlInput.dataset.formGroup = formGroupName;
controlInput.dataset.controlName = controlName;
if (typeof currentValue === "boolean") {
controlInput.type = "checkbox";
controlInput.checked = currentValue;
} else if (typeof currentValue === "number") {
controlInput.type = "number";
controlInput.value = currentValue;
} else {
controlInput.type = "text";
controlInput.value =
currentValue !== null && currentValue !== undefined
? currentValue
: "";
}
controlInput.addEventListener("change", () => {
try {
let newValue;
if (controlInput.type === "checkbox") {
newValue = controlInput.checked;
} else if (controlInput.type === "number") {
newValue = parseFloat(controlInput.value);
} else {
newValue = controlInput.value;
}
control.setValue(newValue);
console.log(
`[Angular Modifier] Actualizado control '${controlName}' en FormGroup '${formGroupName}'`
);
} catch (err) {
alert(
`⚠️ Error al actualizar control '${controlName}': ${err.message}`
);
}
});
form.appendChild(controlLabel);
form.appendChild(controlInput);
} catch (err) {
console.warn(
`[Angular Modifier] Error al mostrar control '${controlName}':`,
err
);
}
});
}
}
function handleFileUpload(event, formGroups, signals) {
let file = event.target.files[0];
if (!file) return;
let reader = new FileReader();
reader.onload = function (event) {
try {
let jsonData = JSON.parse(event.target.r###lt);
applyJsonToForm(jsonData, formGroups, signals);
} catch (err) {
alert("⚠️ Error al cargar JSON: " + err.message);
}
};
reader.readAsText(file);
}
function applyJsonToForm(jsonData, formGroups, signals) {
if (jsonData.properties) {
Object.keys(jsonData.properties).forEach((prop) => {
if (IGNORED_PROPERTIES.includes(prop)) return;
try {
const inputElement = document.querySelector(
`input[data-prop-name="${prop}"]`
);
if (inputElement) {
if (inputElement.type === "checkbox") {
inputElement.checked = jsonData.properties[prop];
} else {
inputElement.value = jsonData.properties[prop];
}
inputElement.dispatchEvent(new Event("change"));
}
} catch (err) {
console.warn(
`[Angular Modifier] Error al aplicar propiedad '${prop}':`,
err
);
}
});
}
if (jsonData.signals) {
Object.keys(jsonData.signals).forEach((signalName) => {
if (signals[signalName]) {
try {
const signalValue = jsonData.signals[signalName];
signals[signalName].set(signalValue);
const signalInput = document.querySelector(
`input[data-signal-name="${signalName}"]`
);
if (signalInput) {
if (signalInput.type === "checkbox") {
signalInput.checked = signalValue;
} else {
signalInput.value = signalValue;
}
}
console.log(`[Angular Modifier] Signal '${signalName}' actualizado`);
} catch (err) {
console.warn(
`[Angular Modifier] Error al actualizar signal '${signalName}':`,
err
);
}
}
});
}
if (jsonData.formGroups) {
Object.keys(jsonData.formGroups).forEach((groupName) => {
if (formGroups[groupName]) {
let formGroup = formGroups[groupName];
const groupData = jsonData.formGroups[groupName];
Object.keys(groupData).forEach((controlName) => {
if (formGroup.controls[controlName]) {
try {
formGroup.controls[controlName].setValue(
groupData[controlName]
);
const controlInput = document.querySelector(
`input[data-form-group="${groupName}"][data-control-name="${controlName}"]`
);
if (controlInput) {
if (controlInput.type === "checkbox") {
controlInput.checked = groupData[controlName];
} else {
controlInput.value = groupData[controlName];
}
}
console.log(
`[Angular Modifier] Campo '${controlName}' de '${groupName}' actualizado`
);
} catch (err) {
console.warn(
`[Angular Modifier] Error al actualizar control '${controlName}':`,
err
);
}
}
});
}
});
}
}
function exportToJson(component, formGroups, signals) {
let exportData = {
properties: {},
formGroups: {},
signals: {}
};
Object.keys(component).forEach((prop) => {
if (IGNORED_PROPERTIES.includes(prop)) return;
let value = component[prop];
if (
typeof value !== "function" &&
value !== null &&
typeof value !== "object"
) {
exportData.properties[prop] = value;
}
});
// Export signals
Object.keys(signals).forEach((signalName) => {
try {
const signal = signals[signalName];
const value = signal();
// Only export primitive values that can be safely serialized
if (value === null || typeof value === "undefined" ||
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean") {
exportData.signals[signalName] = value;
}
} catch (err) {
console.warn(
`[Angular Modifier] Error al exportar signal '${signalName}':`,
err
);
}
});
Object.keys(formGroups).forEach((groupName) => {
const formGroup = formGroups[groupName];
exportData.formGroups[groupName] = {};
if (formGroup.controls) {
Object.keys(formGroup.controls).forEach((controlName) => {
try {
exportData.formGroups[groupName][controlName] =
formGroup.controls[controlName].value;
} catch (err) {
console.warn(
`[Angular Modifier] Error al exportar control '${controlName}':`,
err
);
}
});
}
});
const jsonString = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonString], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `angular-component-${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function saveCurrentState(component, componentId, formGroups, editableProps, signals) {
let state = {
properties: {},
formGroups: {},
signals: {}
};
Object.keys(editableProps).forEach((prop) => {
if (IGNORED_PROPERTIES.includes(prop)) return;
const input = editableProps[prop].input;
if (input.type === "checkbox") {
state.properties[prop] = input.checked;
} else if (input.type === "number") {
state.properties[prop] = parseFloat(input.value);
} else {
state.properties[prop] = input.value;
}
});
// Save signal values
Object.keys(signals).forEach((signalName) => {
try {
const signal = signals[signalName];
const value = signal();
// Only save primitive values that can be safely serialized
if (value === null || typeof value === "undefined" ||
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean") {
state.signals[signalName] = value;
}
} catch (err) {
console.warn(
`[Angular Modifier] Error al guardar signal '${signalName}':`,
err
);
}
});
Object.keys(formGroups).forEach((groupName) => {
const formGroup = formGroups[groupName];
state.formGroups[groupName] = {};
if (formGroup.controls) {
Object.keys(formGroup.controls).forEach((controlName) => {
try {
state.formGroups[groupName][controlName] =
formGroup.controls[controlName].value;
} catch (err) {
console.warn(
`[Angular Modifier] Error al guardar control '${controlName}':`,
err
);
}
});
}
});
savedStates[componentId] = state;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(savedStates));
alert(`✅ Estado guardado correctamente para ${componentId}`);
} catch (err) {
console.error("[Angular Modifier] Error al guardar estado:", err);
alert("⚠️ Error al guardar estado: " + err.message);
}
}
function restoreSavedState(
component,
componentId,
formGroups,
editableProps,
signals
) {
const savedState = savedStates[componentId];
if (!savedState) {
alert("⚠️ No hay estado guardado para este componente");
return;
}
if (savedState.properties) {
Object.keys(savedState.properties).forEach((prop) => {
if (IGNORED_PROPERTIES.includes(prop)) return;
if (editableProps[prop]) {
const input = editableProps[prop].input;
const value = savedState.properties[prop];
if (input.type === "checkbox") {
input.checked = value;
} else {
input.value = value;
}
try {
component[prop] = value;
} catch (err) {
console.warn(
`[Angular Modifier] Error al restaurar propiedad '${prop}':`,
err
);
}
}
});
}
// Restore signal values
if (savedState.signals) {
Object.keys(savedState.signals).forEach((signalName) => {
if (signals[signalName]) {
try {
const signalValue = savedState.signals[signalName];
signals[signalName].set(signalValue);
const signalInput = document.querySelector(
`input[data-signal-name="${signalName}"]`
);
if (signalInput) {
if (signalInput.type === "checkbox") {
signalInput.checked = signalValue;
} else {
signalInput.value = signalValue;
}
}
console.log(`[Angular Modifier] Signal '${signalName}' restaurado`);
} catch (err) {
console.warn(
`[Angular Modifier] Error al restaurar signal '${signalName}':`,
err
);
}
}
});
}
if (savedState.formGroups) {
Object.keys(savedState.formGroups).forEach((groupName) => {
if (formGroups[groupName]) {
const formGroup = formGroups[groupName];
const groupData = savedState.formGroups[groupName];
Object.keys(groupData).forEach((controlName) => {
if (formGroup.controls[controlName]) {
try {
formGroup.controls[controlName].setValue(
groupData[controlName]
);
const controlInput = document.querySelector(
`input[data-form-group="${groupName}"][data-control-name="${controlName}"]`
);
if (controlInput) {
if (controlInput.type === "checkbox") {
controlInput.checked = groupData[controlName];
} else {
controlInput.value = groupData[controlName];
}
}
} catch (err) {
console.warn(
`[Angular Modifier] Error al restaurar control '${controlName}':`,
err
);
}
}
});
}
});
}
if (typeof ng.applyChanges === "function") {
ng.applyChanges(component);
console.log(`[Angular Modifier] Se restauró el estado del componente`);
}
alert(`✅ Estado restaurado correctamente para ${componentId}`);
}
})();