Using debounce in rxjs client side webapp - javascript

I am moving into functional reactive code starting with rxjs and have some questions about how to best import it as I am getting mixed results.
Currently, I have a simple implementation where I check a web service for if a username already exists.
Here is the code that works perfectly:
$(window).load(function() {
// setup an observable
submission = Rx.Observable.fromEvent($("#submitbutton"), "click").map(
event => {
return "justsomeusername";
}
);
submission
.concatMap(username => {
return Rx.Observable.fromPromise(
$.get(
"{{ site.serveraddress }}/checkuser?&username=" + username
)
).catch(() => Rx.Observable.empty());
})
.subscribe(rval => {
console.log(rval);
});
});
This all works great BUT when I try to add a debounce like submission.debounce(500).concatMap((username) =>
Rx.js:8512 Uncaught TypeError: this.durationSelector.call is not a function
at DebounceSubscriber._next (Rx.js:8512)
at DebounceSubscriber.Subscriber.next (Rx.js:424)
at MapSubscriber._next (Rx.js:5715)
at MapSubscriber.Subscriber.next (Rx.js:424)
at HTMLButtonElement.handler (Rx.js:3913)
at HTMLButtonElement.dispatch (jquery.min.js:3)
at HTMLButtonElement.r.handle (jquery.min.js:3)
That is what appears in the console.
I think that the way I am importing or referencing the library is the cause, this is where there is some confusion.
I am importing like this at the top of an HTML page:
<script src="https://unpkg.com/#reactivex/rxjs#5.5.6/dist/global/Rx.js"></script>
If I try to import any other reference the Rx object seems not to exist? Does this have to do with the filename maybe becoming the object namespace?
For example, I downloaded all the latest rxjs release and in the dist see rx.all.js but if I import locally one of the latest dists like:
<script src="/myapp/javascript/rx.all.js"></script>
I get Rx undefined. What is the nuance occurring between these two different ways of reference.
What is the surefire way to import rxjs for client-side use? Is this the likely cause of the debounce not working (not having the full lib)?
Thanks!

You should use debounceTime for this, not debounce. debounce takes a function parameter which must determine dynamically the debounce time, whereas debounceTime can be used exactly as you state:
$(window).load(function() {
// setup an observable
submission = Rx.Observable.fromEvent($("#submitbutton"), "click").map(
event => {
return "justsomeusername";
}
);
submission
.debounceTime(500)
.concatMap(username => {
return Rx.Observable.fromPromise(
$.get(
"{{ site.serveraddress }}/checkuser?&username=" + username
)
).catch(() => Rx.Observable.empty());
})
.subscribe(rval => {
console.log(rval);
});
});
If you wanted to use debounce, you'd do something like this:
submission
.debounce((val) => {
// Add whatever logic for determining the right
// debounce time for the value entered by the
// user, in this case, simply returning a hard
// coded 500 which will do exactly the same as
// debounceTime(500)
return 500;
})
.concatMap(username => {
// ... snip ...
});
});
It doesn't look like there is any problem with how you are importing RxJS - although I would advise if you're just starting with RxJS to go to v6 as it changes how a lot of things are done with operators like debounce and concatMap - it's a pain to change it all later so better to make the change when you're just starting!

Related

"process is not defined" when used in a function (Vue/Quasar)

The following snippet represents a Pinia store in my Vue 3 / Quasar 2 application. This store uses the environment variable VUE_APP_BACKEND_API_URL which shall be read from either the window object or process.env.
However I don't understand why the first variant is wokring but the second is not. Using the getEnv function always results in a Uncaught (in promise) ReferenceError: process is not defined error.
import { defineStore } from 'pinia';
function getEnv(name) {
return window?.appConfig?.[name] || process.env[name];
}
// 1. this is working
const backendApiUrl = window?.appConfig?.VUE_APP_BACKEND_API_URL || process.env.VUE_APP_BACKEND_API_URL;
// 2. this is NOT working
const backendApiUrl = getEnv('VUE_APP_BACKEND_API_URL');
export const useAppConfigStore = defineStore('appConfig', {
state: () => ({
authorizationUrl: new URL(
'/oauth2/authorization/keycloak',
backendApiUrl,
).toString(),
logoutUrl: new URL('/logout', backendApiUrl).toString(),
backendApiUrl: new URL(backendApiUrl).toString(),
}),
});
NodeJS-specific stuff like process doesn't exist in the browser environments. Both Webpack and Vite implementations work by replacing process.env.XYZ expressions with their values on build time. So, just process.env, or process.env[name] will not be replaced, which will lead to the errors you are experiencing. See the caveats section and related Webpack/Vite docs and resources. So, unfortunately, the only easy way seems to be the first long and repetitive way you've tried(const backendApiUrl = window?.appConfig?.VUE_APP_BACKEND_API_URL || process.env.VUE_APP_BACKEND_API_URL;). You can try embedding this logic in a single object, then use the function to access it.
const config = {
VUE_APP_BACKEND_API_URL: window?.appConfig?.VUE_APP_BACKEND_API_URL || process.env.VUE_APP_BACKEND_API_URL
}
export function getEnv(name) {
return config[name];
}
This way it will be longer and more repetitive to define it the first time, but at least you will be able to use it easily through the code base.
This is late, but it might help someone, I was able to resolve this by adding below to my quasar.conf.js
build: {
vueRouterMode: 'hash', // available values: 'hash', 'history'
env: {
API_ENDPOINT: process.env.API_ENDPOINT ? process.env.API_ENDPOINT : 'http://stg.....com',
API_ENDPOINT_PORT: process.env.API_ENDPOINT_PORT ? process.env.API_ENDPOINT_PORT : '0000',
...env
},
}
For more information ge here: https://github.com/quasarframework/quasar/discussions/9967

Blazor enable dynamic root components / Error: Dynamic root components have not been enabled in this application

I want render a Blazor component from javascript.
See https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-rc-1/ "Render Blazor components from JavaScript"
I have a HTML file:
<script src="/_framework/blazor.server.js"></script>
<div id="counter"></div>
<script>
async function ready() {
let containerElement = document.getElementById('counter');
await Blazor.rootComponents.add(containerElement, 'counter', { incrementAmount: 10 });
}
document.addEventListener("DOMContentLoaded", ready);
</script>
And do
builder.Services.AddServerSideBlazor(options =>
{
options.RootComponents.RegisterForJavaScript<Counter>("counter");
});
Error message (JavaScript):
test.html:14 Uncaught (in promise) Error: Dynamic root components have not been enabled in this application.
at E (blazor.server.js:1)
at Object.add (blazor.server.js:1)
at HTMLDocument.ready (test.html:8)
How can i enable dynamic root components?
The error is happening because of the delay between the document loading and Blazor being "ready" to process your request to add a component.
There doesn't appear to be any official documented solution for this, but something like this is possible
Change Blazor to manual start:
index.html
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
<script src="js/script.js" defer></script>
Start Blazor, then try to add components - repeat until success
script.js
document.addEventListener("DOMContentLoaded", startBlazor)
function startBlazor() {
Blazor
.start()
.then(() => {
requestAnimationFrame(AddBlazorComponents)
})
.catch((error) => console.error(error));
}
let attempts = 0
const maxAttempts = 120 // arbitrary choice
function AddBlazorComponents() {
if (attempts > maxAttempts) {
// give up after trying too many times
console.error("Could not load Blazor components.")
return
}
const containerElement = document.querySelector('#app')
Blazor.rootComponents.add(containerElement, 'app', {})
.catch(reason => {
if (reason.message === "Dynamic root components have not been enabled in this application.")
requestAnimationFrame(AddBlazorComponents)
else
console.error(reason)
})
attempts++
}
Better solution?
You could, possibly, add a JSInterop call to your .NET code to facilitate this, after the application starts - but it would be different for the different hosting models : Blazor Server / Blazor WebAssembly.
Even better might be to make a PR to the aspnetcore repo with a method of pre-registering components for the framework to load when it is ready.
Update 29 sep 2021
It turns out there is some currently undocumented functionality that helps here. I have copied my comment from #Swimburger 's github issue below:
The new JavaScript Initializer afterStarted is called (potentially) too soon for Dynamic components, but it turns out you can pass an initializer to be called for a dynamic component - but it is not well documented or intuitive imho.
To make this work, I changed the app startup like this (adding javaScriptInitializer: "loadApp") in program.cs:
builder.RootComponents.RegisterForJavaScript<App>(identifier: "app", javaScriptInitializer: "loadApp");
Then, and this is where it was non-intuitive, I added an export to my module (like afterStarted) called loadApp - but it wasn't called.
It turned out that loadApp is only found if I add it to the window object, so I added this to index.html
<script src="_framework/blazor.webassembly.js"></script>
<script>
window.loadApp = function (component,params) {
let containerElement = document.querySelector('#app')
window.Blazor.rootComponents.add(containerElement, component, params)
}
</script>
This feels like I am missing something in how to export my custom initializer OR it really does need to be added to window directly, which seems odd...

Not counting DOM elements in React site with Cypress?

I can't count the number of DOM elements on a site written in React.
/// <reference types="cypress" />
context('Checking all components', () => {
beforeEach(() => {
cy.visit('https://news-israel.com');
});
it('Checking posts', () => {
cy.get('.posts-wrapper').find('a').should('exist');
cy.get('.posts-wrapper').find('a').its('length').should('be.gte', 100);
});
});
In this case, it doesn't find the "a" tags because React rendering them asynchronously and dynamically.
The "post-wrapper" class finds, followed by an exception:
The following error originated from your application code, not from Cypress.
Cannot read property 'substr' of undefined
When Cypress detects uncaught errors originating from your application it will automatically fail the current test.
How to correctly count the number of elements in this case, so that you can "wait for the elements"?
The site I'm testing is in production - https://news-israel.com
The error is coming from the app itself, and ultimately should be fixed in the app source.
But see this note in the log
This behavior is configurable, and you can choose to turn this off by listening to the uncaught:exception event.
This links to an event handler you can use to debug. Add this to the top of the test to suppress the test failing when the error occurs.
Cypress.on('uncaught:exception', (err, runnable) => {
// returning false here prevents Cypress from
// failing the test
return false
})
Now the test works, provide you use the correct class posts-wrapper not post-wrapper.
If you are able to fix the source, the error comes from the react-typed library, which is used in BreakingNews.js at line 75
<Typed
strings={posts}
typeSpeed={15}
backSpeed={10}
backDelay={5000}
loop
/>
the posts variable is initially undefined, so you need a fallback value, e.g strings={posts || []}
To globally Handle Uncaught exceptions, Go to cypress/support/index.js and write:
Cypress.on('uncaught:exception', (err, runnable) => {
return false
})
Now to count the number of elements you do it via each() or by using Cypress.$
Using each():
cy.get('div.post-title').each((ele, list) => {}).then((list) => {
cy.log(list.length)
})
Using Cypress.$
cy.get('div').find('.post-title').then(ele => {
cy.log(Cypress.$(ele).length)
})
OR, As suggested by #Hiram
cy.get('div.post-title').then(ele => {
cy.log(Cypress.$(ele).length)
})

How to implement an handler lookup for routerjs

I'm trying to use routerjs as a standalone library but can't figure out how to implement the handler lookup as it's said in the documentation, and couldn't find any example.
What I've got so far:
var router = new Router['default']()
router.map(function(match) {
match('/').to('index')
match('/chat').to('chat')
})
I've tried using callbacks in the routes like this, but every callback are called not matter what url I am on so I'm not sure how to use it:
router.map(function(match) {
match('/').to('index', function() {
console.log('always called')
})
match('/chat').to('chat', function() {
console.log('always called as well')
})
})
Has anybody successfully used it?

Meteor: Template is displayed although collection is not loaded yet

Hi fellow Meteor friends!
Please note: I am using Tom's router!
So I'm trying to only display my template when the mongo collection is ready but for some reason it does not work! :(
I first followed this post: LINK
So I have my publish functions in the server.js and I subscribe to these functions inside my router, so no Deps.autorun() involved here (btw: is this the right approach? Deps.autorun() did not work for me properly):
So I have something like:
'/myroute': function(bar) {
Meteor.subscribe("myCollection", bar, function() {
Session.set('stuffLoaded', true);
});
return 'stuffPage';
}
In the template, where the data loaded from "myCollection" is displayed, I will have something like this:
<template name="stuffPage">
{{#if stuffLoaded}}
<!-- Show the stuff from the collection -->
{{else}}
<p>loading!</p>
{{/if}}
</template>
For some reason "loading!" is never displayed.
Also, for a couple of milliseconds, the "old data" from the last time the same template was displayed (but with another "bar" value provided to the publish function --> different data) is displayed.
This of course is not good at all because for a couple of ms the user can see the old data and suddenly the new data appears.
To avoid this "flash" I want to display "loading!" until the new data is loaded but again: this does not work for me! :-(
What am I doing wrong?
Thx in advance for your help!
EDIT:
Ok so the problem with the answer in the first post provided by #user728291 is the following:
For some reason the router stuff get's called AFTER the Deps.autorun() ... what is wrong here? :( (please note: eventsLoaded == stuffLoaded.)
Where do you guys put your Deps.autorun() for the subscriptions or in other words: What's your code mockup for this?
I actually really think that my code mockup is just plain wrong. So how do you make different subscriptions based on the route (or in other words: based on the template which is currently shown)?
AND: Where do you put the Deps.autorun()? Inside the router.add() function? Or just inside of (Meteor.isClient)?
I think #user728291's answer is pretty spot on, I'd just add that Meteor.subscribe returns a handle that you can use to check readiness:
Keep a reference to the handle
Deps.autorun(function() {
stuffHandle = Meteor.subscribe(Session.get('bar'));
});
Then check it in your template:
{{#if stuffHandle.ready}}
...
{{/if}}
Template.barTemplate.helpers({stuffHandle: stuffHandle});
And control it via the session:
'/myroute': function(bar) {
Session.set('bar', bar);
return 'barTemplate';
}
Better to put the subscription in a Deps.autorun and use Session variable to pass arguments from the router. Also, make sure you are setting stuffLoaded to false before the subscribe runs. Otherwise it just keeps its old value.
'/myroute': function(bar) {
if ( ! Session.equals( "bar", bar ) ) {
Session.set( "stuffLoaded", false); //subscription needs to be run
Session.set( "bar", bar ); // this change will trigger Dep.autorun
}
return 'stuffPage';
}
Deps.autorun ( function (){
Meteor.subscribe("myCollection", Session.get( "bar" ), function() {
Session.set("stuffLoaded", true);
});
});
You might need some initial default values for the Session variables if you are not getting what you want on the first time the page loads.
First off, you may be missing the actual function name for the callback as demonstrated in this post.
Meteor.subscribe("myCollection", bar, function onComplete() {
Session.set('stuffLoaded', true);
});
Which seems to be great practice. I don't usually miss a beat using this method.
Secondly, I'm not sure subscriptions inside routes work well? I'd rather do the following:
'/myroute': function(bar) {
Session.set("myCollectionParam", bar)
return 'stuffPage';
}
So then the subsciption finally looks like this:
Meteor.subscribe("myCollection", Session.get("myCollectionParam"), function onComplete() {
Session.set('stuffLoaded', true);
});
OR (not sure which works correctly for you, depending on your publish function):
Meteor.subscribe("myCollection", {bar: Session.get("myCollectionParam")}, function onComplete() {
Session.set('stuffLoaded', true);
});
Good luck!
EDIT
Just mentioning something about the publish function:
While Session.get("myCollectionParam") could return null, you can ensure the behaviour a bit more by using the following publish method:
Meteor.publish("myCollection", function(myCollectionParam) {
check(myCollectionParam, String);
return MyCollection.find({_id: myCollectionParam});
});

Categories