Model location: model, subViews, and children

Do not assume every root exposes its data as window.model. Some composite roots publish child models through window.subViews, while others keep the relevant model on a nested view.

function findModel(windowObject, predicate) {
  if (windowObject.model && predicate(windowObject.model)) {
    return windowObject.model;
  }
  const subViews = windowObject.subViews || {};
  for (const key of Object.keys(subViews)) {
    const candidate = subViews[key] && subViews[key].model;
    if (candidate && predicate(candidate)) {
      return candidate;
    }
  }
  return null;
}

Use model fields or the native view identity for discovery; DOM class names are often generated and less durable.

Replace placeholders instead of appending duplicates

Tooltips and grouped widgets frequently create empty placeholders for each resource type. Fill the matching placeholder and preserve its aggregated fields rather than appending a second entry.

  • Identify the placeholder by stable type identity.
  • Preserve aggregate depot count and expiry information.
  • Replace only the presentation fields owned by the feature.
  • Keep native collection order.
  • Do not mutate unrelated variants that happen to share a similar icon path.

Match by stable identity, then use presentation fallbacks narrowly

Prefer native IDs or typed keys. Presentation assets can be lossy: a compact surface may normalize a premium/tier suffix or reuse one image path for several variants.

If a fallback must ignore presentation detail, scope it to the one projection that performs that normalization. Applying a tier-insensitive match globally can decorate the wrong full-size card.

Preserve native ordering

When projecting new entries into an existing collection, use the native resource order or sorter. Click order, discovery order, or object dictionary order should not rearrange familiar UI.

const order = ['vehicleXp', 'credits', 'combinedXp'];
items.sort((left, right) => {
  return order.indexOf(left.resourceType) - order.indexOf(right.resourceType);
});

Preserve native collection ownership

When a native Wulf or Gameface view builds and owns a collection, prefer calling its normal builder and changing only the smallest supported fields on the resulting models. Replacing the entire collection or builder transfers more lifecycle responsibility than the visible update suggests.

  1. Call the native population method exactly once and preserve its return value.
  2. Enter the verified transaction or mutation context if the generated model requires one.
  3. Post-process only fields the feature owns; keep native item identity, array ownership, and disposal behavior intact.
  4. Test refresh, confirmation, cancellation, route exit, and view destruction—not only the populated screen.

Update the model and notify the host

A collection mutation may not be visible if the host presenter also caches derived properties such as hasItems, activeCount, or a version token.

  1. Enter the model’s transaction or mutation context.
  2. Update the smallest owned fields or collection entries.
  3. Update related derived properties when the native contract requires it.
  4. Trigger the host’s normal update signal or event.
  5. Reject the update if the view generation is already destroyed.
# Exact methods are model-specific and must be verified.
with model.transaction() as tx:
    _replace_owned_entry(tx.getItems(), entry)
    tx.setHasItems(True)
    tx.setActiveCount(_count_active(tx.getItems()))
view.onUpdate()

Model readiness is not DOM readiness

The native model may be populated before React renders the target nodes, and the DOM may exist before its layout dimensions settle.

  • Subscribe to the narrowest native data-change signal.
  • Queue DOM work rather than performing it repeatedly in one event burst.
  • Use a MutationObserver for React-owned nodes that can be recreated.
  • Use one or two animation frames when layout must settle; do not use unbounded polling.
  • Stop observers, timers, and model subscriptions when the owning view disappears.

Route-specific repairs

Result screens and lobby containers may reuse a parent application while switching routes or tabs beneath it. A repair should be restricted to the verified route and model schema.

  • Gate by route, mode, and result type.
  • Use a stable account/vehicle/efficiency key rather than row index.
  • Copy only presentation already exposed elsewhere in the same result model.
  • Exclude modes whose identity or efficiency schema differs until verified.
  • Reapply after native model replacement, not on every animation frame.

Presentation layers and pointer behavior

Decorative injected layers should not change card interaction.

  • Place background effects below the native artwork.
  • Place borders or highlights above the artwork.
  • Use pointer-events: none for non-interactive decoration.
  • Avoid filters that make an enabled item look disabled.
  • Scope CSS selectors beneath an owned root class to avoid affecting unrelated cards.

Listener identity

JavaScript event removal requires the exact function object used for registration. Every call to this.method.bind(this) creates a new object.

class ProjectionController {
  constructor(model) {
    this.model = model;
    this.onDataChanged = this.onDataChanged.bind(this);
  }

  start() {
    this.model.onDataChanged(this.onDataChanged);
  }

  stop() {
    this.model.offDataChanged(this.onDataChanged);
  }
}

Diagnostics

Symptom Likely cause Check
Data changes but UI does not The collection changed without the host’s derived fields or update event. Trace the native presenter update path.
CSS loads but cards are unaffected The injection is attached to a nested model owner, not the DOM-owning root. Inspect the child-view graph and Gameface environment.
Duplicate cards appear A placeholder was appended instead of replaced. Inspect the native empty entries and aggregation fields.
Wrong item stays highlighted A lossy icon-path fallback was used across all projections. Match by ID first and scope presentation fallback narrowly.
Repair works only on one tab Other routes use different model keys or rebuild the collection. Trace each route separately and subscribe to its update signal.

Verification checklist

  • Initial load, native data refresh, route change, and view recreation.
  • Empty, one-item, and multi-item collections.
  • Variants sharing similar icon paths.
  • Native ordering after insertion or replacement.
  • Native-owned collection cancellation and teardown after post-processing.
  • Model transaction and host update event.
  • React rerender and DOM replacement.
  • Battle/lobby transition and replay rewind where applicable.
  • All listeners, observers, timers, and injected children removed on teardown.