User:Adamw/Drafts/Component templates RFC

This document describes a suggested enhancement to wikitext templates. It began as an exploration for the WMDE Technical Wishes, but has drifted into “rogue RFC” waters.

Proposal
Introduce a new format for single-file templates, we call it “component” templates here, that flexibly encapsulates multiple heterogeneous blocks of different types, such as wikitext templates, Lua scripts, front-end ECMAScript, template docs, TemplateData, i18n messages, and stylesheets. This integrated format is inspired by the recent Web Components standard and by trends towards single-file components.

A component template may name its child elements, which makes them addressable. For example, a script could render multiple subtemplates independently, looking each up by name.

Component templates are backwards-compatible with discrete pages for each content type, and the two paradigms can be safely mixed. In fact, discrete pages are one way to override component fragments.

Motivation
To quickly paint a backdrop for Wikipedia’s template support, it was added in small steps, beginning with transclusion in 2003. This was a time before standardized languages for template processing (PHP itself was considered such a language, for example), this was a Wild West in which each company would invent their own frameworks. The architectural mantra “model-view-controller” was dominant, and programs were often organized into “layers”. The Template namespace is one such layer, and was originally meant to hold pure presentation. By 2006 the “#if” statement was added to the language, bringing with it the burden of Turing-completeness. An attempt was made to divert this flow of logic into templates by introducing Lua, a full programming language callable from the original templating system.

The main problem is that the dependencies are pointing the wrong way: in MVC the controller is supposed to manipulate models, then model updates propagate to the view. A presentation template shouldn’t have to “know” anything requiring advanced switching logic. If we flip this dependency and wikitext templates are no longer responsible for logic and control flow, extracting that logic will simplify the presentation.

Moving to a single-file format addresses a second major shortcoming, that the template “layer” obscures important structural facts, that templates exist to accomplish a limited behavior, and that they exist in a tightly coupled matrix of supporting content types. The boundaries between components cut across these different types. Collecting related fragments together gives us logical locality, it becomes easier to reason about a feature.

Putting these two points together, scripted logic keeps control and can compose the component’s content in any way desired.

A single-file format should lower the barriers to reusable “global templates”, because components are structurally compact and portable.

The “lang” attribute adds the flexibility to experiment with a variety of templating and scripting languages serving the same purpose, by supporting transparent interchangeability.

Encapsulation and embedding of the different supporting content types (, , Lua logic, docs) is already taking place, so this is partially consistent with existing momentum for change.

Implementation
The prototype can be written as a tag extension. Wikitext enclosed in “ ” tags will be treated specially, and its contents will be interpreted by the extension.

Status: Successfully implemented the  tag and local template transclusion, can run the examples in this document.

Proof of concept: https://gitlab.com/adamwight/ComponentTemplate

Namespace
Components will live in the Template namespace, or in MediaWiki if using privileged tags.

New tags
The page content is a root  element. Child blocks like,   and   build up the content and functionality.

Each child element may optionally have a name. This allows for multiple wikitext or style snippets, individually addressable and overridable. Various flavors such as wikitext vs. mustache, or less vs. css can be chosen through the “lang” attribute.

Atomicity
Atomicity is a nice-to-have which legacy templates are lacking.

It should be possible to avoid a broken, intermediate component in the event that multiple fragments need to be updated simultaneously. For example, a stylesheet change corresponding to a newly added element. This requires that component source code and cached object code can be updated atomically rather than each fragment updating in sequence.

Transclusion syntax
Component transclusions have the same syntax as normal templates: double curly braces, pipe characters, and string values:""Component templates are called like legacy templates.

They can also be transcluded directly from Lua,"frame:expandTemplate( ‘ABC’, args = { 1 = ‘alpha’, legend = ‘Alphabet’ } )"Lua to call a component, same as for legacy templates.

It would be possible to wrap Lua’s “require”, so that a component can be loaded like a module, but still take along its payload of presentation, etc.

Parameter binding
A component template receives its arguments as a {string:string} map bound to the script’s frame.args, just like legacy templates.

Entrypoint
Compilation of a component goes like,


 * Evaluate the Lua script if present (see below).
 * If no script tag is present, then the first template element is evaluated and returned.

Let’s create a new convention for Lua scripts when used in a component template context: if the export table includes the function “ ”, that will be called with the template arguments. This simplifies script integration and reduces the boilerplate in each call. “ ” becomes “ ”. If the “ ” function is available on a Lua script’s returned interface, this becomes the entrypoint and receives all template parameters.

As a courtesy, when the Lua script is missing a  function, we assume that the script is written for   and inject a compatibility shim which behaves as below. It takes the first template parameter, uses it as a function name, and shifts the remaining parameters. In the above example, we could call the template like “ ”. This is how the shim is implemented: function p.__call(self, func_name, ...)

return self[func_name](...)

end Implicit compatibility shim dispatches to a named function.

Static elements
Stylesheets, client-side scripts, embedded images, and other static elements are carried over to the final output, and deduplicated rather than appearing multiple times.

Lua can control whether static elements are output, addressing them by name.

Security
There will have to be a distinction made between types of fragment, so that permissions can be differentially applied to each. For example, template doc subpages should be left open to all editors even when the template itself must be protected (because popular templates are highly-valuable to vandals).

One alternative is to disallow certain tags in the Template namespace, but allow in the admin-only MediaWiki namespace. The disallowed tags might include frontend scripts and stylesheets.

Another more complex alternative would be to block page save only if insecure fragment types are modified, in any namespace.

Caching
For the prototype we can rely on normal page caching. If transpilation steps cause latency, we can cache the object code.

Customization
Many wikis will want to customize a template, its presentation, and translations. Forking a template is simple and effective, but causes problems in the long-term. We would like to do better with components.

Any named element can be overridden by creating a subpage with that name. To override a subtemplate in Template:Foo called “name”, create Template:Foo/name. To protect an element from customization (“closed for modification” / “final” / “private”), either don’t give it a name, or use an ad-hoc indicator such as a leading underscore: “_name”.

If Template:Foo calls, the lookup order will be:


 * First, check for a subpage override: “Template:Foo/name”.
 * Next, search for named siblings within the component: “Template:Foo#name”.
 * Finally, look for a normal template: “Template:name”. [may be undesirable to mix local and global template lookup]

Migration
A legacy wikitext template can be turned into a component by wrapping it in “ ” and “ ” tags. Style blocks, subtemplates, and additional elements can be added incrementally.

Examples
These snippets can be evaluated using the proof-of-concept ComponentTemplate extension.

Use case: Move logic out of templates, and presentation out of code
Even something as simple as parameter fallbacks have proven unwieldy with wikitext templates:"Author name:" }}Template:Infobox writer—tainted by logic

One might try to shift the complexity of parameter aliasing to Lua, where we expand another template or perform the template’s presentation work using string concatenation. For example, local p = {}

function p.clean(frame)

local args = mw.getCurrentFrame:getParent.args

local author = args.penname or

args.pseudonym or args.name

return “Author name: “ .. author

end

return p Module:Infobox writer—tainted by presentation

But moving the concatenation into code also goes against the isolation of logic, this is a presentation detail and should be left in a template.

Here’s the same logic as a component, but with the dependency reversed so that the script expands a template.

Author name:

local p = {}

function p.__call(frame)

local args = frame.args

return mw.ext.ComponentTemplate.expandLocalTemplate(

"Template:Infobox writer as component", {

author = args.penname or args.pseudonym or args.name

})

end

return p

Template:Infobox writer—as component

Use case: Simplify Lua integration; move templating out of code
Lua scripts have so far been in the Module namespace, and the Lua invocation is almost always hidden in a dedicated, wikitext, Template page. Using templates from within Lua is rarely done, probably because of the danger that templates will change and break tight coupling. This friction in both directions could be reduced if the Lua and its wikitext template could be packaged together.

For example, Lua modules are often wrapped in a shim template like this one:

{   “description”: “Given a city name, display a rain gauge with the historical average rainfall and decorations appropriate for the current weather.”, "params": { "city_name": { "label-en": "City name", “description-en”: “Nearest city with recorded weather.”, "example-en": "bar.", "type": "string", "required": true }   } } Template:City rainfall—Non-component implementation

Currently, many Lua modules generate wikitext and HTML directly using the `mw.html` library and string concatenation.

 local weatherLookup = require('Module:Weather lookup').main

local function renderRow(location) return [[ end
 * class=”field-name”
 * ]] .. location.city_name .. ‘\n’ .. [[
 * ]] .. location.average_rainfall .. ‘\n’
 * ]] .. location.average_rainfall .. ‘\n’

local function renderTable(weatherReport) local tableRows = {}

for location in weatherReport.locations do       table.insert(tableRows, renderRow(location)) end

return ! !  ( .. weatherReport.unit .. ‘)\ ‘ .. tableRows .. ‘|}’ end

local p = {}

function p.__call = function(frame) return renderTable(         weatherLookup{            city = frame.args.city,           fields = { ‘rainfall’ }        }    ) end

return p

Module:City rainfall—Non-component implementation

As a component, the pieces could be bundled together as one page, and templates fully extracted from the script. The component renderer can detect whether this is a transclusion or template page view, and emulate the / logic implicitly.

{   “description”: “Given a city name, display a rain gauge with the historical average rainfall and decorations appropriate for the current weather.”, "params": { "city_name": { "label-en": "City name", “description-en”: “Nearest city with recorded weather.”, "example-en": "bar.", "type": "string", "required": true }   } }  , HTML, or whatever, documenting your template.    local weatherLookup = require('Module:Weather lookup').main local function renderRow(frame, location) -- This is an exciting new function which loads -- templates from the component itself. return frame:expandLocalTemplate(       name = “table-row”, location    ) end local function renderTable(frame, weatherReport) local tableRows = {} for location in weatherReport.locations do   table.insert(tableRows, renderRow(frame, location)) end return frame:expandLocalTemplate(        name = ‘main-table’,        {            “tableRows” = tableRows,            “unit” = weatherReport.unit        }    ) end local p = {} function p.__call = function(frame) return renderTable(        frame,         weatherLookup{             city = frame.args.city,            fields = { ‘rainfall’ }         }     ) end return p Template:City rainfall—Component implementation
 * class=”field-name” |
 * class=”field-name” |

Use case: Frontend interaction technologies
The single-file design is extensible, all we need to do is match elements to a preprocessing handler. Additional element types might include an open choice of transpiled, front-end languages which enable interactivity and modern tooling. See the “Future Directions” section. Platforms we could support include React, Vue, and TypeScript. As an example, an interactive timeline might respond to clicks by highlighting the corresponding article sections:

  Timeline leading to the general strike.    import TimelineSlider from 'timeline-slider-component'; import 'timeline-slider-component/theme/material.css'; export default class TimelineSlider export default class CustomizedTimelineSlider extends Vue { private articleContentTimeline: number[]; ... } Future template with transpiled, front-end scripts.