Quick-start guide: 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 optional.

For the purpose of this example we are going to call our Tui component 'local_quickstart', and we'll have a server-side plugin under server/local/quickstart. This guide will not go over creating the server-side plugin, only touching the necessary files to create and view our Tui component.

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. For example, if Totara were developing a new component we'd use totara as the vendor.

npm run tui-init local_quickstart vendorname mkdir client/component/local_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/local_quickstart/tui.json

{ "component": "local_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/local_quickstart/src/pages. It's worth noting that all component files use the PascalCase naming convention.

client/component/local_quickstart/src/pages/Ping.vue

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

Now we have a simple Tui component, we want to render it via PHP. We'll do this using the MVC (Model, view, controller) pattern. For more information, see the MVC pattern documentation.

This guide doesn’t cover creating the plugin. Here we only touch the files we need to in order to render the component.

Step 4a: Creating a page controller

First, we'll start out with the page controller. In your plugin's server directory (in this example it's /server/local/quickstart) ensure you have the classes directory, and within it make the controllers directory.

In the controllers directory, let's create our PHP controller class for our page ping. We'll name the controller the same as the page we want to control to make it easy to understand and follow.

Now, we'll define the controller class. The controller class will define a method called action which returns the Tui component we want to render (via tui_view).

server/local/quickstart/classes/controllers/ping.php

Step 4b: Creating the PHP page

Now, in the plugin directory we want to create the PHP page to serve the controller. We name this the same as the page, the same as we did with the controller, to make it easier to follow.

Now, in the PHP page, we need to call the controller to serve the Tui component. We do this by initialising the controller and calling the process method.

server/local/quickstart/ping.php

Now, when we navigate to the <site_URL>/local/quickstart/ping.php page we should see our Tui component.

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/local_quickstart/src/components/ping.

client/component/local_quickstart/src/components/ping/PingStatus.vue

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/local_quickstart/src/pages/Ping.vue

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

Step 7: 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 reusable core button component here.

client/component/local_quickstart/src/pages/Ping.vue

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 8: 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 for 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/local_quickstart/src/components/ping/PingStatus.vue

Step 9: Request data using Apollo and GraphQL

We have introduced Apollo Client to help manage data requested with GraphQL. This provides us with a useful API for fetching GraphQL from Vue components and helps us to 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:

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/local_quickstart/src/pages/Ping.vue

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

client/component/local_quickstart/src/components/ping/PingStatus.vue

Step 10: 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/local_quickstart/src/pages/Ping.vue

Step 11: Add a unit test with Jest

Please see our documentation on testing frontend code here.

To run the tests we run the following the command:

Terminal

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 limitations and its usefulness.

  • Please feel free to ask questions by contacting our Support team. 

Legacy rendering from PHP

Legacy implementations had a different way of including components on the page via PHP, the following code snippet shows how this was done. This is no longer the recommended approach, but you may come across this type of implementation.