diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html
index a075cf5847..733ed0a906 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html
+++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html
@@ -64,107 +64,12 @@
@case ('form') {
}
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts
index 835c1c8cb4..53dc3ac523 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts
+++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts
@@ -108,7 +108,6 @@ export class TbDeviceInstallDialogComponent extends DialogComponent = {};
reviewMode = false;
// Variable resolution state
@@ -456,9 +455,9 @@ export class TbDeviceInstallDialogComponent extends DialogComponent's [resolveImagePath] input.
+ // Defined as a property so the template binding stays stable across change detection.
+ readonly resolveImagePathFn: (path: string) => string = (path: string) => this.resolveImagePath(path);
// --- Variable Resolution ---
@@ -537,7 +536,6 @@ export class TbDeviceInstallDialogComponent extends DialogComponent
+@if (formGroup) {
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/install-form-renderer/install-form-renderer.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/install-form-renderer/install-form-renderer.component.scss
new file mode 100644
index 0000000000..daaa824570
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/install-form-renderer/install-form-renderer.component.scss
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016-2026 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+:host {
+ display: block;
+}
+
+.tb-install-form-field-help {
+ font-size: 12px;
+ color: rgba(0, 0, 0, 0.6);
+ margin-top: 4px;
+}
+
+.tb-install-form-field-help-image {
+ max-width: 100%;
+ margin-top: 8px;
+}
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/install-form-renderer/install-form-renderer.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/install-form-renderer/install-form-renderer.component.ts
new file mode 100644
index 0000000000..ea129ce0ea
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/install-form-renderer/install-form-renderer.component.ts
@@ -0,0 +1,82 @@
+///
+/// Copyright © 2016-2026 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { UntypedFormGroup } from '@angular/forms';
+import { FormFieldDefinition, FormFieldType } from '@shared/models/iot-hub/device-package.models';
+import { generateSecret } from '@core/utils';
+
+const DEFAULT_RANDOM_SIZE = 20;
+
+/**
+ * Reusable form renderer for the device install dialog's SHOW_FORM step. Consumes
+ * the FormFieldDefinition[] parsed from the package's form.json directly.
+ *
+ * The renderer is presentation-only. The caller owns the FormGroup and supplies
+ * the FormFieldDefinition[] array. Optional resolveImagePath callback maps
+ * relative help-image paths to data URIs (the dialog supplies it from the parsed
+ * package ZIP).
+ */
+@Component({
+ selector: 'tb-install-form-renderer',
+ templateUrl: './install-form-renderer.component.html',
+ styleUrls: ['./install-form-renderer.component.scss'],
+ standalone: false
+})
+export class InstallFormRendererComponent implements OnChanges {
+
+ @Input() fields: FormFieldDefinition[] = [];
+ @Input() formGroup!: UntypedFormGroup;
+ @Input() resolveImagePath?: (path: string) => string;
+ /** When true, PASSWORD inputs render unmasked by default (used by review mode). */
+ @Input() reviewMode = false;
+
+ passwordVisible: Record = {};
+
+ readonly FormFieldType = FormFieldType;
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.fields || changes.reviewMode) {
+ this.passwordVisible = {};
+ if (this.reviewMode) {
+ for (const f of this.fields) {
+ if (f.type === FormFieldType.PASSWORD) {
+ this.passwordVisible[f.key] = true;
+ }
+ }
+ }
+ }
+ }
+
+ togglePasswordVisible(key: string): void {
+ this.passwordVisible[key] = !this.passwordVisible[key];
+ }
+
+ regenerate(field: FormFieldDefinition): void {
+ const control = this.formGroup.controls[field.key];
+ if (!control) return;
+ control.patchValue(generateSecret(field.randomSize ?? DEFAULT_RANDOM_SIZE));
+ control.markAsDirty();
+ }
+
+ getPatternErrorMessage(field: FormFieldDefinition): string {
+ return field.validators?.[0]?.message || 'Invalid format';
+ }
+
+ imagePath(path: string): string {
+ return this.resolveImagePath ? this.resolveImagePath(path) : path;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts
index bdd8dfa65f..cb37201432 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts
+++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts
@@ -26,6 +26,7 @@ import { TbIotHubUpdateDialogComponent } from './iot-hub-update-dialog.component
import { TbIotHubDeleteDialogComponent } from './iot-hub-delete-dialog.component';
import { TbIotHubUnpublishedWarningDialogComponent } from './iot-hub-unpublished-warning-dialog.component';
import { TbDeviceInstallDialogComponent } from './device-install-dialog/device-install-dialog.component';
+import { InstallFormRendererComponent } from './device-install-dialog/install-form-renderer/install-form-renderer.component';
import { TbIotHubSearchComponent } from './iot-hub-search.component';
import { TbIotHubInstalledItemsTableComponent } from './iot-hub-installed-items-table.component';
import { TbIotHubInstalledItemsDialogComponent } from './iot-hub-installed-items-dialog.component';
@@ -51,7 +52,8 @@ import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link
TbIotHubInstalledItemsDialogComponent,
TbPeConnectivityMethodPromptComponent,
TbIotHubMarkdownComponent,
- SolutionInstallDialogComponent
+ SolutionInstallDialogComponent,
+ InstallFormRendererComponent
],
imports: [
CommonModule,