0

I'm new to AngularJS and attempting to redo some work on my previous application using the framework. I'm trying to create a Message Box service in my application (AngularJS v1.6.6) that modules call to display a pop-up in the UI.

I can't seem to figure out how I can bind the service's variables to the UI itself. I've tried creating a local 'MessageBox' variable in the controller and referencing its contents as well as simply calling the injected variable with no luck. Is there some way I can accomplish this through a service? I was thinking of a global variable but I've seen advice against this.

A service seemed like the best route to go but I'm open to any suggestions that make more sense here. The other modules need to display their own error or success messages so I thought it'd be easier to create a standard way to handle that.

My code is below:

message-box.template.html

<div id="message" ng-show="$ctrl.MessageBox.display" style="background-color: white; border: 1px solid black; margin: 5px; float: right; top: 60px; right: 2%; padding: 5px; position: fixed; z-index: 100;">
    <img id="message-image" height="30" width="30" alt="{{ $ctrl.MessageBox.Image.alt }}" ng-src="{{ $ctrl.MessageBox.Image.source }}" /> <span id="message-text">{{ $ctrl.MessageBox.text }}</span>
</div>

message-box.module.js

angular.module("components.message-box", ["components"])
    .controller("MessageBoxController", ["$scope", "MessageBox", function MessageBoxController($scope, MessageBox) {
    }]);

message-box.component.js

/**
 * message-box.component.js
 * Contains the 'message-box' component
 */
angular.module("components.message-box")
    .component("messageBox", {
        templateUrl: "/Scripts/cop/components/message-box/message-box.template.html",
        controller: "MessageBoxController"
    });

message-box.service.js

/**
 * message-box.service.js
 * Defines the 'message-box' service
 */

angular.module("components.message-box")
    .service("MessageBox", function () {
        var self = this;

        self.display = false;
        self.Image = {
            alt: null,
            source: null
        };
        self.text = null;
        self.timeout = null;

        /**
         * Hides the displayed message
         * @public
         */
        self.hideMessage = function () {
            self.display = false;
            self.timeout = null;
            self.Image.alt = null;
            self.Image.source = null;
            self.text = null;
        };

        /**
         * Displays the provided message to the user
         * @public
         * @param {string} text Message text
         * @param {Object} Image Image display data
         * @param {number} [hideTimeoutMs=null] Number of miliseconds to wait before hiding the message
         */
        self.showMessage = function (text, Image, hideTimeoutMs) {
            self.text = text;
            self.Image.alt = Image.alt;
            self.Image.source = Image.src;
            self.display = true;

            // Cancel timer for current timeout if it exists
            if (self.timeout !== null) {
                self.timeout = null;
            }

            // Set a new timeout if specified
            if (hideTimeoutMs !== undefined) {
                self.timeout = setTimeout(self.hideMessage, hideTimeoutMs);
            }
        };
    });

Here's an example of attempting to call the service:

visual.module.js

angular.module("core.visual", [
    "ngRoute",
    "core.visual.settings"
])
    .controller("VisualController", ["$routeParams", "$scope", "MessageBox",
        function VisualController($routeParams, $scope, MessageBox) {
            var self = this;
            self.MessageBox = MessageBox;
            
            self.MessageBox.showMessage("This is a message", "/Content/Images/loading.png", "Loading", 10000);
        }
    ]);

I've also tried not using MessageBox as a local controller variable with no success - this is just my latest attempt.

Am I on the right track? Is this even possible?

Cory Gehr
  • 105
  • 1
  • 7

1 Answers1

0

I found this question which helped me find my answer.

Below are the changes I made to my MessageBox service. I was able to make it all work without changing how the service functions are called.

message-box.service.js

/**
 * message-box.service.js
 * Defines the 'message-box' service
 */

angular.module("components.message-box")
    .factory("MessageBox", ["$rootScope", "$timeout", function ($rootScope, $timeout) {
        var self = {};

        // Declare the Message variables
        self.display = false;
        self.imageSrc = null;
        self.text = null;
        self.timeout = null;

        /**
         * Hides the displayed message
         * @public
         */
        self.hideMessage = function () {
            // Nullify all properties
            self.display = false;
            self.imageSrc = null;
            self.text = null;
            self.timeout = null;
        };

        /**
         * Displays the provided message to the user
         * @public
         * @param {string} text Message text
         * @param {Object} imageSrc Image source location
         * @param {number} [hideTimeoutMs=null] Number of miliseconds to wait before hiding the message
         */
        self.showMessage = function (text, imageSrc, hideTimeoutMs) {
            self.text = text;
            self.imageSrc = imageSrc;
            self.display = true;

            // Set a new timeout if specified
            if (hideTimeoutMs !== undefined) {
                self.timeout = hideTimeoutMs;
            }
            else {
                self.timeout = null;
            }

            // Broadcast changes
            // Use a timeout so messages don't fail before controller instantiation
            $timeout(function () { $rootScope.$broadcast("message:updated", self); });
        };

        return self;
    }]);

The biggest change is not handling the timeout here and instead holding the values; the service now broadcasts when a change has been made to the message and it's handled by the controller below.

I'm still clearing the local variables with hideMessage() for consistency. I also removed the 'alt' attribute because I can't figure out how to let Angular manage that value (using {{}} doesn't seem to work).

The $timeout used here adds a slight delay before broadcasting the change. In some cases the message wouldn't appear if showMessage() was called immediately in a controller (before it was completely constructed) so this fixes that. In a larger application that may not be sufficient but this is relatively small.

The bulk of the changes are in the component's controller:

message-box.module.js

/**
 * message-box.module.js
 * Defines the 'components.message-box' module
 */

angular.module("components.message-box", ["components"])
    .controller("MessageBoxController", ["$scope", "MessageBox", function MessageBoxController($scope, MessageBox) {
        // Listen for message changes
        $scope.$on("message:updated", function (evt, data) {
            // Update global message variable
            $scope.$apply(function () {
                $scope.Message = data;
            });
            // Check for a timeout
            if (data.timeout !== null) {
                setTimeout(function () {
                    $scope.$apply(function () {
                        data.hideMessage();
                    });
                }, data.timeout);
            }
        });
    }]);

The controller now listens for the message updated event. When that happens the controller replaces the scope value with the new message using $apply so the UI also updates. Similarly, it now handles the timeout value for when the message disappears (if applicable) and also uses $apply since the UI still needs to know when to no longer show the message.

message-box.template.html

<div id="message" ng-show="Message.display" style="background-color: white; border: 1px solid black; margin: 5px; float: right; top: 60px; right: 2%; padding: 5px; position: fixed; z-index: 100;">
    <img id="message-image" height="30" width="30" alt="Information" ng-src="{{ Message.imageSrc }}" /> <span id="message-text" ng-bind="Message.text"></span>
</div>

(message-box.component.js did not change)

Hopefully this helps someone in a similar situation. This solution is working for me but if there's a better way to handle this please let me know.

Cory Gehr
  • 105
  • 1
  • 7