userFlow with AngularJS? - javascript

I can't get UserFlow to work for our AngularJS app.
The product runs on old AngularJS (1.8) and we love the concept of UserFlow , but the typical injection and init model runs in the core JS scope which AngularJS does not have access to... so, even after following the onboarding instructions, every user that registers is appearing to UserFlow as the same {{userId}}
We believe this is happening (UserFlow is not able to receive the user ID in userflow.identify as described here) because the user ID is not known outside of the AngularJS digest. i.e. - the method was called in a place where angularJS is not taking effect, so the handlebars never get rewritten.

Got it fixed. An overview of how we fixed it is below:
Simply split UserFlow's initialization into two distinct steps:
userflow.init() - this can be directly in your index.html or otherwise injected into the <body>
userflow.identify() - this has to be done within your AngularJS controller
.
DETAILED STEPS---------------
1. init() normally, but use a build variable and don't identify yet
In index.html, at the bottom of the <body> tag, add the following script:
<!-- UserFlow -->
<script ng-if="customization.features.userflow">
!function(){var e="undefined"==typeof window?{}:window,t=e.userflow;if(!t){var r="https://js.userflow.com/";t=e.userflow={_stubbed:!0};var n=e.USERFLOWJS_QUEUE=e.USERFLOWJS_QUEUE||[],o=function(e){t[e]=function(){var t=Array.prototype.slice.call(arguments);i(),n.push([e,null,t])}},s=function(e){t[e]=function(){var t,r=Array.prototype.slice.call(arguments);i();var o=new Promise((function(e,r){t={resolve:e,reject:r}}));return n.push([e,t,r]),o}},a=function(e,r){t[e]=function(){return r}},u=!1,i=function(){if(!u){u=!0;var t=document.createElement("script");t.async=!0;var n=e.USERFLOWJS_ENV_VARS||{};"es2020"===(n.USERFLOWJS_BROWSER_TARGET||function(e){for(var t=[[/Edg\//,/Edg\/(\d+)/,80],[/OPR\//,/OPR\/(\d+)/,67],[/Chrome\//,/Chrome\/(\d+)/,80],[/Safari\//,/Version\/(\d+)/,14],[/Firefox\//,/Firefox\/(\d+)/,74]],r=0;r<t.length;r++){var n=t[r],o=n[0],s=n[1],a=n[2];if(e.match(o)){var u=e.match(new RegExp(s));if(u&&parseInt(u[1],10)>=a)return"es2020";break}}return"legacy"}(navigator.userAgent))?(t.type="module",t.src=n.USERFLOWJS_ES2020_URL||r+"es2020/userflow.js"):t.src=n.USERFLOWJS_LEGACY_URL||r+"legacy/userflow.js",t.onerror=function(){u=!1,console.error("Could not load Userflow.js")},document.head.appendChild(t)}};o("_setTargetEnv"),o("closeResourceCenter"),o("init"),o("off"),o("on"),o("prepareAudio"),o("registerCustomInput"),o("remount"),o("reset"),o("setCustomInputSelector"),o("setCustomNavigate"),o("setCustomScrollIntoView"),o("setInferenceAttributeFilter"),o("setInferenceAttributeNames"),o("setInferenceClassNameFilter"),o("setResourceCenterLauncherHidden"),o("setScrollPadding"),o("setShadowDomEnabled"),o("setPageTrackingDisabled"),o("setUrlFilter"),o("openResourceCenter"),o("toggleResourceCenter"),s("endAll"),s("endAllFlows"),s("endChecklist"),s("group"),s("identify"),s("identifyAnonymous"),s("start"),s("startFlow"),s("startWalk"),s("track"),s("updateGroup"),s("updateUser"),a("getResourceCenterState",null),a("isIdentified",!1)}}();
userflow.init('##grunt_userflow')
</script>
Since we use Grunt as a build tool (which I don't recommend, but you can replicate the same pattern with different technologies), we put the environment-specific token, ##grunt_userflow, into our build script which replaces the individual token to match the respective environment.
You'll notice here we're not calling userflow.identify() yet...
2. Execute the UserFlow identify() directly within the controller
When the user first logs in, now you need to execute the userflow.identify() function and pass in the right IDs. Personally, I like putting AngularJS-agnostic functions like this outside of the controller and then inherit them in:
const startUserFlow = function(userId, login) {
userflow.identify(userId, {
email: login
});
};
And, now calling that function from within AJS:
$scope.processCredentials($scope.username, response.data.access_token).then(function (result) {
trackEvent('signIn', $rootScope.userProfile.id);
startUserFlow($rootScope.userProfile.id, $scope.username);
3. Finally, to reinitialize your Content, use ng-click=() on any HTML you'd like
That's right - since we're scoping it in and doing this the AngularJS way, use ng-click like any other function and bind it directly. Example below.
$scope.launchUserFlowChecklist = function () {
userflow.start('[insert content ID here]');
};
I hope this helps! Cheers.

Related

Apex - Default options for widget/component type

I know I can specify init options for single widgets in "Advanced - Javascript Initialization Code" section in page builder, using the following syntax:
function (options) {
....
return options;
}
But I'd like to know if there's a way of doing the same for all components of a type (i.e. interactiveGrid) in the same application/workspace.
Maybe inside a shared js file referenced in "User Interface" section, or inside "Global" (page 0).
No idea how to do this at the moment :(
Create a dynamic action to execute Javascript on the Page Load Event in the global page (page 0).

jQuery scope with multiple functions

I have a tool that allows a user to create a dashboard and add widgets to it. Each widget is its own JS file that is loaded into the page.
To keep things the same across the board, I create a function called load which is triggered when the document is ready.
The problem is, each of the other modules that are included all have the same function called load which is causing problems.
While I can change the function name to something unique, I would like to see if its possible to keep them all the same where they are locked down to the scope of the file they are in?
/*
Module Details: Department Links - A collection of links specific to the users department or selected department
*/
$(function() {
// Define our moduleID
var moduleID = 1;
// Load our module
load(moduleID, '', false);
// Create a event for dropdown change
$('body').on('select2-selecting', '#Department_' + moduleID, function (e) {
// When the user picks a department, reload this module and fetch those department links.
load(moduleID, e.val, true);
});
});
/*
Load the module
#moduleID: required.
#departmentID: if not passed, the SP will use the viewers departmentID.
#reload: passed when changing the dropdown so we only render the new data and not the whole module
*/
function load(moduleID, departmentID, reload){
... Do other stuff here
}
I guess my question is.. With multiple functions called load in the various js files included, how can I trigger the one specific to its own file?
#binariedMe is correct.. There is nothing like file scope in JavaScript.. You can do like if you have any object which is separate in every file so you can write as follows...
`Object.load = function (){
// do stuff
}`
And you can call object.load for every module you are loading...!!
The problem may be because you would be declaring load function in each js file globally. I have created a plunker with the same setup and each load function is working isolated to other.
Please check the plunker here https://plnkr.co/edit/uZJ8FFwJMfEvPsWAFWwE?p=preview
(function(){ //stuffs of each file
// Don't put any global variable unless you want no scope isolation for the same.
})();

Exclude HTML from script tag

I'm trying to learn Handlebars.js and thought of a way to get use of it in a site im making. It's a one page site which will have two containers with three divs in each which will contain embedded Soundcloud players through their API.
When including the divs which contain the API requests in the script tag handled by Handlebars, the site behave very unreliable and shows only some of the six players. It's not really consistent but can show different players all the time. The problem seems to be in the Soundcloud javascript SDK but I don't feel to familiar to dig around in there too much.
Therefore I thought of some way to exclude the player divs (see code) so that they're loading instantly and not being Handled as javascript, but still show up beneath the artist - title in the placeholder div (which is set to contain the result of the Handlebar script).
The problem is that I can't come up with a nice way of doing this, are there any easy function (with Handlebars helpers maybe) that will help me do what I want?
<div id="placeholder"></div>
<script id="player-template" type="text/x-handlebars-template">
<div id="container1">
Artist1 - {{title1}}
<div id="player1"></div>
Artist2 - {{title2}}
<div id="player2"></div>
Artist3 - {{title3}}
<div id="player3"></div>
</div>
<div id="container2">
Artist4 - {{title4}}
<div id="player4"></div>
Artist5 - {{title5}}
<div id="player5"></div>
Artist6 - {{title6}}
<div id="player6"></div>
</div>
</script>
<script src="js/handlebars_title_script.js"></script>
One solution is of course to make one Handlebar template for each Artist - Title div and set the placeholder of each template to a div containing only Artist1 - {{title1}} but that really destroys the point of using Handlebars to minimize my HTML coding.
Anyone got any tip for me how to solve this?
Edit 1:
I found another solution by changing in my javascript (which I didn't post at first so obviously you couldn't help me with that).
$(document).ready(function() {
var hey = "heya";
SC.get("/users/artist/tracks", {limit: 1}, function(tracks){
var title_data1 = tracks[0].title;
hey = tracks[0].title;
alert(title_data1);
alert(hey)
});
//Data that will replace the handlebars expressions in our template
var playerData = {
title1 : hey,
};
document.getElementById( 'player-placeholder' ).innerHTML = playerTemplate( playerData );
});
Sorry for bad intendetion. The only problem with this code is that title1 (in the variable playerData which is the Handlebars context) gets the first value of the variable hey ("heya"). When it's alerted it pops up the real title, how can I make title1 use this value instead without nesting the variable in more javascript (since that's what causes the before mentioned error with players showing up weird)?
Note: throughout the comments this answer has changed drastically. Please view the earlier revisions if you would like to see the evolution of this answer.
After getting a hold of your JsFiddle example I was able to get it working in a way I think you wanted.
Working Demo
HTML:
<body>
<div id="wrapper">
<div id="player-placeholder"><!-- rendered template goes here --></div>
<!-- handlebars template: -->
<script id="player-template" type="text/x-handlebars-template">
{{#each tracks}}
<div class="track">
<header class="header">
<span class="artist">{{user.username}}</span> - <span class="title">{{title}}</span>
</header>
<section class="player" data-uri="{{permalink_url}}">
</section>
</div>
{{/each}}
</script>
</div>
</body>
JavaScript:
$(document).ready(function() {
/*
get your template string See:
http://api.jquery.com/id-selector/
http://api.jquery.com/html/
*/
var source = $('#player-template').html();
// compile the template into a handlebars function
var template = Handlebars.compile(source);
// initialize sound cloud api
SC.initialize({
client_id: '90fb9e15c1e26f39b63f57015ab8da0d'
});
/*
This function will be called once the HTTP transaction
started by SC.get(...) completes. Note, there's nothing
wrong with doing this as an anonymous function, I'm
simply assigning it to a variable to show that this
is a distinct function that's called later
*/
var callback = function(tracksResponse){
/*
once a response has been received, we'll use the response
to generate a new context to pass to the template function.
Note, you can use the template function in here because its
within a closure. See:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures
*/
var context = { tracks: tracksResponse };
var html = template(context);
/*
assign the rendered html to your placeholder on the page
see: http://api.jquery.com/html/
*/
$('#player-placeholder').html(html);
/*
Now that the html is rendered and on the page, its time to
setup the sound cloud players. Note the css classes I assigned
to the track/player. This line selects all of the player's and
runs the function over each. See:
http://api.jquery.com/class-selector/
http://api.jquery.com/each/
*/
$('.track .player').each(function(index, e){
var $this = $(this); // jQuery reference to the current object in 'each loop'
/*
I assigned the permalink_url of each track to an attribute called 'data-uri'
This line gets the value of that attribute. See:
http://api.jquery.com/data/#data2
*/
var permalink = $this.data('uri');
var urlParameters = '/&maxheight=100&maxwidth=300&format=json&sharing=false';
/*
finally we call the sound cloud oEmbed function feeding it the url
stored in the element, as well as the actual element.
(see the second argument of the each function: http://api.jquery.com/each/)
*/
SC.oEmbed(permalink + urlParameters, e);
});
};
// get tracks for your artist
// Note the "limit" in the object controls the number of items returned
// by sound cloud
SC.get("/users/theshins/tracks", {limit: 5}, callback);
});
What went wrong?
JavaScript is a single-threaded, asynchronous, event-driven language. That giant mouth-full means JavaScript doesn't really have a notion of threading (I'm intentionally ignoring WebWorkers). To work around that limitation, almost all IO in JavaScript is non-blocking (asynchronous).
Whenever an asynchronous IO transaction begins it immediately returns to the caller and code execution continues. Almost all IO transactions take a 'callback' or have an event that will be called when the IO transaction completes. That means the basic pattern for all IO operations follows something like this:
Create a callback function
Call IO operation, passing it the arguments it requires to complete, plus the callback
Execution returns immediately
Sometime in the future, the callback function is called
In your original example $(document).ready(function() { ... }) queues an anonymous function to fire when the document.onReady event is raised. Your original example, however, had two callbacks assigned. This isn't a problem, an in fact .ready(...) is designed to accept and queue many callbacks. However, where you went wrong is you had two separate blocks of code that called SC.get(...).
Technically if done right this wouldn't be a problem, but your first on ready callback's purpose was tasked to setup the page's HTML while your second callback tried to initialize the player controls based on html on the page. Remember these events and IO operations are asynchronous, they'll fire in whatever order. Essentially this became a timing issue, you were attempting to initialize controls on the page, and generate HTML to display on the page at the same time.
How it was fixed
To fix the timing issue you need to synchronize when you get your info, when you build your template HTML, and when you initialize your controls. There's a lot of ways to do this and many frameworks support the idea of promises to help gain control over the order that asynchronous events are fired, and their callbacks called.
I took the simple route and combined all of your SC.get calls in to one, then within it's callback I render the handlebars template and initialize the SoundCloud players.

Override DateTimeShortcut in Django Admin

I'm trying to implement the top solution here, the one that only uses javascript:
Django: how to change the choices of AdminTimeWidget
It basically uses regex to create the different time choices by overriding the time options.
The only problem I have is that my script loads before DateTimeShortcuts.js, so I get an Uncaught ReferenceError: DateTimeShortcuts is not defined. Does anyone know how I can force the DateTimeShortcuts.js file to load after my js file that references it?
If I create a second reference to DateTimeShortcuts.js, it'll work properly, but I'll have two clocks up there, only the 2nd one will modify, because it's loading after the 2nd DateTimeShortcuts.js
I'm calling my file like this, where admin_clock.js references DateTimeShortcuts.js and has the override code:
class EventAdmin(admin.ModelAdmin):
list_filter = ('film', 'partner',)
list_display = ('id', partner', 'film', 'date_time', 'venue_name', 'city')
class Media:
js = ('tiny_mce/tiny_mce.js', 'tiny_mce/textareas.js', 'admin_clock.js',)
Apologies for not commenting on the original answer, I need more points to comment there.
The way I solved this was to add the javascript directly to the extrahead block of my version of the change_form template, overriding that block in the template: https://docs.djangoproject.com/en/1.6/ref/contrib/admin/#overriding-admin-templates

initial $broadcast from service is received in controller under <head> but not under <body>

I have a directive that reads sync data from <title> tag. Then it trigger a service which $broadcast the data to all controllers.
But the controllers under <body> tag are not receiving this. However if I move ng-app attr from html to body, and move the directive with the controller from head to body. Then all the controllers will work properly.
Here is my sample code: http://jsbin.com/oBAMOs/4/edit?html,js,console,output
From the code I believe you can pretty much guess what I am trying to do. So why is this happening and is there a better way to achieve this?
navCtrl doesn't exist at the point at which you send your broadcast. You can confirm that by putting log statements at the beginning of each controller and the send. You'll see you send before navctrl is created. ("title" happens then "send" then "nav")
An easy way to resolve this is to push your $broadcast till after the browser finishes all current queued up tasks (which will include rendering the rest of the DOM, and thus the instantiation of navCtrl). You can accomplish this by placing the broadcast within a $timeout that has a delay of 0. As follows:
$timeout (function() {
$rootScope.$broadcast('processed');
},0);
and make sure to pass timeout in:
.factory('syncPageid', ['$rootScope','$timeout', function($rootScope,$timeout){
This is a by product of the single threaded nature of javascript. For a discussion on why timeout works here and the underlying issue you can check out: setTimeout with zero delay used often in web pages, why? and http://ejohn.org/blog/how-javascript-timers-work/
Instead of just
$rootScope.$broadcast('processed');
you can have
$rootScope.$broadcast('processed', "data_you_need_to_pass_around");
and catch that like
$scope.$on('processed', function (e, args){
$scope.title = args;
console.log('titleCtrl: ' + args);
});
http://jsbin.com/oBAMOs/11/edit
And then ofcourse your syncPageid factory can expose it to all other code bits that is interested in pageid. But having a factory just to facilitate passing data between places bits makes no good sense.

Categories

Resources