I have a div (ItemsContainer) that has an array of items (Item) being rendered inside of it. The div has a dynamic size, depending on the size of the screen.
As I'm mapping through the array, I'd like to be able to make a check to see if there is enough space to render the current item. If there isn't, I'd like to stop rendering the items and instead add another item that says "+(number of items not rendered in array)". See the included picture for reference.
So far, this is what my code looks like. I'm using React typescript. I haven't attempted adding the "+5" box yet, because I'm wondering if it's actually possible? My initial thought is to just have a fixed number of items be displayed and then display the + item if there are more items not rendered, but I was hoping you could do it a bit more dynamic.
const Items: FC<Props> = ({ items }) => {
return (
<ItemsContainer>
{items.map((item, index) => (
<Item key={index}>{item.name}</Item>
))}
</ItemsContainer>
);
};
Here is a partially working solution: https://codepen.io/Latcarf/pen/WNKZmBN.
The main part of the code is the following:
const Tags = ({tags}) => {
const [hiddenCount, setHiddenCount] = React.useState(0)
const shownTags = hiddenCount == 0 ? tags : tags.slice(0, -hiddenCount);
const ref = React.useRef();
React.useLayoutEffect(() => {
if (!ref.current) {
return;
}
if (ref.current.scrollWidth > ref.current.clientWidth) {
setHiddenCount(count => count + 1);
}
});
return (
<div class="container" ref={ref}>
{shownTags.map(tag => <div class="tag">{tag}</div>)}
{hiddenCount > 0 && <div class="tag">{`+${hiddenCount}`}</div>}
</div>
)
};
The idea is to keep a hiddenCount state variable that contains the number of hidden tags. It starts at 0, and then a layout effect checks whether there is space and keeps incrementing it until there is enough space. It's a bit inefficient if you have many hidden items, as it will rerender many times until it reaches the correct amount, but it should properly deal with edge cases like if adding +1 would actually be longer than displaying the last tag, or stuff like that.
In order to make it update automatically on resize, you would use some kind of useResizeObserver hook to reset hiddenCount to 0 after a resize. Somehow I couldn’t manage to import other packages in CodePen, so I just made a button that forces a rerender so that you can test it (resize the div and then click the button).
Related
I'm trying to figure out a way to build a progress bar with React. I have a forEach loop that iterates through an array of about 7,000 indexes. Each time I validate a row, I want to update a state variable with percentage completion (and render this on the page live). I've tried iterating through these indexes, and updating my state variable (hoping to update the page) in the loop but I'm realizing that will not work. I obviously can't do this with a normal variable as it will reset when the component re-renders. Can anyone give me some insight on this topic?
Thanks.
Here is a code snippet from what I'm looking at:
parsedAssets.forEach(asset => {
newAssetValidated = validateBulkUpload(asset, parsedAssets, assetList, accountLogged, jobSites);
!newAssetValidated.reject_err ? validatedAssetList.accepted.push(newAssetValidated) : validatedAssetList.rejected.push(newAssetValidated);
setStateAssets({ ...stateAssets, validatedAssetList });
});
}
So essentially, as each asset is either accepted or rejected we add it to "stateAssets", and I'm hoping to build the progress bar from the length of the combined arrays that are getting set in stateAssets. However, when the forEach loop is completed, only the last validated asset is getting set due to it not updating until the forEach loop is completed.
Personally I can't imagine such a heavy validation, that you need progress-bar, but anyway.
First solution is to separate validation itself from state update for progress-bar into separate "threads". But since JS is single threaded, you may use some tricks with setTimeout or setInterval functionality. It may be very tricky, and in general not recommended practice with React.
Another way is - to set the work into queue & process 1 item at a time.
As an example I would do it something like this:
function ComponentWithProgress({parsedAssets, setParsedAssets}) {
const [validatedAssetList, setValidatedAssetList] = useState([])
const [progress, setProgress] = useState(0)
const [toDo, setToDo] = useState([])
if(parsedAssets && parsedAssets.length>0) {
setToDo(parsedAssets)
// clear parsedAssets in parent component to: [], false, null ...
// so you put it into toDo only once
setParsedAssets([])
}
if(toDo.length > 0) {
const asset = toDo[0]
const newToDo = toDo.slice(1) // All but 0th element
const newAssetValidated = validateBulkUpload(asset);
setValidatedAssetList([ ...validatedAssetList, newAssetValidated ]);
setToDo(newToDo)
setProgress( newToDo.length / ( validatedAssetList.length + newToDo.length ) * 100 )
}
// ... Render here
// If you need only accepted
const accepted = validatedAssetList.filter(v => !v.reject_err)
}
This example maybe not work for you as is, because you didn't showed us the context, but the main idea is here.
I have an issue rendering parameters in Angular 8, I get datas from API that I need to render in divs that matchs those datas but I'm having an issue where the datas shows in every divs, here's what it looks like :
Here, "Drops","Misc","Network" are the main divs that need to render the lower-categories.
Altought, what I want is for example, to have only "Drops Aluminium" inside the main "Drops", only "VANNE" inside "Misc" and "Main" in "Network". The lower categories should only renders when they have their ids inside the main id ( see picture 2 below ).
What I have tried :
Binding the values inside the divs, since all main and lower categories have ids like so :
Here is a stackblitz example : https://stackblitz.com/edit/angular-me2ppb?file=src%2Fapp%2Fapp.component.ts
Thank you in advance for your time and help, it's much appreciated !
If I understand correctly, could you just solve it with a nested loop in the template?
<div *ngFor="let main of total_by_level | keyvalue">
{{label_name[main.key]}}
<div *ngFor="let sub of main.value | keyvalue">
{{label_name[sub.key]}}
</div>
</div>
This would result in:
Network
Main
Drops
Drops Aluminium
Misc
VANNE
You have 2 options here:
Adapt your HTML to loop around total_by_level and query label_name appropriately
Build the output in code
It looks like you have attempted both, and so are open to either. Personally, I prefer to do as much as possible in the code and keep the HTML as dumb as possible, so I would take approach 2.
In ngOnInit() (which should be where you do any initial processing), I would build an array based on the structure on total_by_level.
output: any[];
ngOnInit() {
this.output = Object.keys(this.total_by_level).map(levelKey => {
const child = this.total_by_level[levelKey];
return {
level: {
label: this.label_name[levelKey]
},
children: Object.keys(child).map(childKey => ({
label: this.label_name[childKey],
value: child[childKey]
}))
};
});
}
It then becomes simple to bind to this array in your HTML:
<div *ngFor="let item of output">
{{item.level.label}}
<div *ngFor="let child of item.children">
{{child.label}}
{{child.value}}
</div>
</div>
You are dealing with some odd data structures, and I'm not sure of your terminology, so I have guessed a little bit here. You can take the concept of this idea and work with it. I am also assuming that there is only ever 1 nested child in total_by_level.
DEMO: https://stackblitz.com/edit/angular-upqdex
I'm rendering a map of items retrieved from a database and filtered via the value state of an input field and attempting to then set the state of the input field as the value stored in some list item on click. I figured that using document.getElementById().innerHTML would allow me to retrieve the content stored within the appropriate tag and then set it to state which does work, the issue I'm facing is that it will only retrieve the innerHTML of the first item rendered in the map.
I've tried solutions ranging from applying UUID to making the mapped content available to the window and transfering the state of the individual objects but each disparate solution only moves the value of the first item to state - any ideas?
Rendered Content:
window.filteredItems = this.state.items.filter(
(item) => {
return item.companyNameObj.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1;
}
);
<div className="fixed-width">
<div className="search-container">
<form>
<input type="text" name="search" className="search-bar" placeholder="Search: " onChange={this.handleChange} value={this.state.search} />
</form>
<ul className="search-results">
{window.filteredItems.map((item) => {
return (
<div className="distinct-result-container">
<li key={item.id}>
<div className="image-container">
<img src={item.imageObj} alt={item.companyNameObj + " logo."}/>
</div>
<div className="company-container">
<span onClick={this.stateTransfer}><h3 id={"ID"}>{item.companyNameObj}</h3></span>
<p>Owned by: {item.ownerNameObj}</p>
</div>
</li>
</div>
)
})}
</ul>
</div>
<Footer />
</div>
);
stateTransfer()
stateTransfer(id) {
var search = this.state.search;
var uniqueID = document.getElementById("ID").innerHTML;
this.setState({
search: uniqueID
});
}
The current content of stateTransfer() doesn't represent any significant attempts at approaching a solution to this issue, it's just the minimum required implementation to move the innerHTML content to the input fields value.
EDIT: I've further clarified on the task at hand and a potential solution in the comments below (which follow this), I'm just hoping someone is able to help me with the actual implementation.
#DILEEPTHOMAS The list is comprised of data pulled from a Firebase Realtime Database and is rendered via mapping the filteredList and a search query; that functoionality works fine - what I need is to be able to click the element of any distinct li and have the innerHTML (the text stored in that li's item.companyNameObj) be moved to the value of the input field (so users can navigate the search content with re-typing).
#JoshuaLink I can't necessarily configure the items of the list any
further as it's just data pulled from an external database - I believe
the appropriate solution is to somehow provide a unique HTML ID value
to each newly rendered li and have that selected ID moved to
stateTransfer() where it can be set as the input fields value, I'm
just struggling with the actual implementation of this.
EDIT 2: I've managed to figure out a solution to both parts of the problem as described above - I'll post it as an answer below.
I managed to solve both parts of my problem:
The key issue, which was moving the text stored in each distinct li to the input value, which was apparently easily solved by making my stateTransfer() function accept an event and passing the .innerText value of the h3 through the event (I assumed I would have to use .innerHTML, which would require me to provide each distinct li with a unique generated ID) as follows:
stateTransfer(e) {
var search = this.state.search;
var innerText = e.target.innerText
this.setState({
search: innerText
})
}
The secondary issue, (which I incorrectly assumed was integral to implementing a solution to my question), assigning unique HTML id values to my procedurally generated li's was solved by implementing a for-loop in a componentDidUpdate() function which iterates through the current total length of the list and and assigns an id with the loop iterator concatenated to the end of the string as follows:
componentDidUpdate() {
var i;
var searchCompanyNames = document.querySelectorAll('.comapnyNames');
for(i = 0; i < searchCompanyNames.length; i++) {
searchCompanyNames[i].id = 'companyName-' + i;
}
}
Whilst I didn't need to assign unique ID's to the li's in the correct implementation, it's a useful trick worth noting nonetheless.
I have created this simple address form from devexpress components in MVC view. It consists of two groupboxes - the first is with border for 4 rows. The second is inside first groupbox at last line which consists of 3 elements. The first element is 30px wide, the second 60px and the third should just fill the rest of space so he doesn't have fixed width. The whole box is 250px wide. My problem are spaces between textboxes in second groupbox which I can't get rid of. I want the second textbox to be aligned right behind the first and the third textbox right behind the second which will take all the remaining space. I tried all sort of horizontal alignment but the spaces still persist and I start to be desperate - viz picture belove:
Thank you for your help.
Edit: the code here:
var group1 = settings.Items.AddGroupItem().FormLayoutGroup(1, Unit.Percentage(100), GroupBoxDecoration.Box);
... // other textboxes
var group2 = group1.Items.AddGroupItem().FormLayoutGroup(3, Unit.Percentage(100), GroupBoxDecoration.None);
group2.Items.Add(model => model.AdresaOsobyId.Adresa.Stat.ID).ToLambda(x =>
{
x.TextBox()
x.Width = 30;
x.HideCaption();
});
group2.Items.Add(model => model.AdresaOsobyId.Adresa.Psc).ToLambda(x =>
{
x.HideCaption();
x.TextBox();
x.Width = 60;
});
group2.Items.Add(model => model.AdresaOsobyId.Adresa.Posta).ToLambda(x =>
{
x.HideCaption();
x.TextBox();
});
I need to implement a filter-type search which hides items in core list if they do not match the search. I created a .hidden class that is applied to an item if an expression returns false:
class = {{ {hidden: !match(model.host, hQuery)} | tokenList }}
The elements are hidden, but the list does not reflow elements unless I click on a visible row. Is there a way to force a reflow using a function call?
After a week of struggling, hiding list items is just not the right way to handle this. Iterate through the original array, push any matching objects to a temporary array, and then replace core-list's .data array with the temporary array: this.$.list_id.data = tmpArray. Performance is good for lists up to 10K records.
This is what I'm doing in my code, and it works:
<div style="{{hide_part1}}">
...content to show/hide...
</div>
....
Switching it based on route change(flatron-director):
routeChanged: function(oldValue, newValue) {
if ('some_route_1' == this.route) {
this.hide_part1 = ''
this.hide_part2 = 'display: none;'
} else if ('some_route_2' == this.route) {
this.hide_part1 = 'display: none;'
this.hide_part2 = ''
}
},
Also using core-list's updateSize() and especially scrollToItem(0), i.e. back to top, here and there helps as I also had problems with the 'reflow':
https://stackoverflow.com/questions/29432700/polymer-core-list-is-not-rendering-some-of-the-elements