Flow/Architecture/Templating

From MediaWiki.org
Jump to navigation Jump to search

The Flow/Epic Front-End rewrite of April 2014 uses Handlebars to render in JavaScript, and the lightncandy PHP implementation. This allows templates to be shared between front-end and back-end.

MediaWiki is in the middle of Requests for comment/HTML templating library, but the Flow team couldnt wait.

Using[edit]

The templates are in the handlebars subdirectory.

For example, handlebars/flow_post.html.handlebars:

{{#with revision}}
   <div class="flow-post{{#if isModerated}} flow-post-moderated{{/if}}">
       {{#with author}}
           <span class="flow-author"><a href="{{links.contribs.url}}" title="{{links.contribs.title}}" class="mw-userlink flow-ui-tooltip-target">{{name}}</a> <span class="mw-usertoollinks">(<a href="{{links.talk.url}}" class="new flow-ui-tooltip-target" title="{{links.talk.title}}">{{l10n "Talk"}}</a>{{#if links.block}} | <a class="flow-ui-tooltip-target" href="{{links.block.url}}" title="{{links.block.title}}">{{l10n "block"}}</a>{{/if}})</span></span>
       {{/with}}
       <div class="flow-post-content">
           {{html content}}
       </div>
...

This

  1. sets the scope to the 'revision' key of the API response.
  2. outputs some HTML
  3. uses handlebars' limited logic to conditionally output a class if the key isModerated is true in this scope
  4. sets the scope to the 'author' key within 'revision', in preparation of outputting the usual MediaWiki Username (talk | contribs) HTML.
  5. outputs the value of various bits within the response.revision.author structure.
  6. Uses the l10n helper function to output some message strings.
  7. Then uses the html helper function (described below) to render the revision.content.

General template structure[edit]

In handlebars templates:

  • {{> template_name}} includes in another template
  • {{func_name}}
  • {{var_name(.varname2 ...)}} outputs the string value of some key in the current "this" scope
  • ~ inside the curly braces eats whitespace, thus {{~var_name}}eats all whitespace before the {{ and {{~var_name~}} eats all trailing whitespace.
  • {{#func_name}} ... {{/func_name}} invokes a block function, like {{#if someboolean}} ... {{/if}} above. Flow has its own block helper function, like {{#eachPost}} ... {{#eachPost}}
  • {{#-- Comment here --}} for comments

Note that templates aren't JavaScript (or PHP). You can't have expressions in template parameters. For example, {{#if foo && bar}} won't work. You have to supply appropriate values to the template, or in this case nest #if blocks.

In general one main template includes other templates, often in #if or #each or #eachPost blocks.

PHP[edit]

We compile the templates into PHP in advance by make compile-lightncandy, and we check the compiled templates into git.

On your development server, set wgFlowServerCompileTemplates = true to compile templates as needed (you may have to change permissions on handlebars/compiled.

JavaScript[edit]

The API response is in JSON.

We use a ResourceLoader add-on in extension:Mantle allowing us to specify "resource blobs". This makes the template available to front-end. See that extension's page for some documentation.

TODO compile JS templates in advance? Use ResourceLoader to do this?

Helper functions[edit]

Handlebars helper functions have to be re-implemented in JavaScript (in modules/new/flow-handlebars.js) and PHP (includes/TemplateHelper.php) in order for a template to work both front-end and back-end.

In a template, {{hFuncName this extra_params}} in a template invokes hFuncName( context, extra, params, options ). You can also call helpers with key-value parameters: {{hFuncName foo=1 bar=“baz”}}.

E.g. {{html content}} invokes the html helper function to render HTML (rather than escaping '<' and '>', etc.).

Another example, the template code

 {{uuidTimestamp postId "time_ago"}}

invokes the uuidTimestamp helper to get the timestamp from the UUID postId and format it using the 'time_ago' i18n message.

It's fine to create a helper which in turn calls a template with extra parameters.

In JavaScript[edit]

In JS, you define the helper implementation FlowHandlebars.prototype.MyFunc = function ( params ) {..., then call handlebars.registerHelper( 'helpername', FlowHandlebars.prototypeMyFunc ) to associate it with the helper helpername in Handlebars templates.

SafeString is for when you know you're outputting safe HTML, it's used by Template:Html helper.

Wiring up functions in JavaScript[edit]

flow-handlebars.js wraps Mantle extension which provides a generic interface to JS templating.

mw.flow.FlowHandlebars.prototype.processTemplate( templateName, object } will return a DocumentFragment of the named template rendered using the data in the provided object. It caches the compiled template, so there's no reason to get templates before rendering them.

mw.flow.FlowHandlebars.prototype.processTemplate(
    'timestamp',
    {
        time_iso: "1984-04-01 4:20",
        time_readable: "back in the eighties"
    }
)

Note: Seems _tplcache[ name] duplicates Mantle's cache of compiled templates!?

Init[edit]

flow.js runs mw.flow.initComponent() for every component on the page.

It looks for data-flow-component="some_name" tags in the page HTML, then instantiates that component's class

E.g. a mention of data-flow-component="board" on page will initialize the FlowBoardComponent JavaScript object.

flow-board.js has mw.flow.registerComponent( 'board', FlowBoardComponent ); That's how the data-flow-component "board" and class FlowBoardComponent are glued together. The class extends FlowComponent, does the usual OO dance of constructor, calling parent (FlowComponent), creating an object.

FlowBoardComponent is currently the only component.

Associating actions[edit]

Typical front-end interaction code locates some elements in the HTML with selectors and binds various actions to them. Flow has a better way:

  1. give flow-xxx classes to HTML elements in templates that indicate that something should be done
  2. a data-flow-yyy attribute on the same HTML element can give information, e.g. the handler to be called.
  3. declare the handler function in the appropriate handlers part of the FlowBoardComponent so flow.js can find it.

Load handlers[edit]

For example

  • in HTML class="flow-load-interactive" data-flow-load-handler="MyFunc"
  • define the function FlowBoardComponent.UI.events.loadHandlers.MyFunc

It will be invoked at load time for that element. Flow load handlers include topicElement, timestamp

Click handlers[edit]

Similarly, for click interactions

  • in HTML class='flow-click-interactive' data-flow-interactive-handler="MyFunc" on elements
  • define the function FlowBoardComponent.UI.events.interactiveHandlers.myFunc

and on click this will be called. Flow click handlers include cancelForm, topicCollapserToggle

The class name is only necessary for non-interactable elements. A, BUTTON, and INPUT can all have the data-flow-interactive-handler alone.

General approach: bind on the root container rather than individual bits of HTML, let the event bubble up to this and then call the component function.


Example: add an interactive handler function for music, define in Handlers.xxx

add to some .html.handlebars template class="flow-click-interactive" data-flow-interactive-handler="music"

See Gerrit change 137359

API handlers[edit]

  • in HTML code data-flow-interactive-handler="apiRequest" data-flow-api-handler="myCallbackFunc
  • in JavaScript, add the callback for the API request as FlowBoardComponent.UI.events.apiHandlers.myCallbackFunc

For example you would add this to some <a href=... link, The built-in handler apiRequest intercepts the usual link/form target and submits the anchor or the form as an API call. It parses out the URL and turns the GET params or the form parameters into api params and turns it into an API call. There's a translation layer to translate from URL/Form paramerters to API parameters. Your myCallbackFunc function is called when the API call returns. Typically that would replace part of the page with the results of the API call, by rendering parts of the JSON response using templates.

The apiHandler callback handles both success and error state.

Questions

  • How does Flow know what the API call is? It seems flowApiRequestFromAnchor() converts an existing static URL into an API call, but I don't see anything that calls this?

api pre-handler[edit]

Sends an overrideobject to the API to modify the request parameters.

If you need one of these you typically give it the same name as your api handler callback.

e.g. the activateEditHeader() api handler has a matching api pre-handler which returns some API parameters for editing header that are not in the a href URL parameters. Another example: the preview() API pre-handler changes the action from the default flow to flow-parsoid-utils.

Register these with FlowBoardComponent.UI.events.apiPreHandlers

Can return false to stop an API request from being made, e.g. form isn't ready.

Other[edit]

data-flow-initial-state= collapsed / hidden
sets initial state of a Flow form (such as textarea + Cancel Preview Reply) in a Flow board component to be collapsed or hidden

Debugging[edit]

Comment out a line in View.php to view the JSON output before the PHP template.

If you see [Object object] in output instead of the contents you expect, it's because the API return value was some complex object. Templates expected a string.

Best practices[edit]

Handlebars has implicit {{{ }}} triple-braces to output pre-computed HTML. This is easy to miss, so invoke an explicit {{ html keyName}} when we want to output HTML

TODO: have a flag to disable this?