Quickstart: Creating your first component

If you haven't already read the pages on setting up your development environment and the build process then we suggest you read those first.

Make sure that you have put Tui into development mode, and have NPM installed and ready to run.

Objective

The objective is to create a server ping component that pings Totara Core using GraphQL, and displays the last time the ping was successful. 

Rather than create this as a component within an existing bundle, we are instead going to create a new bundle at the same time.

Step 1: npm run tui-build-watch

Rather than manually trigger the build when we need to we're going to run the following command, and leave it running until we're complete.

Each time we save a change to our component it will generated updated build files.

npm run tui-build-watch

Step 2: Creating a new Tui component

Vue components, pages, and modules are organised into Tui components.

Core functionality is presented through the tui Tui component, located at client/component/tui.

How you organise your components is up to you, but we recommend grouping them together based upon functionality. To keep it easy, in core we have used the frankenstyle name from Totara Core as the name for Tui components. For instance if you had a custom local plugin installed at server/local/myplugin, then we would recommend grouping your Vue components into a local_myplugin Tui component located at client/component/local_myplugin.

The use of a frankenstyle name is entirely optional. For the purpose of this example we are going to call our Tui component 'quickstart'.

You could create the file structure manually, but to save time will use the tui-init generator.

Run the following commands, replacing 'vendorname' with a unique string identifying you or your employer. The vendor string has no inherent meaning, it's just used to categorise components within the build system - you can set it to anything you want.

npm run tui-init quickstart vendorname
mkdir client/component/quickstart/src/components/ping

Each Tui component needs a tui.json file that defines its name and the vendor who created it.

This will be automatically generated by the above command with the following content:

client/component/quickstart/src/tui.json
{
  "component": "quickstart",
  "vendor": "vendorname"
}

After running the tui-init command you will need to restart tui-build-watch to pick up the new Tui component.

Step 3: Creating a page component

With the build watcher running with the command described earlier, you're ready to create a component. We have different types of components. Firstly we have small re-usable ones that are encapsulated with the intention of performing a limited set of functionalities or visual styles - these are sometimes called presentational components. We assemble these small components and then write additional logic to wire them up together into whole User Interfaces.

We have larger components too, ones that tie together presentational components and implement the logic for the application, often including their own styles as well.

We are going to create a component that will handle the logic for pinging the server and a component that will display the ping result, which we will use from our first component.

We'll start out by creating a page component called Ping.vue in the directory client/component/quickstart/src/pages. It's worth noting that all component files use the PascalCase naming convention.

client/component/quickstart/src/pages/Ping.vue
<template>
  <div class="tui-ping">
    Hello
  </div>
</template>

<script>
export default {};
</script>

Notice the individual technology chunks as described above, <template> for HTML and <script> for JavaScript, both contained within a single component.

Step 4: Creating a page in Totara Core that uses the component so that we can test it

To now include our component on a page we need to render it from PHP, the following code snippet shows how this is done. Below it you'll find the code for a test page that you can use.

Rendering a component
$component = \totara_tui\output\framework::vue('quickstart/pages/Ping');
...
$OUTPUT->render($component);

// OR

$component = new \totara_tui\output\component('quickstart/pages/Ping');
$component->register($PAGE);
...
$OUTPUT->render($component);

To set up a complete page that we can test with create the following file:

server/ping.php
<?php

require_once('config.php');

$PAGE->set_url('/ping.php');
$PAGE->set_context(context_system::instance());
$PAGE->set_title('Ping');
$PAGE->set_heading('Ping');

require_login();

$component = \totara_tui\output\framework::vue('quickstart/pages/Ping');

echo $OUTPUT->header();
echo $OUTPUT->render($component);
echo $OUTPUT->footer();

If you visit this page in a web browser you should now see 'Hello'.

Step 5: Create a component to implement the ping status UI

You could implement this directly in the page component, but in order to keep our page component clean and so we can reuse the display UI in other places we'll create a second component to display the ping result and embed it in our page component. Create PingStatus.vue in the directory client/component/quickstart/src/components/ping.


client/component/quickstart/src/components/ping/PingStatus.vue
<template>
  <div class="tui-pingStatus">
    <h4>Status</h4>
    ...
    <h4>Time</h4>
    ...
  </div>
</template>

<script>
export default {};
</script>

Step 6: Include the ping status component

We now need to tell `Ping.vue` to import our status presentational component and then register it so that we can include it in our template. Using the `<PingStatus />` tag will now pull in our new component.

client/component/quickstart/src/pages/Ping.vue
<template>
  <div class="tui-ping">
    <PingStatus />
  </div>
</template>

<script>
import PingStatus from 'quickstart/components/ping/PingStatus';

export default {
  components: {
    PingStatus,
  },
};
</script>

In the browser the word 'Hello' will have been replaced by the contents of our `PingStatus` component.


Step 7: Adding strings from a language pack

Previously In PingStatus.vue we hard-coded two strings, Status and Time. Like with our other technology concerns, we will now switch these to use strings from a language pack and add them to the component too. To use language packs you first need to declare all required strings within the <lang-strings> tags. The <lang-strings> tags must contain valid JSON otherwise the npm run tui-build-watch build will fail. You can then include strings inside the template using $str('string name', 'totara_component').

client/component/quickstart/src/components/ping/PingStatus.vue
<template>
  <div class="tui-pingStatus">
    <h4>{{ $str('status', 'core') }}</h4>
    ...
    <h4>{{ $str('time', 'core') }}</h4>
    ...
  </div>
</template>

<script>
export default {};
</script>

<lang-strings>
{
  "core": [
    "status",
    "time"
  ]
}
</lang-strings>

The available language strings can generally be found by looking in the lang/en/component_name.php file in a Totara component, or in server/lang/en/component_name.php.

'core' here is a special component that is currently resolved on the server side to server/lang/en/moodle.php.

Step 8: Include a button component

A container component will usually include several presentational components. A presentational component is a generic reusable component focused on the way things look and may contain some logic specific to only that component. We are going to use the re-usable core button component here.

client/component/quickstart/src/pages/Ping.vue
<template>
  <div class="tui-ping">
    <Button :text="$str('reload', 'core')" @click="reload" />

    <PingStatus />
  </div>
</template>

<script>
import Button from 'tui/components/buttons/Button';
import PingStatus from 'quickstart/components/ping/PingStatus';

export default {
  components: {
    Button,
    PingStatus,
  },

  methods: {
    reload() {
      console.log('Button clicked');
    },
  },
};
</script>

<lang-strings>
{
  "core": [
    "reload"
  ]
}
</lang-strings>

We have imported and registered the button component and then included it in the template. We have then provided the button component with a string and told it to call the reload() method when its click event is triggered.

Step 9: Add SCSS styles to our status component

For our Vue components we are using SCSS instead of LESS or plain CSS, and we are including our styles directly within Vue to the corresponding component, as with the HTML and JS concerns.

We are going to add some basic styling for the PingStatus.vue component.

client/component/quickstart/src/components/ping/PingStatus.vue
<template>
  <div class="tui-pingStatus">
    <h4>{{ $str('status', 'core') }}</h4>
    ...
    <h4>{{ $str('time', 'core') }}</h4>
    ...
  </div>
</template>

<script>
export default {};
</script>

<lang-strings>
{
  "core": [
    "status",
    "time"
  ]
}
</lang-strings>

<style lang="scss">
.tui-pingStatus {
  max-width: 50rem;
  background: var(--color-neutral-2);
  margin-top: var(--gap-2);
  padding: var(--gap-2);
}
</style>

Step 10: Request data using Apollo and GraphQL

We have introduced Apollo Client to help manage data requested with GraphQL, this provides us with a nice API for fetching GraphQL from Vue components and helps us be more efficient with the requests we make. We already have a service available for pinging the server which returns a status and timestamp, we are going to call this service when our component is initialised and re-request each time the button is clicked. Each time we get a response we will update the template content.

First we need to import our GraphQL query. In order to import a GraphQL query we need to know the frankenstyle name of the component or plugin that owns the query, and the name of the query. We can then write the import in our component as:

import QueryName from '{component_or_plugin}/graphql/{queryname}';

Once we have imported the query we can request it using the Vue Apollo integration, this will make the request when the component is initialised. We then update the template to wait for the query response before rendering PingStatus and to pass the query response as a prop to PingStatus. Finally we update the button click method to use the Vue Apollo API to re-request the data.

client/component/quickstart/src/pages/Ping.vue
<template>
  <div class="tui-ping">
    <Button :text="$str('reload', 'core')" @click="reload" />

    <PingStatus v-if="ping" :ping="ping" />
  </div>
</template>

<script>
import Button from 'tui/components/buttons/Button';
import PingStatus from 'quickstart/components/ping/PingStatus';
import statusQuery from 'totara_webapi/graphql/status_nosession';

export default {
  components: {
    Button,
    PingStatus,
  },

  data() {
    return {
      // default value for ping query, until it loads
      // this can be left out, but makes unit testing easier
      ping: null,
    };
  },

  apollo: {
    ping: {
      query: statusQuery,
      update: data => data.totara_webapi_status,
    },
  },

  methods: {
    // Re-request query from server on button click
    reload() {
      this.$apollo.queries.ping.refetch();
    },
  },
};
</script>

<lang-strings>
{
  "core": [
    "reload"
  ]
}
</lang-strings>

We also need to update PingStatus to render the data we're passing along.

client/component/quickstart/src/components/ping/PingStatus.vue
<template>
  <div class="tui-pingStatus">
    <h4>{{ $str('status', 'core') }}</h4>
    {{ ping.status }}
    <h4>{{ $str('time', 'core') }}</h4>
    {{ ping.timestamp }}
  </div>
</template>

<script>
export default {
  props: {
    ping: Object,
  },
};
</script>

<lang-strings>
{
  "core": [
    "status",
    "time"
  ]
}
</lang-strings>

<style lang="scss">
.tui-pingStatus {
  max-width: 50rem;
  background: var(--color-neutral-2);
  margin-top: var(--gap-2);
  padding: var(--gap-2);
}
</style>

Adding a filter to the Apollo helper

The webapi_status_nosession query doesn't except any filters, but the following shows how you could include filters with your query.

client/component/quickstart/src/pages/Ping.vue
<template>
  <div class="tui-ping">
    <Button :text="$str('reload', 'core')" @click="reload" />

    <PingStatus v-if="ping" :ping="ping" />
  </div>
</template>

<script>
import Button from 'tui/components/buttons/Button';
import PingStatus from 'tui/components/ping/PingStatus';

export default {
  components: {
    Button,
    PingStatus,
  },

  data() {
    return {
      ping: null,
      filterValue: '123',
    };
  },

  apollo: {
    ping: {
      query: statusQuery,
      variables() {
        return {
          filter: this.filterValue,
        };
      },
      update: data => data.totara_webapi_status,
    },
  },

  methods: {
    // Re-request query from server on button click
    reload() {
      this.$apollo.queries.ping.refetch();
    },
  },
};
</script>

<lang-strings>
{
  "core": [
    "reload"
  ]
}
</lang-strings>

Step 12: Add a unit test with Jest

As part of our new framework we have introduced Jest to allow us to write JavaScript unit tests. All components are required to have a test file with coverage for all emitted events, all methods and a snapshot. We are going to create our test file Ping.spec.js in the following directory client/component/quickstart/src/tests/unit/pages/.

The test below currently just creates a snapshot which allows us to easily track changes.

client/component/quickstart/src/tests/unit/pages/Ping.spec.js
import { shallowMount } from '@vue/test-utils';
import Ping from 'quickstart/pages/Ping';

describe('Ping', () => {
  let wrapper;

  beforeAll(() => {
    wrapper = shallowMount(Ping);

    // set query response
    wrapper.setData({
      ping: {
        status: 'ok',
        timestamp: 1234567890,
      },
    });
  });

  it('matches snapshot', () => {
    expect(wrapper.element).toMatchSnapshot();
  });
});

To run the tests we run the following the command:

Terminal
npm run tui-test-watch

Wrap up

This guide has covered the basics to get you started, although the reality of building reusable components is more complex. After getting comfortable with the above walkthrough, your next best step is to try the following:

  • Review "Totara Samples", a collection of reusable presentational components. This is currently available within the product by browsing to /totara/tui/index.php.
  • Experiment with existing components on a local branch to understand how the JavaScript (and data), HTML, SCSS and lang strings work individually and together.
  • Brush up on Vue documentation. It is a framework for managing some complex and repeatable aspects of front end development. It doesn't solve everything, so learning about it will help understand its limits and its usefulness.
  • Please feel free to ask questions by contacting our Support team.