Cannot injectScript in Custom Variable Template - google-tag-manager

I'm trying to injectScript via Custom Variable Template (not tag).
Here is simplified code:
const log = require('logToConsole');
const setTimeout = require('callLater');
const setInWindow = require('setInWindow');
const copyFromWindow = require('copyFromWindow');
const copyFromDataLayer = require('copyFromDataLayer');
const injectScript = require('injectScript');
const pixelSend = function(eventType, eventParams, tries) {
// logic
log('success')
};
log('event - ', copyFromDataLayer('event'));
if (copyFromDataLayer('event') === 'gtm.js') {
injectScript('https://vk.com/js/api/openapi.js', // this one should create **"VK"** object in global scope, used to actually send the events
pixelSend(),
data.gtmOnFailure);
}
return true;
Unfortunately openapi.js never gets injected (checking in network tab) and thus VK object never gets created and I cannot use it.
If I just run in console:
var head = document.getElementsByTagName('head')[0]
var js = document.createElement('script');
js.src = 'https://vk.com/js/api/openapi.js';
head.appendChild(js);
It gets injected and VK object becomes available.
What am I doing wrong?
Just in case:
queryPermission('inject_script', 'https://vk.com/js/api/openapi.js') = true

I tried this, and there were just a few minor bugs - line 11 is missing a semicolon, and you did not mention if you allowed access to read the "event" key from the datalayer.
After that was fixed, the script worked as expected.
Obviously it will only work on the page view trigger (since this is the only case when event equals gtm.js. I probably would move the condition from the tag to the trigger).
Instead of "return true" you should end this will a call to data.gtmOnSuccess(), else you might have trouble using this tag in a tag sequence.
If in the template UI you hit the "run code" switch you will actually get information on all error in your code (alas one at a time, since execution stops at the first error). You can also write tests with mock input, for templates that require settings via input fields.

Related

How to send a POST request from GTM custom tag template?

I'm developing a simple custom tag template for Google Tag Manager. It's supposed to bind to some events and send event data to our servers as JSON in the body of a POST request.
The sandboxed GTM Javascript runtime provides the sendPixel() API. However, that only provides GET requests.
How one sends a POST request from within this sandboxed runtime?
You can use a combination of the injectScript and copyFromWindow APIs found here Custom Template APIs.
Basically, the workflow goes like this.
Build a simple script that contains a function attached to the window object that sends a normal XHR post request. The script I made and use can be found here: https://storage.googleapis.com/common-scripts/basicMethods.js
Upload that script somewhere publically accessible so you can import it into your template.
Use the injectScript API to add the script to your custom template.
The injectScript API wants you to provide an onSuccess callback function. Within that function, use the copyWindow api to grab the post request function you created in your script and save it as a variable.
You can now use that variable to send a post request the same way you would use a normal JS function.
The script I included above also includes JSON encode and Base64 encode functions which you can use the same way via the copyWindow api.
I hope that helps. If you need some specific code examples for parts I can help.
According to #Ian Mitchell answer - I've made similar solution.
This is the basic code pattern that can be used inside GTM template code section in such as scenario:
const injectScript = require('injectScript');
const callInWindow = require('callInWindow');
const log = require('logToConsole');
const queryPermission = require('queryPermission');
const postScriptUrl = 'https://myPostScriptUrl'; //provide your script url
const endpoint = 'https://myEndpoint'; //provide your endpoint url
//provide your data; data object contains all properties from fields tab of the GTM template
const data = {
sessionId: data.sessionId,
name: data.name,
description: data.description
};
//add appropriate permission to inject script from 'https://myPostScriptUrl' url in GTM template's privileges tab
if (queryPermission('inject_script', postScriptUrl)) {
injectScript(postScriptUrl, onSuccess, data.gtmOnFailure, postScriptUrl);
} else {
log('postScriptUrl: Script load failed due to permissions mismatch.');
data.gtmOnFailure();
}
function onSuccess() {
//add appropriate permission to call `sendData` variable in GTM template's privileges tab
callInWindow('sendData', gtmData, endpoint);
data.gtmOnSuccess();
}
It's important to remember to add all necessary privillages inside GTM template. Appropriate permissions will show automatically in privillages tab after use pertinent options inside code section.
Your script at 'https://myPostScriptUrl' may looks like this:
function sendData(data, endpoint) {
var xhr = new XMLHttpRequest();
var stringifiedData = JSON.stringify(data);
xhr.open('POST', endpoint);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(stringifiedData);
xhr.onload = function () {
if (xhr.status.toString()[0] !== '2') {
console.error(xhr.status + '> ' + xhr.statusText);
}
};
}
It is not strictly necessary to load an external script. While still a workaround, you can also pass a fetch reference into the tag through a "JavaScript Variable" type variable:
Create a GTM variable of type "JavaScript Variable" with the content "fetch", thus referencing "window.fetch"
Add a text field to your Custom Tag, e. g. named "js.fetchReference".
Use data.fetchReference in your Custom Tag's like you normally would use window.fetch
Make sure the tag instance actually references the variable created in step 2 with {{js.fetchReference}}
I jotted this down with screenshots at https://hume.dev/articles/post-request-custom-template/

AFRAME: Event on completion of dynamic adding of component

My use-case is as follows:
In a loop, entities are being created and components are being set up. This is via a json-object that is being passed to the function. The question I have is how best to get an event that the whole set of entities and their components are being initialised. The code is something like this
var parent = document.querySelector('#parent');
var ent = document.createElement('a-entity');
parent.appendChild(ent);
for(var i =0; i = components.length; i++) {
var arr = components[i];
var cl = arr[0]; // class name
var attr = arr[2]; // component name
var attrV = arr[3]; // component data
ent.setAttribute('class', cl);
AFRAME.utils.entity.setComponentProperty(ent, attr, attrV);
//ent.setAttribute(attr, attrV); tried with this too
}
console.log('loop completed')
The loop completed gets logged before the completion of the loading of some of the components. I would like to have some sort of a call back to know that all the components have been completed loaded.
There seems to be an event componentinitialized but it sends a return for only 1 component. My real requirement (not reflected in above code) is that an entity can have multiple components added.
To use the above, I may have to set this event for every component and keep track of whether it has been completed or not. Just wondering if there is a more elegant way to do it. Thanks
Entities emit the "loaded" event. It should be easier than listening for each component initialization within the entity.
Try out:
entity.addEventListener("loaded", (e) => {
console.log(e)
})
like i did here.

Meteor - Reactive Objects/Classes

TLDR
I like to really focus on keeping business logic away from the view model / controller. I find this sometimes rather hard in Meteor. Maybe I'm missing the point but I am after one of two things really:
1) A really good document explaining at a really low level how reactive values are being used.
2) A package that somehow manages an object so that if any of the setters are altered, they notify all of the get functions that would change as a result.
Unfortunately I've not seen either.
My Example
I have a fair bit ob business logic sitting behind a dialog used to document a consultation. I might have an event that sets a change of state.
I'd like to do something like this in the event:
const cc = new ConsultationEditor();
cc.setChiefComplaint(event.target.value);
console.log(cc.data());
ConsultationDict.set("consEdit", cc.data() );
When the user has updated this value, I'd then like to show a number of fields, based on the change. For this I have a helper with the following:
fields: function(){
console.log("trying to get fields");
const obj = ConsultationDict.get('consEdit');
cc = new ConsultationEditor(obj);
return cc.getFields();
}
But unfortunately this does not work for me.
What is your ConsultationDict?
The way you describe it, you want it to be a ReactiveDict as in the official ReactiveDict package.
https://atmospherejs.com/meteor/reactive-dict
Check this tutorial for examples:
https://themeteorchef.com/snippets/reactive-dict-reactive-vars-and-session-variables/
If you really need more fine tuning in your reactivity, you can also set a dependency tracker tracker = new Tracker.Dependency, and then refer to it wherever you change a variable with tracker.changed() and where the data needs to be notified with tracker.depend() like this:
var favoriteFood = "apples";
var favoriteFoodDep = new Tracker.Dependency;
var getFavoriteFood = function () {
favoriteFoodDep.depend();
return favoriteFood;
};
var setFavoriteFood = function (newValue) {
favoriteFood = newValue;
favoriteFoodDep.changed();
};
getFavoriteFood();
See the full Tracker doc here:
https://github.com/meteor/meteor/wiki/Tracker-Manual
I also found this gist to be useful to build reactive objects:
https://gist.github.com/richsilv/7d66269aab3552449a4c
and for a ViewModel type of behavior, check out
https://viewmodel.meteor.com/
I hope this helps.

How to create a template helper after a template has been rendered in Meteor?

Template.prices.rendered = function() {
OrderFormContent = new Meteor.Collection(null);
var orderSubmission = function() {
//code that inserts stuff into the OrderFormContent collection
//the key **sqft** is assigned the value of **4000** };
orderSubmission();
};
Template.prices.helpers({
sqft: function() {
return OrderFormContent.findOne().sqft;
}
});
The code above doesn't load. Meteor tries to create the helper of {{sqft}} but can't because OrderFormContent does not get defined until after the page renders. It appears that Meteor tries to define the helper before the page is even rendered.
But I need to define this helper. And I need to have it defined only after the template is rendered (not created).
I cannot just nest Template.prices.helpers inside Template.prices.rendered.
Clarification:
If I comment out the Template.prices.helpers code the page will load. If I then run OrderFormContent.findOne().sqft manually in the console a value of 4000 is returned.
When I un-comment the Template.prices.helpers code the page fails to load and I get a Exception from Deps recompute function: ReferenceError: OrderFormContent is not defined error.
1) Defining global variables inside a function is against good practices of Javascript, and is invalid in strict mode (and thus will be invalid in the future when strict mode becomes a standard).
2) You can easily achieve your goal without defining helper after rendering. In fact, the error is not thrown when the helper is created, but when it's called. To fix this problem it's enough to include a simple check.
var OrderFormContent = null;
var orderFormContentDep = new Deps.Dependency();
Template.prices.rendered = function() {
OrderFormContent = new Meteor.Collection(null);
...
orderFormContentDep.changed();
};
Template.prices.helpers({
sqft: function() {
orderFormContentDep.depend();
if(!OrderFormContent) return null;
var item = OrderFormContent.findOne();
if(!item) return null;
return item.sqft;
});
});
When I got that error I moved the template helper to the client js and it went away. Only that didn't work for my purposes because it executed too often. So, I put it into an Iron Router route method to be rendered.

google earth plugin does not respond to appendchild and removechild

I am dynamically adding kml files to google earth. For this, I have written javascript functions to add a kml and to remove a kml. These functions work fine for the first time for a kml. But if called again they do not respond. This happens for each kml that I try to add or remove. If I keep the page on browser for some time, then these functions again respond once and again become unresponsive.
function add(id, fileurl)
{
var link = ge.createLink('');
var href= fileurl;
link.setHref(href);
var networkLink = ge.createNetworkLink("'" + id + "'");
networkLink.set(link, true, true);
ge.getFeatures().appendChild(networkLink);
}
function remove(id)
{
for(var i=0; i<ge.getFeatures().getChildNodes().getLength(); i++)
{
if(ge.getFeatures().getChildNodes().item(i).getId() == id || ge.getFeatures().getChildNodes().item(i).getId() == "'" + id + "'")
{
id = ge.getFeatures().getChildNodes().item(i).getId();
ge.getFeatures().removeChild(ge.getElementById(id));
break;
}
}
The issue is that you can't re-add a feature using an ID that you have already used until all references to it have been released. This is usually done by the internal garbage collector - but you can also force it by calling release() on the object you are deleting. This ...
Permanently deletes an object, allowing its ID to be reused.
Attempting to access the object once it is released will result in an
error.
Also when an object is created with the API the object does not have a base address. In this case, the object can be returned by passing only its ID to getElementById(). This can then be used to remove the feature.
e.g.
function remove(id) {
ge.getElementById(id).release();
}
Really though I would look to avoid using IDs altogether and would simply keep a variable that points to the feature, then use that to remove. e.g.
function add(fileurl) {
var link = ge.createLink(''); //no id
link.setHref(fileurl);
var networkLink = ge.createNetworkLink(''); //no id
networkLink.set(link, true, true);
ge.getFeatures().appendChild(networkLink);
return networkLink;
}
var link1 = add("http://yoursite.com/file.kml");
var link2 = add("http://yoursite.com/file2.kml"); // etc...
// then to remove, simply...
link1.release();
link2.release();
OK. So I figured out that if you remove an object from GE, and then try to add another object with the same id, GE complains and won't create the object - unless some time (approx. 30 seconds in my case) has passed. This time actually is required by JavaScript to garbage collect the object.
Setting the object to null doesn't give immediate result but may help Garbage Collector.
Also release() method offered by GE does not help.

Resources