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" />
I have the following piece of code in my meteor app
if (Meteor.isClient) {
Meteor.startup(function(){
var onSuccess = function(acceleration){
alert( acceleration);
};
var onError = function(acceleration){
return "error";
}
var options = { frequency: 3000 }; // Update every 3 seconds
var getAcc = navigator.accelerometer.watchAcceleration(onSuccess, onError, options);
});
}
What that does is retrieve the android phones accelerometer data every 3 seconds, and on a successful poll it will show the object in an alert. This works fine.
However, I don't want this polling code to be in the startup function. I want to have more control over when this is executed
I have a template where I want to display the accelerometer values. I changed the onSuccess method in the code above to return the object instead of alerting (the rest of the startup code is the same):
var onSuccess = function(acceleration){
return acceleration;
};
My template looks like this:
Template.rawData.helpers({
acc: function(){
alert(getAcc);
return getAcc;
}
});
What I'm expecting to happen is for the accelerometer data to be stored in getAcc in the startup function, but then to return it through acc to my webpage. This does not seem to happen. The alert in the template doesn't occur either
Is there a way to access these cordova plugins from outside of the startup function? Am I just incorrectly returning the objects between the startup and template sections?
I guess my other overarching question is this: I'm not sure how to display those accelerometer values through a template if theyre gathered in the startup function, and not from a template helper
You need to have the template update reactively when the data changes. To do that set up a reactive variable that gets updated by the callback. First, install the package:
meteor add reactive-var
Then, when the template is created, create the reactive variable and start watching the callbacks:
Template.rawData.onCreated(function() {
self = this;
self.rawValue = new ReactiveVar();
self.accWatchID = navigator.accelerometer.watchAcceleration(
function(acc) { self.rawValue.set(acc); }, function() {}, { frequency: 3000 }
);
});
Your template helper can then return the value of the reactive variable, and your template will be updated whenever it changes:
Template.rawData.helpers({
acc: function() {
return Template.instance().rawValue.get();
}
});
(Note that in your original code, since the alert wasn't being called, there must be a problem in your template. Does it have the right name?)
Finally, you should stop the callback when the template is destroyed:
Template.rawData.onDestroyed(function() {
navigator.accelerometer.clearWatch(this.accWatchID);
});
Note that I've just typed that code here without testing it. You may need to fine tune it a little if it doesn't work straight away.
I am new to using meteor, so I am hoping to receive a very basic explanation of how these functions work, and how I am supposed to be using them. Otherwise, if there is a method that would be better suited to what I am hoping to achieve, the knowledge would be appreciated.
The functionality I was hoping to achieve:
I have a Mongo collection that contains a number value within documents that are assigned to specific users.
I will use the value that I get from the document to populate a width: xx% in some css in-line styling on a 'progressbar'. But the other use I have for it is performing some kind of 'reactive' function that runs whenever this value changes which can update the background color of this progress bar dynamically based off of the current value of the progressbar. Think 'red' for low and 'green' for high:
project.html:
<template name="progressBar">
<div id="progress-bar" style="width:{{curValue}}; background-color:*dynamicColor*;"></div>
</template>
project.js:
Progress = new Mongo.Collection("progress");
Template.progressBar.helpers({
curValue: function () {
return Progress.findOne({user: Meteor.userId()}).curValue;
}
});
The above sometimes works. But it doesn't seem to be reliable and isn't working for me right now. I get errors about cannot read property 'curValue' of undefined. From what I have researched online, that means that I am trying to access this document before the collection has loaded. But I really cannot find a direct solution or wrap my head around how I am supposed to be structuring this to avoid that error.
The next problem is observing changes to that value and running a function to change the background color if it does change.
Here are a few of the types of autorun/observe pieces of code I have tried to make work:
Session.set('currentValue', Progress.findOne({user:Meteor.userId()}).curValue);
Tracker.autorun(function(){
var currentValue = Session.get('currentValue');
updateColor(currentValue);
});
var currentValue = Progress.findOne({user:Meteor.userId()}).curValue);
var handle = currentValue.observeChanges({
changed: function (id, currentValue) {
updateColor(currentValue);
}
});
To sum up the question/problem:
I want to use a value from a mongo db document in some in-line css, and also track changes on that value. When the value changes, I want a function to run that will update the background color of a div.
Update
Using #Ethaan 's answer below, I was able to correct my subscription/template usage of my collection data. I did a bit more digging and come to a greater understanding of the publish/subscribe methods and learned how to properly use the callbacks on subscriptions to run my Tracker.autorun function at the appropriate time after my collection had loaded. I was able to expand on the answer given to me below to include a reactive Tracker.autorun that will run a function for me to update my color based on my document value.
The code that I ultimately got working is as follows:
project.js
if (Meteor.isClient) {
Progress = new Mongo.Collection("progress");
Template.content.onCreated(function () {
var self = this;
self.autorun(function () {
self.subscribe("progress", function(){
Tracker.autorun(function(){
var query = Progress.findOne({user: Meteor.userId()}).level;
changeColor(query);
});
});
});
});
Template.content.helpers({
value: function(){
return Progress.findOne({user: Meteor.userId()}).level;
}
});
function changeColor(value){
//run some code
}
}
if (Meteor.isServer) {
Progress = new Mongo.Collection("progress");
Meteor.publish("progress", function () {
return Progress.find({user: this.userId});
});
}
project.html
<head>
<title>Project</title>
</head>
<body>
{{> standard}}
</body>
<template name="standard">
...
{{> content}}
</template>
<template name="content">
{{#if Template.subscriptionsReady}}
{{value}}
{{else}}
0
{{/if}}
</template>
that means that I am trying to access this document before the
collection has loaded
Seems like you get the problem, now lets get ride to some possible solutions.
Meteor version 1.1
If you are using the new meteor version 1.1 (you can check running meteor --version)
use this.
First on the onCreated function use this.
Template.progressBar.onCreated(function () {
var self = this;
self.autorun(function () {
self.subscribe("Progress");
});
});
See more about subscriptionReady on the DOCS.
Now on the HTML use like this.
<template name="progress">
{{#if Template.subscriptionsReady}}
<div id="progress-bar" style="width:{{curValue}}; background-color:*dynamicColor*;"></div>
{{else}}
{{> spinner}} <!-- or whatever you have to put on the loading -->
{{/if}}
</template>
Meteor under 1.0.4
You can have on the router something like a waitOn:function(){}
waitOn:function(){
Meteor.subscribe("Progress");
}
or since helper are asynchronous do something like this (not recommendable).
Template.progressBar.helpers({
curValue: function () {
query = Progress.findOne({user: Meteor.userId()}).curValue;
if(query != undefined){
return query;
}else{
console.log("collection isn't ready")
}
}
});
I’m utilizing crossroadsJS and hasherJS on an SPA powered by handlebarsJS. In retrospect I probably should have built the whole thing on Ember, but this late in it’s not wise to start over.
I have hash routes set up that switch my handlebars templates in and out based on whatever was clicked. I’m able to move backwards and forwards between the routes with no problem. However, if I refresh the page, I’m always brought back to my #/home view. I’ve done some research into window.onhashchange and window.onbeforeunload, but none of these seem to be solving the issue.
I have a very specific way of how I’m handling my views. I have a global views object constructed like so:
views.procedures = function (template) {
this.initialize = function () {
this.el = $('<div/>');
this.el.attr('id', 'procedures');
this.el.on('click', '.returned-list li a', this.toResults); // list of API objects by name
};
this.render = function () {
var parts = {
title: 'Procedures View',
};
this.el.html(template(parts));
return this;
};
this.toResults = function () {
cache.justClicked = $(this).attr('data-id');
crossroads.addRoute('procedures/{name}', function () {
api.endpoints.procedure(cache.justClicked); // constructed elsewhere
});
};
this.initialize();
};
I mention this because I can’t simply add in JQuery listeners for any type of hash changes in a $(document).ready() or $(window).onhashchange
I’ve read through both the hasherJS and crossroadsJS documentations multiple times, but I’m not able to think of anything to solve the refresh issue. Thanks in advance for any help.
I would need more information from you, but I'm assuming you're doing
var DEFAULT_HASH = 'home';
if (! hasher.getHash()) {
hasher.setHash(DEFAULT_HASH);
}
This is basically saying your default route is always going to be your home route, which is why you're seeing it when you refresh your current route. Try the following:
var DEFAULT_HASH = 'home', url = hasher.getBaseURL();
if (hasher.getURL() === url) {
hasher.setHash(DEFAULT_HASH);
}
This will check to see what your base URL is (the one that's loaded when you first visit the page) and append a #/home route to the base URL, allowing you to refresh your currently viewed route.
Hope this helps.
I have a template helper called {{renderNav}} in a template Nav
e.g.
Template.Nav.renderNav
and within that helper function I want to parse the rendered output of another helper within a different template
For example the helper
Template.contentWindow.content
which provides the html for
{{content}}
and my renderNav helper wants to part the html that replaces {{content}} to generate the html for
{{renderNav}}
how would I do this? right now the {{renderNav}} helper executes for or runs more quickly and so it is unable to parse the html that replaces {{content}}
#Hugo - I did the following in my code as you suggested
Template.contentWindow.rendered = function() {
debugger;
return Session.set('entryRendered', true);
};
Template.Nav.renderNav = function() {
debugger;
var forceDependency;
return forceDependency = Session.get('entryRendered');
};
When I run it, the debugger first stops when executing the renderNav helper. (Which makes sense with what I am seeing in terms of the race condition). Then contentWindow renders and I hit the breakpoint above the Session.set('entryRendered', true). But then the renderNav doesn't run again as you suggest it should. Did I misinterpret or incorrectly implement your suggestion?
You need a dependency in the template that you want to rerun. There are few possibilities, depending on what data you want to get.
For example, you can set a reactive marker in the content template that will notify renderNav that it's done with drawing.
Template.contentWidnow.rendered = function() {
...
// Set this on the very end of rendered callback.
Session.set('contentWindowRenderMark', '' +
new Date().getTime() +
Math.floor(Math.random() * 1000000) );
}
Template.renderNav.contentData = function() {
// You don't have to actually use the mark value,
// but you need to obtain it so that the dependency
// is registered for this helper.
var mark = Session.get('contentWindowRenderMark');
// Get the data you need and prepare for displaying
...
}
With further information you've provided, we can create such code:
content.js
Content = {};
Content._dep = new Deps.Dependency;
contentWindow.js
Template.contentWidnow.rendered = function() {
Content.headers = this.findAll(':header');
Content._dep.changed();
}
renderNav.js
Template.renderNav.contentData = function() {
Content._dep.depend();
// use Content.headers here
...
}
If you want the navigation to be automatically rebuilt when contentWindow renders, as Hubert OG suggested, you can also use a cleaner, lower level way of invalidating contexts:
var navDep = new Deps.Dependency;
Template.contentWindow.rendered = function() {
...
navDep.changed();
}
Template.renderNav.contentData = function() {
navDep.depend();
// Get the data you need and prepare for displaying
...
}
See http://docs.meteor.com/#deps for more info.
If, on the other hand, you want to render another template manually, you can call it as a function:
var html = Template.contentWindow();
The returned html will not be reactive. If you need reactivity, use:
var reactiveFragment = Meteor.render(Template.contentWindow);
See the screencasts at http://www.eventedmind.com/ on Spark and reactivity for details on how this works.
UPDATE
To add a rendered fragment to your DOM:
document.body.appendChild(Meteor.render(function () {
return '<h1>hello</h1><b>hello world</b>';
}));
You can also access the rendered nodes directly using the DOM API:
console.log(reactiveFragment.childNodes[0]);