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 'configure-default-runtime';
import editor from 'editor';
import { register } from 'runtime';
register('Editor', editor);This shows how to register the editor component, also by importing “configure-default-runtime” it will automatically register the following core components:
- Ajax Form
- Ajax List
- Amount
- Date Time Offset
- List
- Pager (used by the List and Ajax List components)
The editor component is quite large, which is why it's not a core component and must be registered manually.
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 example from 'example';
import { register } from 'runtime';
register('Example', example);Setting Data
The same way you register components, you can also register global data, for example:
register('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:
- 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.
- 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
To attach a click event handler to all elements with the class .click-me, use the runtime’s onLoad hook:
import { onLoad } from 'runtime';
onLoad(context => {
context.querySelectorAll('.click-me').forEach(element => {
element.addEventListener('click', () => {
console.log('Clicked!');
});
});
});Why This Works
The onLoad hook automatically runs:
- After the initial page load
- After AJAX navigation (if enabled)
- Whenever new HTML is dynamically inserted and processed by the runtime
The context parameter represents the root element of the DOM that was just mounted or updated. This means your event handlers will always be attached to the correct elements, even if they were added later.
Handling Dynamically Inserted HTML
If you insert HTML manually after the page has loaded, you should tell the runtime to process it:
import { process } from 'runtime';
const element = document.getElementById('new-content');
await process(element);This ensures that any onLoad hooks are applied to the new content.
Alternative: Event Delegation
If your use case allows it, event delegation can still be a simpler and more efficient approach:
document.addEventListener('click', e => {
if (!e.target.closest('.click-me')) return;
console.log('Clicked!');
});This works because the listener is attached to document, which always exists, and can handle events from elements added later.
