Knockout.js - cross-iframe observable binding - javascript

I would like to use knockout.js in cross iframe binding. Existence of iframes is dictated by actual app structure I am working on.
This is the scenario (simplified):
Main window: Knockout.js included. window.top.DATA is a global container for data, ex. var DATA = { username: ko.observable('John') };
Module iframe window: Knockout.js also included. View wants do display data stored in window.top.DATA object, using code: <div data-bind="text: window.top.DATA.username></div>
What is the result?
DIV's innerHTML contains ko.observable().toString() contents instead of John.
The cause
Knockout.js is unable to recognize an observable created in parent frame while performing binding, because knockout checks if variable is observable with ko.hasPrototype by comparing references. Since prototypes are different between parent and child frame ko instances, it is impossible to bind values.
Solutions
The simplest solution would be writing something like: window.ko = window.top.ko || setupKO() on the top of script file. Unfortunately, in this case binding like with: window.someLocalObj is referencing to window.top instead of window - we are not able to access local variables and also local templates using template binding.
Another way to fix the problem is simply allow knockout to recognize observables as it should, what would allow observables to track dependency, bind values and just work well. Unfortunately I expect it might be difficult thing to achieve. What options do you see here?
Thank you for all your responses.
Edit
Knockout.js version: 3.2.0.

One solution is to use a single ko instance to handle main window and its frames elements at the same time. iframe elements are acessible through window.frames[frame_index].document:
var DATA = { username: ko.observable('John') };
ko.applyBindings(DATA);
ko.applyBindings(DATA, window.frames[0].document.body);
Working example: Plunker

Related

Reading OData contexts in onInit of controller

I've tried to prepare data from an OData source to show it in a bar graph in my fiori app. For this, I setup the OData model in the manifest.json. A test with a list, simply using
items="{path : 'modelname>/dataset'}
works fine and shows the content.
To prepare data for a diagram (VizFrame), I used the onInit() function in the controller of the view (mvc:XMLView). The data preparation is similar to the one discussed in question.
At first I obtain the ODataModel:
var oODataModel = this.getOwnerComponent().getModel("modelname");
Next I do the binding:
var oBindings = oODataModel.bindList("/dataset");
Unfortunately, the oBindings().getContexts() array is always empty, and also oBindings.getLength() is zero. As a consequence, the VizFrame shows only "No Data".
May it be that the data model is not fully loaded during the onInit() function, or do I misunderstand the way to access data?
Thanks in advance
Update
I temporary solved the problem by using the automatically created bind from the view displaying the data as list. I grep the "dataReceived" event from the binding getView().byId("myList").getBindings("items") and do my calculation there. The model for the diagram (since it is used in a different view) is created in the Component.js, and registered in the Core sap.ui.getCore().setModel("graphModel").
I think this solution is dirty, because the graph data depends on the list data from a different view, which causes problems, e.g. when you use a growing list (because the data in the binding gets updated and a different range is selected from the odata model).
Any suggestions, how I can get the odata model entries without depending on a different list?
The following image outlines the lifecycle of your UI5 application.
Important are the steps which are highlighted with a red circle. Basically, in your onInit you don't have full access to your model via this.getView().getModel().
That's probably why you tried using this.getOwnerComponent().getModel(). This gives you access to the model, but it's not bound to the view yet so you don't get any contexts.
Similarly metadataLoaded() returns a Promise that is fullfilled a little too early: Right after the metadata has been loaded, which might be before any view binding has been done.
What I usually do is
use onBeforeRendering
This is the lifecycle hook that gets called right after onInit. The view and its models exist, but they are not yet shown to the user. Good possibility to do stuff with your model.
use onRouteMatched
This is not really a lifecycle hook but an event handler which can be bound to the router object of your app. Since you define the event handler in your onInit it will be called later (but not too late) and you can then do your desired stuff. This obviously works only if you've set up routing.
You'll have to wait until the models metadata has been loaded. Try this:
onInit: function() {
var oBindings;
var oODataModel = this.getComponent().getModel("modelname");
oODataModel.metadataLoaded().then(function() {
oBindings = oODataModel.bindList("/dataset");
}.bind(this));
},
May it be that the data model is not fully loaded during the onInit()
function, or do I misunderstand the way to access data?
You could test if your model is fully loaded by console log it before you do the list binding
console.log(oODataModel);
var oBindings = oODataModel.bindList("/dataset");
If your model contains no data, then that's the problem.
My basic misunderstanding was to force the use of the bindings. This seems to work only with UI elements, which organize the data handling. I switched to
oODataModel.read("/dataset", {success: function(oEvent) {
// do all my calculations on the oEvent.results array
// write result into graphModel
}
});
This whole calculation is in a function attached to the requestSent event of the graphModel, which is set as model for the VizFrame in the onBeforeRendering part of the view/controller.

How to Trace JavaScript Dynamically Created Elements to Their Origins in Code?

Have a SAPUI5 application. Html view created by xml. JavaScript controllers. Some elements of this application, e.g. buttons are created somewhere dynamically within the controllers of the respective views. I.e. cannot use the id of an element to get it in the controller because the ids are dynamically created. Would like to get those dynamically created elements of the application to modify them, e.g. modify the buttons. What's the best way to trace a dynamically created element back to its code where it's created? E.g. how to trace a button back to it's origin in the JavaScript controller? It's a huge application and a view has multiple controller. I'm not just lazy.
The render function in the component's renderer class is what (eventually) creates the DOM element. You can inject a debugger statement into it like so:
let __buttonRender = sap.m.ButtonRenderer.render;
sap.m.ButtonRenderer.render = function() {
let control = arguments[1];
console.log('Creating button: ', control.sId);
debugger;
return __buttonRender.apply(this, arguments);
}
You can probably modify the control object based on the Id here. I'm not exactly sure what your end goal is here. The renderer is probably taking attributes and data from XML, so if you want to modify the buttons, I'd do it at the data source. The above could would allow you to hack the model before it reaches the DOM, but it's kinda nasty.

Is it possible to access the parent data context of a template loaded using Blaze.render()?

I load a template in a bootbox using Blaze.render() or Blaze.renderWithData():
Blaze.renderWithData(Template.myTemplate,
{"arg1":false, "arg2":"blabla"},
$("#dialogAnchor")[0]);
While it is usually possible to access directly parent data context using Template.parentData() in a template loaded with spacebars (e.g. {{> myTemplate}}), it is not possible to do the same when using Blaze.render() or Blaze.renderWithData()
Is there a way to achieve that?
I am now familiar with this problem and since no one answered, here is how I deal with it:
First, a template added to the page using Blaze.render() or Blaze.renderWithData() will have no Template.parentData() context.
As a workaround, it is still possible to pass it as an argument, like this:
Blaze.renderWithData(Template.myTemplate, {parentDataContext: Template.currentData()})
However, note that it will not be reactive.
If you need reactivity, there is another workaround. You can store the context elements that you want to share in a ReactiveDict() (not a ReactiveVar()) and pass it too as an argument:
Template.myTemplate.rendered = function(){
this.dataContext = new ReactiveDict()
this.dataContext.set("contextVariable1", this.data.whatever)
this.dataContext.set("contextVariable2", this.data.ImAwesome)
// etc.
Blaze.renderWithData(Template.myTemplate, {parentDataContext: this.dataContext})
}
This way, you can share a reactive local context between a parent template and a child template loaded using Blaze.render() or Blaze.renderWithData().

Is there a way to "watch" an IndexedDB record so I can respond when it's been changed?

I am building an Angular + TypeScript application that uses IndexedDB for storing data locally.
I have an Angular directive that sets the value of a scope variable to be some data that was returned from an IndexedDB request. It's pretty simple and does something like:
Directive A:
// getGroup() is making the requests to IndexedDB:
this.dataService.getGroup().then((groupOne) => {
this.scope.items = groupOne.items;
});
The view for my directive loops through each of these items:
<div ng-repeat="item in items">{{ item }}</div>
I have another Angular directive (let's call it Directive B) that is updating/inserting the items associated with groupOne.
Directive B:
groupOne.items.push("a new item");
this.dataService.updateGroup(groupOne).then(() => {
// groupOne has been changed!
// how can I let Directive A know about this, so Directive A can update its scope.items?
});
Of course, Directive A does not know about the changes Directive B made to groupOne unless it does another request. And therefore, my view is "static".
I know I could wrap Directive A's IndexedDB request into an interval and be checking for updates, but that seems like a strange way to solve this problem.
Is there a way with IndexedDB to be notified of this change? Is there something Angular provides that could help with this (something similar to $scope.$watch()) ?
It's not quite what I wanted (which is why I accepted dgrogan's answer), but in case anyone who stumbles upon this question is curious about what I ended up doing:
Manually broadcasting a custom event whenever I change groupOne (or anything else I care about) solves my problem for now.
I used $scope.$broadcast() from a controller (that's managing
a few interactions between directives) to let Directive A know about
the change Directive B made using $scope.$on().
This article was really helpful: http://toddmotto.com/all-about-angulars-emit-broadcast-on-publish-subscribing/
There is not yet a way to do this with pure IndexedDB but it is being prototyped. The name is "Observers". You can try to use the polyfill if you are so inclined.

backbone.js - how to communicate between views?

I have a single page web app with multiple backbone.js views. The views must sometimes communicate with each other. Two examples:
When there are two ways views presenting a collection in different ways simultaneously and a click on an item in one view must be relayed to the other view.
When a user transitions to the next stage of the process and the first view passes data to the second.
To decouple the views as much as possible I currently use custom events to pass the data ($(document).trigger('customEvent', data)). Is there a better way to do this?
One widely used technique is extending the Backbone.Events -object to create your personal global events aggregator.
var vent = {}; // or App.vent depending how you want to do this
_.extend(vent, Backbone.Events);
Depending if you're using requirejs or something else, you might want to separate this into its own module or make it an attribute of your Application object. Now you can trigger and listen to events anywhere in your app.
// View1
vent.trigger('some_event', data1, data2, data3, ...);
// View2
vent.on('some_event', this.reaction_to_some_event);
This also allows you to use the event aggregator to communicate between models, collections, the router etc. Here is Martin Fowler's concept for the event aggregator (not in javascript). And here is a more backboney implementation and reflection on the subject more in the vein of Backbone.Marionette, but most of it is applicable to vanilla Backbone.
Hope this helped!
I agree with #jakee at first part
var vent = {};
_.extend(vent, Backbone.Events);
however, listening a global event with "on" may cause a memory leak and zombie view problem and that also causes multiple action handler calls etc.
Instead of "on", you should use "listenTo" in your view
this.listenTo(vent, "someEvent", yourHandlerFunction);
thus, when you remove your view by view.remove(), this handler will be also removed, because handler is bound to your view.
When triggering your global event, just use
vent.trigger("someEvent",parameters);
jakee's answer suggests a fine approach that I myself have used, but there is another interesting way, and that is to inject a reference to an object into each view instance, with the injected object in turn containing references to as many views as you want to aggregate.
In essence the view-aggregator is a sort of "App" object, and things beside views could be attached, e.g. collections. It does involve extending the view(s) and so might not be to everybody's taste, but on the other hand the extending serves as a simple example for doing so.
I used the code at http://arturadib.com/hello-backbonejs/docs/1.html as the basis for my ListView and then I got the following to work:
define(
['./listView'],
function (ListView) {
var APP = {
VIEWS : {}
}
ListView.instantiator = ListView.extend({
initialize : function() {
this.app = APP;
ListView.prototype.initialize.apply(this, arguments);
}
});
APP.VIEWS.ListView = new ListView.instantiator();
console.log(APP.VIEWS.ListView.app);
}
);

Categories