I have a simple javascript AJAX application that allows search and selection of records. Selection updates the location.hash and loads the associated record detail, and ideally vice-versa also (loading a record when the hash changes). Of course a careless implementation can cause loops and extra panel flashes.
I want a predictable and concise implementation of this bidirectional binding.
One approach is to only load a record on the hashchange event, and when a record is selected in the UI, set location.hash. This seems most concise, but I'd be concerned this would diminish record-click responsiveness in older browsers with a polled hashchange shim.
Another approach is to record a navigating (e.g.) state when selecting a record, and clear it when handling hashchange. That's covered in this question. However, that seems like certain event sequences, like tapping Back multiple times rapidly, might result in inconsistency between the displayed content and URL.
Have you seen an implementation that solves these problems?
Why not to use HTML5 history API instead? All modern browsers support it
To make things easier you can use history.js library to work with History/State APIs
Using that library you can subscribe to URL updates
History.Adapter.bind(window, 'statechange', function () {
// event handler code here
var state = History.getState();
}
or push new URL into history
History.pushState(null, "name", "http://newurl");
I'm not sure which JS framework you would like to use to get bidirectional binding, but with KnockoutJS you can create ko.computed object with read and write methods
I think there's a simple answer in my case, since it's a read-only/idempotent operation (well, it actually logs a view).
I'll just store the state displayed by the current content, and test it on each event that would load content (including the 'redundant' hashchange events), ignoring the event if it matches the currently-displayed state.
Seems cheap, for better or worse. :)
here's my approximate/pseudo-code:
var activeRecordId;
function loadHash() {
var idList = window.location.hash.substring(1).split(',');
if (idList.length > 1) loadSpecificRecordsToList(idList);
else if (idList != '') loadDetailRecord(idList[0]);
}
function loadDetailRecord(id) {
if (id != activeRecordId) {
activeRecordId = id;
doDetailLoadAjaxAndSuch(id);
}
}
$(function () {
loadHash();
$.bind('hashchange', loadHash);
});
Related
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 understand that when a view is removed through .remove(), .stopListening() is called on that view to remove any event listeners associated with that view in Backbone. From the Backbone docs:
remove view.remove()
Removes a view from the DOM, and calls stopListening to remove any bound events that the view has listenTo'd.
I have views that are appended to a container that only have events related to dom actions on themselves through Backbone's events hook.
var View = Backbone.View.extend({
events : {
'input keyup' : 'searchDropdown'
},
searchDropdown: function () {
$('dropdown').empty();
//Appends views based on search
}
});
My question is really whether or not I'm leaking any memory (significant or not) when calling $.empty() on a container that effectively removes the view(s) appended inside of it. And if I am, is there any good convention for accessing and calling .remove() on those views?
You don't need any special framework for this but it's a good idea to implement removal properly and not depend on the browser being smart enough to do this. Sometimes in a large app you will find you specifically need to override the remove method to do some special cleanup - for instance you are using a library in that view which has a destroy method.
A modern browser tends to have a GC which is smart enough for most cases but I still prefer not to rely on that. Recently I came on to a project in Backbone which had no concept of subviews and I reduced the leaking nodes by 50% by changing to remove from empty (in Chrome 43). It's very hard to have a large javascript app not leak memory, my advice is to monitor it early on: If a DOM Element is removed, are its listeners also removed from memory?
Watch out for things which leak a lot of memory - like images. I had some code on a project that did something like this:
var image = new Image();
image.onLoad(.. reference `image` ..)
image.src = ...
Basically a pre-loader. And because we weren't explicitly doing image = null the GC never kicked in because the callback was referencing the image variable. On an image heavy site we were leaking 1-2mb with every page transition which was crashing phones. Setting the variable to null in a remove override fixed this.
Calling remove on subviews is as easy as doing something like this:
remove: function() {
this.removeSubviews();
Backbone.View.prototype.remove.call(this);
},
removeSubviews: function() {
if (!_.isEmpty(this.subViews)) {
_.invoke(this.subViews, 'remove');
this.subViews = [];
}
}
You just need to add your subview instances to an array. For example when you create a subview you could have an option like parentView: this and add it to the array of the parent. I have done more intricate subview systems in the past but that would work fine. On initialize of the views you could do something like:
var parentView = this.options.parentView;
if (parentView) {
(parentView.subViews = parentView.subViews || []).push(this);
}
So, basically all my events(there's min. 360 of them) have team1 vs. team2 or - vs. team2 or team1 vs. - placeholders.
And on the initial render events change color depending on whether the event has one or two teams.
Orange color for the one team , and green for the two teams. Also, the event changes color on click.
But mostly, I'm interested in increasing performance with rendering events.
Rendering performance is going really bad in fullCalendar, and I couldn't find any solution to this problem.
So here's my code:
eventRender: function (event, element) {
$(element).append((event.teams[0] != null ? event.teams[0] : '-') + '</br> vs. </br>' + (event.teams[1] != null ? event.teams[1] : '-'));
if (event.teams.length === 1) {
$(element).css('background', 'orange');
}
else if (event.teams.length > 1) {
$(element).css('background', 'green');
}
}
My main issue is that when I click on event to change its color, the script automatically goes to the eventRender or eventAfterRender event, and its behavior is exactly like the for statement - it iterates over events and then it does the stuff that I want to do with the individual event, but only when the loop lands on the clicked event.
Also, in the eventClick I've called $('#myCalendar').fullcalendar('updateEvent',event) and I think there is a bug, because it automatically goes to the eventAfterRender or the eventRender, iterating over the whole events collection again.
Even tough 'updateEvent' parameter should instruct fullCalendar to update/render only the specific event.
Does anyone have any advice on this subject?
Fullcalendar now supports the renderEvents method: https://fullcalendar.io/docs/renderEvents.
Simply build your events list and send them all at once:
$("#calendar").fullCalendar('renderEvents', events, true);
I know this is an old question, but i solved the same performance problem in v5 of the fullcalendar with this configuration option:
https://fullcalendar.io/docs/rerenderDelay
It basically adds a delay after each operation that would trigger a render event.
if the framework detects another operation within that delay, it renders these events in one operation and thereby increases performance.
setting the value to 1 (so 1 millisecond delay) did the trick for me. I simply added it to the configuration in my angular component:
calendarOptions: CalendarOptions = {
...,
rerenderDelay: 1,
}
In fullcalendars source-code (at least in my version of it) there is the renderEvent-handler, that calls reportEvents -function which is the bottleneck of performance. I worked my way around this issue, by adding handling of mass-rendering events to the source-code.
I wrote a short function:
function massRenderEvents(events, stick) {
var i;
for (i = 0; i < events.length; i += 1) {
normalizeEvent(events[i]);
if (!events[i].source) {
if (stick) {
stickySource.events.push(events[i]);
events[i].source = stickySource;
}
cache.push(events[i]);
}
}
reportEvents(cache);
}
Under "EventManager" -function, and added it to EventManagers exports, like:
t.massRenderEvents = massRenderEvents;
Now, for every batch of rendered events, the heavy and slow reportEvents is called just once. Note, that massRenderEvents -function is very similar to the original renderEvent -function.
I have changed
$("#calendar").fullCalendar('renderEvent', eventData1, true);
to
$("#calendar").fullCalendar('addEventSource', eventData1, true);
and that worked for me. I have read the issue on several related website and as per their suggestion I have done this.
The main difference between renderEvent and addEventSource is that the first one tries to interact with calendar when even a single event created which take much time because of regular callback function, and the second one sends a bucket of JSON events to calendar which require only single callback function which improve the performance and take less time.
I'm working on a project in JavaScript where we're building a Greasemonkey plugin to an organizational site we're using in our office. We're having trouble getting our changes to stay rendered, since we can't simply inject our changes into the existing render function.
As a result, we need to find every event where rendering happens and inject our own render function there. However, there are some events that we can see happening, but we can't hook into them. What I'd like to know is how to bind a function to an object's data member, so that the function is called whenever that member changes. One of our team members seemed to think it was possible, but the method he told us to use didn't seem to work.
What we tried was something along the lines of
window.Controller.bind("change:idBoardCurrent", OMGITWORKED);
where idBoardCurrent is a member of window.Controller and OMGITWORKED is the function we'd like to be called when window.Controller.idBoardCurrent is changed.
I'm not very familiar with JavaScript or data binding, so I have no idea if this is right or wrong, or what is correct or incorrect about it. If someone could point out what to change in this snippet, or if they could suggest another way to go about this, I would be very appreciative.
You can use Object.defineProperty to define a setter and getter for the Objects property
Object.defineProperty(window.Controller,"idBoardCurrent",{
get : function() { return this.val; },
set : function(value) {this.val = value;OMGITWORKED(value); }
});
function OMGITWORKED(param) {
console.log("idBoardCurrent has been Changed to " + param);
}
window.Controller.idBoardCurrent = "Test";
window.Controller.idBoardCurrent = "Test2";
console.log(window.Controller.idBoardCurrent)
Edit: changed the code according to the contexts object
JSBin
As this is specifically Firefox, you can use the mutation events it provides. But note the caveats on them from that page:
The W3C specification for them was never widely implemented and is now deprecated
Using DOM mutation events "significantly degrades" the performance of DOM modifications
If you're able to restrict yourselves to Firefox 14 and higher, you can use the new mutation observers stuff instead.
This is, when I am not totally wrong, more a question of javascript.
I found some information about that topic
Listening for variable changes in JavaScript or jQuery
jQuery trigger on variable change
Javascript Track Variable Change
Sorry when I didn't understand the topic.
All the best
I am using history.js to handle back button. In history.js statechange is firing whenever i do a pushstate. Why?
Wanted to add, yes this is the expected behaviour of History.js.
At the same time there are more discussions that critize this behaviour as it is not the W3C standard and does create some confusion.
In short, to answer your question: In the History.js pushState() function is a call to statechange at the end.
Upside of this solution is that you can just change (push) your new state and let the onstatechange()-function handle the transition. Downside is that you are not able to handle exceptions/or have to write them into the onstatechange event-handler.
I personally prefer the W3C way of handling this, as you can distinguish between back/forward button and pushState. The History.js maintainers are working on an internal flag solution, that enables you to change this behaviour:
Notice how the above calls [pushstate-calls] trigger statechange events, if for some
reason you do not want this to happen then inside your statechange
handler you can use the following:
if ( History.getState().internal ) { return; }
*This feature is currently in development and can only be used with the 'dev' version of History.js! Hope this will help some other people in the future :)
After trying to accomplish this for a day now, I finally found the solution here: https://github.com/browserstate/history.js/issues/47#issuecomment-25750285
The code is pretty damn simple, the following is quoted from the link:
When you push your state
History.pushState({
_index: History.getCurrentIndex(),
someData: ...
}, someTitle, someUrl);
and then in the event binding
History.Adapter.bind(window, 'statechange', function () {
var currentIndex = History.getCurrentIndex();
var internal = (History.getState().data._index == (currentIndex - 1));
if (!internal) {
// your action
}
});
According to this discussion on github, it's expected behaviour of history.js
This pull request claims to have modified history.js to be more inline with W3C Specs.