Skip to main content
Version: 3.x

KenedoView, in Detail

Version
3.x
Updated
View markdown

A deep reference for KenedoView — the view layer of CBX's bespoke "Kenedo" MVC framework — plus the minimum of KenedoController and KenedoModel needed to see how the three connect. The goal is that a new hire can extend KenedoView itself with confidence, and a third-party integrator can understand what sits underneath the customization layer (template overrides, custom views, asset hooks) they build on.

This complements com_configbox_kenedo_mvc.md (the whole-MVC overview, esp. §2 controllers and §3–§4 the property-driven model). Where that doc summarizes the view in one section, this one is the full picture. All paths are relative to docroot/components/com_configbox/; the framework lives under external/kenedo/. Line references track current master — when one has drifted, search for the method name.

The base class is external/kenedo/classes/KenedoView.php (955 lines). Line references below are to it unless another file is named.


0. Mental model

A KenedoView is a PHP-template renderer with two personalities:

  1. A property-driven CRUD view. Pair it with a KenedoModel and it renders — with almost no code of its own — either the model's list (a sortable/filterable grid) or its edit form (one widget per model property). This is how every admin* screen in CBX is built. You write a ~15-line view class and a property-definition array on the model; the framework does the rest.

  2. A general-purpose HTML component. Ignore the model entirely, declare some public properties, override display(), and render whatever you want (the configurator page, a pricing block, a checkout step). It's just a class that produces a buffered HTML string.

Both personalities share one machine:

  • Instances are resolved by class name through getView() (customization dir wins over core for new view classes). Note: despite a static $instances field and "singleton" wording in the source, getView() actually returns a fresh instance every call (§1.1) — don't rely on it caching.
  • Output is buffered: getHtml() wraps display() in an ob_start()/ob_get_clean(), so a view can be rendered inline into any string — this is how views embed other views.
  • A view owns its assets. It declares which stylesheets and which JS init calls it needs; the framework injects them correctly on both a full page load and a later XHR injection.
  • A view isolates its markup. Its wrapper <div> always carries view-<viewname>, and the CSS convention is that every selector in a view's stylesheet starts with .view-<viewname> so styles can't leak.

The rest of this document: the lifecycle (§1), CRUD conventions (§2), multi-purpose views (§3), the class-property + curryable-setter conventions (§4), the asset system (§5), the CSS-isolation convention (§6), how the controller and model connect (§7), template resolution and the customization layer (§8), and extension guidance (§9).


1. The view lifecycle

1.1 Resolution — getView()

KenedoView::getView($className, $path = NULL) (:181) is the view factory. (It declares a static $instances registry (:69) and its docblock says "singleton", but that field is vestigialgetView() ends in return new $className(...) (:224) and never reads or writes $instances, so every call yields a fresh instance. Treat views as per-call objects.) Given a class name it:

  1. Derives the component and view name from the class name (:192-195): getComponentNameFromClass() (:259) → text before View (ConfigboxViewAdmincountrycom_configbox); getViewNameFromClass() (:263) → text after View, lowercased (admincountry).
  2. Looks for the view file core-first: views/<viewname>/view.html.php, then the customization dir data/customization/views/<viewname>/view.html.php (:198-207). (For view classes the loader is core-first / new-only — a customization view file only loads a class core doesn't define; to change an existing core view class use system_overrides/. See com_configbox_kenedo_mvc.md §7.)
  3. require_onces the file and returns new $className($className, $path).

The constructor (:228) stashes component, view, className, viewPath, defines the base-template path constants, and computes returnUrl from the return request param (base64-url-decoded) or the HTTP referer (:247-255).

A view name is derived from the class name, not storedConfigboxViewFooBar is always view foobar, folder views/foobar/. Keep the class name, folder name and (for CRUD) the controller pairing consistent or getView() won't find the file.

1.2 Rendering — display()renderView()

getHtml() // ob_start(); display(); return ob_get_clean(); (:271)
└─ display() // prepareTemplateVars(); renderView(); (:277)
├─ prepareTemplateVars() // splits on $this->listing (§2) (:282)
└─ renderView($tmpl=NULL) // picks a template, includes it (:459)
  • getHtml() (:271) is the "render me into a string" entry point — used when embedding a view in another view (§3.3).
  • display() (:277) is the "render me to the output buffer" entry point — used by the controller.
  • renderView($template) (:459) is where a template file is chosen and include()d with $this bound to the view. It first calls addAssets() (if the document type is html), resolves the template name (layout request param, else default, sanitized against path traversal, :470-481), then walks the 3-slot template precedence chain (§8.1) and echoes the first that exists — falling back to default when a request-supplied layout has no template for this view (§8.1).
  • getViewOutput($template) (:430) is renderView() captured to a string — used to render a sub-template of the same view (e.g. a footer or navigation partial) inline.

prepareTemplateVars() (:282) adds the base CSS classes then branches on the $this->listing flag (set by the controller, §7) into prepareTemplateVarsList() or prepareTemplateVarsForm(). A multi-purpose view that overrides display() (§3) may skip this split entirely.


2. Convention 1 — the backend CRUD view

This is the dominant use. A CRUD entity is four files following a strict plural = list / singular = form naming convention (worked example: Countries):

FileClassRole
controllers/admincountries.phpConfigboxControllerAdmincountriesthe controller (list + form factories)
models/countries.phpConfigboxModelCountriesthe data + property definitions
views/admincountries/view.html.phpConfigboxViewAdmincountriesthe list view (plural)
views/admincountry/view.html.phpConfigboxViewAdmincountrythe form view (singular)

2.1 The view classes are tiny

A CRUD view declares four things and inherits everything else. The list view (views/admincountries/view.html.php):

class ConfigboxViewAdmincountries extends KenedoView {
public $component = 'com_configbox';
public $controllerName = 'admincountries'; // where this view's tasks post to
function getDefaultModel() { return KenedoModel::getModel('ConfigboxModelCountries'); }
function getPageTitle() { return KText::_('Countries'); }
}

The form view (views/admincountry/view.html.php) is identical except it points at the singular title and (usually) the same model:

class ConfigboxViewAdmincountry extends KenedoView {
public $component = 'com_configbox';
public $controllerName = 'admincountries'; // note: still the PLURAL controller
function getDefaultModel() { return KenedoModel::getModel('ConfigboxModelCountries'); }
function getPageTitle() { return KText::_('Country'); }
}

There is no per-field code here. Whether this renders a grid or a form is decided by the $this->listing flag, which the controller sets (display()true, edit()false, §7).

2.2 What the base class does for a list (prepareTemplateVarsList, :295)

When listing = true, the base class:

  • asks the model for getPropertiesForListing()$this->properties (only props flagged into the list, ordered by positionList);
  • reads filters, pagination and ordering from persisted UI state (getFiltersFromUpdatedState()/getPaginationFromUpdatedState()/getOrderingFromUpdatedState(), :550/:575/:600);
  • calls $model->getRecords($filters, $pagination, $ordering)$this->records, builds the filter input HTML (getFilterInputs(), :623), pagination HTML, and the listingData map of hidden form fields the list's AJAX uses (:339-371);
  • $this->pageTasks = $model->getListingTasks() (the Add/Remove/Copy toolbar, §7.3).

The shared template external/kenedo/tmpl/default-list.phpdefault-table.php then loops $this->properties and $this->records, asking each property for its cell/header HTML. (See com_configbox_kenedo_mvc.md §4.2 for the property render hooks.)

2.3 What the base class does for a form (prepareTemplateVarsForm, :375)

When listing = false:

  • loads the record: $model->getRecord($id) for an existing id, else $model->initData() for a blank one honoring defaults/prefills (:380-385);
  • $this->properties = $model->getProperties() (all form props, ordered by positionForm);
  • computes pageTitle (appends the record's title/name if present, :392-398), recordUsage (reverse-references, for delete-safety), and pageTasks = $model->getDetailsTasks() (the Save/Save-and-Close/Cancel toolbar).

The shared template external/kenedo/tmpl/default-form.php is the payoff — the form is literally a loop over the model's properties (tmpl/default-form.php):

foreach ($this->properties as $property) {
$property->setData($this->record);
// <div class="kenedo-property ..." data-property-definition="...">
// <div class="property-label">…getLabelAdmin()…</div>
// <div class="property-body">…getBodyAdmin()…</div>
}

Same property list drives the SQL, validation, storage and this form — one source of truth. To add a field to a CRUD entity you add one property definition to the model (and a migration for the column); the form widget, list column, request parsing, validation and storage all follow. See com_configbox_kenedo_mvc.md §4 and §6.

2.4 Extending a CRUD form without abandoning the property loop

You usually don't override display() on a CRUD view. Instead you extend a hook and call parent::. ConfigboxViewAdminproduct (views/adminproduct/view.html.php) is the canonical example — it stays a property-driven form but adds custom JS and extra content:

function getJsInitCallsOnce() {
$calls = parent::getJsInitCallsOnce();
$calls[] = 'configbox/adminShapediverV2::initBackendPropsOnce'; // §5
return $calls;
}
function prepareTemplateVarsForm() {
parent::prepareTemplateVarsForm(); // keep the property loop
if (KenedoPlatform::getName() == 'wordpress') {
$this->contentAfterTitle = '…shortcode hint…'; // extra content under the title
}
}

contentAfterTitle (:80, allows HTML) is rendered by the base form template right under the page title — a clean seam for adding notices without a template override.


3. Convention 2 — the multi-purpose (non-CRUD) view

For anything that isn't a model grid/form — the configurator page, a pricing block, a navigation strip, a visualization — you write a view that returns NULL from getDefaultModel() and overrides display() (or prepareTemplateVars()) to render whatever it needs.

3.1 Anatomy (worked example: blockpricing)

ConfigboxViewBlockpricing (views/blockpricing/view.html.php) renders the price-overview block:

class ConfigboxViewBlockpricing extends KenedoView {
public $component = 'com_configbox';
public $controllerName = ''; // not tied to a controller's tasks

/** @var int $pageId Page the visitor is on @see setPageId */
public $pageId;
/** @var array $pricing @see ConfigboxModelCartposition::getPricing() */
public $pricing;
// …~30 more typed public template vars, each with a @var docblock…

function getDefaultModel() { return NULL; } // NOT property-driven

function getViewCssClasses() { // extra isolation classes (§6)
$classes = parent::getViewCssClasses();
$classes[] = 'configbox-block';
$classes[] = 'block-pricing';
return $classes;
}

function display() { // fully custom rendering
// … gather data into $this->* …
?><div <?php echo $this->getViewAttributes();?>> // §5: wrapper + asset attributes
<?php echo $this->getViewOutput(); /* main template */ ?>
<?php $this->renderView('footer'); /* a sub-template */ ?>
</div><?php
}

/** @return ConfigboxViewBlockpricing */
function setPageId($pageId) { $this->pageId = $pageId; return $this; } // §4.2
}

Note the pieces a multi-purpose view still uses from the base class:

  • getViewAttributes() for the wrapper <div> (so it participates in the asset system, §5);
  • getViewOutput('<tmpl>') / renderView('<tmpl>') to render its templates and sub-templates;
  • getViewCssClasses() override to add block-scoping classes.

3.2 When to override display() vs prepareTemplateVars()

  • Override prepareTemplateVars() (and keep renderView()) when you want the standard "prepare data → include the default template" flow but with your own data assembly. The configurator page does this (views/configuratorpage/view.html.php:326).
  • Override display() entirely when you want full control over the output structure (e.g. rendering the same template twice with different data, as blockpricing does for regular vs recurring pricing).

3.3 Embedding a view in another view

Because getHtml() returns a string, any view can render another and drop the result into its own markup. The idiom is getView → curryable setters → getHtml():

$selectionsHtml = KenedoView::getView('ConfigboxViewBlockpricing')
->setPageId($this->pageId)
->getHtml(); // views/configuratorpage/view.html.php:547

The configurator page embeds the pricing block, the navigation block, and the visualization block exactly this way. This is the primary reason setters are curryable (§4.2).


4. Convention 3 — class properties & curryable setters

4.1 Declare every template variable as a typed public property with a docblock

A view's public properties are its template's API — the templates read $this->x. The convention is to declare each one explicitly (not rely on dynamic assignment), with a @var docblock and, where useful, a @see pointing at the data's source. blockpricing and configuratorpage are the models to copy: ~30 declared public $…; fields each, every one documented (views/blockpricing/view.html.php:9-157, views/configuratorpage/view.html.php:9-230).

Why bother, given PHP would let you assign undeclared properties?

  • The declarations document the template contract — a template author (or an integrator writing an override) can read the class to know exactly what's available.
  • IDEs resolve $this->x in templates when the view is type-hinted (/** @var ConfigboxViewFoo $this */ at the top of each template).
  • It's the difference between a view that's safe to hand to another developer and one that isn't.

The base class also offers assign($key, $value) / assignRef($key, $value) (:516-522) as Joomla-style dynamic setters, but the declared-property style above is the convention for anything new.

4.2 Curryable (fluent) setters — return $this

A setter that configures the view before rendering should set the field and return $this, so calls chain:

/**
* @param int $pageId
* @return ConfigboxViewBlockpricing // annotate the CONCRETE class so chaining resolves in IDEs
*/
function setPageId($pageId) {
$this->pageId = $pageId;
return $this;
}

This is what makes the embedding idiom read well (§3.3): KenedoView::getView('ConfigboxViewSdvisualization')->setProductId($id)->setPositionId($pos)->getHtml() (views/configuratorpage/view.html.php:527-528, definitions at sdvisualization/view.html.php:133-149). The convention across the block/embeddable views (blockpricing, blocknavigation, blockvisualization, configuratorpage, sdvisualization, checkout, customerform, position) is: one field per setter, return $this.

The @return docblock is not uniform in the existing code — you'll find all three of @return <ConcreteViewClass> (blockpricing:335, sdvisualization, customerform), @return $this (blocknavigation, checkout), and @return static (position). Recommendation for new code: annotate the concrete subclass (e.g. @return ConfigboxViewBlockpricing) — it's the most useful of the three because the next ->set…() in the chain then type-resolves to the subclass in an IDE (@return $this/static on a non-final class resolve less precisely). Pick one and be consistent.

Curryable setters are for inputs you pass before rendering (ids, filters, a preset foreign key). They are not a substitute for prepareTemplateVars() — put derived/loaded data there, keep setters trivial.


5. Convention 4 — how a view controls its CSS and JS

A view declares its assets; the framework guarantees they load correctly both on a full page load and when the view is injected later by XHR. This is the part most worth understanding before you extend KenedoView.

5.1 The wrapper carries the manifest — getViewAttributes() (:906)

Every view template starts with <div <?php echo $this->getViewAttributes();?>>. That method emits:

<div class="cb-content kenedo-view view-<viewname> …"
data-view-id="<viewname>"
data-text-search="Type to search" // KText'd placeholder for Tom Select search fields
data-bs-theme="light|dark" // admin views on non-Joomla platforms only
data-stylesheets='["…url…", …]' // populated when output_mode=view_only (XHR), else '[]'
data-init-calls-once='["module::method", …]'
data-init-calls-each='["module::method", …]'>

The client AMD loader (assets/main.js) walks .cb-content:not(.view-init-started) on first paint and on every cbViewInjected event, marks the node view-init-started, injects the data-stylesheets (de-duped), runs the …-once calls once per page and the …-each calls on every appearance, then marks the node view-init-done. Full mechanics: customization/com_configbox_assets_and_amd.md §4.

To wait for a view, use view-init-done. view-init-started is set synchronously and only means "this node has been claimed"; the init calls it schedules run inside an async cbrequire callback and have not happened yet. view-init-done (and the cbViewInitialized event, which carries the view element) is added once they have — that is the point at which the view's handlers exist and a click on it will be heard. The start marker was called view-processed until 2026-08-16, a name that invited exactly the wrong reading.

data-stylesheets is now always emitted but only populated for output_mode=view_only (XHR) renders — it is the literal '[]' otherwise (:931-937). On a full page load the stylesheets go into the document <head> instead (via includeStylesheets()), and listing them again in the attribute would fight the CSS combiner. So: head on full load, attribute on injection — same URL list, two delivery paths.

Two newer attributes ride the same wrapper (:913-929): data-text-search — the localized placeholder for the search field the Tom Select enhancers put atop long dropdowns; it lives here because this is the one wrapper every view emits, and a string shown to visitors has to come from KText, not JS. And data-bs-theme — the resolved admin light/dark scheme, stamped on admin views on non-Joomla platforms only (Joomla's Atum template already sets it on <html>; auto is left unstamped so the prefers-color-scheme fallback in admin-theme.css follows the OS).

5.2 Stylesheets — getStyleSheetUrls() (:740)

Override this to add view-scoped CSS. The convention is parent:: then append:

function getStyleSheetUrls() {
$urls = parent::getStyleSheetUrls(); // bootstrap-namespaced, fontawesome, general.css, (admin.css)
$urls[] = KenedoPlatform::p()->getUrlAssets().'/css/configurator.css';
return $urls;
}

(Real example: views/configuratorpage/view.html.php:232-237.) The base getStyleSheetUrls() already adds the namespaced Bootstrap + FontAwesome, general.css, the admin theme/UI sheets for admin* views, the host-adaptation stylesheets, and — last, so it always wins — the customization custom.css if it exists (:767-769).

You don't call these URLs directly. getOptimizedStylesheetUrls() (:667) post-processes the whole list: merges in the properties' stylesheets (§5.4), de-dupes, moves custom.css to the end, swaps foo.cssfoo.min.css when use_minified_css is on and the min file is fresh, and appends a ?version= cache-buster for first-party URLs (:695-730).

5.3 JS — getJsInitCallsOnce() / getJsInitCallsEach() (:782 / :799)

A view names AMD modules (and optionally a method) to run. Format is "moduleId::method", or just "moduleId" to load a module without calling anything. Same parent:: then append convention:

function getJsInitCallsOnce() { // once per page
$calls = parent::getJsInitCallsOnce();
$calls[] = 'configbox/adminShapediverV2::initBackendPropsOnce';
return $calls;
}
function getJsInitCallsEach() { // every time the view is (re)injected
$calls = parent::getJsInitCallsEach();
$calls[] = 'configbox/adminShapediverV2::initBackendPropsEach';
return $calls;
}

(Real example: views/adminproduct/view.html.php:23-36; the configurator page uses configbox/configurator::initConfiguratorPage / …Each, views/configuratorpage/view.html.php:239-254.) The method receives the view (a jQuery collection) as its argument. The base class already wires configbox/admin::initBackendOnce/Each for admin* views (:786-805).

Once vs Each is the load-bearing distinction:

  • …Once — page-global setup that must not repeat (bind one document-delegated handler, init a singleton).
  • …Each — per-view wiring that must run on every injection (a view can be injected more than once). Make …Each init idempotent: scope work to the passed view and guard already-initialized nodes with a marker class.

5.4 Properties contribute assets too

For a CRUD form, individual properties can declare their own CSS/JS (a colorpicker property ships its picker's assets). getViewAttributes() merges the properties' calls via getPropertyInitCalls() (:822) and getOptimizedStylesheetUrls() merges their CSS via getPropertyStyleSheetUrls() (:855) — both work because $this->properties is populated (list or form) before the wrapper renders. So a property added to a model automatically brings its assets into any view that renders it, with no view change. See KenedoProperty::getStyleSheetUrls()/getJsInitCallsOnce()/getJsInitCallsEach() (external/kenedo/classes/KenedoProperty.php:931/954/976) and customization/com_configbox_custom_properties.md §8.


6. Convention 5 — CSS selector isolation (.view-<viewname>)

getViewCssClasses() (:878) always puts view-<viewname> on the wrapper <div> (alongside cb-content and kenedo-view). The matching stylesheet convention is: start every selector with .view-<viewname> so a view's CSS can only ever affect that view.

/* assets/css/configurator.css */
.view-configuratorpage .block-navigation {}
.view-configuratorpage .overviews .wrapper-selections {}

/* assets/css/checkout.css */
.view-checkout .trigger-place-order {}

This is followed consistently and pervasively across the codebase — e.g. ~137 rules under .view-adminruleeditor, ~108 under .view-customerform, ~64 under .view-configuratorpage, ~64 under .view-checkout. When you write CSS for a new view, scope every rule the same way. For a sub-component that appears inside a view (a "block"), add an extra class in getViewCssClasses() (as blockpricing adds configbox-block block-pricing, :166-171) and scope under that.

Two well-understood exceptions to the strict per-view prefix:

  • A shared stylesheet that styles several sibling views scopes each block under its own .view-<name> (e.g. configurator.css carries .view-configuratorpage, .view-blockpricing, .view-blockvisualization, .view-sdvisualization).
  • A rule that intentionally applies to any Kenedo view scopes under the shared .cb-content class (also added by getViewCssClasses(), :881) instead of a per-view class — e.g. configurator.css has .cb-content .question, .cb-content .answer { … }. That is still isolated to CBX content, just at the component level rather than the view level. Use .cb-content only when the rule should be view-agnostic; otherwise default to .view-<name>.

Why it matters: CBX runs inside a host CMS (Joomla/WordPress/Magento) whose theme CSS is on the same page, and views get injected/removed dynamically. The .view-<name> prefix is the whole isolation strategy — there is no Shadow DOM, no CSS modules, no build-time scoping. Break the convention and your rules leak into the host theme or sibling views.


7. How the Controller and Model connect to the View (the minimum)

KenedoView is the V; here is just enough of the C and M to see the wiring. Full detail: com_configbox_kenedo_controller.md (the controller in depth), com_configbox_kenedo_model.md (the model in depth), and com_configbox_kenedo_mvc.md §2–§3.

7.1 Controller → View

A controller extends KenedoController and implements the view factories. The plural controller owns both views (controllers/admincountries.php):

protected function getDefaultModel() { return KenedoModel::getModel('ConfigboxModelCountries'); }
protected function getDefaultView() { return $this->getDefaultViewList(); }
protected function getDefaultViewList() { return KenedoView::getView('ConfigboxViewAdmincountries'); } // list
protected function getDefaultViewForm() { return KenedoView::getView('ConfigboxViewAdmincountry'); } // form

The base tasks pick the view and set the listing flag that the view branches on:

  • display() (KenedoController.php:312): $view = getDefaultView(); $view->listing = true; then wrapViewAndDisplay($view).
  • edit() (:336): $view = getDefaultViewForm(); $view->listing = false; then wrapViewAndDisplay($view).

wrapViewAndDisplay($view) (:1265) chooses the chrome by output mode:

  • view_only (AJAX) or in_html_doc (embed) → $view->display() bare.
  • an admin* view (and not admin itself) → wrap it in ConfigboxViewAdmin (the admin shell: menu + frame) by setting $wrapper->contentHtml = $view->getHtml(). This is a view embedding a view via getHtml() — the same idiom as §3.3.

So "which template" ultimately comes from three inputs the controller/request set on the view: listing (list vs form), output_mode (bare vs wrapped), and the layout request param (default vs a named template).

7.2 Model → View (data)

The view pulls everything from getDefaultModel():

  • getProperties() / getPropertiesForListing() (KenedoModel.php:1243/1414) → $this->properties (the form/list field set).
  • getRecord($id) / initData() (:1616/:1476) → $this->record (form).
  • getRecords($filters, $pagination, $ordering) (:1694) → $this->records (list).
  • getRecordUsage($id) (:2157) → reverse-reference report (delete-safety on the form).
  • getFilterNames() (:1877) → the list's available filters.

7.3 Model → View (toolbar)

getListingTasks() (:1434) and getDetailsTasks() (:1449) return the button definitions the templates render into the toolbar (Add/Remove/Copy for lists; Save and Close/Save/Cancel/Help for forms). Each entry is ['title'=>…, 'task'=>…, 'primary'=>bool]; the task string is the controller method the button triggers. Override these on a model to change a screen's toolbar.


8. Template resolution & the customization layer

8.1 The 3-slot precedence chain

renderView() (:459) tries these paths in order and includes the first that exists (:493-520):

  1. Joomla template overrideKenedoPlatform::p()->getTemplateOverridePath($component, $viewName, $template) (the host theme's html/com_configbox/<view>/<tmpl>.php).
  2. Customization templatedata/customization/templates/<viewname>/<tmpl>.php.
  3. Core template — the view's own views/<viewname>/tmpl/<tmpl>.php.

<tmpl> is the layout request param, else default, sanitized against /, \, . (:470-481). So an integrator overrides a screen's markup by dropping a file in slot 2 without touching core, and the view class stays the same. (The question view adds two extra slots for its per-type templates — see com_configbox_configurator_questions.md §3.)

A layout from the request falls back to the view's default (:493-497). A layout named in the URL is meant for the page's main view — but every view rendered during the same request sees the same request variable, and would look for a template named after someone else's layout. That template does not exist for it, and the whole page used to die with "View template not found": /account.html?layout=login took the site down wherever the currency-selector module was published (the comment block at :487-492 tells the story). So when the template name came from the request and is not found, the chain is walked again for default. A template name passed by a caller gets no such mercy — it must exist, so a typo in your own renderView('footer') still fails loudly.

Template ≠ view class. Templates are customization-first (your file in slot 1/2 shadows the core template). View classes are core-first (a customization views/<name>/view.html.php only loads if core has no such class). To change an existing view class's PHP behavior, use system_overrides/, not a same-named file under views/. This asymmetry trips people up — see customization/com_configbox_overriding_views_and_templates.md.

8.2 What a template gets

The template is include()d with $this bound to the view, so it reads $this->record, $this->records, $this->properties, $this->pageTitle, and any custom public properties you declared (§4.1). Convention: start each template with /** @var ConfigboxViewFoo $this */ for IDE resolution, and open with <div <?php echo $this->getViewAttributes();?>> so the view participates in the asset system.


9. Extending KenedoView — practical guidance

To build a new admin CRUD screen (most common): don't touch KenedoView at all. Write the four files (§2), put the real work in the model's getPropertyDefinitions(), deliver the column via a migration. The adminmvcmaker admin tool scaffolds all four from templates. Override a view hook (getJsInitCallsOnce/Each, prepareTemplateVarsForm, getListingTasks) only when you need something the property model can't express (§2.4).

To build a new frontend component: write a multi-purpose view (§3) — getDefaultModel() returns NULL, declare typed public properties (§4.1), override display() or prepareTemplateVars(), give it curryable setters if it'll be embedded (§4.2), scope its CSS under .view-<name> (§6), declare its assets (§5). Model it on blockpricing/configuratorpage.

To modify KenedoView's own behavior (the "improve KenedoView functionality" case): the base class is in external/kenedo/ and shared across all platforms, so treat changes as framework-level:

  • The extension points are protected/overridable methodsgetViewCssClasses(), getViewAttributes(), getStyleSheetUrls(), getJsInitCallsOnce/Each(), prepareTemplateVarsList/Form(), renderView(). Prefer adding a new overridable method or a new hook over inlining behavior into display().
  • The asset manifest is the most likely thing to extend. If you add a new data-* channel, remember that data-stylesheets is only populated on view_only renders ('[]' otherwise, :931-937) — anything that must also work on a full page load needs a head-side delivery path too. Mirror that split.
  • getView() returns a fresh instance every call (the static $instances "singleton" is vestigial, §1.1). So two getView('ConfigboxViewX') calls are independent objects — fine for embedding the same view twice with different setters. Don't assume caching (state doesn't carry between calls); if you ever add real singleton caching, audit callers that rely on getting a clean instance.
  • No DI, no namespaces, manual output buffering, hsc() for escaping — match the surrounding style (see com_configbox_kenedo_mvc.md §8 for the framework-wide caveats). Don't introduce PSR/Joomla-MVC idioms into Kenedo code.

Gotchas worth internalizing:

  • View name is derived from the class name; keep class/folder/controller names consistent.
  • …Each JS and per-view init must be idempotent (views inject more than once).
  • Scope all CSS under .view-<name>; there is no other isolation.
  • custom.css loads last by design (override layer) — don't rely on source order for your own sheets.
  • Curryable setters annotate the concrete subclass so chains type-resolve.

See also

  • technical/com_configbox_kenedo_controller.md — the controller layer in depth (the other half of the C↔V handoff in §7): request dispatch, the store() save flow, the task catalog, and authorization.
  • technical/com_configbox_kenedo_model.md — the model layer in depth (the data source behind §7.2/§7.3): the property-driven CRUD engine, getRecord/getRecords, getListingTasks/getDetailsTasks, and the model customization seams.
  • technical/com_configbox_kenedo_mvc.md — the whole MVC: controller base tasks (§2), the property-driven model (§3–§4), the customization/override precedence rules (§7), and framework caveats (§8). Read alongside this doc.
  • technical/com_configbox_configurator_questions.md — a large real-world case study of views + assets: the configurator page view, per-type question views, and the AMD view-asset engine end to end.
  • customization/com_configbox_overriding_views_and_templates.md — the integrator's how-to for template overrides and new views (the template-vs-view-class precedence asymmetry).
  • customization/com_configbox_assets_and_amd.md — the client side of §5: the AMD loader, the data-init-calls-* / data-stylesheets consumption, and custom.css/custom.js entry points.
  • customization/com_configbox_custom_properties.md §8 — how a property contributes its own CSS/JS into the view's asset manifest (§5.4).
  • Key source: external/kenedo/classes/KenedoView.php, external/kenedo/tmpl/{default-form, default-list,default-table}.php, external/kenedo/classes/KenedoController.php (display/edit/wrapViewAndDisplay :312/:336/:1265), external/kenedo/classes/KenedoModel.php (getProperties/getRecords/getListingTasks/getDetailsTasks), and views admincountry/admincountries (minimal CRUD), blockpricing (multi-purpose + curryable setter), adminproduct (CRUD + custom assets), configuratorpage (rich multi-purpose).