Changing Calendar widget titleAttr to optional reference?

0
Hello Mendix Community: I am trying to add a couple of modest features to the Calendar widget. I am using Modeler 5.10.0-beta and the latest version of the Calendar widget. It is my understanding the prior to Mendix 5, the Calendar widget allowed the titleAttr (the "name" of each event that shows up on the calendar) to be either an attribute or a reference. In the current version, the titleAttr can only be an attribute and I believe that I am not alone in wishing this could be either an attribute or an attribute of an association once again. The modification required in calendar.xml seems simple enough: Change: <property key="titleAttr" type="attribute" entityProperty="contextEntity"> to: <property key="titleAttr" type="attribute" isPath="optional" pathType="reference" entityProperty="contextEntity"> Unfortunately, however, the changes in calendar.js are less obvious and my meager skill set have caused me to hit a wall. From https://community.mendix.com/questions/3524/ I understand that I should be using the split() function to break apart the portions of the association into the name of the association (split[0]), the entity referenced by the association (split[1]) and the attribute of the referenced entity (split[2]), but I'm struggling to actually get the associated entity, much less the desired attribute from the associated entity. Here is the relevant original code from calendar.js: //get the dates var start = new Date(obj.get(self.startAttr)); var end = new Date(obj.get(self.endAttr)); //create a new calendar event var newEvent = { title : obj.get(self.titleAttr), start : start, end : end, allDay : obj.get(self.alldayAttr), editable : self.editable, mxobject: obj //we add the mxobject to be able to handle events with relative ease. }; Here is my attempt, to test whether I have a simple attribute or a reference and, if it is a reference, to try to get the referenced entity and ultimately the desired attribute of the referenced entity: Note: I've added a bunch of "console.debug" statements to help me see how far I am getting: var start = new Date(obj.get(self.startAttr)); var end = new Date(obj.get(self.endAttr)); split = self.titleAttr.split("/"); console.debug("Title attribute: " + self.titleAttr); console.debug("split.length: " + split.length); if ( split.length == 3 ) { // This is a reference ... console.debug("split[0]: " + split[0]); console.debug("split[1]: " + split[1]); console.debug("split[2]: " + split[2]); // This gets the GUID of the referenced entity ... and seems to work. var targetObjGUID = obj.getAttribute(split[0]); console.debug("targetObGUIDj: " + targetObjGUID); // This is where I try to get the targetObj from the GUID // ... but this doesn't work. if (/\d+/.test(targetObjGUID)) { // guid only mx.processor.get({ // fetch object first guid : targetObjGUID, nocache : true, callback : function (targetObj) { targetObj.get(split[2]); } } }, this); } // Because I don't have targetObj, this doesn't work either title = targetObj.getAttribute(split[2]); } else { // This is a simple attribute ... title = obj.get(self.titleAttr); } console.debug("Title string: " + title); //create a new calendar event var newEvent = { title : title, start : start, end : end, allDay : obj.get(self.alldayAttr), editable : self.editable, mxobject: obj //we add the mxobject to be able to handle events with relative ease. }; In short, I believe that I have three valid elements in the split array that correspond to the name of the association (Reservation_Student), the entity referenced by the association (Student), and the attribute of that entity that I want to use as the titleAttr (FullName). I also think that I'm getting the GUID of the Student whose FullName I want do display: "split[0]: MyFirstModule.Reservation_Student" "split[1]: MyFirstModule.Student" "split[2]: FullName" "targetObjGUID: 9288674231451650" However, I seem unable to get the associated entity (the actual Student) based on it's GUID. If anyone can help me to understand where I've gone wrong, I'd be most appreciative. Thanks, John
asked
3 answers
1

Functions in javascript have their own scope. If you use

                                         callback : function (targetObj) {
                                            targetObj.get(split[2]);
                                        }

that targetObj parameter is a new one, and making the variable targetObj defined before unreachable and unusable. If you use another name (refobj) that problem is fixed.

The split array is local defined so you can use that. However you need a variable to store the result. This would be more correct code (not garantueed to fix all problems). BTW take a look at widget that allows to retrieve data over a reference.

                          var start = new Date(obj.get(self.startAttr));
                            var end = new Date(obj.get(self.endAttr));
                            split = self.titleAttr.split("/");
// inserted
                            var title = '''';
                            console.debug("Title attribute: " + self.titleAttr);
                            console.debug("split.length: " + split.length);
                            if ( split.length == 3 ) {  // This is a reference ...
                                console.debug("split[0]: " + split[0]);
                                console.debug("split[1]: " + split[1]);
                                console.debug("split[2]: " + split[2]);
                                // This gets the GUID of the referenced entity ... and seems to work.
                                var targetObjGUID = obj.getAttribute(split[0]);
                                console.debug("targetObGUIDj: " + targetObjGUID);
                                // This is where I try to get the targetObj from the GUID
                                // ... but this doesn't work.
                                if (/\d+/.test(targetObjGUID)) { // guid only
                                    mx.processor.get({ // fetch object first
                                            guid : targetObjGUID,
                                                nocache : true,
// changed
                                                callback : function (refobj) {
                                                   title = refobj.get(split[2]);
                                            }
                                            }
                                        }, this);
                                }
                                // Because I don't have targetObj, this doesn't work either
                                title = targetObj.getAttribute(split[2]);
                            } else {  // This is a simple attribute ...
                                title = obj.get(self.titleAttr);
                            }
                            console.debug("Title string: " + title);
                            //create a new calendar event
                            var newEvent = {
                                    title : title,
                                    start : start,
                                    end     : end,
                                    allDay : obj.get(self.alldayAttr),
                                    editable : self.editable, 
                                    mxobject: obj //we add the mxobject to be able to handle events with relative ease.
                            };

Edit 1: Hi John,

You are right, the ajax call is asynchronous, and the title is set after the calender is rendered. You a need 'chain' of functions to fix this. Move the get before the the rendering and in the callback call the next function. Use dojo.hitch to pass the context. Other widgets contain examples of that. If needed I can help you in more detail, but you are near the finish.

answered
0

Chris et al:

Chris' tip that I had not properly realized that each function in javascript has it's own scope was exactly on target.

Making the changes that Chris suggested has indeed allowed me to properly set the title string ... with one serious problem that is, I fear, due to a flawed approach on my part.

In case anyone is looking to replicate this, here is the problem: I believe that the mx.processor.get() calls in the loop initiate an asynchronous call to the database. Unfortunately, it appears, that data has not yet been returned and as a result title is still "" (an empty string), by the time that the newEvent() call creates each new calendar event. So, the first time that the calendar is displayed, all of the titles are still blank. If I then go to another page and then back to the calendar, the titles are properly displayed.

I have been able to confirm this timing problem by putting console.debug statements both inside the function(refObj) definition and just prior to the newEvent() calls.

For a calendar that had 9 events, the console debug first showed 9 instances of the title being the empty string when the calendar events were created. That preceded a total of 9 logged lines where the title set by the function(refObj) call was getting the proper name:

"Title string before newEvent(): " mxui.js line 16 > Function line 1 > eval:174 (repeated 9 times) "Title string in refObj: Joe Student" mxui.js line 16 > Function line 1 > eval:163 (repeated 3 times) "Title string in refObj: Another Student" mxui.js line 16 > Function line 1 > eval:163 (repeated 6 times)

So, it appears as if I need to adopt an approach that either doesn't try to display the calendar until the associated title information has been returned or I need to somehow have the calendar subscribed to or listening to that data when it is returned from the database.

In other words, instead of calling "Ready, Set, Go!", my approach is resulting in "Ready, Go!, Set".

Thanks again to Chris to getting me one important step closer, but I'd be appreciative if anyone can suggest the proper approach to first collecting the data that I need and THEN creating a series of calendar events that use that data.

John

answered
0

Chris, Robert, et al:

Thank you for sharing your expertise with me ... particularly as asynchronous calls and callback methods are new to me.

I believe that I now have a working approach that handles calendar event titles that are either simple attributes or referenced attributes.

I basically split the original createEvents(objs) method into a prepareEvents(objs) method that contains the asynchronous mx.processor.get() call in the case of a title attribute that is a reference. Basically, prepareEvents(objs) takes the original list of objects and creates an array named objTitles whose keys are the GUIDs of the objects to be displayed and the values are the actual titles. Then, once the objTitles array has been assembled (with either simple attributes or referenced attributes), I now call createEvents(objs, objTitles). The new createEvents() method is virtually unchaged from the original except that it now takes the event title field from the objTitles array rather than from the original object. The only other change is that the fetchObjects() function now makes calls to dojo.hitch(this, this.prepareEvents) instead of to dojo.hitch(this, this.createEvents).

In case there are serious flaws to my approach, I am including the source code for the newly created prepareEvents method. I've tried to include a reasonably complete set of commendat. Note: I still also have a bunch of console.log() statements that I used in getting this working. Those will be removed soon.

I expect that experts in the Mendix community will find flaws with my approach ... and, if you do, please let me know where I have missed the boat.

Chris and Robert, I really appreciate your help in getting as far as I have.

John

    prepareEvents : function(objs) {
        // this function takes a set of objects and gets a title for each based on whether titleAttr
        // is a simple attribute or a reference.  When titles are collected, we call
        // createEvents with both the original objects and the objTitles array.
        // Note: for referenced titles, the createObjects call is made from the callback of mx.processor.get()
        var objTitles = []; // key = object GUID, value is ultimately the title string
        // Note: for referenced title attributes, the value is initially set to the GUID
        // of the referenced object.  Later, during the mx.processor.get() callback, it
        // is replaced with the actual title string.
        var objRefs = []; // Array containing the referenced object GUIDs.
        var refTitles = [];  // key = referenced object GUID, value is referred title
        // Note: both objRefs and refTitles will be the same length, but both of them can be shorter
        // than the length of objRefs.
        var self = this;
        split = self.titleAttr.split("/");
        console.log("Number of objects: " + objs.length);
        console.log("Objects: " + objs);
        if (split.length == 1 ) {
            // titleAttr is a simple attribute and the key of objTitles is
            // the GUID of the object and the title is the attribute.
            $.each(objs, function(index, obj){
                console.log("Processing objectGUID: " + obj.getGUID());
                objTitles[obj.getGUID()] = obj.get(self.titleAttr);
                console.log("objTitles key: " + obj.getGUID() + " value: " + objTitles[obj.getGUID()]);
            });
            // Call createEvents() now.
            this.createEvents(objs, objTitles);
        } else if (split.length == 3 ) {
            // titleAttr is a reference and we have more work to do.
            var thisRef; // Contains the GUID of one of the referred objects.
            $.each(objs, function(index, obj){
                console.log("Processing objectGUID: " + obj.getGUID());
                thisRef = obj.getAttribute(split[0]);
                objTitles[obj.getGUID()] = thisRef;
                // objRefs should only contain the unique list of referred objects.
                if (objRefs.indexOf(thisRef) < 0) {
                    objRefs.push(thisRef);
                }
                console.log("objTitles key: " + obj.getGUID() + " value: " + objTitles[obj.getGUID()]);
            });
            console.log("Size of objRefs: " + objRefs.length);
            console.log("objRefs: " + objRefs);//
            // Now get the actual title strings from the list of referred objects ...
            // This is an asynchronous call.
            mx.processor.get({
                guids : objRefs,
                nocache : false,
                callback : function (refObjs) {
                    // Get the title string for each referenced object and store it
                    // as the value in the refTitles array.
                    for (var i = 0; i < refObjs.length; i++ ) {
                        console.log("Processing refObjGUID: " + refObjs[i].getGUID());
                        refTitles[refObjs[i].getGUID()] = refObjs[i].get(split[2]);
                        console.log("refTitles key: " + refObjs[i].getGUID() + " value: " + refTitles[refObjs[i].getGUID()]);
                    }
                    // Now, loop through the objTitles array and replace the value (which is
                    // is the GUID of the referred object) with the actual title string extracted
                    // from the referred object.
                    for (var index in objTitles) {
                        var thisValue = objTitles[index];
                        console.log("Index: " + index + " Starting value: " + objTitles[index]);
                        objTitles[index] = refTitles[objTitles[index]];
                        console.log("Index: " + index + " Ending value: " + objTitles[index]);
                    }
                    // Now that we finally have all of the referenced titles, we can call
                    // createEvents()
                    this.createEvents(objs, objTitles);
                }
            }, this);
            //this.createEvents(objs, objTitles);
        } else {
            // this should never happen and is likely an error
            console.log("Error in titleAttr: " + self.titleAttr);
        }
    },
answered