12

During my working on converting existing pages to be lightning experience style (as known as slds style). One of the challenges I have faced is to convert <apex:pageMessages/> . Usually the controller side of code is something like below:

public class ConvertPageMessagesController{
    public ConvertPageMessagesController(){
        ApexPages.addmessage(new ApexPages.message(ApexPages.severity.FATAL,'Please enter Account name'));
        ApexPages.addmessage(new ApexPages.message(ApexPages.severity.ERROR,'Please enter Account number'));
        ApexPages.addmessage(new ApexPages.message(ApexPages.severity.WARNING,'Please enter Account phone'));
        ApexPages.addmessage(new ApexPages.message(ApexPages.severity.INFO,'Please enter Account site'));
        ApexPages.addmessage(new ApexPages.message(ApexPages.severity.CONFIRM,'Please enter Account industry'));
    }
}

There are definitely walkaround on this. I can use a page_message and a serverity_level property and check the page status and the page message and use front-end code to handle the logic. But the code will inevitable be long and tedious. Is there a good way of handling this?

19

Here is what I do:

Class used by all my controllers:

VF Messages Abstract:

public abstract class VF_Messages_Abstract {


    public void createError(ApexPages.Severity s, String message) {
        ApexPages.addMessage(
                New ApexPages.Message(
                        s, string.escapeSingleQuotes(message.escapeJava())
                )
        );

    }

    /**@description Indicates if a custom message is present*/
    public boolean hasMessages {
        get {
             return ApexPages.hasMessages();
        }
    }

    /**@description The custom error message */
    public String errorMessage {
        get {
            if (hasMessages) {
                return ApexPages.getMessages()[0].getDetail();
            }

            return null;
        }
    }

    /**@description The type of message: 'error' or 'success' */
    public string alertType {
        get {
            if (hasMessages) {
                return ApexPages.getMessages()[0].getSeverity() == ApexPages.Severity.CONFIRM ? 'success' : 'error';
            }

            return 'error';
        }
        private set;
    }



}

All controller for VF extend this class

Then in my vf pages I use the following:

Messages Toast

    <!-- ERROR ALERT DIV -->
    <apex:outputPanel layout="block" id="msg_block" style="display: none;">
        <div id="err_wrapper"
             class="slds-notify slds-notify--alert slds-theme--{!alertType} slds-theme--alert-texture"
             role="alert">
            <h2>
                <div xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
                    <svg aria-hidden="true"
                         class="slds-icon icon-text-email slds-icon--small slds-m-right--x-small">
                        <use xlink:href="{!URLFOR($Resource.SLDS, '/assets/icons/utility-sprite/svg/symbols.svg#' + if(alertType = 'success','check','ban'))}"></use>
                    </svg>
                    <span id="err_text"></span>
                </div>
            </h2>
        </div>

    </apex:outputPanel>

Helper Panel

        <!--APEX VARIABLES DIV-->
        <apex:outputPanel id="post_processing">

            <script>

                var hasMessages = {!hasMessages};
                var errorMessage = "{!errorMessage}";

                checkMessages();

            </script>

        </apex:outputPanel>

Check Messages Code (Using jQuery)

function checkMessages() {
    if (hasMessages) {
        $('#err_text').html(errorMessage);
        $('[id$=msg_block]').show();
    } else {
        $('[id$=msg_block]').hide();
    }

}

My buttons, action functions, etc rerender the post_processing and msg_block

Creating an error In the controller when I catch or want to display an error I simply:

createError(ApexPages.Severity.[ERROR | CONFIRM],YOURMESSAGEHERE);

This allows me to control the messages, when to display it and which theme to show.

Note: The xmlns= on the div containing the SVG is a MUST as if you try to rerender without it the page will simply stall. Important trick to remember for anytime you are re rendering a container containing an SLDS SVG

If you need a example all put together let me know.

End result is (Error):

enter image description here

Or (Confirm)

enter image description here

I also have implemented a way to pass messages between transactions so if you need to say DML on button click and then Callout oncomplete of button click, if there was an error in the first step, when the second step happens it does not clear the message like it normally would (not shown in code)

  • Thank you for sharing, Eric. I will post that method in my blog, too, if you don't mind. It has a bit more code, but definitely has two obvious advantageous: 1, can work on multiple types of messages: toast, alert, dialog. 2, don't need to have the assumption the implementation of VF is unchanged. – Lance Shi Nov 23 '16 at 1:24
  • @LanceShi - I do not mind at all. Hope it helps someone. I have not completely made the transition yet to Lightning so I am getting good at working with VF and SLDS (at least I think I am which may be worse :) ) – Eric Nov 23 '16 at 1:26
  • 1
    @AdrianLarson - No idea, she was there when I created the DE org from salesforce. So (I think) she is in all DE orgs created from the Env Hub. She is a contact on United Oil & Gas, Singapore. I'll crop it just in case – Eric Nov 23 '16 at 1:35
  • @AdrianLarson yea not for this answer. It essentially hides the modal loader/spinner. I'll remove it to remove confusion – Eric Feb 6 '17 at 2:20
  • I wonder how hard it is to scale this solution for an arbitrary number of messages. Finally getting my feet wet in this arena. – Adrian Larson Feb 6 '17 at 2:21
10

Eric's answer inspired me to build a more generic tool that can support an arbitrary number of page messages of any severity.

Code

Controller

public class ToastBuilderController
{
    public String getMessageJSON() { return JSON.serialize(getSafeMessages()); }
    public List<PageMessage> getSafeMessages()
    {
        List<PageMessage> messages = new List<PageMessage>();
        for (ApexPages.Message message : ApexPages.getMessages())
            messages.add(new PageMessage(message));
        return messages;
    }
}

Serializable PageMessage

global class PageMessage
{
    global final String summary, detail;
    global final String sprite, theme;
    global final ApexPages.Severity severity;
    global PageMessage(Exception e)
    {
        this(ApexPages.Severity.FATAL, e.getMessage());
    }
    global PageMessage(ApexPages.Message message)
    {
        this(message.getSeverity(), message.getSummary(), message.getDetail());
    }
    global PageMessage(ApexPages.Severity severity, String summary, String detail)
    {
        this.severity = severity;
        this.summary = summary;
        this.detail = detail;

        String s = String.valueOf(severity);
        this.sprite = sprites.get(s);
        this.theme = themes.get(s);
    }
    global PageMessage(ApexPages.Severity severity, String summary)
    {
        this(severity, summary, null);
    }

    static final Map<String, String> themes = new Map<String, String>
    {
        String.valueOf(ApexPages.Severity.FATAL) => 'error',
        String.valueOf(ApexPages.Severity.ERROR) => 'error',
        String.valueOf(ApexPages.Severity.WARNING) => 'warning',
        String.valueOf(ApexPages.Severity.CONFIRM) => 'success'
    };
    static final Map<String, String> sprites = new Map<String, String>
    {
        String.valueOf(ApexPages.Severity.FATAL) => 'ban',
        String.valueOf(ApexPages.Severity.ERROR) => 'warning',
        String.valueOf(ApexPages.Severity.WARNING) => 'warning',
        String.valueOf(ApexPages.Severity.CONFIRM) => 'success',
        String.valueOf(ApexPages.Severity.INFO) => 'info'
    };
}

Toast Builder Component

Probably should move the JS to a Static Resource but I was trying to whip this together quickly.

<apex:component controller="ToastBuilderController">
    <apex:slds />
    <div class="slds-scope">
        <div id="messages"></div>
    </div>
    <script>
    (function (D, w) {
        "use strict";
        var T = w.ToastBuilder = w.ToastBuilder || {};
        T.sprites = {
            "success": "{!URLFOR($Resource.SLDS214, '/assets/icons/utility-sprite/svg/symbols.svg#success')}",
            "error": "{!URLFOR($Resource.SLDS214, '/assets/icons/utility-sprite/svg/symbols.svg#error')}",
            "info": "{!URLFOR($Resource.SLDS214, '/assets/icons/utility-sprite/svg/symbols.svg#info')}"
        };
        T.getMessages = function () {
            return JSON.parse("{!JSENCODE(messageJSON)}");
        };
        T.clear = function () {
            while (T.targetDiv.hasChildNodes()) {
                T.targetDiv.removeChild(T.targetDiv.lastChild);
            };
        };
        T.buildToasts = function (messages) {
            if (!messages) messages = T.getMessages();
            T.clear();
            messages.forEach(function (message) {
                T.buildToast(message);
            });
        };
        T.buildToast = function (message) {
            var toast = D.createElement("div");
            toast.className = "slds-notify slds-notify--alert slds-theme--alert-texture";
            if (message.theme) toast.className += " slds-theme--" + message.theme; 
            toast.setAttribute("role", "alert");

            var innerDiv = toast.appendChild(D.createElement("h2"))
                .appendChild(D.createElement("div"));
            innerDiv.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
            innerDiv.setAttribute("xmlns", "http://www.w3.org/2000/svg");
            innerDiv.appendChild(D.createElement("span"))
                .appendChild(D.createTextNode(message.summary));

            var svg = innerDiv.appendChild(D.createElement("svg"));
            svg.className = "slds-icon icon-text-email slds-icon--small slds-m-right--x-small";
            svg.setAttribute("aria-hidden", true);

            var use = svg.appendChild(D.createElement("use"));
            use.setAttribute("xlink:href", T.sprites[message.sprite]);

            T.targetDiv.appendChild(toast);
        };
        T.addError = function (message) {
            T.clear();
            T.buildToast({
                theme: "error", sprite: "warning", summary: message
            });
        };
        T.confirm = function (message) {
            T.clear();
            T.buildToast({
                theme: "success", sprite: "success", summary: message
            });
        };
        D.addEventListener("DOMContentLoaded", function () {
            T.targetDiv = D.getElementById("messages");
            T.buildToasts();
            Object.freeze(T);
        });
    }(document, window));
    </script>
</apex:component>

Usage

RemoteAction

(function (w) {
    "use strict"
    MyRemotingController.doSomething("Some Parameter", function (result, event) {
        if (event.status) {
            if (result.length === 0) {
                // success handling
            } else {
                w.ToastBuilder.buildToasts(result);
                // other error handling
            }
        } else {
            w.ToastBuilder.addError(event.message);
        }
    });
}(window));

Remote Objects

(function (w) {
    "use strict";
    new SObjectModel.Contact().update(
        ["{!Contact.Id}"], {"SomeField__c": someValue}, function (error) {
            if (error) {
                w.ToastBuilder.addError(error);
                // other error handling
            } else {
                // success handling
            }
        }
    );
}(window));

ApexPages

If you use the standard approach to adding page messages to the page, calling buildToasts will take the messageJSON property and use it to build toasts corresponding to ApexPages.getMessages().

(Controller)

try
{
    // do stuff
}
catch (SomeException e)
{
    ApexPages.addMessages(e);
}

(Page)

<apex:commandButton action="{!doStuff}" oncomplete="ToastBuilder.buildToasts()" />
  • var T = w.ToastBuilder = w.ToastBuilder || {}; Is this one a type? – Lance Shi Feb 9 '17 at 23:01
  • The T is just shorthand, and w.ToastBuilder is just a namespace to group a few functions together. Not sure exactly what you're asking? You are not able to reference T outside the closure, but ToastBuilder is added to the window (referenced as w within the closure). – Adrian Larson Feb 9 '17 at 23:03
  • I mean should it be just var T = w.ToastBuilder || {}; ? Why there is an extra = w.ToastBuilder = w.ToastBuilder || {}? – Lance Shi Feb 9 '17 at 23:05
  • How do you handle messages when one action calls a second action and the message from first action needs displayed? Or does it do that. In vanilla vf the message would've cleared in second action. Use case. First action dml second action callout if first ok, dml if first not ok. That was the point of my persistent message. Great work. Makes me feel like a newbie – Eric Feb 9 '17 at 23:05
  • Ah I see what you're saying. It's a way of checking if it's already been defined on the page. In other words, if there is already a ToastBuilder on the window, use that, otherwise instantiate a new empty object. – Adrian Larson Feb 9 '17 at 23:06
3

Below is the current approach I have found and used. But I am open to any other suggestions.

The idea and code originally is coming from: https://vishnuvaishnav.wordpress.com/2016/02/21/convert-standard-page-messages-in-lightning-design-system/

I have also written a blog about this idea at: http://sfdcinpractice.com/index.php/2016/11/22/convert-apexpagemessages-slds-style/

Basically, it is a little bit hacking into the Visualforce page by changing the style class into slds ones.

The css:

<style>
.msgIcon {
    display: none!important
}
.customMessage * {
    color: #fff!important
}
.customMessage {
    margin: 5px 0!important;
    max-width: 1280px;
    opacity: 1!important;
    width: 100%;
    font-size: 12px;
    border: 0px;
    padding-left: 10px;
}
.message {
    opacity: .1
}
</style>

The Javascript code:

<script>
    $(document).ready(function(){
       overridePageMessages();    
    });

    function overridePageMessages(){    
        var textureEffect = '';
        //Uncomment below line for texture effect on page messages
        //textureEffect = 'slds-theme--alert-texture';

        $('.warningM3').addClass('slds-notify slds-notify--toast slds-theme--warning customMessage '+textureEffect);          
        $('.confirmM3').addClass('slds-notify slds-notify--alert slds-theme--success  customMessage '+textureEffect);    
        $('.errorM3').addClass('slds-notify slds-notify--alert slds-theme--error customMessage '+textureEffect);                  
        $('.infoM3').addClass('slds-notify slds-notify--toast customMessage '+textureEffect);    

        $('.errorM3').removeClass('errorM3'); 
        $('.confirmM3').removeClass('confirmM3'); 
        $('.infoM3').removeClass('infoM3');   
        $('.warningM3').removeClass('warningM3');  
    }
</script>

Add the script to document.ready() or the oncomplete function.

  • Your Javascript code assumes jQuery is present, but you don't even need jQuery for any of this functionality. – Adrian Larson Nov 23 '16 at 0:45
  • 1
    @AdrianLarson Yes, you are right. It is definitely okay to use vanilla javascript and it is not hard to convert into vanilla javascript as well. But it doesn't hurt to use jQuery in most cases and it is a bit easier to write. I will include a pure javascript version in my blog. – Lance Shi Nov 23 '16 at 1:16
  • 3
    If you're going to use jQuery, you should at the very least use jQuery.noConflict() and probably also use IIFE to protect the global scope. – Adrian Larson Nov 23 '16 at 1:27
  • @AdrianLarson I am not a big fan of using jQuery.noConflict(). But since you have mentioned, I will definitely take a look at it and learn more about the benefits of it. Thank you for pointing it out. – Lance Shi Nov 23 '16 at 1:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.