React JS Conditional Rendering like Eval - reactjs

I have a dynamic button being created. The problem is that the CurrentPage and the KeyIndex are already evaluated prior to rendering.
private DisplayButtons = () => {
let cont = [];
let startAt, endAt;
startAt = this.state.StartAt;
endAt = this.state.EndAt;
if (this.state.CurrentPage == 1) {
endAt = endAt =
this.state.TotalRecord / this.state.PageSize >= 5
? 5
: Math.ceil(this.state.TotalRecord / this.state.PageSize);
}
for (var i = startAt; i <= endAt; i++) {
cont.push(
<li onClick={this.onPagerClick} data-id={i} className="sui-pager-element">
<a
className={
this.state.CurrentPage == this.state.KeyIndex
? 'sui-selected sui-pager-element'
: 'sui-pager-element'
}
>
{i}
</a>
</li>,
);
}
this.setState({
DataButtons: cont,
});
};

It is difficult to understand this isolated code. However, I make some comments that I hope will improve its functioning.
I understand that DisplayButtons is an internal function of a class component. If that is the case, it should be called displayButtons with first lower case.
You can refactor like this:
const { startAt, CurrentPage, TotalRecord, PageSize, KeyIndex } = this.state;`
let { endAt } = this.state;
Better use FP in the for loop. It is not very clear.
cont should be an array of objects instead of html. Then, in the render, you do something like this: { DataButtons.map(b => <ButtonView>) }.

Related

React assign values to an array

I'm trying to handle some star rating according to some data from an api. The problem is that I have created an array for the stars and nothing is rendered. If I console .log inside the fetchMovieDetails I can see there is data. What am I missing? I even tried var stars = useState([]) but still same result
export default function MovieDetails({ match }) {
const [movie, setMovie] = useState({});
var stars = [];
useEffect(() => {
fetchMovieDetails();
}, []);
async function fetchMovieDetails() {
// get data from api
// Handle star rating
var rating = (response.rating/ 2).toFixed(1);
for (var i = 0; i < 5; i++) {
var star = {};
if (i < ~~rating) star = 'filled';
else if (rating % 1 === 0) star = 'half-filled';
stars.push(star);
}
setMovie(response.data);
}
return (
<div className="movie-container">
<div className="movie-grid">
<div className="movie-rating">
<div className="star-rating">
// nothing is rendered here
{stars.map((star) => (
<span className="{`icon-star ${star}`}">
<span className="path1"></span>
<span className="path2"></span>
</span>
))}
{(movie.rating/ 2).toFixed(1)} / 5
</div>
</div>
</div>
</div>
);
}
I guest var stars is always re-declare. Why don't you using the useState like:
const [stars, setStarts] = useState([]);
useEffect(() => {
fetchMovieDetails();
}, []);
async function fetchMovieDetails() {
// get data from api
// Handle star rating
var rating = (response.rating/ 2).toFixed(1);
var stars = [];
for (var i = 0; i < 5; i++) {
var star = {};
if (i < ~~rating) star = 'filled';
else if (rating % 1 === 0) star = 'half-filled';
stars.push(star);
}
setStarts(stars);
setMovie(response.data);
}
If you prefer to not to keep stars in the state, and if you need to move the star logic in to a separate section then useMemo will help with it.
async function fetchMovieDetails() {
// get data from api
.....
setMovie(response.data);
}
const stars = useMemo(() => {
if (!movie.rating) {
//if movie is not fetched then return an empty array
return [];
}
const rating = (movie.rating/ 2).toFixed(1);
const starsTemp = [];
for (let i = 0; i < 5; i++) {
var star = {};
if (i < ~~rating) star = 'filled';
else if (rating % 1 === 0) star = 'half-filled';
starsTemp.push(star);
}
return starsTemp;
}, [movie]); //when movie gets changed call this function
Why i prefer this is, lets say you can add a rating by clicking on a star, and then lets say you need to show the updated rating, with your approach you have to change two states, rating property of movie and the stars, so basically if a one state is depending on a another state, its better to do like this.

How do I render/animate out items that leave array

Currently, I have a scraper that scrapes slack messages and stores them in a db somewhere.
On the frontend, I am pulling every second to see if new messages pop up. And then I render those messages on screen.
If anyone on slack replies or emotes on a message, the message gets removed from the backend thus getting removed from the frontend.
What I am trying to do now is when an item gets removed, I would like to animate it somehow.
Here is some of my current code:
async componentDidMount() {
await this.grab_channels()
await this.grab_slack_user_data()
await this.grab_items()
setInterval(() => {
this.grab_items()
}, this.state.settings.seconds_per_slack_messages_pull * 1000 )
}
grab_items() {
let url = this.state.settings.api_url + 'channel/' + this.state.selected_channel + '/now'
return new Promise( resolve => {
axios.get( url )
.then( res => {
this.setState( { items: res.data } )
resolve()
} )
})
}
And finally, items get rendered:
this.props.items.map( t => {
return (
<Item
key={ t.usr + '_' + t.ts }
task={ t }
user={ this.props.slack_users[ t.usr ] }
settings={ this.props.settings }
now={ this.state.now }
/>
)
} )
I was thinking of doing some sort of check within grab_items() but I wouldn't know how to continue after that. It would be easy to determine which ones should be rendered out but the problem is actually doing it.
Anyone have experience building something like this out?
Thanks!
Using Transition Groups is one way to do this:
https://github.com/reactjs/react-transition-group
Take a look at this example:
https://reactcommunity.org/react-transition-group/transition-group
For the check part in your function grab_items
/* include "clone" so that we don't modify state directly */
import clone from 'clone'
grab_items() {
let url = this.state.settings.api_url + 'channel/' + this.state.selected_channel + '/now'
return new Promise(resolve => {
axios.get(url).then(res => {
/* figure out what items to remove before you set the state */
let itemsToShow = []
for (let i = 0; i < this.state.items.length; i++) {
let ifFound = false
let t = clone(this.state.items[i])
for (let j = 0; j < res.data.length; j++) {
if (t.key === res.data[j].key) {
ifFound = true
}
}
/* if ifFound is false, it means it is not in the messages any more. */
if(!ifFound){
t.haveAnimation = true
itemsToShow.push(t)
}
itemsToShow = itemsToShow.concat(res.data)
this.setState(itemsToShow)
}
})
})
}
Then every second when it re-pull the data, you will have a list of items to show. The list has the items need to have the "disappear" animation and also it has the new messages.
To make the animation work, in the render part:
this.props.items.map(t => {
return (
<Item
key={t.usr + '_' + t.ts}
className={t.haveAnimation ? 'animationCSS' : ''}
task={t}
user={this.props.slack_users[t.usr]}
settings={this.props.settings}
now={this.state.now}
/>
)
}
Above code should attach the css class to the Item. You can put whatever css animation in the class

Vue: Array is only updated on console, not on screen

I'm still learning the basics of vue. I have different elements in one component, that have to be assigned in a second component. When I use console.log the array is shown correctly, but when i want to show the array in dynamically in template it is still the default value. What am I doing wrong?
Component one:
<template>
<div>
{{nr}}
{{containertyp}}
<button #click="click(0)"></button>
</div>
</template>
I have many more buttons with different parameters, just to make it shorter here.
export default: {
data: function {
return {
nr: [],
containertyp: [],
}
}
methods: {
click(number) {
for (var i = 0; i < 27; i++) {
this.nr[i] = false;
if (number == i) {
this.nr[i] = true;
}
};
EventBus.$emit('containerclicked');
}
},
created: function() {
let i;
//default
for (i = 0; i < 27; i++) {
this.nr[i] = false;
}
for (var index = 0; index < 27; index++) {
this.containertyp[index] = 'bs';
}
},
mounted() {
const self = this
EventBus.$on('containertypchosen', function (containertyp) {
for (let j = 0; j < 27; j++) {
if (self.nr[j] == true) {
self.containertyp[j] = containertyp
}
}
})
Component two:
<template>
<div>
<button :disabled = "disabled == true" v-on:click="chosetype('bs')" "> bs </button> <br />
</div>
</template>
export default: {
data: function() {
return {
disabled: true
}
}
mounted () {
const self = this
EventBus.$on('containerclicked', function (){
self.disabled = false
})
},
methods: {
chosetype: function (containertyp) {
this.containertyp = containertyp
EventBus.$emit('containertypchosen', containertyp)
}
}
}
You can't update arrays using indexes, the changes won't be detected by the reactivity system.
https://v2.vuejs.org/v2/guide/list.html#Caveats
So, for example, this won't work:
this.nr[i] = true;
Instead you'd need to use Vue.set, or the alias vm.$set:
this.$set(this.nr, i, true);
An alternative would be to create a new array and then replace this.nr entirely, i.e. this.nr = newArray.
You'll need to make a similar change everywhere that you're updating an array by index. There are updates to both nr and containertyp that currently have this problem.
It's not immediately clear from your code whether nr even needs to be an array. It seems that all the values are false apart from one, so you might be better off just holding the index of the true value instead of using an array.

Trying to addEventListener to looped array

I'm pretty sure I've coded the event handler part wrong. When the array goes through and loops it does product a list on the page. I just need that list clickable with an event listener, then a series of re-direct based on if statements for comparison...ideas ?
window.onload = eventMonitor;
function eventMonitor(){
document.getElementById("names").addEventListener('click', reRoute, false);
document.getElementById("hiddenText").addEventListener('mouseover', treasureRoute, false);
function createName(){
var streetNames = ["Carmen", "Napoli", "Oscar", "Haven", "Tampa"];
//this top partscript creates a for loop that will take each array [item] and simply right it to the screen.
for (i=0; i<streetNames.length; i++){
var mName = "Martin";
var node = document.createElement("li");
var textNode = document.createTextNode(mName + " " + (streetNames[i]));
node.appendChild(textNode);
document.getElementById("names").appendChild(node);
}
}
function reRoute(){
if(names === 'Martin Carmen'){
routeWindow = window.open("https://en.wikipedia.org/wiki/MSC_Carmen");
}
else if(names === 'Martin Napoli'){
routeWindow = window.open("https://en.wikipedia.org/wiki/MSC_Napoli")
}
else if(names === 'Martin Oscar'){
routeWindow = window.open("https://en.wikipedia.org/wiki/MSC_Oscar")
}
else if(names === 'Martin Haven'){
routeWindow = window.open("https://en.wikipedia.org/wiki/MT_Haven")
}
else if(names === 'Martin Tampa'){
routeWindow = window.open("https://en.wikipedia.org/wiki/MV_Tampa")
}
}
function treasureRoute(){
shipWindow = window.open("file:///Users/User/something.html");
}
}
What you want to do does not require JavaScript. JavaScript, while useful in many circumstances, is error prone as you have noticed. The anchor tag is used to navigate between pages, which is why you inject <li>Text</li> elements into the DOM.
If you would nonetheless want to use JavaScript in a situation like yours, you would program your event handler to comply with the addEventListener API:
const names = document.getElementById('names');
names.addEventListnener('click', event => {
const { target } = event;
console.log(`${target.tagName} was clicked!`);
});

Using Lodash _.map and Typescript to create new column for filtering

In my controller I have code that filters just fine but i want to create a new field that concatenates two fields for an Angular filter in html. This is what I have that doesn't work.. Is this possible?
private filterByColumns: string = "";
getData = (): void => {
var vm = this;
this.carHopService.getDetails({ id: this.$state.params["id"], type: this.$state.params["type"] }).then(
(data: any) => {
vm.primaryCarHopData = _.filter(data.carHopList, {
carHopType: "Primary"
});
---> **vm.primaryCarHopData = _.map(data.carHopList, {
vm.filterByColumns=fullName + " " + age
});**
});
};
That's not how map works. In the callback function, you need to return something:
_.map([0,1,2], (x) => x + 1)
> [1,2,3]
// old syntax
_.map([0,1,2], function (x) { return x + 1 })
> [1,2,3]
You can simply replace _.map with _.forEach and you will have your mapped data in data.carHopList.
Clarification:
I'm not good with words so let me put here very simple implementations for both forEach and map:
// these functions do not mock lodash counterparts 100%
// as lodash fns can work with objects too
// and they have some shortcuts, see docs
function forEach(arr, callback) {
for(var i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
return arr;
}
function map(arr, callback) {
var newArr = [];
for(var i = 0; i < arr.length; i++) {
newArr[i] = callback(arr[i], i);
}
return newArr;
}
Someone had steered me in the direction of map. What I ended up doing was using Angular by adding this to controller:
filterTextByCols = (row) => {
return (angular.lowercase(row.fullName).indexOf(angular.lowercase(this.filterQuery) || '') !== -1 ||
angular.lowercase(row.birthDate).indexOf(angular.lowercase(this.filterQuery) || '') !== -1);
}
And then using filterTextByCols in filter:
<div ng-repeat="person in vm.persons | orderBy:sortType:sortReverse | filter: vm.filterTextByCols">

Resources