A custom coach view has four parts - layout (HTML template with the content box if it is a container), Behavior (JavaScript handlers), configuration options (typed inputs that coach authors set), and included files (CSS / JS). Structure that has proven maintainable:
<!-- Layout: keep it minimal; give a root element with a stable class and, for containers, a content box -->
<div class="cv-signature">
<canvas class="cv-signature-pad"></canvas>
<div class="cv-signature-actions"><button type="button" class="cv-signature-clear">Clear</button></div>
</div>
// Behavior > load: build the widget, wire events, subscribe to binding changes; nothing that needs the element's size
load: function () {
var self = this, el = this.context.element;
this._pad = new Packages_free_SignaturePad(el.querySelector(".cv-signature-pad")); // library from an included JS file
this._onClear = function () { self._pad.clear(); self.context.binding.set("value", ""); };
el.querySelector(".cv-signature-clear").addEventListener("click", this._onClear);
this._pad.onEnd = function () { self.context.binding.set("value", self._pad.toDataURL()); self.context.trigger(); }; // trigger = fire boundary event if configured
if (this.context.options.readOnly) this._readOnlyHandle = this.context.options.readOnly.bindAll(function (e) { self.setReadOnly(e.newVal); }, this); // option binding
},
// view: size-dependent work; runs again when the view becomes visible
view: function () { this._pad.resize(this.context.element.clientWidth, 150); if (this.context.binding.get("value")) this._pad.fromDataURL(this.context.binding.get("value")); },
// change: data changed from outside (setData, service result) -> refresh the widget without re-firing our own events
change: function (event) { if (event.type === "binding" && this._pad && event.newVal !== this._pad.toDataURL()) this._pad.fromDataURL(event.newVal || ""); },
// unload: release everything
unload: function () { this.context.element.querySelector(".cv-signature-clear").removeEventListener("click", this._onClear); this._pad.destroy(); this._pad = null; },
// public API for coach authors (this is what ${MyView}.method() calls)
getSignature: function () { return this.context.binding.get("value"); },
setReadOnly: function (ro) { this._pad.readOnly = ro; this.context.element.classList.toggle("is-readonly", !!ro); }- Configuration options for everything a coach author might vary (labels, sizes, read-only, colours); read them once in load; options can be bound to variables, so subscribe with bindAll when they may change at runtime.
- Binding: one business data binding, typed (String, a business object, a list); write through this.context.binding.set so that other views and validation see it; never keep the "real" value only inside the widget.
- Events: expose behaviour through the view's public methods and coach events (the UI Toolkit style: "On change", "On click" event options that run author-supplied JavaScript); fire a boundary event with this.context.trigger() only if the coach author asks for it (an option), because boundary events leave the coach.
- Styling isolation: prefix every CSS class (cv-signature-*), scope rules under the root class, never style toolkit classes globally; include CSS as a managed file in the view, not inline in coaches.
- Composition: for a container control, put a content box in the layout - the framework manages the child views (they load before your load handler), and the parent view can reach them through the context's subview access.
- Table rows: your view will be instantiated per row - make load cheap and stateless; heavy libraries should be initialised lazily in view.
- Upgrade safety: depend on the UI Toolkit's documented APIs (setData / getData, events) and on your own view's API; do not reach into the DOM of toolkit controls - their markup changes between versions.
Test the view in a small test coach in the same toolkit with every option combination, read-only mode, inside a table and inside a modal section; that catches most lifecycle bugs before the view is used in ten apps.
References