In a previous blog, I showed how to use a configuration page that attaches a Site Column to a Managed Metadata TermSet. This was done using JavaScript. With SharePoint Online and sandbox solutions, if you are unable to configure SharePoint using declarative XML, the only option you have left (apart from manually point and clicking after deployment) is using JavaScript.
In all the Sandbox solutions I have created there is always a core.wsp file that has a bunch of configuration. Recently I created an additional wsp that on first load I needed to run the same configuration scripts, however the configuration data needed to be different. For example, different quick launch navigation, different branding, different logo at the top left of the screen. This was a perfect scenario for using the JQuery extend to merge the JavaScript files together.
The demo today won’t actually show you any configuration code, however it will show you two solutions. The first solution, will display JavaScript on a page. This JavaScript is a copy of the main JavaScript configuration. After uploading the second solution the second JavaScript configuration will be merged with the first and displayed on the page. This is all done using the following code
if (CF.SharePoint.CoreSiteConfiguration.Data != null) { jQuery.extend(CF.SharePoint.CoreSiteConfiguration.Data, CF.SharePoint.SiteConfiguration.Data); };
Before I start walking through the solution, I want to thank Pumbaa80 on StackOverflow who provided me with the syntaxHighlight(json) function which allowed me to display JavaScript within a webpage prettified.
Solution 1 MergingJavascript.wsp
All the files from the first solution can be seen above. I’m just using JQuery library. The configuration data is in a file called CF.SharePoint.CoreSiteConfiguration.js and looks like below:
var CF = CF || {}; CF.SharePoint = CF.SharePoint || {}; CF.SharePoint.CoreSiteConfiguration = CF.SharePoint.CoreSiteConfiguration || {}; CF.SharePoint.CoreSiteConfiguration.Data = { Branding: { ColorPaletteUrl: "/_catalogs/theme/15/CF.spcolor", FontSchemeUrl: null, MasterPageUrl: "/_catalogs/masterpage/CF/CF.master", CustomMasterPageUrl: "/_catalogs/masterpage/CF/CF.master", BackgroundImageUrl: null }, TaxonomyFields: [ { Id: '{F5F171D8-47C7-4698-8B28-78A86A95A2E8}', Name: 'CF_TaxCountries' }, { Id: '{C03CC67A-4E66-4723-9D0F-3D7CB117DA0C}', Name: 'CF_TaxNavigation' }, ], ContentTypeBindings: [ { ContentTypeId: "0x0101003C7036AFEEB24CFC95926C7E222CE2BC", ContentTypeName: "CF Document", ListTitle: "Documents", ListUrl: "Shared Documents" } ], DefaultContentTypes: [ { ContentTypeId: "0x0101003C7036AFEEB24CFC95926C7E222CE2BC", ContentTypeName: "CF Document", ListTitle: "Documents", ListUrl: "Shared Documents" } ], RequiredSharePointJSFiles: [ "sp.js", "sp.ui.dialog.js", "sp.taxonomy.js", "sp.publishing.js" ] };
I’ve declared a namespace at the top of the JavaScript file, then it’s just a bunch of objects declared within CF.SharePoint.CoreSiteConfiguration.Data.
I upload jQuery and my configuration.js file to a folder called Assets at the site collection. You can see in my solution, I have a Module called MO_Assets and a folder called Scripts. The elements file for this Module is as follows:
<?xml version="1.0" encoding="utf-8"?> <Elements xmlns="http://schemas.microsoft.com/sharepoint/"> <Module Name="Assets"> <File Path="MO_Assets\Scripts\jquery-2.1.1.min.map" Url="Assets/Scripts/jquery-2.1.1.min.map" ReplaceContent="TRUE" /> <File Path="MO_Assets\Scripts\jquery-2.1.1.min.js" Url="Assets/Scripts/jquery-2.1.1.min.js" ReplaceContent="TRUE" /> <File Path="MO_Assets\Scripts\CF.SharePoint.CoreSiteConfiguration.js" Url="Assets/Scripts/CF.SharePoint.CoreSiteConfiguration.js" ReplaceContent="TRUE"/> </Module> </Elements>
This JavaScript is inserted into my page using a Custom Action CA_JavascriptConfiguration.
Take note: JQuery is Sequence 10, the CoreSiteConfiguraiton.js is inserted as Sequence 11.
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?> <Elements xmlns="http://schemas.microsoft.com/sharepoint/"> <CustomAction ScriptSrc="~SiteCollection/assets/Scripts/jquery-2.1.1.min.js" Location="ScriptLink" Sequence="10"> </CustomAction> <CustomAction ScriptSrc="~SiteCollection/assets/Scripts/CF.SharePoint.CoreSiteConfiguration.js" Location="ScriptLink" Sequence="11"> </CustomAction> </Elements>
Lastly I have an aspx page that will display the JavaScript. This page is just a standard SharePoint aspx page you might write.
<%@ Assembly Name="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Import Namespace="Microsoft.SharePoint.WebPartPages" %> <%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Import Namespace="Microsoft.SharePoint" %> <%@ Assembly Name="Microsoft.Web.CommandUI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Page Language="C#" MasterPageFile="~masterurl/default.master" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage,Microsoft.SharePoint,Version=15.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" EnableViewState="false" meta:webpartpageexpansion="full" meta:progid="SharePoint.WebPartPage.Document" %> <asp:Content ID="Content1" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server" > </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea" runat="server"> Display JavaScript Page </asp:Content> <asp:Content ID="Content3" ContentPlaceHolderID="PlaceHolderSearchArea" runat="server" /> <asp:Content ID="Content4" ContentPlaceHolderID="PlaceHolderMain" runat="server"> <SharePoint:FormDigest ID="FormDigest1" runat="server"/> </asp:Content>
Within the PlaceHolderMain after the FormDigest1 we add a <pre> box to display the code.
<pre id="DisplayText"></pre>
Within the PlaceHolderAdditionalPageHead we will add the CSS and the Javascript that will get the ConfigurationData, then Stringify it, then lastly syntax highlight it to prettify it for the demo, before adding it to the <pre> html tag ID “DisplayText”. Note: If the JavaScript within this page was the actual configuration functions, it would probably be in a separate file, then added to the page using a Custom Action. This custom action sequence would need to be higher that the JQuery and the Configuration data we added earlier. Ideally, it should be a few sequence’s lower, so that you can add more JavaScript files before your configuration code if needed. In this case, because it is on the page, it is ran after the custom actions anyway.
<style type="text/css"> #DisplayText {outline: 1px solid #ccc; padding: 5px; margin: 5px; } .string { color: green; } .number { color: darkorange; } .boolean { color: blue; } .null { color: magenta; } .key { color: red; } </style>
<script type="text/javascript"> function syntaxHighlight(json) { json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { var cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return '<span class="' + cls + '">' + match + '</span>'; }); } function loadFunction() { var str = JSON.stringify(CF.SharePoint.CoreSiteConfiguration.Data, undefined, 4); jQuery("#DisplayText").append(syntaxHighlight(str)); }; _spBodyOnLoadFunctionNames.push("loadFunction"); </script>
The Page module has an elements file, here I’m just adding the DisplayJavascript.aspx to the web file which will only show up in the RootWeb.
<?xml version="1.0" encoding="utf-8"?> <Elements xmlns="http://schemas.microsoft.com/sharepoint/"> <Module Name="MO_Pages" Path="MO_Pages" RootWebOnly="TRUE"> <File Path="DisplayJavascript.aspx" Url="DisplayJavascript.aspx" Type="Ghostable" ReplaceContent="TRUE" /> </Module> </Elements>
After adding the Module, Custom action and the Page to the Web Feature, we can package up the WSP and deploy to our teamsite. Activate the Web Feature, mine is called CFSP Merging JavaScript. Then navigate to your site [sitecollection]/DisplayJavascript.aspx.
Solution 2 AdditionalJavaScript.
Ok, so we have the basic configuration in place. This would be the JavaScript objects that would be used during configuration. However in a second web site, we still want the Core solution to be installed, but when we configure we want the configuration script to use a different/merged set of JavaScript objects. This demo solution is made of up just the new JavaScript objects and a custom action. Below is the solution structure.
The Module MO_SiteConfigurationFile has a JavaScript file which is added to the same Assets library of the original Site Configuration data file. This file doesn’t overwrite the Site Configuration, it is just added to the folder.
var CF = CF || {}; CF.SharePoint = CF.SharePoint || {}; CF.SharePoint.SiteConfiguration = CF.SharePoint.SiteConfiguartion || {}; CF.SharePoint.SiteConfiguration.Data = { Branding: { ColorPaletteUrl: "/_catalogs/theme/15/CF.orange.spcolor", FontSchemeUrl: "/_catalogs/theme/15/CF.spfont", MasterPageUrl: "/_catalogs/masterpage/CF/CFWithLeftNav.master", CustomMasterPageUrl: "/_catalogs/masterpage/CF/CFWithLeftNav.master", BackgroundImageUrl: null }, NonPublishingNavigation: { QuickLaunchNavigation: { List: [ { Title: 'Home', Url: '/' }, { Title: 'Notebook', Url: '/SiteAssets/$WebTitle Notebook' }, { Title: 'Documents', Url: '/Documents/Forms/AllItems.aspx' }, { Title: 'Calendar', Url: '/Lists/Calendar/calendar.aspx' }, { Title: 'Tasks', Url: '/Lists/Tasks/AllItems.aspx' }, { Title: 'Site Contents', Url: '/_layouts/15/viewlsts.aspx' } ], KeepRecent: false } }, RequiredSharePointJSFiles: [ "sp.js", "sp.ui.dialog.js", "sp.taxonomy.js", "sp.publishing.js", "sp.userprofiles.js" ] } //Merge new and updated objects over existing objects. if (CF.SharePoint.CoreSiteConfiguration.Data != null) { jQuery.extend(CF.SharePoint.CoreSiteConfiguration.Data, CF.SharePoint.SiteConfiguration.Data); };
You will notice that I’m using a different namespace. CF.SharePoint.SiteConfiguration, where the configuration data file of the core solution was CF.SharePoint.CoreSiteConfiguration. The Branding is different, I’m using a different colour file, different master pages too. I’m not making any changes to the original Taxonomy Fields, Content Type Bindings, or Default Content Types. I have added configuration for the Quick Launch Navigation, and the RequiredSharePointJSFiles has an extra value “sp.userprofiles.js”. Note: When you are merging the files, an object structure needs to be copied over as a whole, and any values that are to remain the same need to be included in the new file. Otherwise it will assume this value is replace everything.
For example the RequiredSharePointJSFiles.
If in my new configuration data file I just put:
RequiredSharePointJSFiles: [ "sp.userprofiles.js" ]
When merged, only that value would be remaining, it merges at the top object level (Branding, TaxonomyFields, ContentTypeBindings, DefautlContentTypes, NonPublishingNavigation, RequiredSharePointJSFiles). Therefore to ensure all the original values are there including your new ones, you need to include everything you want to keep at that top level too.
RequiredSharePointJSFiles: [ "sp.js", "sp.ui.dialog.js", "sp.taxonomy.js", "sp.publishing.js", "sp.userprofiles.js" ]
Lastly at the bottom of this file, we are checking if the original CoreSiteConfiguraiton exist, and if so Merge CF.SharePoint.SiteConfiguration.Data into CF.SharePoint.CoreSiteConfiguration.Data, using the jQuery.extend function. The merge is happening at the .Data level.
The element file for the SiteConfigurationFile module as stated earlier is added to the same asset folder we added our JavaScripts file to in the first solution.
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Module Name="MO_SiteConfigurationFile">
<File Path="MO_SiteConfigurationFile\CF.SharePoint.Configuration.Data.js" Url="Assets/Scripts/CF.SharePoint.Configuration.Data.js" ReplaceContent="TRUE"/>
</Module>
</Elements>
The custom action inserts this JavaScript into the page. Note: The Sequence number for CF.SharePoint.Configuration.Data.js is higher in value than CF.SharePoint.SiteConfiguration.Data.js meaning it is loaded after the CF.SharePoint.SiteConfiguration.Data.js file. As stated earlier this sequence number needs to be a higher value than the original configuration data script file, but lower than the actual configuration script. In our case, our configuration script is on the page, meaning it will be loaded last.
<?xml version="1.0" encoding="utf-8"?> <Elements xmlns="http://schemas.microsoft.com/sharepoint/"> <CustomAction ScriptSrc="~SiteCollection/assets/Scripts/CF.SharePoint.Configuration.Data.js?version=1.0.0.0" Location="ScriptLink" Sequence="12"> </CustomAction> </Elements>
Add both these modules to the solutions web feature. My feature title is called CFSP.AdditionalJavaScript. Package up your solution and then deploy to your site where your original solution already is.
Activate the web feature, then navigate back to the page located at [sitecollection]/DisplayJavascript.aspx.
You can now see that file files have truly been merged together. We never touched the original JavaScript code on the DisplayJavaScript.aspx page, but yet the file that had a different namespace is clearly merged in with the original configuration data. Branding now has the new values, the NonPublishingNavigation is now there, and the RequiredSharePointJSFiles have the “sp.userprofiles.js” included.
To prove that it has truly merged, using Console in Chrome, the NonPublishingNavigation is a new value that is actually part of the Namespace CF.SharePoint.SiteConfiguration.Data, but if you type in CF.SharePoint.CoreSiteConfiguration.Data, you can clearly see that NonPublishingNavigation is part of this namespace now.
Link to this solution can be found on my OneDrive