Skip to main content

Vanilla vs Framework

Oidis provides utility classes that replace many vanilla JavaScript patterns. However, the approach is pragmatic, not dogmatic — sometimes vanilla code is simpler and more appropriate. This guide helps you decide.

Technology selection​

Keep the project's established language, runtime, framework, build and test tools as the default. This rule covers application code and committed helper scripts, data conversion, anonymization, generators, replay harnesses, CI and operational tooling. In an Oidis TypeScript solution, use TypeScript on the existing Node.js runtime and integrate execution and checks through Oidis Builder.

Before adding another language, runtime or parallel toolchain:

  1. Check the existing framework APIs, dependencies and Builder extension mechanisms.
  2. Describe the concrete requirement the current stack cannot reasonably meet, or provide evidence of a material benefit for this task: capability, correctness, performance, or total implementation and maintenance cost.
  3. Compare implementation, packaging, CI, security updates, debugging and long-term ownership costs. Prefer the smallest exception that delivers the benefit.
  4. Obtain the owner's explicit approval before implementation, then record the rationale, scope, owner and validation approach in the project's documentation.

Agent familiarity, shorter code, an interpreter being available locally, or calling code "only a helper" do not justify an exception. General authorization to implement a feature does not authorize introducing another stack. Do not retroactively treat a committed implementation as an approved exception.

Existing documented technologies do not need repeated approval. Thin JavaScript Builder module entry points and existing shell launchers remain valid integration surfaces; substantial reusable logic belongs in TypeScript. Routine use of existing OS tools is not a new language stack. Where an additional OS tool or native binding is required, document that dependency and assess its maintenance cost rather than reimplementing it in another language.

Approved exceptions: the Rust/Tauri desktop shell of io-oidis-desktop (owner, 2026-10-13), see Desktop Shell.

The Builder's ability to execute other interpreters is a capability, not a recommendation or policy exemption. Project and agent instructions should link to this policy so Claude, Codex and human contributors follow the same decision process.

Decision Rules​

CategoryFrameworkVanillaRule
Console outputLogIt.*—Always framework — console.* is a lint error
DOM queriesdata-oidis-bind—Always framework — querySelector is a lint warning
Event listenersEventsBinding.*—Always framework — addEventListener is a lint warning
UI element creationnew Control() + .AddChild()—Always framework — createElement/innerHTML bypasses type safety
Null/empty checkObjectValidator.IsEmptyOrNull()?. / ??Framework for unknown data, vanilla for known structures
String operationsStringUtils.*str.includes(), etc.Framework when input might be null, vanilla for known strings
Parse numbersStringUtils.ToInteger()parseInt()Framework — handles edge cases
Object deep mergeJsonUtils.Extend()—Always framework — no vanilla equivalent
Object shallow copyJsonUtils.Clone(){ ...obj }Either — spread is fine for simple cases
Date formattingConvert.DateToCsFormat()—Always framework — consistent locale output
Unique ID generationthis.getUID()crypto.randomUUID()Always framework — equivalent result, framework-consistent
Array operations—.map(), .filter(), .find(), .forEach()Always vanilla — framework doesn't wrap these
Optional chaining—?.Always vanilla — cleaner syntax
Nullish coalescing—??Always vanilla — cleaner syntax
Template literals—`${var}`Always vanilla — cleaner than StringUtils.Format() for static templates
Regex—RegExp, .match(), .test()Always vanilla — framework doesn't cover regex
Trim/case change—.trim(), .toLowerCase()Always vanilla — straightforward
Type guards—typeof, instanceofVanilla is fine — ObjectValidator.Is*() for consistency
Error handlingLogIt.Error() + ShowMessage()throwBoth — throw, catch, LogIt, ShowMessage
Child processesTerminal.Execute() / .Spawn()—Always framework — never use child_process directly
String building—`${var}`Always vanilla — template literals preferred over + concatenation
Filesystem pathsConfig / env vars / process.cwd()—Always configuration — never hardcode absolute paths

Key Principles​

1. Never Use console.*​

This is non-negotiable. The linter flags it as an error.

// ❌ Lint error
console.log("Debug:", data);
console.error("Failed:", ex);

// ✅ Correct
LogIt.Info("Debug:", data);
LogIt.Error("Failed:", ex);

2. Never Use querySelector or addEventListener​

The linter flags these as warnings. They bypass the framework's type safety and lifecycle management.

// ❌ Lint warning
const btn = document.querySelector(".save-btn");
btn.addEventListener("click", handler);

// ✅ Correct
EventsBinding.SingleClick($instance.saveButton, handler);

3. Use ObjectValidator for External Data​

When data comes from outside your code (API responses, LCP transport, user input), use ObjectValidator.IsEmptyOrNull() for maximum safety:

// ✅ Data from backend — use ObjectValidator
const record = await MyRecord.FindById($id);
if (ObjectValidator.IsEmptyOrNull(record)) {
return;
}

4. Use Optional Chaining for Known Structures​

When you know the object's shape but a field might be absent, ?. and ?? are cleaner:

// ✅ Known structure — optional chaining is fine
const groupName = record?.Group?.Name ?? "No group";
const count = options?.limit ?? 50;

5. Use this.getUID() for Unique IDs​

Never use crypto.randomUUID() directly. The framework base class provides getUID() as a framework-consistent equivalent:

// ❌ Vanilla — avoid
const id = crypto.randomUUID();

// ✅ Framework
const id = this.getUID();

getUID() is available on any class that extends a Oidis base class (controllers, connectors, models).

6. Never Use Raw DOM for UI Construction​

When building UI, always use the framework's OOP component system. Never create HTML elements imperatively — instantiate framework controls and use .AddChild() instead.

// ❌ WRONG — raw DOM manipulation
const div = document.createElement("div");
div.innerHTML = `<button>Save</button>`;
div.querySelector("button").addEventListener("click", handler);
container.appendChild(div);

// ❌ WRONG — innerHTML injection for interactive elements
$instance.container.InstanceOwner().innerHTML = `<button id="btn">Save</button>`;
document.getElementById("btn").addEventListener("click", handler);

// ✅ CORRECT — framework OOP approach
const button : Button = new Button();
button.Text("Save");
EventsBinding.SingleClick(button, handler);
$instance.container.AddChild(button);

This rule applies to all interactive UI creation. The only acceptable raw HTML is:

  • Bootstrap layout classes in innerHtml() — <div class="row">, <div class="col-md-6">, wrapper containers
  • Static non-interactive text via .Content() — e.g., label.Content("<strong>Active</strong>")
  • InstanceOwner() for DOM measurements, scrolling, or third-party library integration (not for building UI)

For the full component API (.AddChild(), .Clear(), .Value(), .Text(), etc.), see Panels and Controllers.

7. Use Vanilla Arrays​

Oidis doesn't wrap array methods. Use vanilla .map(), .filter(), .find(), .forEach(), .reduce(), and for...of directly:

// ✅ Vanilla arrays — this is correct
const activeItems = items.filter(($item) => $item.IsActive);
const names = items.map(($item) => $item.Name);
const found = items.find(($item) => $item.Id === targetId);

// ✅ for...of is preferred over for(;;)
for (const item of items) {
await processItem(item);
}

8. Never Use child_process Directly​

All child process operations must go through the framework Terminal class from io-oidis-localhost. Terminal handles cross-platform differences, PATH normalization, proxy forwarding, and proper logging.

// ❌ WRONG — direct child_process
import * as childProcess from "child_process";
const result = childProcess.spawnSync("git", ["status"], { encoding: "utf-8" });

// ✅ Correct — framework Terminal
const terminal : Terminal = Loader.getInstance().getTerminal();
const result : IExecuteResult = await terminal.ExecuteAsync("git", ["status"], {
cwd : projectPath,
verbose: false
});

See the full Terminal documentation for API details.

9. Prefer Template Literals Over Concatenation​

Use template literals (backticks) for string interpolation. String concatenation with + is discouraged when variables are involved.

// ❌ Discouraged — string concatenation with variables
LogIt.Info("Processing " + item.Name + " (" + item.Id + ")");
const path = basePath + "/" + fileName;

// ✅ Correct — template literals
LogIt.Info(`Processing ${item.Name} (${item.Id})`);
const path = `${basePath}/${fileName}`;

Use StringUtils.Format() only for dynamic/runtime format templates where the template string itself is a variable.

10. Never Hardcode Filesystem Paths​

Absolute filesystem paths must never appear as literals in source code. They must come from configuration: environment variables, JSONP files, CLI arguments, or process.cwd().

// ❌ WRONG — hardcoded path
const sourcePath = "D:/__PROJECTS__/Oidis/Source";

// ❌ WRONG — hardcoded fallback
const sourcePath = process.env.OIDIS_SOURCE_PATH || "D:/__PROJECTS__/Oidis/Source";

// ✅ Correct — fail if not configured
const sourcePath = process.env.OIDIS_SOURCE_PATH;
if (ObjectValidator.IsEmptyOrNull(sourcePath)) {
throw new Error("OIDIS_SOURCE_PATH environment variable is not set");
}

// ✅ Correct — derive from CWD
const projectPath = process.cwd();

11. Ask Before Writing Vanilla Utilities​

If you're about to write a utility function that seems generally useful (formatting, parsing, conversion), check first whether the framework already provides it. The framework has extensive utilities across:

  • ObjectValidator — Type checking and null safety
  • StringUtils — String manipulation and parsing
  • JsonUtils — Object operations
  • Convert — Type conversion, date boundaries, and formatting
  • Echo — Screen output and stream capture
  • Stopwatch — Performance timing
  • Reflection — Runtime class discovery and type checking
  • Counters — Sequential ID generation
  • Threads — Async helpers (Sleep())
  • FileFetch — File download handling

See the full Utilities Overview for details.

Examples​

Good: Mixed Approach​

public async FetchData() : Promise<void> {
try {
const result = await this.client.ListItems({ offset: 0, limit: 50 });

// ObjectValidator for API response (unknown structure)
if (ObjectValidator.IsEmptyOrNull(result?.items)) {
LogIt.Warning("No items returned");
return;
}

// Vanilla array methods (always fine)
const activeItems = result.items.filter(($item) => $item.IsActive);

// Vanilla template literal (cleaner than StringUtils.Format for this)
LogIt.Info(`Loaded ${activeItems.length} active items`);

// Framework date formatting (consistent locale)
activeItems.forEach(($item) => {
$item.displayDate = Convert.DateToCsFormat($item.Created);
});

} catch (ex) {
// Framework error handling (mandatory)
LogIt.Error("Failed to fetch items", ex);
this.ShowMessage("Failed to load data", ToastType.ERROR);
}
}

Bad: Over-Using Vanilla​

// ❌ Several violations
public async FetchData() : Promise<void> {
console.log("Fetching..."); // Use LogIt
const el = document.querySelector("#table"); // Use data-oidis-bind
if (result === null || result === undefined) { // Use ObjectValidator
console.error("No result"); // Use LogIt
}
const msg = "Error in " + this.Name; // Use template literal
const r = childProcess.spawnSync("git", []); // Use Terminal
}

Bad: Over-Using Framework​

// ❌ Unnecessarily verbose
if (ObjectValidator.IsString(knownString)) { // typeof check is fine here
const parts = StringUtils.Split(knownString, ","); // str.split() is fine for known strings
const upper = StringUtils.ToUpperCase(parts[0]); // This doesn't even exist — use vanilla
}

See also: Naming Conventions | Formatting Rules | Utilities