Client-side Scripting

Introduction

As we mentioned previously in the static files page, our JavaScript files are compiled using Babel which allows us to write to the latest standards. In previous years we would often use jQuery to accomplish many tasks, however since the advancement in JavaScript and CSS transitions it has now become largely redundant. Click here for a great guide on moving from jQuery to using JavaScript. Also click here for a guide on migrating specific features.

If you have to use jQuery then you can import it the same way you would import any other library:

import $ from 'jquery';

You can also use jQuery plugins as you would've done previously, however it is preferred to use components. See below for more information.

Components

In KIT our preference is to use a trimmed down version of Vue.js called petite-vue (which is specially optimized for server-side rendering) to write our components as opposed to web components since it offers greater browser compatibility and better templating.

Registering Components

Components must be registered before they can be used. This can be done either within a script within our view or the theme's default script (if applicable) by simply adding the following to the top of the file:

import { registerComponents } from 'component-utils';
import components from 'core-components';

onLoad();

export function onLoad() {
    registerComponents(components);
}

This imports the following core components:

  • Ajax Form
  • Ajax List
  • Amount
  • Date Time Offset
  • List
  • Pager (used by the List and Ajax List components)

Additional components can be merged into the core components collection when registering them. For example the following will register the core components aswell as the editor component:

import { registerComponents } from 'component-utils';
import components from 'core-components';
import editor from 'editor';

onLoad();

export function onLoad() {
    registerComponents({ ...components, ...{ Editor: editor } });
}

The editor component is quite large and therefore by not registering it, it saves the user some time downloading the page when it is first loaded.

Components can only be registered once per request. Therefore if you have already registered them within your theme and you wish to register additional components for a particular page, then you should make sure the code to register the components in the theme is not executed.  To do this first wrap the call to the “registerComponents" method within the theme with the following condition: config.registerComponents !== false (if you haven't done so already). Now you just need to set this condition to false by making sure your view model implements ”IScriptConfigResolve” and adding config.Set("registerComponents", false); within the ”GetConfig” method.

Finally it is important to note that the form widget uses the ajax list component and therefore any pages which use this widget should make sure the core components are registered.

Creating Your Own Components

To illustrate how to create your own components we'll provide an example.

First create the following file called “example.vue” in the “Assets/js/components” folder:

<template>
    <button @click="count++">
        You clicked me {{ count }} times.
    </button>
</template>

<script>
    export default function(props = {}) {
        return {
            count: props.count ?? 0
        };
    };
</script>

Note: The above file is known as a single file component, which allows you to write your template, script and styles in the same file.

Next you need to add the following to the “Assets.json” file in the root of the project (create the file if one doesn't exist):

{
  "inputFiles": [
    "Assets/js/components/example.vue"
  ],
  "outputFileName": "wwwroot/js/components/example.js"
}

Now when you save your “example.vue” file it will be compiled and copied to the “wwwroot” folder in a format that the browser will recognise.

Finally all you have to do is register it and your custom component tag will be available to use within your views.

The path to import your component will be “{StaticWebAssetBasePath}/js/components/example.js” (click here for more information about the "StaticWebAssetBasePath"). However we can create an alias for any libraries we are often importing. This is done via a “Scripts.json” file at the root of the project. For example:

{
  "imports": {
    "example": "/assets/my-project/js/components/example.js"
  }
}

Note: This assumes the “StaticWebAssetBasePath” is set to “my-project”.

Now you can register your component by saying:

import { registerComponents } from 'component-utils';
import components from 'core-components';
import example from 'example';

onLoad();

export function onLoad() {
    registerComponents({ ...components, ...{ Example: example } });
}

Setting Data

When registering the components the optional second parameter allows you to pass in initial data. This is known as the root scope. For example:

registerComponents(components, {
   isSidebarCollapsed: false
});

Alternatively you can set an inner scope within our views using a "v-scope" attribute. These can be nested and not just used for passing in data but also for initializing our components. Click here for an example of this.

Data Binding

If you would like to data bind your component to a value then you can either data bind it to a variable (two-way) or to a value.

Please note that data should be passed down to a component as a property but should be propagated up from an event:

this.$refs.root.dispatchEvent(new CustomEvent('update:value', { detail: value }));

If you data bind to a fixed value (one-way) then you should store an internal variable so that it can be modified internally. To get the benefit of both one-way and two-way data binding you should update both the internal and external (data bound) variables. Click here for an example of this. Notice how we use a “ref” attribute so we can reference elements within our component.

Server-side Rendering

We have so far rendered our component's template within the component. This has the benefit of rendering the data upfront. However there is a delay between when the data is initially rendered and when the component is mounted. To solve this we should render any data and override it with any databound data. This means when it initially renders it will render the same response as when it is mounted so the user will not notice the delay.

Click here to see an updated version of our earlier data binding example.

Directives

Directives are an alternative to using components. You should use a directive when it can be applied to any element. For example “v-show”.

KIT currently has one custom directive “v-kit-show”. This has two advantages over the existing “v-show” directive:

  1. By default it adds a transition effect, the duration can be changed by saying “v-kit-show:0” where 0 will make it appear immediately.
  2. It will display the element the directive is applied to when the condition is updated to true and the server initially rendered a “collapse” attribute against it. This fixes an issue with “v-show” where if the condition is updated to true then the server rendered style is not overridden.

    The reason this doesn't work for “v-show” is because "v-show" will only render a "display: none" style if the condition is false and doesn't render anything if the condition is true, therefore the initial rendered style is not overridden. However “v-kit-show” calls the jQuery show/hide functions if the condition is true/false respectively which will render the appropriate display style for both conditions.

    Note: It would never render the “collapse” class if the "v-kit-show" condition is true when it initially renders as these would contradict each other.

Event Handling

The following is an example of attaching a click event handler to all elements with the class “click-me”:

export function onLoad(context = document) {
    context.querySelectorAll('.click-me').forEach(element => {
        element.addEventListener('click', () => {
            console.log('Clicked!');
        });
    });
}

This is triggered by the call to onLoad(). This works fine as long as the element exists at the time the onLoad function is called. If this is not the case (e.g. when the content is added after an Ajax request) then you will need to listen for the “DOMContentUpdated” event to trigger the onLoad function. This makes sure when the DOM is updated the event handler will be attached to all elements within the class “click-me” within the updated content (the context variable will reference the updated content's container).

document.addEventListener('DOMContentUpdated', e => onLoad(e.target));

Alternatively if you can add the following attribute to the element directly within the view:

@click="dispatch('clickMe', 'Clicked!')"

Now you just need to specify the following to handle the event:

export function onLoad(context = document) {
   context.addEventListener('clickMe', e => {
       console.log(e.detail.argument);
   });
}

The “clickMe” event is bubbled up through the DOM until it reaches the current context (in this case the document). Since the document will always exist, then even if the element with the “@click” attribute is added after an Ajax request, the event handler will always be triggered.