OfficeJS.dialogs Updated to v1.0.9

Like yesterday with easyEws, it has been a while since I have touched the OfficeJS.Dialogs library. I updated it yesterday with a quick fix and some content updates. I have had questions about using it from a CDN. The primary issue is that the displayDialogAsync() API does not support CDN. It just displays the HTML as raw text in the dialog. There are some ways I can provide this as a workaround, but it involves me support infrastructure. So, the better thing is to just wait for the API to support CDN’s.

If you have any questions about using this library, please let me know.

Updated Outlook Web Add-in Sample

I will be presenting a demo at the MVP Summit 2018 for Outlook and also helping with some labs in Excel on the OfficeJS platform. In preparation, I updated my Outlook Sample on my GitHub. This sample was created in VSCode via a Yeoman template.

What the add-in does is a check of all users on the To/CC/BCC line, splits apart any groups (or groups in groups) and then checks to see if any of the user emails are external to your domain. If any external users are found it prompts you with dialog asking if you are sure you want to send:

Outlook Blocking Add-in

The updated add-in demonstrates:

  • The OnSend event
  • The use of dialogs
  • And the newly added ExpandDL function for Exchange Web Services through the makeEwsRequestAsync() call.

In this sample, I am using both of my libraries:

  • easyEws – to help with the ExpandDL call. Updated in a previous post.
  • OfficeJS.dialogs – to display the dialog you see above.

I also had to create a set of asynchronous functions and asynchronous recursive function calls to perform this work – which got a tad complex. For now all the code is in the function-file.js, but to help with splitting out all recipients and groups I might build this into a new library. For example, here is the function to recursively call itself asynchronously (splitting groups in groups in groups in groups…):


/**
* Splits a group and calls the completed function
*/
function splitGroupsAndFindExternalsRecursivelyAsync() {
if(groups.length == 0) {
// if no groups stop
completedCallback();
} else {
/** @type {string} */
var group = groups.pop();
// call expandGroup to get users
easyEws.expandGroup(group, function(groupUsers) {
groupUsers.forEach(function(groupUser, index){
if(groupUser.MailboxType() == "PublicDL") {
groups.push(groupUser);
} else {
/** @type {string} */
var emailDomain = getDomain(groupUser.Address());
if(emailDomain != domain) {
externals.push(groupUser.Address());
}
}
}); // groupUsers.forEach
splitGroupsAndFindExternalsRecursivelyAsync(); // recursive
}, function(error) {
console.log(error);
// just fail
completedCallback();
}); // easyEws.expandGroup
} // end-if
}

Anyway, I will be demonstrating this add-in and the functionality required at the MVP Summit. So if you are attending, I hope to see you there.

OfficeJS.dialogs v1.0.7 Published

So, I kind of skipped v1.0.6, which I promised in this post yesterday. There were some unchecked changes on a different laptop that I forgot in v1.0.6, so I published v1.0.7 this morning with all the updates. Some notable update:

  • Moved from CloseDialog() to CloseDialogAsync() on all the dialogs using the original close method. This is to workaround the issue described in the post yesterday.
  • Fixed dialog flashing by hiding the body until the form is ready to be shown. Before you might get a flash of all the controls on the form as they were not hidden before.
  • Internal clean-up and message handling. In the message pump between the dialogs.html and dialogs.js, I cleaned up the pump to use internally standardized messages.
  • Fixed some JSDoc information that was wrong. And there is likely still more. wlEmoticon-disappointedsmile.png
  • Fixed some things that were not working correctly with the progress dialog.
  • Small tweeks and comments, code clean-up

The latest version is now on GitHub. I have also completed all the documentation and updated it to the latest usage information, code examples and screenshots.

I also published the latest version to NPM. Simply type this in you code terminal window:

npm -install officejs.dialogs

CDN Update

I still have a published CDN for OfficeJS.dialogs, but at this point I am going to suggest against using it. I have encountered a number of issues with my message pump when loaded cross-domain. Specifically:

  • you will be able to show a MessageBox, but the Update() method will not work
  • you will be able to show a Progress dialog, but the Update() method will not change the dialog. The progress bar will never advance.

What is happening is that I was going to need for **you** the  developer to provide me your own proxyHTML file (pointing to my CDN/proxy.js library) that I will do the following:

  • Add an iframe, load my html file in that iFrame, post the dialog settings to the iFrame where my dialog in the iFrame gets it, and writes it to localStorage on the CDN domain.
  • Window.replace the contents with my dialogs.html from the CDN. And since the localStorage now has the dialog settings, configure the dialog.
  • So far at this point everything worked. From HERE is where I am pushing my CORS and JavaScript to the edge of sanity…
  • I then create an hidden iFrame in my dialog and then load your proxy html file. I attach to the body change event inside the iFrame.
  • I then start a message pump in the proxy looking for changes to localStorage.
  • If a new message comes from your code with an Update() on MessageBox or Progress, I then get it from the localStorage in a message and write it to the body, therefore updating the iFrame in my dialog, thereby triggering the change event I latched on to, allowing me to write that value to the CDN localStorage where the message pump in dialog will get the message and update the form.

That hurt my brain writing that out. And maybe that is more for my posterity since it took me Binging the Internet to death (I don’t use to the G word folks wlEmoticon-hotsmile.png), just to get that far. My pretzel, I mean head hurts.

Bottom line, I want to get OfficeJS.dialogs to work cross-domain. I am not sure if the above method is the right approach. I have asked far brighter minds than my own and watched Autie Anne’s Syndrome set in almost immediately. So, if any of you have some ideas on how I can establish 2-way communication between domains like this (without using some crazy huge library), please let me know. wlEmoticon-smile.png

OfficeJS: Second Dialog Does not Display

I have been spending a lot of time in the Officeui.dialog lately. One of my customers has been too and it has been an adventure working out exactly the best way to get messages displayed while running code in the background asynchronously. wlEmoticon-disappointedsmile.png

I am not sure if this problem is limited to the online version of Outlook, but this is where I have been seeing the problem (and where I have spent virtually all of my time). If my code tried to open two dialogs (using Office.context.ui.displayDialogAsync()) one right after the other, the second dialog would not ever be displayed. If I waited a period and then tried again, it would. But we don’t want that. We want boom-boom, dialogs. When I looked at the console, I would see an error like the following:

Uncaught TypeError: Cannot read property ‘addEventHandler’ of undefined

Or, if I read the error object from the displayDialogAsync() function/asyncResult, I would see this:

{name: “Display Dialog Error”, message: “The operation failed because this add-in already has an active dialog.“, code: 12007}

Here is an example of some code that will reproduce the issue:

[code lang=”javascript” collapse=”true” title=”click to expand if the github.com embedding below is not visible.”]
var i = 0;
function displayDialog() {
var url = "https://localhost:3000/test.html";
Office.context.ui.displayDialogAsync(url,{height:20, width:30, displayInIframe:true},
function (asyncResult) {
var dialog = asyncResult.value; // get the dialog
var error = asyncResult.error;
if(dialog == undefined && error.code > 0) {
// log the error
console.log(error.message);
} else {
// attache the events
dialog.addEventHandler(Office.EventType.DialogEventReceived, function (arg) {
// close this dialog, open the next
dialog.close();
i++;
if(i<4) {
displayDialog();
}
});
dialog.addEventHandler(Office.EventType.DialogMessageReceived, function (arg) {
// close this dialog, open the next
dialog.close();
i++;
if(i<4) {
displayDialog();
}
});
}
});
}
[/code]


var i = 0;
function displayDialog() {
var url = "https://localhost:3000/test.html&quot;;
Office.context.ui.displayDialogAsync(url,{height:20, width:30, displayInIframe:true},
function (asyncResult) {
var dialog = asyncResult.value; // get the dialog
var error = asyncResult.error;
if(dialog == undefined && error.code > 0) {
// log the error
console.log(error.message);
} else {
// attache the events
dialog.addEventHandler(Office.EventType.DialogEventReceived, function (arg) {
// close this dialog, open the next
dialog.close();
i++;
if(i<4) {
displayDialog();
}
});
dialog.addEventHandler(Office.EventType.DialogMessageReceived, function (arg) {
// close this dialog, open the next
dialog.close();
i++;
if(i<4) {
displayDialog();
}
});
}
});
}

view raw

dialogIssue.js

hosted with ❤ by GitHub

Notice I had used dialog.close(), but it did not work as designed. What I believe is happening is that the previous dialog is still in memory and has not been cleaned up. What needs to likely happen is a closeAsync().

In order to resolve this, I created the following function: dialogCloseAsync(). This works by issuing the close() and then attempting to add an event handler to the dialog in ansyc (setTimeout) loop. When it errors, we trap the error and issue the async callback. It is a bit ugly as we are trapping an error to get around the problem, but this was the only way I could find a way around the problem. Here is what the function looks like:

[code lang=”javascript” collapse=”true” title=”click to expand if the github.com embedding below is not visible.”]
/**
* Closes the currently open dialog asynchronously.
* This has an ugly workaround which is to try to set a new
* event handler on the dialog until it fails. When it failed
* we know the original dialog object was destroyed and we
* can then proceed. The issue we are working around is that
* if you call two dialogs back to back, the second one will
* likely not open at all.
* @param {Office.context.ui.dialog} dialog The dialog to be closed
* @param {function()} asyncResult The callback when close is complete
*/
function dialogCloseAsync(dialog, asyncResult){
// issue the close
dialog.close();
// and then try to add a handler
// when that fails it is closed
setTimeout(function() {
try{
dialog.addEventHandler(Office.EventType.DialogMessageReceived, function() {});
dialogCloseAsync(dialog, asyncResult);
} catch(e) {
asyncResult(); // done – closed
}
}, 0);
}
[/code]


/**
* Closes the currently open dialog asynchronously.
* This has an ugly workaround which is to try to set a new
* event handler on the dialog until it fails. When it failed
* we know the original dialog object was destroyed and we
* can then proceed. The issue we are working around is that
* if you call two dialogs back to back, the second one will
* likely not open at all.
* @param {Office.context.ui.dialog} dialog The dialog to be closed
* @param {function()} asyncResult The callback when close is complete
*/
function dialogCloseAsync(dialog, asyncResult){
// issue the close
dialog.close();
// and then try to add a handler
// when that fails it is closed
setTimeout(function() {
try{
dialog.addEventHandler(Office.EventType.DialogMessageReceived, function() {});
dialogCloseAsync(dialog, asyncResult);
} catch(e) {
asyncResult(); // done – closed
}
}, 0);
}

I had been encountering this issue with different systems when developing the OfficeJS.dialogs library and had tried to set a timeout before I showed each dialog. That worked on some systems, but on others the timeout needed to be longer. So, setting a default timeout did not work. Using this in the original sample, provided above, the code would look like this:

[code lang=”javascript” collapse=”true” title=”click to expand if the github.com embedding below is not visible.”]
var i = 0;
function displayDialog() {
var url = "https://localhost:3000/test.html&quot;;
Office.context.ui.displayDialogAsync(url,{height:20, width:30, displayInIframe:true},
function (asyncResult) {
var dialog = asyncResult.value; // get the dialog
var error = asyncResult.error;
if(dialog == undefined && error.code > 0) {
// log the error
console.log(error.message);
} else {
// attache the events
dialog.addEventHandler(Office.EventType.DialogEventReceived, function (arg) {
// close this dialog, open the next
dialogCloseAsync(dialog, function() {
i++;
if(i<4) {
displayDialog();
}
});
});
dialog.addEventHandler(Office.EventType.DialogMessageReceived, function (arg) {
// close this dialog, open the next
dialogCloseAsync(dialog, function() {
i++;
if(i<4) {
displayDialog();
}
});
});
}
});
}
[/code]


var i = 0;
function displayDialog() {
var url = "https://localhost:3000/test.html&quot;;
Office.context.ui.displayDialogAsync(url,{height:20, width:30, displayInIframe:true},
function (asyncResult) {
var dialog = asyncResult.value; // get the dialog
var error = asyncResult.error;
if(dialog == undefined && error.code > 0) {
// log the error
console.log(error.message);
} else {
// attache the events
dialog.addEventHandler(Office.EventType.DialogEventReceived, function (arg) {
// close this dialog, open the next
dialogCloseAsync(dialog, function() {
i++;
if(i<4) {
displayDialog();
}
});
});
dialog.addEventHandler(Office.EventType.DialogMessageReceived, function (arg) {
// close this dialog, open the next
dialogCloseAsync(dialog, function() {
i++;
if(i<4) {
displayDialog();
}
});
});
}
});
}

As I found this workaround, I have updated OfficeJS.dialogs to use dialogCloseAsync(). Now, the MessageBox, Wait and Form objects will use closeDialogAsync() commands to replace the original closeDialog() commands I provided previously. I will be blogging about the updates to v1.0.6, shortly. wlEmoticon-hotsmile.png