"setInitialFocusId" error after _oDialog.destroy() - javascript

I get the error following error when trying to open a Dialog fragment a second time after calling this._oDialog.destroy():
Uncaught TypeError: Cannot read property 'setInitialFocusId' of null
My problem is like the problem stated here: How to clear dialog/xmlfragment content after close? However, the solution apparently just seems to be "Don't use the property setInitialFocus", which I do not use anywhere in my code.
Controller
openDialog: function() {
if (!this._oDialog) {
this._oDialog = sap.ui.xmlfragment("myFragmentPath", this);
this.getView().addDependent(this._oDialog);
}
this._oDialog.open();
},
onExit: function () {
if (this._oDialog) {
this._oDialog.destroy();
}
},
afterClose: function () {
if (this._oDialog) {
this._oDialog.destroy();
}
},
handleClose: function (oEvent) {
this._oDialog.close();
}
Dialog Fragment
<Dialog xmlns="sap.m" afterClose=".afterClose">
<!-- ... -->
</Dialog>
Main XML View
<Button press=".openDialog" />
Additional info:
The error message occurs in the Controller line when this._oDialog.open(); is called.
I am using the sap library version 1.60.1.

if (this._oDialog) {
this._oDialog.destroy();
this._oDialog = null; // make it falsy so that it can be created next time
}
After close, the dialog is destroyed in your code. However, the this._oDialog is still there.
Since this._oDialog is not a falsy value but just a destroyed dialog instance, there is no new Dialog created in openDialog() second time. Hence you're trying to open a destroyed dialog.
When the dialog is destroyed, its internal oPopup is set to null, which explains the error message.
⚠️ Note
There is usually no need to destroy the dialog after closing. When the view gets destroyed, the dialog will be destroyed automatically since the fragment is dependent to the view. If the intention was to reset data values, try unbinding properties instead of destroying and recreating the entire fragment every time which is quite costly.
Since UI5 1.56, the factory function sap.ui.xmlfragment is deprecated because it fetches the fragment via sync XHR (blocking the main thread). Use one of the new asynchronous APIs.
A simpler option is to add the fragment declaratively in your view definition with <core:Fragment fragmentName="..." type="XML" /> to the <dependents> aggregation of a certain control. Like in this sample.

Related

Event emitting but parent not listening

[Edit]:
I ended up just using Vuex and it works fine with this method.
I am having trouble with catching an event.
If I look in the vue dev tools, the event is being emitted but the parent function is not being activated. I have this problem any time I try to use emit throughout this whole project. I am trying to avoid Vuex because this is a Vue/Inertia.js/Laravel stack with all login data and vars being passed from controllers and Laravel session, so I feel like adding Vuex would be redundant.
I'm sorry I can't post the full code because it is for work and I signed an NDA, but if more parts are needed I can provide snippets.
PARENT (<Collection>)
<CollectionFooter v-if="itemsSelected > 0"
:itemsSelected="itemsSelected" #modifySelected="modifyItemsSelected(operation)" />
data(){
return {
itemsSelected: 0
}
},
methods: {
modifyItemsSelected(operation){
if(operation === "add") {
this.itemsSelected += 1;
} else {
this.itemsSelected -= 1;
}
console.log(this.itemsSelected)
}
},
CHILD (<CollectionItemCard>)
<button v-if="active" color="sm-white" #click="changeState">added</button>
<button v-else color="sm-yellow" #click="changeState">add</button>
methods:{
changeState(){
this.active = !this.active;
let operation;
if(this.active) {
operation = "add";
} else {
operation = "remove";
}
this.$emit('modifySelected', operation);
}
},
vue dev tools screen
As zcoop98 mentioned above,
moving #modifySelected="modifyItemsSelected(operation)" to the <CollectionItemCard> component was the key.
I had to have the listener on the same component which emitted. Not just any parent element.
TL;DR – The problem is the way you've written your inline handler.
Change modifyItemsSelected(operation) to just "modifyItemsSelected" or "modifyItemsSelected($event)", and you should be good to go. Detailed explanation as to why is below.
Vue Event Handler Rundown
There's two ways to handle events using v-on in Vue: Inline Handlers, and Method Handlers. It's really easy to miss the difference, but it's important.
Inline Handlers
Inline handlers are probably what you're most familiar with, since it's exactly what you're using in the example.
This handler type is exactly what it sounds like: you pass a bit of JavaScript inline to v-on, and Vue runs this code when the event is handled.
For example:
<button #click="alert('Hi!')">Click Me!</button>
When the click event is emitted by this button, the code alert('Hi!') is run, just as it's written.
This is the key to your problem– it runs just as its written, references and all.
In your event handler:
<CollectionFooter
...
#modifySelected="modifyItemsSelected(operation)"
/>
You've told Vue to run modifyItemsSelected(operation) when modifySelected is handled. The problem is that you've given the inline function an argument which doesn't exist.
Instead of reading in the payload from the event as a variable called operation, Vue interprets this syntax as a call to modifyItemsSelected with some existing variable named operation.
Vue goes looking for operation in the template context, and then in your component context, and when it doesn't find it, it throws an error, killing execution of the handler.
Method Handlers
Method handlers are also just what they sound like: instead of passing an inline snippet of JavaScript to v-on, you pass it a defined method name, only, which Vue binds the handler to directly. A crucial difference between this type and inline is that you don't include arguments in the call– Vue handles the argument passing implicitly.
This means that your provided example simply becomes:
<CollectionFooter
...
#modifySelected="modifyItemsSelected"
/>
The payload will be passed as an argument (or arguments, you can pass as many as you'd like into $emit()) to modifyItemsSelected for it to use.
A Note on $event
A final thing worth mentioning is some special Vue syntax: $event. This is a placeholder you can use in inline handlers to signify the event payload.
Using #event="myHandler($event)" will pass the event payload as the first argument to myHandler. This is especially useful when you need some crucial data only accessible from the template in your event handler (eg. in a v-for loop), because it allows you to pass both the event payload and custom data into your handler function at once. (Eg. #event="myHandler($event, element.index)")
This means there's another way to format your event handler and have it work (purely a matter of preference in this case):
<CollectionFooter
...
#modifySelected="modifyItemsSelected($event)"
/>
Snippet Example
Finally, here's an example to illustrate this information dump.
Three buttons, all calling the same method a different way. The given method simply prints a message and the (stringified) event to console.
Clicking the button with your current handler throws an error into the console, without printing anything (the development version of Vue is nice enough to give a warning and still print, but the production version isn't so kind). The other two print successfully.
new Vue({
el: '#app',
methods: {
foo(e) {
console.log('Handled!');
console.log('Event:', JSON.stringify(e));
},
},
});
.as-console-wrapper { max-height: 85px !important; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<button #click="foo(bar)">Inline Handler:</button><span><code> #click="foo(bar)"</code> (Your example code)</span>
<hr>
<button #click="foo">Binded Handler:</button><span><code> #click="foo"</code> (Alternative #1)</span>
<br><br>
<button #click="foo($event)">Inline Handler w/ $event:</button><span><code> #click="foo($event)"</code> (Alternative #2)</span>
</div>

Form Event OnSave not executing Promise

I have a web resource in Dynamics CRM where I am trying to add logic to execute on save. I am using the addOnSave() method to attach my logic to the save. When I use a Promise in my save logic, Save & Close exits the page before my save completes. How can I get my save logic to fully execute before the web resource is closed?
pseudocode
Xrm.Event.addOnSave(function () {
// Code makes it here
Promise.all([promises]).then(function(){
// Code never makes it here
secondPromise.then(function(){
showAlert();
setTimeout(function(){
closeAlert();
}, 5000);
});
});
});
You want to cancel the save and then reissue it, like this:
Xrm.Page.data.entity.addOnSave(function (context) {
var eventArgs = context.getEventArgs();
eventArgs.preventDefault(); // cancels the save (but other save handlers will still run)
Promise.all([promises]).then(function(){
// Code never makes it here
secondPromise.then(function(){
showAlert();
setTimeout(function(){
closeAlert();
// reissue the save
Xrm.Page.data.entity.save('saveandclose');
}, 5000);
});
});
});
In response to your comment about the bug where preventDefault doesn't properly stop a Save and Close event: use the Ribbon Workbench from the XrmToolbox to override the Save and Close button to point to a custom function which might look something like this:
function customSaveAndClose() {
if (customSaveIsNeeded) {
// execute your custom code
} else {
Xrm.Page.data.entity.save('saveandclose');
}
}
You can for sure override the S&C button at the Application Ribbon level which would override it for all entities, but I believe you can override it for just one entity at a time as well.
If you don't want to mess with editing the ribbon (it's a little intimidating if you've never done it before) and if you don't have strict requirements regarding unsupported customizations, you can also take the easier route of simply overriding the Mscrm.RibbonActions.saveAndCloseForm function which is what the native S&C buttons call. That would look something like this:
// defined in /_static/_common/scripts/RibbonActions.js
Mscrm.RibbonActions.saveAndCloseForm = function() {
// your code here
}
Some things to note about this approach:
It's not supported and could break with any update
CRM forms consist of multiple frames, so if you define that function in your custom script and it doesn't get executed, change your definition to top.Mscrm instead of just Mscrm.
If you have to support mobile clients, you should probably avoid this approach and override the ribbon button instead.

Trigger Action from Push Notification

I have a cordova application that uses push notification (still using the old plugin :-().
The application uses ngRouter and the navigation is relatively basic - in that I mean that my main menu changes ngView but popups/modals are not part of the navigation and are either triggered by some bound controller property or through a call to a controller function (e.g. $scope.openMyModal).
I am trying to be able to call such function on one of my controllers after I received push notification (and the controller is loaded).
I implemented some code using a timeout to broadcast an event which should be caught in the relevant controller and open the modal. Roughly the code is:
In app.js:
onNotification() {
// some code for determining the type of notification
// then
setTimeout(function() {
$rootScope.$broadcast("someEventCode");
}, 10); // or 1000 in case of cold start
}
In MyController.js:
.controller('MyController', function($scope, $rootScope, $modal,...) {
$scope.openMyModal = function() { // open modal using $model }
$scope.on("someEventCode", function() {
$scope.openMyModal();
});
}
This kind of works but is not consistent/deterministic. For example, in slower devices it may broadcast before the controller is ready to respond to it.
I also tried to set some variable on root scope (in onNotification) and in the controller create a function which is called from the markup (e.g. {{isNotificationReady()}}) but this also doesn't work well.
Another approach was to use double notifications - set a flag in root scope when the notification arrives, wait for an event from the target controller (indicating it is loaded) and then, at $rootScope again, if flag is set, broadcast the "open dialog" event (and delete the flag). Following this approach, I am not sure how to trigger the "loaded" event so I use a function from the markup:
In MyController.js:
$scope.isLoaded = function() {
$scope.$emit("myControllerLoaded");
}
In markup:
<div><!-- the content --></div>
{{isLoaded()}}
In app.js
$rootScope.$on("myControllerLoaded", function(event) {
if ($rootScope.notification === "someEventCode") {
$rootScope.$broadcast("openTheModel");
delete $rootScope.notification;
}
});
This seems like cumbersome and inefficient code. isLoaded() is called multiple times (not sure why) and it is kind of spaghetti code.
My question is - how should I implement something like that in a clear and efficient manner? Just a reminder, the app could be "cold started" or in the background and I need to know when it is "running" (or the controller is ready).
I've found a slightly more robust, timeout based implementation (still not exactly what I was hoping for).
The idea is to set a flag and send (broadcast) the signal after some time. Then resend the signal on interval until the flag is unset by the target controller:
In app.js
function broadcastSomeEvent() {
$rootScope.$broadcast("someEventCode");
if ($rootScope.eventFlag) {
setTimeout(broadcastSomeEvent, 50);
}
}
onNotification() {
// some code for determining the type of notification, then
$rootScope.eventFlag = true;
setTimeout(broadcastSomeEvent, 10); // or 1000 in case of cold start
}
In MyController.js
$scope.$on('someEventCode', function() {
delete $rootScope.eventFlag; // delete flag so event is stopped
$scope.openMyModal();
});
This is still an iff-y implementation to my taste. Even though it does work for both cold start and when the application is in the background I believe that it is not robust as it should.
Still, I wouldn't mark this solution as "the answer".
On the other hand, with no proper state routing, maybe there's not much more than can be done.

Adding an event handler inside a knockoutjs custom binding

I'm a fairly experienced knockout user, so I understand quite a bit of the under the hood stuff, I have however been battling now for a few days trying to figure out how to achieve a given scenario.
I have to create a system that allows observable's within a given knockout component to be able to translate themselves to different languages.
to facilitate this, I've created a custom binding, which is applied to a given element in the following way.
<p data-bind="translatedText: {observable: translatedStringFour, translationToken: 'testUiTransFour'}"></p>
This is in turn attached to a property in my knockout component with a simple standard observable
private translatedStringFour: KnockoutObservable<string> = ko.observable<string>("I'm an untranslated string four....");
(YES, I am using typescript for the project, but TS/JS either I can work with.....)
With my custom binding I can still do 'translatedStringFour("foo")' and it will still update in exactly the same way as the normal text binding.
Where storing the translations in the HTML5 localStorage key/value store, and right at the beginning when our app is launched, there is another component that's responsible, for taking a list of translation ID's and requesting the translated strings from our app, based on the users chosen language.
These strings are then stored in localStorage using the translationToken (seen in the binding) as the key.
This means that when the page loads, and our custom bind fires, we can grab the translationToken off the binding, and interrogate localStorage to ask for the value to replace the untranslated string with, the code for our custom binding follows:
ko.bindingHandlers.translatedText = {
init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
// Get our custom binding values
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
},
update: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
// Get our custom binding values
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
// Ask local storage if we have a token by that name
var translatedText = sessionStorage[translationToken];
// Check if our translated text is defined, if it's not then substitute it for a fixed string that will
// be seen in the UI (We should really not change this but this is for dev purposes so we can see whats missing)
if (undefined === translatedText) {
translatedText = "No Translation ID";
}
associatedObservable(translatedText);
ko.utils.setTextContent(element, associatedObservable());
}
}
Now, thus far this works brilliantly, as long as the full cache of translations has been loaded into localStorage, the observables will self translate with the correct strings as needed.
HOWEVER......
Because this translation loader may take more than a few seconds, and the initial page that it's loading on also needs to have some elements translated, the first time the page is loaded it is very possible that the translations the UI is asking for have not yet been loaded into into localStorage, or may be in the process of still loading.
Handling this is not a big deal, I'm performing the load using a promise, so the load takes place, my then clause fires, and I do something like
window.postMessage(...);
or
someElement.dispatchEvent(...);
or even (my favorite)
ko.postbox.publish(...)
The point here is I have no shortage of ways to raise an event/message of some description to notify the page and/or it's components that the translations have finished loading, and you are free to retry requesting them if you so wish.
HERE IN.... Lies my problem.
I need the event/message handler that receives this message to live inside the binding handler, so that the very act of me "binding" using our custom binding, will add the ability for this element to receive this event/message, and be able to retry.
This is not a problem for other pages in the application, because by the time the user has logged in, and all that jazz the translations will have loaded and be safely stored in local storage.
I'm more than happy to use post box (Absolutely awesome job by the way Ryan -- if your reading this.... it's an amazingly useful plugin, and should be built into the core IMHO) but, I intend to wrap this binding in a stand alone class which I'll then just load with requireJs as needed, by those components that need it. I cannot however guarantee that postbox will be loaded before or even at the same instant the binding is loaded.
Every other approach i've tried to get an event listener working in the binding have just gotten ignored, no errors or anything, they just don't fire.
I've tried using the postmessage api, I've tried using a custom event, I've even tried abusing JQuery, and all to no avail.
I've scoured the KO source code, specifically the event binding, and the closest I've come to attaching an event in the init handler is as follows:
init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
// Get our custom binding values
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
// Set up an event handler that will respond to events on session storage, by doing this
// the custom binding will instantly update when a key matching it's translation ID is loaded into the
// local session store
//ko.utils.registerEventHandler(element, 'storage', (event) => {
// console.log("Storage event");
// console.log(event);
//});
ko.utils.registerEventHandler(element, 'customEvent', (event) => {
console.log("HTML5 custom event recieved in the binding handler.");
console.log(event);
});
},
None of this has worked, so folks of the Knockout community.....
How do I add an event handler inside of a custom binding, that I can then trigger from outside that binding, but without depending on anything other than Knockout core and my binding being loaded.
Shawty
Update (About an hour later)
I wanted to add this part, beacuse it's not 100% clear why Regis's answer solves my problem.
Effectively, I was using exactly the same method, BUT (and this is the crucial part) I was targeting the "element" that came in as part of the binding.
This is my mind was the correct approach, as I wanted the event to stick specifically with the element the binding was applied too, as it was said element that I wanted to re-try it's translation once it knew it had the go-ahead.
However, after looking at Regis's code, and comparing it to mine, I noticed he was attaching his event handlers to the "Window" object, and not the "Element".
Following up on this, I too changed my code to use the window object, and everything I'd been attempting started to work.
More's the point, the element specific targeting works too, so I get the actual event, on the actual element, in the actual binding that needs to re-try it's translation.
[EDIT: trying to better answer the question]
I don't really get the whole point of the question, since I don't see how sessionStorage load can be asynchronous.
I supposed therefore sessionStorage is populated from som asynchronous functions like an ajax call to a translation API.
But I don't see what blocks you here, since you already have all the code in your question:
var sessionStorageMock = { // mandatory to mock in code snippets: initially empty
};
var counter = 0;
var attemptTranslation = function() {
setInterval(function() { // let's say it performs some AJAX calls which result is cached in the sessionStorage
var token = "token"; // that should be a collection
sessionStorageMock[token] = "after translation " + (counter++); // we're done, notifying event handlers
window.dispatchEvent(new Event("translation-" + token));
}, 500);
};
ko.bindingHandlers.translated = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var val = valueAccessor();
var token = val.token;
console.log("init");
window.addEventListener("translation-" + token, function() {
if (token && sessionStorageMock[token]) {
val.observable(sessionStorageMock[token]);
}
});
}
};
var vm = function() {
this.aftertranslation = ko.observable("before translation");
};
ko.applyBindings(new vm());
attemptTranslation();
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="translated: { observable: aftertranslation, token: 'token' }, text: aftertranslation" />

Can an Orbeon event in an XBL be dispatched from Javascript?

Orbeon version: Orbeon Forms 3.8.0.201005270113
I have the following code in a Javascript file. This code is executed, but it seems like the model in the XBL is not found.
ORBEON.xforms.Document.dispatchEvent("model-name", "event-name");
Here is the model in the XBL. There are several models in the XBL. I don't see any message, so it seems as though the model isn't found. I don't see any errors in the logs.
<xforms:model id="model-name" xxforms:external-events="event-name">
<xforms:action ev:event="event-name">
<xforms:message>Test</xforms:message>
</xforms:action>
</xforms:model>
Does anyone know if there is some trick to getting a dispatch to work from Javascript to XBL?
Thanks very much!
UPDATED:
Another thing that could be the problem (maybe?) is that calling the javascript from the XBL using instance(this) isn't working. I wonder if the instance of the class isn't tied to a component instance, therefore it can't find the model?
Here's the call to the javascript from the xbl that doesn't invoke the init method:
<xxforms:script>YAHOO.xbl.fr.myTest.instance(this).init();</xxforms:script>
Here's the call that does invoke the init() method:
<xxforms:script>YAHOO.xbl.fr.myTest.prototype.init();</xxforms:script>
Here's the javascript:
YAHOO.namespace("xbl.fr");
YAHOO.xbl.fr.myTest = function() {};
ORBEON.xforms.XBL.declareClass(YAHOO.xbl.fr.myTest, "xbl-fr-myTest");
YAHOO.xbl.fr.myTest.prototype = {
},
init: function() {
alert('test');
},
valueChanged: function() {
},
};
AFAIK you can't address the XBL-internal model directly from outside, because of its strong encapsulation.
Instead, you'll have to dispatch the event to the xbl component node. For example, if you want an instance of the fr:currency XBL to handle a certain event, you'll have to dispatch the event to that fr:currency element that's part of your XForm.
Inside the XBL, you can define xbl:handlers to act upon that event, triggering some JavaScript action or something else.

Categories

Resources