Today’s post objective is to learn how to develop a Custom Component in Sites Cloud Service that makes use of static files (in our case, a default image that can be override by the contributor) and how to reference the static files to work both in edit mode and when the site is published.

I’m going to use the same bootstrap theme than in previous posts (Modern Business) and will focus on create a custom component to render this specific HTML snippet (obviously managing the image, text and social network links):

member_1

Basically it’s a team member card, but I want to keep the image as the default image and provide the ability to the contributor to select another image hosted in Documents Cloud. Then we have three text that I will convert to make them editable by the contributor and we will play with the different CKEditor toolbars we want to offer to edit each text field. Finally, we will have 3 inputs as settings to configure social network links.

As usual, the first step is to create the component in the component palette of Sites Cloud and sync it in our DoCS Desktop Sync Client so we can start with the development.

membercreation

membercomponentstructure

In this example, we will work with a different approach in our component rendering. When I was creating the carousel, I created a Mustache template and make use of Mustache.js library. In this case, I will have the standard render.js component, but will move the HTML structure to a HTML file instead building the code with JS.

But as we need to start from the beginning, the first thing is to work on the appinfo.json file. We will need 3 nested components (name, job title and description) plus custom settings for the image (we could use the ootb image component, but let’s explore the API) and social network links. Our appinfo.json will looks like this:


{
"id": "modern-business-member",
"settingsData": {
"settingsWidth": 300,
"settingsRenderOption": "panel"
},
"initialData": {
"componentId": "modern-business-member-id",
"componentLayout": "default",
"customSettingsData": {
"imageUrl": "[!-- MY_ASSETS_URL --]/img/BruceWayne.jpg"
},
"marginBottom": 0,
"marginLeft": 0,
"marginRight": 0,
"marginTop": 0,
"nestedComponents": [
{
"id": "nameId",
"type": "scs-title",
"data": {
"toolbarGroups": [],
"userText": "Bruce Wayne",
"placeholderText": "Enter member name",
"styleClassTag":"h3",
"marginBottom": 0,
"marginLeft": 0,
"marginRight": 0,
"marginTop": 0
}
},
{
"id": "jobtitleId",
"type": "scs-title",
"data": {
"toolbarGroups": [{"name": "basicstyles"}],
"userText": "CEO, Wayne Enterprises",
"placeholderText": "Enter member title",
"styleClassTag":"h3",
"styleClass": "small",
"marginBottom": 0,
"marginLeft": 0,
"marginRight": 0,
"marginTop": 0
}
},
{
"id": "descriptionId",
"type": "scs-title",
"data": {
"toolbarGroups": [{"name": "basicstyles"},{"name": "links"}],
"userText": "American billionaire, playboy and philanthropist",
"placeholderText": "Enter member abstract",
"styleClassTag":"p",
"marginBottom": 0,
"marginLeft": 0,
"marginRight": 0,
"marginTop": 0
}
}
]
}
}

There are some important points on above code:

  • [!– MY_ASSETS_URL –] macro: As preview/edit URLs are different than Published URLs, we need to dynamically replace a part of the URL with the use of Sites SDK.
  • toolbarGroups property for scs-title (applicable to scs-paragraph): specifies which CKEditor toolbars you want to use instead the default toolbars configured for each OOTB component. I’ve added 3 different configurations for each of the components that we will use for the name, job title and description.
  • styleClassTag property for scs-title:  defines the default HTML tag to surround the custom text. Can be h1-h6 or p.
  • styleClass property for scs-title (applicable to scs-paragraph): specifies the css class to be used for the container tag surrounding our custom text.

Now, it’s time to build the settings part, but it’s not a big deal, as we have built other settings.html files in the past, so we can easily adapt one of them to fit with our custom settings (just an image and 3 text fields for the social network urls). The final code should looks like this one:


<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<title>Modern Business Member settings</title>

<!-- include sample apps styling -->
					<link href="/_sitescloud/renderer/app/sdk/css/app-styles.css" rel="stylesheet">

<!-- include supporting files -->
<script type="text/javascript" src="/_sitescloud/renderer/app/apps/js/knockout.min.js"></script>
    <script type="text/javascript" src="/_sitescloud/renderer/app/apps/js/jquery.min.js"></script>

<!-- include the Sites SDK -->
<script type="text/javascript" src="/_sitescloud/renderer/app/sdk/js/sites.min.js"></script>
</head>
<body data-bind="visible: true" style="display:none; margin:0px; padding:0px;background:transparent;background-image:none;">
    <!-- ko if: initialized() -->


<div class="scs-component-settings">


<div>
      <!-- Image -->
            <label id="imageAssetLabel" for="imageAsset" class="settings-heading" data-bind="text: 'Image'"></label>
      <input id="imageAsset" data-bind="value: imageName" readonly class="settings-text-box">
      <button id="imageSelect" style="float:left;" type="button" class="save-button" data-bind="click: showFilePicker">Select Image</button>
      <button id="cancelSelect" style="float:right;" type="button" class="save-button" data-bind="click: removeAsset">Remove Image</button>


<div style="clear:both;"></div>


      <!-- facebook -->
      <label id="facebookLabel" for="facebook" class="settings-heading" data-bind="text: 'Facebook Profile'"></label>
            <input id="facebook" data-bind="value: facebook" class="settings-text-box">
      <!-- linkedin -->
      <label id="linkedinLabel" for="linkedin" class="settings-heading" data-bind="text: 'Linkedin Profile'"></label>
            <input id="linkedin" data-bind="value: linkedin" class="settings-text-box">
      <!-- twitter -->
      <label id="twitterLabel" for="twitter" class="settings-heading" data-bind="text: 'Twitter Profile'"></label>
            <input id="twitter" data-bind="value: twitter" class="settings-text-box">
</div>


</div>



<div data-bind="setSettingsHeight: true"></div>


    <!-- /ko -->
    <!-- ko ifnot: initialized() -->


<div data-bind="text: 'waiting for initialization to complete'"></div>


    <!-- /ko -->
    <script type="text/javascript">
        // set the iFrame height when we've fully rendered
        ko.bindingHandlers.scsCompComponentImpl = {
            init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
                var body = document.body,
                    html = document.documentElement;
                SitesSDK.setHeight(Math.max(
                    body.scrollHeight,
                    body.offsetHeight,
                    html.clientHeight,
                    html.scrollHeight,
                    html.offsetHeight));
            }
        };

        // define the viewModel object
        var SettingsViewModel = function () {
            var self = this;

      // create the observables for passing data
      self.facebook = ko.observable();
      self.linkedin = ko.observable();
      self.twitter = ko.observable();
      self.imageID = ko.observable();
      self.imageUrl = ko.observable();
      self.assets = [];

            // create rest of viewModel
            self.initialized = ko.observable(false);
            self.saveData = false;

            showFilePicker = function () {
            // select an image
            SitesSDK.filePicker({
                'multiSelect': false,
                'fileTypes': ['png','jpg']
                }, function (result) {
                    if (result.length === 1) {
                        // update the array of assets
                        self.assets = result;

                        // update the image in customSettingsData
                        self.imageID(result[0].id);
                    }
                });
            };

      removeAsset = function () {
                self.assets = [];
                    // update the image in customSettingsData
                    self.imageID(null);
            };
            // update the display name based on the assets
            self.imageName = ko.computed(function () {
                // console.log('updating filenames...' + self.assets.length);
                var imageName = '';
                var anotherImageID = self.imageID();
                for (var i = 0; i < self.assets.length; i++) {
                    if (self.assets[i].id === anotherImageID) {
                        imageName = self.assets[i].fileName;
                        break;
                    }
                }
                return imageName;
            }, self);

            // Get component assets
            SitesSDK.getProperty('componentAssets', function (assets) {
                self.assets = assets;
                // Get custom settings
                SitesSDK.getProperty('customSettingsData', function (data) {
                    //update observable
          self.facebook(data.facebook);
          self.twitter(data.twitter);
          self.linkedin(data.linkedin);
          self.imageUrl(data.imageUrl);
                    self.imageID(data.imageID);
                    // console.log("imageIDs initial: " + self.imageIDs());
                    self.initialized(true);
                    self.saveData = true;
                });
            });
            // save whenever any updates occur
            self.save = ko.computed(function () {
                var saveconfig = {
          'facebook': self.facebook(),
                    'twitter': self.twitter(),
          'linkedin': self.linkedin(),
          'imageUrl': self.imageUrl(),
          'imageID': self.imageID()
                };

                // save data in page
                if (self.saveData) {
                    SitesSDK.setProperty('componentAssets', self.assets);
                    SitesSDK.setProperty('customSettingsData', saveconfig);
                }
            }, self);
        };
        // apply the bindings
        ko.applyBindings(new SettingsViewModel());
    </script>
</body>
</html>

Just test that settings screen is working by editing an existing site, drag&drop our component to a slot and double click the component bar (or use the component menu) to go to the settings screen and see that everything is working as expected.

modernbusinessmembersettings

Great! Our settings screen is working fine. So let’s continue with the render part. Not a big deal, same as with the settings we have built other components and we know that the process is basically retrieve custom settings and write ko observables and add specific listeners (for the selected image) so we can receive the notification and dynamically re-calculate the url. But as I commented at the beginning of the post, I want to replace the embedded html template with an external HTML file, as it’s easier to maintain and we can create a better separation between settings, logic and rendering. So the only big difference in the next code is the define declaration at the beginning, that will include a reference to a HTML file that we are going to create in the component assets folder.

Let’s do this as the first step. Just create in your file system a file called template.html (or whatever name you want) and just copy and paste the static component code (and statically reference to our component image). Template code should looks like this:

<div class="thumbnail">
<img class="img-responsive" src="/_themes/_components/modernbusiness-member/assets/img/BruceWayne.jpg" alt="">
<div class="caption">
<h3>Bruce Wayne</h3>
<h3 class="small">CEO, Wayne Enterprises</h3>
American billionaire, playboy and philanthropist
<ul class="list-inline">
	<li><a href="#"><i class="fa fa-2x fa-facebook-square"></i></a></li>
	<li><a href="#"><i class="fa fa-2x fa-linkedin-square"></i></a></li>
	<li><a href="#"><i class="fa fa-2x fa-twitter-square"></i></a></li>
</ul>
</div>
</div>

And now let’s create the render.js code which should be similar to this one:


/* globals define */
define(['knockout', 'jquery', 'text!./template.html'], function (ko, $, template) {
'use strict';
// ----------------------------------------------
// Define a Knockout Template for your component
// ----------------------------------------------
var componentTemplate = template;

// ----------------------------------------------
// Define a Knockout ViewModel for your template
// ----------------------------------------------
var SampleComponentViewModel = function (args) {
var self = this,
SitesSDK = args.SitesSDK;

// store the args
self.mode = args.viewMode;
self.id = args.id;

// create the observables
self.facebook = ko.observable();
self.twitter = ko.observable();
self.linkedin = ko.observable();
self.imageID = ko.observable();
self.imageUrl = ko.observable();
self.assetsURL = ko.observable();

self.imageID.subscribe(function (imageID) {
// whenever the image changes get the updated referenced asset
SitesSDK.getProperty('componentAssets', function (assets) {
if(assets && assets.length > 0){
if (assets[0].id === imageID) {
self.imageUrl(assets[0].url);
}
}else if(self.imageUrl()){
self.imageUrl(self.imageURL().replace('[!-- MY_ASSETS_URL --]',self.assetsURL()));
}
});
});
self.imageID.extend({ notify: 'always' });
// handle initialization
self.customSettingsDataInitialized = ko.observable(false);
self.initialized = ko.computed(function () {
return self.customSettingsDataInitialized();
}, self);

//
// Handle property changes
//

self.updateCustomSettingsData = $.proxy(function (customData) {
self.facebook(customData && customData.facebook);
self.twitter(customData && customData.twitter);
self.linkedin(customData && customData.linkedin);
SitesSDK.getProperty('assetsurl', function (assetsURL) {
self.assetsURL(assetsURL);
self.imageUrl(customData && customData.imageUrl && customData.imageUrl.replace('[!-- MY_ASSETS_URL --]', assetsURL));
});

self.imageID(customData && customData.imageID);
self.customSettingsDataInitialized(true);
}, self);
self.updateSettings = function (settings) {
if (settings.property === 'customSettingsData') {
self.updateCustomSettingsData(settings.value);
}
};

// listen for the EXECUTE ACTION request to handle custom actions
SitesSDK.subscribe(SitesSDK.MESSAGE_TYPES.EXECUTE_ACTION, $.proxy(self.executeActionsListener, self));
// listen for settings update
SitesSDK.subscribe(SitesSDK.MESSAGE_TYPES.SETTINGS_UPDATED, $.proxy(self.updateSettings, self));

//
// Initialize customSettingsData values
//
SitesSDK.getProperty('customSettingsData', self.updateCustomSettingsData);
};

// ----------------------------------------------
// Create a knockout based component implemention
// ----------------------------------------------
var SampleComponentImpl = function (args) {
// Initialze the custom component
this.init(args);
};
// initialize all the values within the component from the given argument values
SampleComponentImpl.prototype.init = function (args) {
this.createViewModel(args);
this.createTemplate(args);
this.setupCallbacks();
};
// create the viewModel from the initial values
SampleComponentImpl.prototype.createViewModel = function (args) {
// create the viewModel
this.viewModel = new SampleComponentViewModel(args);
};
// create the template based on the initial values
SampleComponentImpl.prototype.createTemplate = function (args) {
// create a unique ID for the div to add, this will be passed to the callback
this.contentId = args.id + '_content_' + args.mode;
// create a hidden custom component template that can be added to the DOM
this.template = '
<div id="' + this.contentId + '">' +
componentTemplate +
'</div>
';
};
//
// SDK Callbacks
// setup the callbacks expected by the SDK API
//
SampleComponentImpl.prototype.setupCallbacks = function () {
//
// callback - render: add the component into the page
//
this.render = $.proxy(function (container) {
var $container = $(container);
// add the custom component template to the DOM
$container.append(this.template);
// apply the bindings
ko.applyBindings(this.viewModel, $('#' + this.contentId)[0]);
}, this);
//
// callback - update: handle property change event
//
this.update = $.proxy(function (args) {
var self = this;
// deal with each property changed
$.each(args.properties, function (index, property) {
if (property) {
if (property.name === 'customSettingsData') {
self.viewModel.updateComponentData(property.value);
}
}
});
}, this);
//
// callback - dispose: cleanup after component when it is removed from the page
//
this.dispose = $.proxy(function () {
// nothing required for this sample since knockout disposal will automatically clean up the node
}, this);
};
// ----------------------------------------------
// Create the factory object for your component
// ----------------------------------------------
var sampleComponentFactory = {
createComponent: function (args, callback) {
// return a new instance of the component
return callback(new SampleComponentImpl(args));
}
};
return sampleComponentFactory;
});

Reviewing the code, you can see the most useful tip, that is convert our custom macro in a dynamic url to our image (when there is no real image selected by the contributor).

SitesSDK.getProperty(‘assetsurl’, function (assetsURL) {
self.assetsURL(assetsURL);
self.imageUrl(customData && customData.imageUrl && customData.imageUrl.replace(‘[!– MY_ASSETS_URL –]’, assetsURL));
});

SitesSDK.getProperty(property,callback) is a useful SDK function that provides you access to several internal properties, and some of them are the ones that are different depending if you are in the editorial side or in the published side. Specifically, ‘assetsurl’ provides the path to the component assets folder.

Let’s test again our component and we should see how our template is rendered (although currently our settings are not affecting its behavior, because the rendering is currently completely static)

staticmembercomponent

Final step is convert our static template.html into a dynamic template that will use knockout to render the settings and Sites Cloud Service tags to make text editable inline. The final template should be similar to this one:

<!-- ko if: initialized -->
<div class="thumbnail">
<img class="img-responsive" data-bind="attr: {src: imageUrl}">
<div class="caption">
<scs-title params="{ scsComponent: { 'renderMode': mode, 'parentId': id, 'id': 'nameId', 'data': '' } }"></scs-title>
<scs-title params="{ scsComponent: { 'renderMode': mode, 'parentId': id, 'id': 'jobtitleId', 'data': '' } }"></scs-title>
<scs-title params="{ scsComponent: { 'renderMode': mode, 'parentId': id, 'id': 'descriptionId', 'data': '' } }"></scs-title>
<!-- ko if: facebook() || twitter() || linkedin()  -->
<ul class="list-inline">
<!-- ko if: facebook() -->
	<li><a data-bind="attr: {href: facebook}"><i class="fa fa-2x fa-facebook-square"></i></a></li>
<!-- /ko -->
<!-- ko if: linkedin() -->
	<li><a data-bind="attr: {href: linkedin}"><i class="fa fa-2x fa-linkedin-square"></i></a></li>
<!-- /ko -->
<!-- ko if: twitter() -->
	<li><a data-bind="attr: {href: twitter}"><i class="fa fa-2x fa-twitter-square"></i></a></li>
<!-- /ko --></ul>
<!-- /ko --></div>
</div>
<!-- /ko -->

As you can see in the above code, I’ve added KO controls to display social network links only in the case that they exist (I’m not sure if Bruce Wayne has a LinkedIn account, but actually it has Facebook and Twitter profiles 🙂

Last step, as usual, is do a test, change image, modify text fields, add social network links, etc. well, at the end the whole tests to ensure the component is working as expected:

membercomponentfinal1
Image modified
membercomponentfinal2
Social network links
membercomponentfinal3
Removed the image and modified the text, adding the CKEditor capabilities available per field

With this, we have reached out the end of this tutorial. Now it’s your time to build your custom component. Stay tuned for next posts.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s