I have a react component that I am using as checkpoint to check if the user has viewed a certain section of the site.
class ContentSection extends Component {
constructor(props) {
super(props);
}
render() {
const allParagraphs = [];
for (let i = 0; i < this.props.paragraphs.length; i++) {
let p = this.props.paragraphs[i];
allParagraphs.push(
<Paragraph key={i} image={p["img"]} text={p["text"]} />
);
}
return (
<div className="cs">
<ContentSectionCheckPointContainer
uniqueIndex={this.props.uniqueIndex}
/>
<h4 className="sectionTitle">THIS IS A SECTION!!!</h4>
{allParagraphs}
</div>
);
}
}
And this is the ContentSectionCheckPointContainer
const mapDispatchToProps = dispatch => {
return {
unlock: index => dispatch(Unlock_Index_Action(index))
};
};
const mapStateToProps = state => {
return {
CheckPoints: [...state.CheckPoints]
};
};
class ContentSectionCheckPoint extends Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
this.myRect = null;
this.checkVisibility = this.checkVisibility.bind(this);
}
componentDidMount() {
this.checkVisibility();
window.addEventListener('scroll', this.checkVisibility);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.checkVisibility);
}
checkVisibility() {
if (this.myRef.current) {
let rect = this.myRef.current.getBoundingClientRect();
var viewHeight = Math.max(
document.documentElement.clientHeight,
window.innerHeight
);
let b = !(rect.bottom < 0 || rect.top - viewHeight >= 0);
if (b !== this.props.CheckPoints[this.props.uniqueIndex]) {
if (b) {
this.props.unlock(this.props.uniqueIndex);
}else{
this.props.unlock(this.props.uniqueIndex);
}
}
}
}
render() {
this.checkVisibility();
return (
<div ref={this.myRef} className="cscp">
{this.props.CheckPoints[this.props.uniqueIndex] && <p>hi</p>}
</div>
);
}
}
const ContentSectionCheckPointContainer = connect(
mapStateToProps, mapDispatchToProps)(ContentSectionCheckPoint);
As you can see I ran a visibility check on scroll, which works fine. However, I wanted to also run the visibility check immediately when the page is loaded before any scrolling occur.
It is to my understanding that componentDidMount is when React already rendered an element for the first time, so I wanted to do the check then. However, I was trying to render two ContentSection components, each containing their own check point. The latter check point for unknown reason is positioned higher than it appears on screen during componentDidMount, resulting in my visibility check returning true even though it is not visible. If I refresh the page, its position is correct again and the visibility check is fine.
This problem only seem to occur during the first time when I open up a new tab to load my code, but no longer occurs after a refresh on that tab.
Any idea why?
Related
[demo][1]
[1]: https://codesandbox.io/s/react-to-print-rzdhd
I'm using react-to-print library to generate pdf from html. But the problem is that when I try
to generate pdf in my mobile(small screen size), it does not work for me however, it works in
desktop(large screen size) view.
This package uses window.print(), which won't work on mobile devices. You can use the following workaround:
class Example extends React.Component {
printPDF(e) {
return new Promise((resolve: any) => {
e.preventDefault()
var ua = navigator.userAgent.toLowerCase()
var isAndroid = ua.indexOf('android') > -1 //&& ua.indexOf("mobile");
if (isAndroid) {
// https://developers.google.com/cloud-print/docs/gadget
var gadget = new cloudprint.Gadget()
gadget.openPrintDialog()
} else {
window.print()
}
resolve(true)
})
}
render() {
return (
<div>
<ReactToPrint
trigger={() => <button>Print this out!</button>}
print={e => this.printPDF(e)} // HANDLE PRINT FUNCTION
content={() => this.componentRef}
/>
<ComponentToPrint ref={el => (this.componentRef = el)} />
</div>
)
}
}
I'm pretty new to Angular and programming in general.
I wanted to change the background image of my Page by using the setInterval method. It should change every second but for some reason, it changes much faster.
Component:
export class AppComponent implements OnInit {
images: Image[] = [];
changeBackgroundCounter = 0;
constructor(private imagesService: ImagesService) {}
getImage() {
setInterval(() => {
this.changeBackgroundCounter = this.changeBackgroundCounter + 1;
if (this.changeBackgroundCounter > this.images.length - 1) {
this.changeBackgroundCounter = 0;
}
}, 1000);
return this.images[this.changeBackgroundCounter].image;
}
ngOnInit() {
this.images = this.imagesService.getImages();
console.log(this.images[0]);
}
}
Template:
<div [ngStyle]="{'background-image': 'url('+ getImage() + ')'}" [ngClass]="{imageBackground: getImage()}">
Stackblitz link
In your template, you have
<div [ngStyle]="{'background-image': 'url('+ getImage() + ')'}" [ngClass]="{imageBackground: getImage()}">
This means angular keeps calling the getImage() method to find out what the background should be. This will happen very frequently. Each time the method is called, a new interval is created, so there end up being loads of them. You can see this by putting a line of logging within your interval and you will see how often it's being triggered.
setInterval(() => {
console.log('interval triggered'); // <------- add this line to see how often this code is running
this.changeBackgroundCounter = this.changeBackgroundCounter + 1;
if (this.changeBackgroundCounter > this.images.length - 1) {
this.changeBackgroundCounter = 0;
}
}, 1000);
To fix your problem, you need to call getImage() only once, which can be done within ngOnInit(). The template can get the image from images[this.changeBackgroundCounter].image.
You're complicating your code for nothing. Create a variable, equal to a string, and assign it a new value every X seconds in your ngOnInit() !
Then set the background image equals to that variable, and voilĂ !
Here is what it look like in code :
export class AppComponent implements OnInit {
images: Image[] = [];
actualImage: string;
changeBackgroundCounter = 0;
constructor(private imagesService: ImagesService) {}
ngOnInit() {
this.images = this.imagesService.getImages();
this.actualImage = this.images[0].image;
setInterval(() => {
this.changeBackgroundCounter++;
if (this.changeBackgroundCounter > this.images.length - 1) {
this.changeBackgroundCounter = 0;
}
this.actualImage = this.images[this.changeBackgroundCounter].image;
}, 5000);
}
}
I kept as much as possible of your inital code. My new variable is called actualImage, I set a default value in my ngOnInit, right after you get all your images from your service.
Then I call setInterval and set a new value to actualImage every 5 seconds !
https://stackblitz.com/edit/angular-setinterval-f5bghq
CARE: When using setInterval, be used to clear it on ngOnDestroy(), it can lead to some weird bugs you don't want to get involved in.
Simply create an other variable, type any, and do the following :
this.interval = setInterval(() => {...})
ngOnDestroy() {
clearInterval(this.interval);
}
According to a prior SO answer, you can implement getPriority for a forge viewer Tool. And according to another SO answer extending the ToolInterface does not work. Hence, me not extending the ToolInterface implementing my Tool like so:
class MyCustomExtension extends Autodesk.Viewing.Extension {
constructor(viewer, options) {
super(viewer, options);
this.theiaUtil = new TheiaUtil(this);
}
getPriority() {
console.log("Theia#getPriority called! ", (this.getPriority && this.getPriority() || 0));
return 100000;
}
...
}
My tool's priority is returned as 0 in the ToolController, although it shouldn't:
function getPriority(tool)
{
return tool.getPriority instanceof Function && tool.getPriority() || 0;
}
I don't know why this function returns 0 as tool.getPriority instanceof Function returns true if I call MyCustomExtension.getPriority myself.
Note that ToolInterface is implemented like so:
function ToolInterface()
{
this.names = [ "unnamed" ];
this.getNames = function() { return this.names; };
this.getName = function() { return this.names[0]; };
this.getPriority = function() { return 0; };
this.register = function() {};
this.deregister = function() {};
this.activate = function(name, viewerApi) {};
this.deactivate = function(name) {};
this.update = function(highResTimestamp) { return false; };
this.handleSingleClick = function( event, button ) { return false; };
this.handleDoubleClick = function( event, button ) { return false; };
this.handleSingleTap = function( event ) { return false; };
this.handleDoubleTap = function( event ) { return false; };
// ...
}
Because of that, simply extending the ToolInterface class won't work because all these properties and functions added to the instance in the constructor will take precedence over your actual class methods. This is also likely the reason why you're seeing the priority value returned as zero - when you call myTool.getPriority(), you are not actually calling your getPriority method, but rather the default function which was assigned to this.getPriority in ToolInterface's constructor.
To work around this issue I would recommend explicitly deleting the corresponding fields in your class' constructor (something I explain in my blog post on implementing custom Forge Viewer tools):
class DrawTool extends Autodesk.Viewing.ToolInterface {
constructor() {
super();
this.names = ['box-drawing-tool', 'sphere-drawing-tool'];
// Hack: delete functions defined *on the instance* of the tool.
// We want the tool controller to call our class methods instead.
delete this.register;
delete this.deregister;
delete this.activate;
delete this.deactivate;
delete this.getPriority;
delete this.handleMouseMove;
delete this.handleButtonDown;
delete this.handleButtonUp;
delete this.handleSingleClick;
}
register() {
console.log('DrawTool registered.');
}
deregister() {
console.log('DrawTool unregistered.');
}
activate(name, viewer) {
console.log('DrawTool activated.');
}
deactivate(name) {
console.log('DrawTool deactivated.');
}
getPriority() {
return 42; // Or feel free to use any number higher than 0 (which is the priority of all the default viewer tools)
}
// ...
}
TL;DR: Activate the tool in button click event from a toolbar button instead of the extension's load method.
class MyExtension extends Autodesk.Viewing.Extension {
...
onToolbarCreated(toolbar) {
const MyToolName = 'My.Tool.Name'
let button = new Autodesk.Viewing.UI.Button('my-tool-button');
button.onClick = (e) => {
const controller = this.viewer.toolController;
if (controller.isToolActivated(MyToolName)) {
controller.deactivateTool(MyToolName);
button.setState(Autodesk.Viewing.UI.Button.State.INACTIVE);
} else {
controller.activateTool(MyToolName);
button.setState(Autodesk.Viewing.UI.Button.State.ACTIVE);
}
};
}
...
}
I activated the tool instantly after registering it in the Extension's load method. Petr Broz's github repo from his blog post loads the tool from a button in the toolbar. So I moved the activation of the tool to a button click in the toolbar which worked for me.
I've been trying to implement a method in which you can sort a leaderboard in different ways, by toggling a select element which changes the state, causing the component to re-render.
The problem is that, it can sort the default correctly, but whenever I change the value of the select from default to "z-to-a", it does not seem to be updating.
Note: I've added a few console.log statements, which seem to be behaving weirdly.
My JSX:
import React, { useState, useEffect } from 'react';
import './Leaderboard.css';
import LbRow from '../../components/LbRow/LbRow'; /* A row in the leaderboard*/
import points from '../../data/tree-points.json';
function Leaderboard() {
// Initialize the points as the data that we passed in
const [state, setState] = useState({
points: points,
sortBy: "first-to-last"
});
// Changes the sort method used by the leaderboard
const changeSortBy = (event) => {
var newSort = event.target.value;
// Sorts the data differently depending on the select value
switch(newSort) {
case "first-to-last":
sortDescending("points","first-to-last");
break;
case "z-to-a":
sortDescending("tree_name","z-to-a");
console.log(state.points.treePoints); // Logs incorrectly, still logs the same array as in "first-to-last"
break;
default:
sortDescending("points","first-to-last");
}
// Re-renders the component with new state
setState({
points: state.points,
sortBy: newSort
});
}
/* Updates the leaderboard state to be in descending point order */
const sortDescending = (aspect, sortMethod) => {
console.log(sortMethod); // Logs correctly
// Sorts the data in descending points order
let sortedPoints = [...state.points.treePoints].sort((tree1, tree2) => {
if (tree1[aspect] > tree2[aspect]) { return -1; }
if (tree1[aspect] < tree2[aspect]) { return 1; }
return 0;
});
// Actually updates the state
setState({
points: {
...state.points,
treePoints: sortedPoints
},
sortBy: sortMethod
});
console.log(sortedPoints); // Logs correctly
};
/* Calls sortLb on component mount */
useEffect(() =>{
sortDescending("points", "first-to-last");
}
,[]);
// Attributes used for rendering the leaderboard body
var rank = 0;
const sortedData = state.points;
/* Basically all the active trees with the first tree having the top rank */
const lbBody = sortedData.treePoints.map((sortedData) => {
return (
sortedData.active &&
<LbRow rank={++rank} tree_name={sortedData.tree_name} points={sortedData.points} active={sortedData.active}/>
);
});
return (
<div>
<div className="filters">
{/* Allows user to sort by different methods */}
<label htmlFor="sortBy">Sort by:</label>
<select name="sortBy" className="sortBy" value={state.sortBy} onChange={changeSortBy}>
<option value="first-to-last">First to Last</option>
<option value="z-to-a">Z to A</option>
</select>
</div>
{/* The table with sorted content */}
<div className="table">
{lbBody}
</div>
</div>
);
}
export default Leaderboard;
I'm really confused by this behavior, especially since I have the correctly sorted value and supposedly already updated the state. What could be causing this to happen? THanks
There are 3 things you must note
State updates are batched, ie. when you call setState multiple times within a function, their result is batched together and a re-render is triggered once
State updates are bound by closures and would only reflect in the next re-render and not immediately after calling state updater
State updates with hooks are not merged to you do need to keep merging all values in state yourself
Now since you wish to call the state updater twice, you might as well use the callback approach which will guarantee that your state values from multiple setState calls are not merged, since you don't need them to. Also you must update only the fields that you want to
function Leaderboard() {
// Initialize the points as the data that we passed in
const [state, setState] = useState({
points: points,
sortBy: "first-to-last"
});
// Changes the sort method used by the leaderboard
const changeSortBy = (event) => {
var newSort = event.target.value;
// Sorts the data differently depending on the select value
switch (newSort) {
case "first-to-last":
sortDescending("points", "first-to-last");
break;
case "z-to-a":
sortDescending("tree_name", "z-to-a");
break;
default:
sortDescending("points", "first-to-last");
}
// Re-renders the component with new state
setState(prev => ({
...prev,
sortBy: newSort // overrider just sortByField
}));
}
/* Updates the leaderboard state to be in descending point order */
const sortDescending = (aspect, sortMethod) => {
console.log(sortMethod); // Logs correctly
// Sorts the data in descending points order
let sortedPoints = [...state.points.treePoints].sort((tree1, tree2) => {
if (tree1[aspect] > tree2[aspect]) {
return -1;
}
if (tree1[aspect] < tree2[aspect]) {
return 1;
}
return 0;
});
// Actually updates the state
setState(prev => ({
...prev,
points: {
...state.points,
treePoints: sortedPoints
},
}));
};
/* Calls sortLb on component mount */
useEffect(() => {
sortDescending("points", "first-to-last");
}, []);
// Attributes used for rendering the leaderboard body
var rank = 0;
const sortedData = state.points;
...
}
export default Leaderboard;
Another better way to handle this to avoid complicated is to separate out your states into two useState
function Leaderboard() {
// Initialize the points as the data that we passed in
const [points, setPoints] = useState(points);
const [sortBy, setSortBy] = useState(sortBy);
// Changes the sort method used by the leaderboard
const changeSortBy = (event) => {
var newSort = event.target.value;
// Sorts the data differently depending on the select value
switch(newSort) {
case "first-to-last":
sortDescending("points","first-to-last");
break;
case "z-to-a":
sortDescending("tree_name","z-to-a");
console.log(state.points.treePoints); // Logs incorrectly, still logs the same array as in "first-to-last"
break;
default:
sortDescending("points","first-to-last");
}
// Re-renders the component with new state
setSortBy(newSort);
}
/* Updates the leaderboard state to be in descending point order */
const sortDescending = (aspect, sortMethod) => {
console.log(sortMethod); // Logs correctly
// Sorts the data in descending points order
let sortedPoints = [...state.points.treePoints].sort((tree1, tree2) => {
if (tree1[aspect] > tree2[aspect]) { return -1; }
if (tree1[aspect] < tree2[aspect]) { return 1; }
return 0;
});
// Actually updates the state
setPoints({
...state.points,
treePoints: sortedPoints
});
console.log(sortedPoints); // Logs correctly
};
/* Calls sortLb on component mount */
useEffect(() =>{
sortDescending("points", "first-to-last");
}
,[]);
// Attributes used for rendering the leaderboard body
var rank = 0;
const sortedData = points;
/* Basically all the active trees with the first tree having the top rank */
const lbBody = sortedData.treePoints.map((sortedData) => {
return (
sortedData.active &&
<LbRow rank={++rank} tree_name={sortedData.tree_name} points={sortedData.points} active={sortedData.active}/>
);
});
return (
<div>
<div className="filters">
{/* Allows user to sort by different methods */}
<label htmlFor="sortBy">Sort by:</label>
<select name="sortBy" className="sortBy" value={sortBy} onChange={changeSortBy}>
<option value="first-to-last">First to Last</option>
<option value="z-to-a">Z to A</option>
</select>
</div>
{/* The table with sorted content */}
<div className="table">
{lbBody}
</div>
</div>
);
}
export default Leaderboard;
I guess the question boils down how to i pass the instance of a property to another class.
I have something like this:
import timerClass from "./timer";
export class App {
constructor() {
this.timeLeft = 6; //<--- I want to update this
new timerClass(this.timeLeft);
}
activate() {
}
}
and
export default class {
constructor(time) {
this.initialTime = time;
setInterval(function () {
if (--time < 0) {
time = this.initialTime; //<--- From here
}
}, 1000);
}
}
Time is passed in but not reflected in the view when updated.
In knockout this was easy as all observables are functions an I could pass it round all over the place. How would i do the same here, should I wrap it in a function too?
When you call
new timerClass(this.timeLeft);
you pass your variable by value, i.e. the timer just gets 6 and there is no way to modify it there. The easiest way to fix this is indeed pass the callback function. I made it work with the following code.
timer.js:
export default class {
constructor(time, callback) {
this.initialTime = time;
this.currentTime = time;
setInterval(() => {
if (--this.currentTime < 0) {
this.currentTime = this.initialTime;
}
callback(this.currentTime);
}, 1000);
}
}
app.js:
constructor(){
this.timeLeft = 6;
var timer = new timerClass(this.timeLeft, v => this.timeLeft = v);
}
So I did some more reading and came across the aurelia-event-aggregator
http://aurelia.io/docs#the-event-aggregator
This allowed me to try a different angle. As my timer is eventually going to become a game loop this pub/sub way of doing it will work quite nicely.
Im still quite green with the syntax so I imagine its doing some things not entirely "best practice" but hope it helps someone.
main.js
import {inject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import TimerClass from "./timer";
#inject(EventAggregator)
export class Main {
constructor(eventAggregator) {
this.eventAggregator = eventAggregator;
this.timer = new TimerClass(this.eventAggregator);
this.eventAggregator.subscribe('gameLoop', currentTime => {
this.timeLeft = currentTime
});
}
activate() {
this.timer.start();
}
}
timer.js
export default class Timer {
constructor(eventAggregator) {
this.eventAggregator = eventAggregator;
}
start(){
var initalTime = 5;
var currentTime = initalTime;
setInterval(() => {
if (--currentTime < 0) {
currentTime = initalTime;
}
this.eventAggregator.publish('gameLoop', currentTime);
}, 500);
}
}
main.html
<template>
<div>
<h2>Time Left:</h2>
<div>${timeLeft}</div>
</div>
</template>