Components in Core UI are structured in a standardized way. This helps to your project maintain separation between each moving part and ensures that testing is simplified and consistent.
Here we will create a Hello World component which will append a simple text message to an element. It will also want to provide developers the option of overriding the default settings, either at the time of execution or by embedding the settings in a data-
attribute.
Be sure to have the following:
Create a folder called hello-world
to the src/project/components
folder. If you dont have a components
folder go ahead and create one.
Next, inside of the helloWorld
folder, create two more folders, js
and tests
. The former will contain the actual script while the latter will be used for automated testing of the component.
You should have the following structure:
helloWorld/
├─ js/
├─ tests/
Let's flesh out the actual component code. Create a file called helloWorld.js
in the js
folder and add the following boilerplate.
// File: src/project/components/js/helloWorld/helloWorld.js
(function (factory) {
if (typeof define === 'function' && define.amd) {
// Register as an anonymous module (AMD)
define(['jquery'], factory);
}
else {
// Browser globals
factory(jQuery);
}
}(function ($) {
// Component code
}));
This allows the component to be defined as an asynchronous module. All Core UI components are defined this way as it make the components modular in nature. Core UI use the AMD specification because it was written with the web in mind and is the preferred structure for RequireJS modules.
Now we can write the actual code for our component. There are many ways to structure jQuery plugins. Here we will use a highly configurable, mutable plugin pattern, but of course when authoring your own components you may choose a more suitable pattern.
Start by defining the plugin's name under the Component code
comment:
// Plugin name, abstracted for single point of control
var pluginName = 'helloWorld';
This lets us abstract the name from the rest of our code to keep it portable. Note that we are not creating a global variable because we're inside a an anonymous function wrapper.
Next, below the name, add the constructor:
// Plugin constructor
var HelloWorld = function (elem, options) {
this.elem = elem;
this.$elem = $(elem);
this.options = options;
// Look for additional options in the element's data attribute
this.metadata = this.$elem.data('helloworld-options');
};
The contructor is pretty barebones — essentially it gathers and stores the element and any options that were passed.
Next we build the constructor's prototype object which will contain its public properties and methods. Add the following below the constructor:
// Plugin options and methods
HelloWorld.prototype = {
// Default options
defaults: {
message: 'Hello World'
},
// Initialization code
init: function () {
this.config = $.extend({}, this.defaults, this.options, this.metadata);
// Call the default function that should be executed
this.appendText();
return this;
},
// Example method
appendText: function() {
this.$elem.append(document.createTextNode(this.config.message));
}
};
Now we have some default options (message
), an init
function which we'll come back to later, and the "main" function of this example, appendText
.
Since the prototype
properties are public and may be overwritten, we'll store a copy of our default settings in a "private" property:
// Copy the default plugin definitions
HelloWorld.defaults = HelloWorld.prototype.defaults;
Finally, we need to register the plugin with jQuery. By leveraging its $.fn
method we can make our plugin chainable so it behaves like any other jQuery function.
// Create the plugin in the jQuery namespace
$.fn[pluginName] = function (options) {
// Iterate of each plugin individually and create a new instance for each occurance
return this.each(function () {
// Create an instance of the plugin for each individual element
new HelloWorld(this, options).init();
});
};
And now the plugin is ready. For reference, here is the complete helloWorld.js
file.
To try out the component we can create a short HTML file. Let's put it in the tests
folder so we can reuse the file for testing later. Create the file src/project/components/helloWorld/tests/helloWorld.html
and add the following:
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
<title>Hello World Test</title>
<link href="../../css/main.css" rel="stylesheet" type="text/css" media="all">
</head>
<body>
<h2 id="helloWorld"></h2>
<script id="require" src="../../js/main.js"></script>
<script>
require(['jquery', 'helloWorld', 'domReady!'], function ($, helloWorld) {
// ID test
$('#helloWorld').helloWorld();
});
</script>
</body>
</html>
Open the page in a browser and you should see "Hello World!" printed in large letters. If you only see a blank page, make sure you've built the project at least once. When the page loads, the words "Hello World" should appear at the top of the page.
Inside the test page, just under the h2
element add a paragraph tag and use a class
as the script hook for the helloWorld
component.
<p class="helloWorld"></p>
Next, back in the script tag under the ID test selector, add a new jQuery selector using the newly create .helloWorld
class. But this time pass an object that overrides the message text with Hello Class Element!
.
// Class Test
$('.helloWorld').helloWorld({message: 'Hello Class Element!'});
Rebuild the project and refresh the test page. This time two hello messages appear: one saying "Hello World", the other saying "Hello Class Element!"
For a final test of the component, try passing the options using the data-
attribute. Below the other test elements, add an unordered list and a handful of list items. On each of the list items create a data-
attribute similar to the example below:
<ul>
<li data-helloworld-options='{"message":"Hello Item 1!"}'></li>
<li data-helloworld-options='{"message":"Hello Item 2!"}'></li>
<li data-helloworld-options='{"message":"Hello Item 3!"}'></li>
<li data-helloworld-options='{"message":"Hello Item 4!"}'></li>
<li data-helloworld-options='{"message":"Hello Item 5!"}'></li>
</ul>
Next, let's alter the page script one last time. This time using a jQuery selector that selects all of the list items using an element selector.
// HTML test
$('li').helloWorld();
Rebuild the project and reload the test page again. You should see three different sets of customized messages in the h2
, p
, and li
elements:
For reference, here is the complete helloWorld.html
file.
Start by creating a scss
folder alongside the js
and tests
folders. Inside, create a helloWorld.scss
file and add the following:
$primary: #fe57a1;
.helloText {
color: $primary;
}
Now we need to add a class hook in our HTML so the style will get applied. Add another line to the appendText
method in the helloWorld.js
file to add a class to each element:
appendText: function() {
this.$elem.append(document.createTextNode(this.config.message));
this.$elem.addClass('helloText');
}
Rebuild the project again and refresh the test page. The contents of the page should remain the same, but the color of all the appended text should have changed:
For your reference, here is the updated helloWorld.js
file and the complete helloWorld.scss
file.
Sometimes a component is only needed under certain circumstances. It's best practices not to include those additional resources in your base assets (main.js
and main.css
). Instead they should be lazy loaded so they only download when necessary. (This is not to be confused with #conditional-loading which is covered later.)
To make a component lazy-loadable it needs a configuration file. In the root of the helloWorld
component create a file called asset.json
and add the following:
{
"lazy": true
}
Now when you rebuild the project it will recreate the base assets separate from the helloWorld
assets. But one more change has to be made if we want to include styles.
Lazy loaded components and their asset types (script, styles, etc) are not automatically bundled together by default. To give developers the maximum amount of control possible, developers must specify all of the dependencies a component might have when it is lazy loaded.
To add the styles you'll need to edit the helloWorld.js
file. In the define
statement add css!helloWorld-style
to the dependency array.
define(['jquery', 'css!helloWorld-style'], factory);
Note the special css!
prefix — this tells Core UI that the dependency helloWorld-style
is a style sheet and therefore it needs to be loaded in a different manner than other assets.
In the lazy load example you may have noticed that the browser had to make additional http
requests for each asset of the helloWorld
component on every page load. We can avoid that by loading the assets conditionally.
The built in method cui.load
can load components, for example:
cui.load('helloWorld', function() {
// Kick off the helloWorldPlugin
conditionalElem.helloWorld();
});
To run this conditionally we can wrap an if
statement around it. Suppose you only want the message to appear when the page contains a paragraph:
if ($('p').length > 0) {
cui.load('helloWorld', function() {
// Kick off the helloWorldPlugin
conditionalElem.helloWorld();
});
}
To try this, create a second test page in the component's tests
folder called helloWorld-lazy.html
and add the following:
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
<title>Hello World Conditional Test</title>
<link href="../../css/main.css" rel="stylesheet" type="text/css" media="all">
</head>
<body>
<!-- <p></p> -->
<script id="require" src="../../js/main.js"></script>
<script>
// Wait for jQuery to load and for the page to be ready
require(['jquery', 'domReady!'], function ($) {
// Query the page for specific elements with specific classes
var $paragraphs = $('p');
// Check to see if the page has any paragraphs
if ($paragraphs.length > 0) {
// We have elements, so using the `cui` namespace load the helloWorld Component
cui.load('helloWorld', function () {
// Call the helloWorldPlugin
$paragraphs.helloWorld();
});
}
});
</script>
</body>
</html>
Launch the page and notice that no "Hello world!" message appears. If you open your browser's developer tools and watch the network tab you'll notice that the helloWorld assets are never loaded.
Now edit the file to uncomment the <p>
element and refresh the page. This time, the script tag finds a conditional element that meets our requirement and the cui.load
process requests the helloWorld assets.
For reference, here is a complete of the helloWorld-lazy.html test page.
You may also wish to view the repository of example components as a reference. In particular, the jQuery plugin example resembles what you created with this tutorial.