Adding attributes to search hits after query and dynamically re-render in InstantSearch.js - javascript

I am setting up InstantSearch icw Algolia for products of a webshop with the plain JavaScript implementation.
I am able to follow all the documentation, but I am running into the problem that we have prices specific to customer groups, and things like live stock information (need to do another API call for that).
These attributes I would like to ideally load after getting search results, from our own back-end.
I thought it would simply be a matter of manipulating the search results after receiving them and re-rendering only the front-end (without calling the Algolia search API again for new results).
This is a bit tricky. The transformItems functionality is possible, but I want to already display the results and load the other data into the hit templates after, not before displaying the hit results.
So I end up with a custom widget, and I can access and manipulate the results there, but here the problem is that I don’t know how to reflect these changes into the rendered templates.
The code of my widget (trying to set each stock number to 9) is as follows:
{
render: function(data) {
const hits = data.results.hits;
hits.forEach(hit => {
hit.stock = 9
});
}
}
The data is changed, but the generated html from the templates does not reflect any changes to the hit objects.
So how could I trigger a re-render after altering the hits data, without triggering a new search query?
I could not find a function to do this anywhere in the documentation.
Thanks!

There is the transformItems function in the hits widget that allows you to transform, remove or reorder items as you wish.
It is called before items displaying.
If I use your example, it would be something like this :
transformItems(items) {
return items.map(item => ({
...item,
stock: 9,
}));
}
Of course you can add you API call somewhere in that.
The documentation for the vanilla JS library :
https://www.algolia.com/doc/api-reference/widgets/hits/js/#widget-param-transformitems

Related

What's the right approach for multi-level lazy loaded data for React Table?

I have been working with React Query and React Table recently to achieve a multi-level table structure with expandable rows, where each row should do lazy loading when clicked on. It's for product group hierarchy, which looks like this:
unit-1 (clicking fetches category data lazily)
category-1 (clicking fetches product data lazily)
product-1
product-2
category-2
product-3
unit-2
category-3
product-4
product-5
I have two options for the solution:
Sub components lazy load example for React Table (https://react-table.tanstack.com/docs/examples/sub-components-lazy)
This focuses on only one level of depth, so I am not sure how to make it multi-level due to the fact that I am still not very familiar with the React Table API and it seems like the example was not created with that purpose. I tried making it multi-level but I had difficulty in passing some row properties around like (.isExpanded, .id, etc)
A post about manipulating the data by each row click so that rest is handled automatically by the table (https://nafeu.medium.com/lazy-loading-expandable-rows-with-react-table-cd2fc86b0630 - GitHub ex: https://github.com/nafeu/react-query-table-sandbox/blob/main/src/ReactTableExpanding.jsx)
This seems to be a more declarative way to handle things. (We fetch new data, merge it with the existing data and the rest is handled) but I have some concerns like if updating the whole table data causes unnecessary renders. Another thing is since we don't render a sub-component here as in the first one, the API endpoint should be arranged to work with a click event. It's not like having a sub-component that is responsible for fetching its own data when it's mounted.
As a note for the table example above, I was planning to use a single API endpoint that takes params like:
?level=root&id=0 (initial units list data for the table on page load)
?level=unit&id=2 (returns categories of unit-2)
...
But this logic doesn't seem enough for supporting fetching on row click so I guess I'll need at least two endpoints if I follow the second approach.
Thanks for reading, and any ideas are welcome!
Have a nice day.
As an update, I could make things work mostly with the second option above. As a note on the API endpoints, I could use the same API endpoint and the same hook twice with different parameters. (this part is a bit project specific of course but maybe it gives an idea to the ones having similar scenarios)
To get the initial data for the table, I give hardcoded parameters to the hook, so that the table is rendered with the initialData on page load.
const { data: initialData, isLoading } = useGetLevels(
{
level: 'root',
id: [0],
...
}
);
In the second one, I use enabled: false, and when a row is clicked on I call refetch(), so additional data is fetched and merged with the initial data to update the table and the row is expanded accordingly:
const {
data: partialData,
refetch,
} = useGetLevels(
{
level,
id,
...
},
false
);
and the useGetLevels look like this:
function useGetLevels(
filterParams,
enabled
) {
return useQuery(
['levels', filterParams],
() => fetchLevels(filterParams),
{
enabled,
}
);
}

How to add new filters to an embedded Looker dashboard

I’m using JavaScript events on an embedded Looker report and attempting to update the filters on it. It seems reasonable that the dashboard:filters:update method would allow me to update a dashboard with new filters but the documentation explicitly mentions that this cannot be done, though it doesn’t not mention how I actually can add new filters to a dashboard.
Do I need to run the dashboard:load event with additional_filters passed as part of the payload object? Or how can I add filters to a dashboard that doesn’t already have any filters applied? Are there any examples of this?
My current code (triggered on button click) looks like this but the filters are not applied:
const handleButtonClick = () => {
const lookerIframe = document?.querySelector('iframe')?.contentWindow;
const eventPayload = {
type: "dashboard:load",
id: "looker_dashboard_id",
dashboard_filters: {
"Company Name": "XYZ Corp"
},
};
lookerIframe.postMessage(
JSON.stringify(eventPayload),
lookerDashboardUrl,
);
}
Is there a different event I should be triggering or a different property I can pass?
In this situation, the key here is the definition of "add"ing a filter. For my purposes, the Company filter already existed on the dashboard it just hadn't been applied in the UI with any value. Even though it had no value, this didn't mean I had to "add" it, running this event was still valid because it was simply an update.
The documentation means that if you need to add an entirely new filter to the dashboard then you'll need to do this through the Looker dashboard configuration.

Observable fires only once

I decided to pick up this RxJS tutorial over the weekend to learn about reactive programming. The goal is to set up a simple page that generates an interactive list of users from the Github users API using Observables.
The list displayed is a subset of the total number of users retrieved (in my case 3 out of 30). The list needs to be refreshable (show a new set of users), and you need to be able to remove entries from it by clicking the 'remove' button on each respective entry.
I've set up a chain of Observables to define the behavior of the page. Some act as events to trigger processing, and some publish processed results for use in the app. This chain should dynamically cause my list to be updated. Currently, the default flow is like this:
Startup!
Suggestions refresh triggered! (this is where the data is retrieved)
30 new suggestions available!
List update triggered! (this is where I choose 3 users to display)
List updated! (at this point, the list is showing on the page)
A list update is triggered on startup, by refreshing the list and by removing something from the list. However, when I refresh the list, this happens:
Refresh button clicked!
Suggestions refresh triggered!
30 new suggestions available!
As you can see, the trigger to update the list of users is not set off. As I understand it, by emitting some value at the start of a stream, the rest of the stream should be executed consequently. However, this only seems to be happening the first time I run through the chain. What am I missing?
You can find a running version of my project here.
I think the issue is in the way userStream$ Observable is created.
Filtering users not closed and then taking the first 3 is something that can be done directly on the UserModel[] array passed into the pipe chain by displayEvents$ via filter and slice methods of Array.
If you do so, you remove the need of using the from function to create an Observable<UserModel> on which you then have to apply flatMap (which is currently better known as mergeMap) to apply finally toArray to transform it back into an Array of UserModel.
In other words you can simplify the code as in the following example, which as side effect solves the refresh problem.
this.userStream$ = this.displayEvent$.pipe(
map(users => users
.filter((user: UserModel) => !this.closedUsers.has(user))
.slice(0, this.numberOfUsers))
// flatMap((users: UserModel[]) => from(users))
// // Don't include users we've previously closed.
// , filter((user: UserModel) => !this.closedUsers.has(user))
// , take(this.numberOfUsers)
// , toArray()
, tap(() => console.log('List updated!'))
// Unless we explicitly want to recalculate the list of users, re-use the current result.
, shareReplay(1));
To be honest though I have not fully grasped why your original solution, which is a sort of long detour, does not work.

At which point does one fetch a Backbone Model on which a view depends?

I seem to often end up in a situation where I am rendering a view, but the Model on which that view depends is not yet loaded. Most often, I have just the model's ID taken from the URL, e.g. for a hypothetical market application, a user lands on the app with that URL:
http://example.org/#/products/product0
In my ProductView, I create a ProductModel and set its id, product0 and then I fetch(). I render once with placeholders, and when the fetch completes, I re-render. But I'm hoping there's a better way.
Waiting for the model to load before rendering anything feels unresponsive. Re-rendering causes flickering, and adding "loading... please wait" or spinners everywhere makes the view templates very complicated (esp. if the model fetch fails because the model doesn't exist, or the user isn't authorized to view the page).
So, what is the proper way to render a view when you don't yet have the model?
Do I need to step away
from hashtag-views and use pushState? Can the server give me a push? I'm all ears.
Loading from an already-loaded page:
I feel there's more you can do when there's already a page loaded as opposed to landing straight on the Product page.
If the app renders a link to a Product page, say by rendering a ProductOrder collection, is there something more that can be done?
<ul id="product-order-list">
<li>Ordered 5 days ago. Product 0 (see details)</li>
<li>Ordered 1 month ago. Product 1 (see details)</li>
</ul>
My natural way to handle this link-to-details-page pattern is to define a route which does something along these lines:
routes: {
'products/:productid': 'showProduct'
...
}
showProduct: function (productid) {
var model = new Product({_id: productid});
var view = new ProductView({model: model});
//just jam it in there -- for brevity
$("#main").html(view.render().el);
}
I tend to then call fetch() inside the view's initialize function, and call this.render() from an this.listenTo('change', ...) event listener. This leads to complicated render() cases, and objects appearing and disappearing from view. For instance, my view template for a Product might reserve some screen real-estate for user comments, but if and only if comments are present/enabled on the product -- and that is generally not known before the model is completely fetched from the server.
Now, where/when is it best to do the fetch?
If I load the model before the page transition, it leads to straightforward view code, but introduces delays perceptible to the user. The user would click on an item in the list, and would have to wait (without the page changing) for the model to be returned. Response times are important, and I haven't done a usability study on this, but I think users are used to see pages change immediately as soon as they click a link.
If I load the model inside the ProductView's initialize, with this.model.fetch() and listen for model events, I am forced to render twice, -- once before with empty placeholders (because otherwise you have to stare at a white page), and once after. If an error occurs during loading, then I have to wipe the view (which appears flickery/glitchy) and show some error.
Is there another option I am not seeing, perhaps involving a transitional loading page that can be reused between views? Or is good practice to always make the first call to render() display some spinners/loading indicators?
Edit: Loading via collection.fetch()
One may suggest that because the items are already part of the collection listed (the collection used to render the list of links), they could be fetched before the link is clicked, with collection.fetch(). If the collection was indeed a collection of Product, then it would be easy to render the product view.
The Collection used to generate the list may not be a ProductCollection however. It may be a ProductOrderCollection or something else that simply has a reference to a product id (or some sufficient amount of product information to render a link to it).
Fetching all Product via a collection.fetch() may also be prohibitive if the Product model is big, esp. in the off-chance that one of the product links gets clicked.
The chicken or the egg? The collection.fetch() approach also doesn't really solve the problem for users that navigate directly to a product page... in this case we still need to render a ProductView page that requires a Product model to be fetched from just an id (or whatever's in the product page URL).
Alright, so in my opinion there's a lot of ways that you can fix this. I'll list all that I've thought of and hopefully one will work with you or at the very minimum it will inspire you to find your optimal solution.
I'm not entirely opposed to T J's answer. If you just go ahead and do a collection.fetch() on all the products when the website is loading (users generally expect there to be some load time involved) then you have all of your data and you can just pass that data round like he mentioned. The only difference between what he's suggesting and what I normally do is that I usually have a reference to app in all my views. So, for example in my initialize function in app.js I'll do something like this.
initialize: function() {
var options = {app: this}
var views = {
productsView: new ProductsView(options)
};
this.collections = {
products: new Products()
}
// This session model is just a sandbox object that I use
// to store information about the user's session. I would
// typically store things like currentlySelectedProductId
// or lastViewedProduct or things like that. Then, I just
// hang it off the app for ease of access.
this.models = {
session: new Session()
}
}
Then in my productsView.js initialize function I would do this:
initialize: function(options) {
this.app = options.app;
this.views = {
headerView: new HeaderView(options),
productsListView: new ProductsListView(options),
footerView: new FooterView(options)
};
}
The subviews that I create in the initialize in productsView.js are arbitrary. I was mostly just trying to demonstrate that I continue to pass that options object to subviews of views as well.
What this does is allows every view, whether it be a top level view or deeply nested subview, every view knows about every other view, and every single view has reference to the application data.
These two code samples also introduce the concept of scoping your functionality as precise as you possibly can. Don't try to have a view that does everything. Pass functionality off to other views so that each view has one specific purpose. This will promote reuse of views as well. Especially complex modals.
Now to get back to the actual topic at hand. If you were going to go ahead and load all of the products up front where should you fetch them? Because like you said you don't want a blank page just sitting there in front of your user. So, my advice would be to trick them. Load as much of your page as you possibly can and only block the part that needs the data from loading. That way to the user the page looks like it's loading while you're actually doing work behind the scenes. If you can trick the user into thinking the page is steadily loading then they are much less likely to get impatient with the page load.
So, referencing the initialize from productsView.js, you could go ahead and let the headerView and footerView render. Then, you could do your fetch in the render of the productsListView.
Now, I know what you're thinking. Have I lost my mind. If you do a fetch in the render function then there's no way that the call will have time to return before we hit the line that actually renders the productsViewList template. Well, luckily there's a couple of ways around that. One way would be to use Promises. However, the way I typically do it is to just use the render function as its own callback. Let me show you.
render: function(everythingLoaded) {
var _this = this;
if(!!everythingLoaded) {
this.$el.html(_.template(this.template(this)));
}
else {
// load a spinner template here if you want a spinner
this.app.collection.products.fetch()
.done(function(data) {
_this.render(true);
})
.fail(function(data) {
console.warn('Error: ' + data.status);
});
}
return this;
}
Now, by structuring our render this way the actual template won't load until the data has fully loaded.
While we have a render function here I want to introduce another concept that I use every where. I call it postRender. This is a function where I execute any code that depends on DOM elements being in place once the template has finished loading. If you were just coding a plain .html page then this is code that traditionally goes in the $(document).ready(function() {});. It may be worth noting that I don't use .html files for my templates. I use embedded javascript files (.ejs). Continuing on, the postRender function is a function that I have basically added to my boiler plate code. So, any time I call render for a view in the code base, I immediately chain postRender onto it. I also use postRender as a call back for itself like I did with the render. So, essentially the previous code example would look something like this in my code base.
render: function(everythingLoaded) {
var _this = this;
if(!!everythingLoaded) {
this.$el.html(_.template(this.template(this)));
}
else {
// load a spinner template here if you want a spinner
this.app.collection.products.fetch()
.done(function(data) {
_this.render(true).postRender(true);
})
.fail(function(data) {
console.warn('Error: ' + data.status);
});
}
return this;
},
postRender: function(everythingLoaded) {
if(!!everythingLoaded) {
// do any DOM manipulation to the products list after
// it's loaded and rendered
}
else {
// put code to start spinner
}
return this;
}
By chaining these functions like this we guarantee that they'll run sequentially.
=========================================================================
So, that's one way to tackle the problem. However, you mentioned that you don't want to necessarily load all of the products up front for fear that the request could take too long.
Side Note: You should really consider taking out any information related to the products call that could cause the call to take a considerable amount of time, and make the larger pieces of information a separate request. I have a feeling that users will be more forgiving about data taking a while to load if you can get them the core information really fast and if the thumbnails related to each product takes a little longer to load it shouldn't be then end of the world. That's just my opinion.
The other way to solve this problem is if you just want to go to a specific product page then just implement the render/postRender pattern that I outlined above on the individual productView. However note that your productView.js will probably have to look something like this:
initialize: function(options) {
this.app = options.app;
this.productId = options.productId;
this.views = {
headerView: new HeaderView(options),
productsListView: new ProductsListView(options),
footerView: new FooterView(options)
};
}
render: function(everythingLoaded) {
var _this = this;
if(!!everythingLoaded) {
this.$el.html(_.template(this.template(this)));
}
else {
// load a spinner template here if you want a spinner
this.app.collection.products.get(this.productId).fetch()
.done(function(data) {
_this.render(true).postRender(true);
})
.fail(function(data) {
console.warn('Error: ' + data.status);
});
}
return this;
},
postRender: function(everythingLoaded) {
if(!!everythingLoaded) {
// do any DOM manipulation to the product after it's
// loaded and rendered
}
else {
// put code to start spinner
}
return this;
}
The only difference here is that the productId was passed along in the options object to the initialize and then that's pulled out and used in the .fetch in the render function.
=========================================================================
In conclusion, I hope this helps. I'm not sure I've answered all of your questions, but I think I made a pretty good pass at them. For the sake of this getting too long I'm going to stop here for now and let you digest this and ask any questions that you have. I imagine I'll probably have to do at least 1 update to this post to further flush it out.
You started saying:
I have a listing of items in one Collection view
So what does a collection have..? Models..!
When you do collection.fetch() you retrieve all the models.
When the user selects an item, just pass the corresponding model to the item view, something like:
this.currentView = new ItemView({
model: this.collection.find(id); // where this points to collection view
// and id is the id of clicked model
});
This way, there there won't be any delay/ improper rendering.
What if your collections end point returns huge volume of data..?
Then implement common practices like pagination, lazy loading etc.
I construct a Product model with the given ID
To me that sounds wrong. If you have a collection of products, you shouldn't be constructing such models manually.
Have the collection fetch your models before rendering the list view. This way all the problem you mentioned can be avoided.

What is the best method to manage a user list in JavaScript/jQuery?

I am building a chat room application using socket.io but I am having issue understanding the correct and simplest method to keep the chat room user list updated.
At the moment when the client joins the chat room it is sent the user list and updates the user list panel using the below code:
socket.on('names', function(channel,nicks){
for (var nick in nicks) {
$('#users').append('<li class="list-group-item" id="' + nick + '">' + nick + '</li>');
}
});
but at the moment it doesn't account for when another user joins or leaves after this point so the user list is the same for the whole duration from the moment that the client joined.
These events are triggered when another user joins or leaves:
socket.on('join', function(channel,nick){
// need to add the user to the user list, sort the user list
// alphabetically and then update the list group html
});
socket.on('part', function(channel,nick){
// need to delete the user from the user list, sort the user list
// alphabetically and then update the list group html
});
socket.on('quit', function(nick){
// need to delete the user from the user list, sort the user list
// alphabetically and then update the list group html
});
What should I be looking at to accomplish what I need here?
I am guessing that rather than relaying on the server response and building the HTML from that, I should keep a user list array on the client side, updated based on the server responses, and then re-build the HTML from that. Just need point in the right direction really ...
The simplest way to tackle this is to re-render the list whenever it changes. However, for large lists, this can be inefficient and you could save a lot of time by simply adding an ID to each element, then updating or removing them using it.
This process can become a little complex with jQuery, as you'll be managing your model inside the DOM, rather than inside your code. It's definitely possible though.
An alternative approach is to look at a library like React.
React asks you to describe what your interface looks like. Then it will be responsible for re-rendering it whenever you change the model. As a bonus, it checks against the last state of the DOM and only applies updates to bits that have changed.
var UserList = React.createClass({
getInitialState: function() {
users: []
},
componentDidMount: function() {
// set up socket listeners
// every time you want to update the user list
this.setState({ users: updatedVersionOfUsers });
},
render: function() {
var users = this.state.users;
// describe what this component should look like
return (
<ul>
{users.map(function(user) {
return (
<li className='list-group-item'>{user}</li>
);
}}
</ul>
);
}
})
Now, all you have to think about is making sure the model (this.state.users) is up to date. So long as you update it with this.setState, then React will do the rest.
You can even use React alongside jQuery. Simply find the element that you want this React component to live inside and render it there.
React.render(
<UserList />,
$('#someElement')[0]
);
There's a great talk by Ryan Florence on integrating React into an existing project, rather than re-writing it all.

Categories

Resources