Component structure

Overview

Tui components are organised differently to previous front end code. We used to create Mustache templates for logicless HTML, LESS files for compiling to CSS, and AMD for encapsulating JavaScript logic. This provided us with a 'separation of concerns', a way of dividing code into somewhat manageable chunks at a technology level (HTML/JS/CSS). We still want this separation of concerns, but now we do this at a component level. The Vue.js docs sums up this approach succinctly.

A single Tui component contains all of the above technology chunks within the same file not only for convenience, but also to encourage component-based encapsulation. The following images illustrates the change in the separation of concerns between our legacy approach and Tui.

Separation of concerns diagram.

Implementation

We follow the convention outlined in the Vue documentation for Single File Components (SFCs), with some additions. We additionally include a <lang-strings> code block when language is needed within a component, and we also allow for definition of other custom code blocks. 

Template block (HTML mixed with logic)

<template>
	<ul><!-- mandatory requirement of one root element -->
		<li v-for="item in items">{{ item }}</li><!-- embedded logic; referencing props, data, methods and utils defined in the <script /> block -->
	</ul>
</template>

Read more about the Tui framework's usage of HTML.

Script block

<script>
import Loader from 'tui/components/loader/Loader'; // import statements, used to compose components and functionality into this currently authored component

export default { // module definition, this is where props, data, methods, component lifecycle hooks etc are defined and used
  components: {
    Loader,
  },
}
</script>

Read more about the Tui framework's usage of Javascript.

Style block (SCSS)

<style lang="scss"><!-- `lang` ensures the SCSS is processed as the superset of CSS, rather than raw CSS. Note that the `scoped` attribute is *not* used -->
.tui-componentBlock {
	&__componentElement {
		&--componentModifier {
			// BEM naming convention
		}
	}
}
</style>

Read more about the Tui framework's usage of CSS.

Langstring block (i18n) (Totara 13-18)

<lang-strings><!-- must be valid JSON format -->
{
  "container_workspace": [
    "add_members_to_space",
    "filter_users"
  ],
  "totara_core": [
    "search"
  ],
  "core": [
    "name"
  ]
}
</lang-strings>

Read more about the Tui framework's string internationalisation.

Custom block

As with the <lang-strings> block, theme authors can create more custom blocks as needed.

// in /client/component/{custom_component}/src/build.config.js

// custom <my-custom> Vue SFC block
module.exports = {
  webpackTui(config, { addRule }) {
    addRule('block-my-custom', {
      resourceQuery: /blockType=my-custom/,
      use: [require.resolve('./tooling/my_custom_block_loader')],
    });
    return config;
  },
};

Read more about the Tui framework's build process.

Component types

The Tui framework roughly models different 'sizes' of component on two sets of guidelines.

  • Atomic Design, in that there are low-level SFCs that do one thing, medium-sized SFCs that aggregate those smaller components, and page SFCs that control layout and are the entry point for PHP to render a Tui component
  • 'Smart' and 'Dumb' components, in that we would prefer to avoid 'super components' that do everything, breaking them apart into smaller components facilitates re-use, but also can simplify understanding of how a given component works

Organising components

Within Tui core, we mostly follow a structure where components are organised based on what they do, e.g. form components are organised into client/component/tui/src/components/form, as this makes reuse more obvious. Within plugins, however, grouping components by related functionality can make a feature more understandable for other developers, and in the future will make deprecating component trees more manageable when required.

Renderless components

Sometimes creating a wrapper that provides functionality is required, but HTML is not. Adam Wathan describes this well:

A renderless component is a component that doesn't render any of its own HTML.

Instead it only manages state and behavior, exposing a single scoped slot that gives the parent/consumer complete control over what should actually be rendered.

A renderless component renders exactly what you pass into it, without any extra elements:

An example of a renderless component within the Tui framework is /client/component/tui/src/components/form/Upload.vue.

Recommended reading