Watch for a property creation event? - javascript

I need to be able to determine when an object is created (not a DOM element -- a JavaScript object).
An answer to this question has some very useful looking code for creating observable properties, so you can have a function fire when a property changes.
In my situation I need to do something when the object/property is created, not an existing property changed, and my limited understanding of such matters did not help me figure out if or how I could use that code to do this after much squinting.
The situation is: page loads a bunch of scripts. Some of the scripts create things that are needed by other scripts, e.g:
ThisStuff = (function () {
// blah blah
return self;
} ());
Some other code needs to initialize this ThisStuff, whenever it's available, which may be after the DOM is done loading. The user doesn't actually need ThisStuff right away, so it's fine for it to happen whenever the script is done loading. So I would like to do something along lines of:
$(document).ready(function() {
wheneverIsAvailable(window,'ThisStuff', function(object) {
object.init(args);
})
});
I realize there are other solutions to this problem (changing script order, or loading scripts on demand) but those are difficult because of the architecture. So I'm only interested in a way to do this versus other solutions. If jQuery offers some such functionality, that's fine also as I'm using it.

You could have a setInterval checking a number of times a second to watch the specific variable. You can check whether it is created using obj.hasOwnProperty(prop). When it is created, you invoke the function, and clear the interval.
It might be dirty but it might also just work fine for you.
Edit: I coded this for you: http://jsfiddle.net/jhXJ2/2/. It also supports passing additional arguments to the function.
window.__intervals = [];
function wheneverIsAvailable(obj, prop, func) {
var id = (Math.random()+"").substring(2);
var args = arguments;
window.__intervals[id] = window.setInterval(function() {
if(obj.hasOwnProperty(prop)) {
window.clearInterval(window.__intervals[id]);
func(Array.prototype.slice.call(args, 3));
// Call function with additional parameters passed
// after func (from index 3 and on)
}
}, 1000/ 50);
}
wheneverIsAvailable(window, 'test', function() {
alert(arguments[0]);
}, 'Woot!');
window.setTimeout('window.test = 123', 1000);

This is a bit far-fetched but it might work.
You would need to use knockoutjs, a javascript library. It's awesome but is built for a slightly different purpose.
Anyways it has a dependentObservable thing which allows to fire up an event whenever a certain value changes. Now I know you want on creation but you can check whether your variable holds any value (other than what you provided initially), if yes then consider it initialize.
Let me know if you think this sounds feasible.

Related

How to modify results from a read-only ng-app?

I apologize if this is a duplicate, just haven't been able to find anything close to this myself.
The company I work for has an online reporting system that is run by an ng-app applied directly to the body tag. I have been tasked with modifying the result that returns from this ng-app. Following code is called using onload attached to the body tag.
function getElements(){
var list;
list = document.getElementsByClassName("neutral");
[].forEach.call(list, function (listItem) {
addNeutral(listItem);
});
...
Basically, trying to find anything with class "neutral" and apply results from another function to it. The addNeutral function is basically just
element.classList.add("neutralHighlight");
This code seems to run and gathers the correct list of elements, but the new class is never added and no errors occur. So long story short, is there any way to modify the output of a ng-app using code separate from the ng-app itself? Any guidance would be greatly appreciated.
Update 3/5/20
So I implemented Shaun's response and it still isn't working properly. With some debug messages, I can see that it collects the "list" variable as an HTMLCollection. The forEach function doesn't seem to even trig
function getElements(){
var list;
list = document.getElementsByClassName("neutral");
console.log(list); //Debug - Shows in console
[].forEach.call(list, function (listItem) {
console.log(listItem); //Debug - Does not show in console
addNeutral(listItem);
});
}
function addNeutral(element){
angular.element(element).addClass("neutralHighlight");
console.log("!!!end addNeutral"); //Debug - Does not show in console
}
Update 3/9/20 -SOLUTION-
Application is returning the HTML Collection, but it displays with a length of 0 (still displays the objects, but I think that's a Firefox console thing). When trying to loop through the list items, it returns null for the first item, so the function is still being called before the Angular app loads completely.
That being said, I messed around with things a bit this morning and came to a solution! I ended up using the setInterval function with a 5 second interval (since I need it to update, I may change this to optimize it later by adding onChange items to the objects I grab initially). The setTimeout that was proposed would have worked with a delay added to it. This probably isn't the most elegant solution, and there's probably a better way to do it, but this works for anyone interested.
function getElements(){
var list;
list = document.getElementsByClassName("neutral");
for (i = 0; i <= list.length; i++){
var listItem = list.item(i);
addNeutral(listItem);
}
}
function loadFunction(){
setInterval(function(){getElements()}, 5000);
}
I added <script>loadFunction()</script> right before the closing HTML tag to execute.
Update 4/21/20 -IMPROVED SOLUTION- CSS Attributes
So this is a bit specific to my scenario, but I wanted to include it for anybody else who may come across this in the future. I was actually able to accomplish this entirely with CSS attribute selectors. The tags that I wanted to edit all had specific titles assigned to them via the ng-app provided from the company, so I was able to use CSS selectors like div [title~="NotReadyForNextCall"]{<custom styling here>} to select any block that included an agent who was not ready for their next call. This is a much better solution in terms of resources required to operate and overall performance, so I hope it helps anybody looking at this down the line!
You might be able to get around this by using the angular object in your code and adding the class on an angular.element instead. AngularJS doesn't use a virtual DOM but it does use its own node references (which is what makes it so tricky to work with outside of the framework, as Lex pointed out in the comments of your question). Try:
angular.element(element).addClass("neutralHighlight");
Yes, you have access to angular outside of the app! And a last note, addClass() is available on angular.element because AngularJS comes with jqLite.
Further investigation
It looks like the above solution works if the class 'neutral' is being added in angular via the class attribute, but it looks like your app may be adding it programmatically with the ng-class directive after the DOM has rendered.
I wrapped your getElements() function in a setTimeout():
setTimeout(getElements);
This is unfortunately not a guarantee that the ng-class update will have taken place, but what it does is it executes the function after the previous digest cycle has completed and that appears to be working.
An even safer solution would be to use document.ready but again with the angular.element wrapper. This will ensure the initial DOM state has been rendered by AngularJS, including applied directives:
angular.element(document).ready(function() {
getElements();
});
EDIT: Update 3/9/20 -SOLUTION-
The solution proposed in the answer is almost identical to the setTimeout() answer given here. The only difference is setInterval() will keep executing the code every 5 seconds until you tell it to stop.
You can do this with the following:
var loadFunction = setInterval(function() {
var el = getElements();
if (el) clearInterval(loadFunction);
}, 5000);
And just return a bool in your getElements() like so:
function getElements() {
var list;
var found = false;
list = document.getElementsByClassName("neutral");
[].forEach.call(list, function (listItem) {
addNeutral(listItem);
found = true;
});
return found;
}
See: codepen.io/shaunetobias/pen/KKpXRxq

How to store/stash JavaScript event and reuse it later?

From https://developers.google.com/web/fundamentals/app-install-banners/#trigger-m68
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
// Stash the event so it can be triggered later.
deferredPrompt = e;
});
This code is fine, but I want to trigger the stashed event later, in a different place. To perform that, I need to store an event not just in a variable, but somewhere else.
The question: how can an event be stored with its methods?
I tried Local Storage with serialization/deserialization of an object:
> localStorage.setItem('stashed-event', JSON.stringify(e))
>
> JSON.parse(localStorage.getItem('stashed-event'))
But this approach doesn't work as expected, because it's storing only key-values and losing all event methods.
You cannot store an event in this manner. You want to store an object. Only serializable properties are storable for such an object. Functions are not serializable in JavaScript. Functions are not serializable in many languages.
Fundamentally this is basically because when you deserialize an object, its signature can change. If you have ever programmed in java, this is similar to a deserialization error when reading in a serialized object and attempting to reconstruct an object. Because the body of a method function of an object can change in between the time the object is written to some storage and then later read, methods are not serializable. This is because when you serialize an object, it does not serialize its interface definition where methods are defined. It just stores data.
Same reason when you serialize to a json string, it drops the functions.
Instead of storing an event, store the useful information from the event in an object (or let things be implicitly dropped by stringify and use the event directly).
Which method of storage you use just depends on things not mentioned in your question. Such as how long it should be stored, whether it should be available outside of your site's origin, how much data will typically be stored, whether there is more than one object to store, etc. Based on the limited information provided in your question, you are probably fine just using either localStorage or an in memory array.
If you find the need to store hundreds of objects then indexedDB would begin to be more appropriate. But just choosing a different storage medium will have no effect whatsoever on whether you can store functions. You cannot store functions.
There have been loads of talk around this as soon as I/O 2018 mentioned about handling of A2HS event being developer driven from now onwards. This is also captured in the official doc and inspired from it, there is a beautiful article explaining thoroughly how to achieve exactly this scenario. While I'd suggest to go through the complete article for proper understanding of the updated dynamics around the A2HS flow, feel free to jump onto the "The New Add To Homescreen Flow" section for your requirement.
In a nutshell, follow the following steps:
Create a variable outside the scope of the beforeinstallprompt event handler.
Save a reference to the beforeinstallprompt event object in the above handler.
Use this later to trigger the add to homescreen prompt on demand.
The article have the complete code snippets which you can refer/reuse.
Edit: I read your question once again and realized one important aspect you might be specifically looking for, viz., using it "somewhere else". If this means you are referring to using it on a different page, then my suggestion would be to go for storing the event object in:
IndexedDB which is a collection of "object stores" which you can just drop objects into. Disadvantage - Can have browser compatibility restrictions. Also, can result in large amount of nested callbacks.
Or you can choose to use the "in process cache" (heap memory of your application) which doesn't require serializing either. Disadvantage - This cannot be shared across multiple servers though.
Other than this, I cannot foresee a con free solution at the moment. But will try to figure it out and possibly update the thread.
After reading your question a few times, and the answers another few,
The question: how can any javascript Object be stored with its methods?
The answer: there is no how.
However,
Josh properly explained you can extract and store all the serializable properties, say data, from your event.
I will just add you can create an event with somehow that same data later anywhere, this new event will have all the methods any Event has, but by now probably none of use.
Obviously, even serialized in your data, properties like timeStamp, isTrusted, etc... will be overriden at creating the new event.
What you just miss / need is an EventTarget, the value of the event.target property,
the reference which is lost forever when document.body unloads forever, or when serializing the event Object.
But if it is still alive, or if you know what event.target should be, like a DOM Element or any Object you can reference, from wherever you recreate the event (where?), just dispatch your event to that object, if it listens to that event.type,
your brand new event should be at least heard.
Simple example from MDN EventTarget, or see EventTarget.dispatchEvent
As a comment over the extensive answer by cegfault: eval, and text source code... could be <script> text source code </script>... should your script produces a (String) script. If not you ´d probably better go further backwards to where did your script creates the unserializable things that appear in your event, and think about recreating those things, not the event.
TL;DR to accomplish what you are doing, you have three options:
Store a reference to the event in a global value (which is what most tutorials - like your referenced youtube video - will recommend you do). This requires the event to run in the same context (ie web page) as when you store the reference
When you store the reference to the event in localStorage (such as by name or a key/value look up), on the page/context where you want to execute the event, make sure the appropriate functions and libraries are loaded before executing the event
[strongly NOT recommended] Store the javascript source code in your storate and eval() it later [again, please don't do this]
As mentioned by #Josh and #SaurabhRajpal, what you are asking for, strictly speaking, is not possible in JavaScript. What you are doing with JSON.stringify(e) will probably return undefined or null, as the MDN documentation for JSON.stringify says:
If undefined, a Function, or a Symbol is encountered during conversion it is either omitted (when it is found in an object) or censored to null (when it is found in an array). JSON.stringify can also just return undefined when passing in "pure" values like JSON.stringify(function(){}) or JSON.stringify(undefined).
In short, there is no way to store a single function into localStorage (or any other offline storage). To explain why this is not possible, see this example:
function foo() {
console.log("a")
}
function bar() {
return foo()
}
How can you store bar() for later usage? In order to store bar, you would also have to store foo(). This becomes much more complicated when you consider referencing a function which is in, or uses, a large library (like jQuery, underscore, D3, charting libraries, etc). Keep in mind your computer has already parsed the source code down into binary, and as such won't easily know how to read the function for every possible if, for, and switch statements to ensure all possible correlated functions and libraries are saved.
If you really wanted to do this, you would have to write your own javascript parser, and you really don't want to do that!
So what are your options? First, do everything on the same page, and store the reference to the event in a global value (the youtube video you link to in a comment is using this method).
Your second option is to use a reference to the event (not the event itself), and make sure the source code for that reference is use later. For (html) example:
// on page #1
<script src="path/to/my/js/library.js"></script>
...
<script>
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
localStorage.setItem('stashed-event', 'before-install')
})
</script>
// later, on page #2:
<script src="path/to/my/js/library.js"></script>
...
<script>
var evt = localStorage.setItem('stashed-event', 'before-install')
if(evt == 'before-install') {
dosomething() // which would be a function in path/to/my/js/library.js
}
// another option here would be to define window listeners for all possible events
// in your library.js file, and then simply build and trigger the event here. for
// more about this, see: this link:
// https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
</script>
Finally, you can store javascript source code and then eval() it later. Please, please, please do NOT do this. It's bad practice and can lead to very evil things. But, if you insist:
// on page #1
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
SomeAjaxFunction("path/to/my/js/library.js", function(responseText) {
localStorage.setItem('stashed-event', {
name: 'before-install',
call: 'beforeInstallFunction()',
src: responseText
})
})
})
// later, on page #2:
var evt = localStorage.setItem('stashed-event', 'before-install')
if(evt) {
console.log("triggering event " + evt.name)
eval(evt.src)
eval(evt.call)
}
Like I said, this is a really bad idea, but it's an option.
IMHO, I think you're trying to avoid including a library or source code in a later page/app/whatever, and javascript just does not work this way. It's best to pass around references in-memory, and only use key/value storage for names. Everything else is a type of coding gymnastics to avoid simply including your source code in the places it needs ot be included.
You can create a global constant and update it when ever event changes rather than serializing it and de-serializing which is a costly processes. SO this is how you can do it - You can create a window instance and clone the event in the window object so that it wont mutate.(Note this wont won't work across tabs)
window.addEventListener('click', (e) => {
e.preventDefault();
window.deferredPrompt = Object.assign(e);//Don't mutate
});
let someOtherMethod = ()=>{
console.log(window.deferredPrompt)
}
window.setInterval(someOtherMethod, 5000);
Try clicking after 5 seconds in the last window and check after 5 seconds
Here is a simple but successful solution.
The idea is to capture the event in a variable and only fire it when signaled by another window of the same origin (domain etc).
The solution uses localStorage methods as the signaling semaphore.
Here is the code I used. I have tested it successfully in Chrome, both mobile & desktop.
//In event handling window
//Register the ServiceWorker
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js');
};
//Capture beforeInstall event
window.addEventListener('beforeinstallprompt', function(event){
event.preventDefault();
window.deferredPrompt = event;
return false;
})
//Wait for signal
window.onstorage = event => {
if (event.key === 'installprompt') {
//Fire the event when signaled.
window.deferredPrompt.prompt();
// Discard event
window.deferredPrompt = null;
//Discard storage item
localStorage.removeItem('installprompt');
}
}
//In a different window or tab from the same origin fire the event when ready.
localStorage.setItem('installprompt', 'whatever');
Please see if this helps.
Defining an event listener for 'beforeinstallprompt' event
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
//do all the stufff
console.log('Event triggered');
});
When you want to dispatch the event manually.
Create a new event variable.
var myevent = new Event('beforeinstallprompt');
window.dispatchEvent(myevent);
Outputs 'Event triggered' in the console.

How to re-evaluate a script that doesn't expose any global in a declarative-style component

I have been writing a reusable script, let's call it a plugin although it's not jQuery, that can be initialised in a declarative way from the HTML. I have extremely simplified it to explain my question so let's say that if a user inserts a tag like:
<span data-color="green"></span>
the script will fire because the attribute data-color is found, changing the color accordingly.
This approach proved very handy because it avoids anyone using the plugin having to initialise it imperatively in their own scripts with something like:
var elem = document.getElementsByTagName('span')[0];
myPlugin.init(elem);
Moreover by going the declarative way I could get away without defining any global (in this case myPlugin), which seemed to be a nice side effect.
I simplified this situation in an example fiddle here, and as you can see a user can avoid writing any js, leaving the configuration to the HTML.
Current situation
The plugin is wrapped in a closure like so:
;(function(){
var changeColor = {
init : function(elem){
var bg = elem.getAttribute('data-color');
elem.style.background = bg;
}
};
// the plugin itslef looks for appropriate HTML elements
var elem = document.querySelectorAll('[data-color]')[0];
// it inits itself as soon as it is evaluated at page load
changeColor.init(elem);
})();
The page loads and the span gets the correct colour, so everything is fine.
The problem
What has come up lately, though, is the need to let the user re-evaluate/re-init the plugin when he needs to.
Let's say that in the first example the HTML is changed dynamically after the page is loaded, becoming:
<span data-color="purple"></span>
With the first fiddle there's no way to re-init the plugin, so I am now testing some solutions.
Possible solutions
Exposing a global
The most obvious is exposing a global. If we go this route the fiddle becomes
http://jsfiddle.net/gleezer/089om9z5/4/
where the only real difference is removing the selection of the element, leaving it to the user:
// we remove this line
// var elem = document.querySelectorAll('[data-color]')[0];
and adding something like (again, i am simplifying for the sake of the question):
window.changeColor = changeColor;
to the above code in order to expose the init method to be called from anywhere.
Although this works I am not satisfied with it. I am really looking for an alternative solution, as I don't want to lose the ease of use of the original approach and I don't want to force anyone using the script adding a new global to their projects.
Events
One solution I have found is leveraging events. By putting something like this in the plugin body:
elem.addEventListener('init', function() {
changeColor.init(elem);
}, false);
anybody will be able to just create an event an fire it accordingly. An example in this case:
var event = new CustomEvent('init', {});
span.dispatchEvent(event);
This would re-init the plugin whenever needed. A working fiddle is to be found here:
http://jsfiddle.net/gleezer/tgztjdzL/1/
The question (finally)
My question is: is there a cleaner/better way of handling this?
How can i let people using this plugin without the need of a global or having to initialise the script themselves the first time? Is event the best way or am I missing some more obvious/better solutions?
You can override Element.setAttribute to trigger your plugin:
var oldSetAttribute = Element.prototype.setAttribute;
Element.prototype.setAttribute = function(name, value) {
oldSetAttribute.call(this, name, value);
if (name === 'data-color') {
changeColor.init(this);
}
}
Pros:
User does not have to explicitly re-initialize the plugin. It will happen automatically when required.
Cons:
This will, of course, only work if the user changes data-color attributes using setAttribute, and not if they create new DOM elements using innerHTML or via some other approach.
Modifying host object prototypes is considered bad practice by many, and for good reasons. Use at your own risk.

Is saving elements as object properties good practice?

I'm writing a small JavaScript framework for fun and possible implementation similar to backbone(hence the tag). I've started saving elements as object properties, as shown below. I'm not sure if I've seen this done, so I was curious if this causes any issues.
Similarly, If the module depends on other modules I list those at the top of the object in the form of....another object.
I wanted a way to list dependencies ( page elements or JavaScript modules ) and detect any issues up front. This has similar ( not same ) benefits as dependency injection.
This is a specific question on this code review post which explains a bit further on how the framework works.
/*MUserTry
**
**
**
*/
$A.modelAjax({
Name: 'MUserTry',
S: {
DynSma: SDynSma,
DynTwe: SDynTwe,
DynArc: SDynArc,
AniFlipPage: SAniFlipPage,
ClientStorage: SClientStorage
},
E: {
but: $A('#ut_but')[0]
},
J: {
box: $('#ut_box')
},
init: function () {
var pipe = {},
this_hold = this;
this.J.box.draggable();
this.E.but.addEventListener("click", function () {
pipe = $A.definePipe(this_hold.Name);
$A.ajaxMachine(pipe);
}, false);
},
pre: function (pipe) {
pipe.page.email = this.e_button.getAttribute('data-email');
pipe.proceed = true;
},
post: function (pipe) {
this.S.ClientStorage.setAll(pipe.server.smalls);
this.S.DynSma.run(pipe.server.smalls);
this.S.DynArc.run(pipe.server.arcmarks);
this.S.DynTwe.run(pipe.server.tweets);
this.S.AniFlipPage.run('ma');
},
finish: function (pipe) {
$A.log(pipe);
}
});
Ok first off let me offer the obligatory "you'll never get a better wheel by re-inventing the wheel" warning. Whatever you're trying to accomplish, you're almost certainly going to be more successful with it if you use an existing library. And even if there is good cause for you to make your own, it would still benefit you immensely to at least look at existing libraries.
But ... maybe you're just having fun with this project, and looking at other projects isn't fun so you're not doing it. Fair enough.
In any case, if you do look at Backbone, you'll find that this practice is core to the Backbone View class. Every View in Backbone has an "el" and "$el" property, which refer to the raw DOM element for the view and the jQuery-wrapped version of that element.
Backbone has no real performance issues with this because variables/properties in JS are just pointers; in other words, when you set the property of an object to an element, you aren't duplicating that element, you're just adding a reference to it (to put it another way, it's more like you're an A tag rather than a whole new document).
The one time Backbone does have a problem though (and your framework will too) is with stale references. In other words, if you just remove element X from the page, the browser will stop using memory for it (eventually, via garbage collection). But if there is an object out there which points to that element, it won't get garbage-collected, because anything with a reference isn't "garbage".
So, the main thing you have to watch out for is making sure that these objects either:
A) get deleted whenever their elements do, or
B) get rid of their references (eg. delete obj.reference) when their elements get deleted
If you don't do that, things will still probably work just fine ... until you use it on a page with lots of elements being created/deleted, at which point Firefox will start popping up "this script took way too long to run, are you really sure you want to be doing this?" messages.

extend a javascript option to add functionality

I need to call "MyOtherFunction" when "MyFunction"(which creates an element) completes, without MyFunction knowing what MyOtherFunction is.
The reason I need this is for extension of a jquery powered fileupload User Control that is used in several places with different functionality. A specific page shows a header and file count for it, and when the upload completes, I need to modify the file count according to how many files are displayed(by created elements) I thought :
$(UserControl).on(MyFunction, UploadElem, MyOtherFunction);
but this route is not accomplishing anything. The most I can alter the User Control is add in a function call, but without effecting the original user control functionality.
I'm not sure if because MyFunction isn't an event and doesn't bubble up or if it just isn't possible to use a defined function as a parameter of .on() is the reason I cannot get this code to work. Any suggestions?
Easiest way I can think of, is duck punching respectively hooking that method:
var _oldMyFunction = MyFunction;
MyFunction = function() {
_oldMyFunction.apply( this, arguments );
MyOtherFunction();
};
I managed to solve my own issue, but the context is important for the answer:
// Using a Global JavaScript object I created:
GlobalNameSpace.ExtensionFunction = function(oParam1, oParam2, oParam3)
{
/// <summary>All parameters are optional</summary>
return; // For instances when it is not being overwritten, simply return
}
//In the Code for the user control:
GlobalNameSpace.UploadControl.UploadComplete(oSender, oArgs)
{
///<summary>Handles the Upload process</summary>
// process the upload
GlobalNameSpace.ExtensionFunction(oSender, oArgs);
}
//and finally in the code to extend the functionality
GlobalNameSpace.Page.Init
{
///<summary>Initializes the page</summary>
// redefine the extension function
GlobalNameSpace.ExtensionFunction = function(oSender, oArgs)
{
GlobalNameSpace.Page.Function(oSender, oArgs);
}
}
This allows me to extend anything I need it to without polluting my objects, and having something generic already existing to call on to make my changes. This solution solves my problem of needing a onCreate function for the elements I create to represent my uploaded items to trigger the header displaying the number of files. Very useful

Categories