ngAfterContentChecked triggered before element actually being "changed/deleted" from DOM? - angular13

Here's my code:
ngAfterContentChecked() {
this.updateGUI();
}
#HostListener('window:resize', ['$event'])
onResize(event) {
this.updateGUI();
}
updateGUI() {
let mainToolbar = this.helpers.getBoundingClientRect(document.getElementById('main-toolbar'));
this.mainSidenavContentHeight = document.documentElement.clientHeight - mainToolbar.height;
let recipeBuilder = this.helpers.getBoundingClientRect(document.getElementById('recipe-builder'));
if (recipeBuilder !== null) {
this.mainSidenavContentHeight -= recipeBuilder.height;
console.log("recipeBuilder");
}
console.log("updateGUI");
}
// helpers
static getBoundingClientRect(element) {
if (element === null) {
return null;
}
var rect = element.getBoundingClientRect();
return {
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
};
}
Switching to a component (i.e. load by route) where recipe-builder is not more present anymore, the first time it logs:
recipeBuilder
updateGUI
Buw ff I click some seconds later, only:
updateGUI
So it seems first time I load the component, recipe-builder is still active on DOM, thus it calculate its height (which I don't want, of course).
What's the correct way to manage this in Angular? i.e. retrieve DOM properties of a HTML component and be sure they are unloaded when "destroyed"?

Related

The app gets slower with every change in props [duplicate]

This question already has an answer here:
React won't draw anything in the canvas in useEffect
(1 answer)
Closed 16 days ago.
I use Matter js for physics and canvas for rendering 100 items, after every change in props the app gets slower and slower. At the same time, subsequent downloads with the same props occur instantly, i.e. past images and elements remained and were not deleted, which loads the application.
enter image description here
"Top" dropdown menu changes props for bubbles component, code:
const Bubbles = ({ coins }) => {
var winwidth = window.innerWidth;
var winheight = window.innerHeight - 80;
const [physics, setPhysics] = useState({
frictionAir: 0,
friction: 0.2,
frictionStatic: 0.2,
restitution: 0.1,
inertia: Infinity,
density: 0.8,
});
useEffect(() => {
setSize(coins);
// create an engine
const engine = Engine.create();
const world = engine.world;
// create a renderer
const render = Render.create({
element: document.querySelector(".bubbles"),
engine: engine,
options: {
width: winwidth,
height: winheight,
showAngleIndicator: false,
background: "transparent",
wireframeBackground: "transparent",
wireframes: false,
showDebug: false,
},
});
const bubbles = [];
// create a canvas
const canvas = document.createElement("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.querySelector(".bubbles").appendChild(canvas);
const ctx = canvas.getContext("2d");
// create 100 random bubbles
for (let i = 0; i < coins.length; i++) {
const x = Math.random() * window.innerWidth;
const y = Math.random() * window.innerHeight;
const bubble = Bodies.circle(x, y, coins[i].size / 2, {
...physics,
render: {
fillStyle: "transparent",
},
});
Composite.add(engine.world, bubble);
// store the bubble and its content data in an object
bubbles.push({
bubble,
content: {
radius: coins[i].radius,
text: `${i + 1}`,
},
});
}
// update the canvas on each engine update
Matter.Events.on(engine, "afterUpdate", function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < bubbles.length; i++) {
const bubble = coins[i];
const pos = bubbles[i].bubble.position;
const size = bubble.size;
const text = bubble.symbol;
const priceChange = bubble.price_change_percentage_24h.toFixed(1);
const image = new Image();
image.src = bubble.image;
if (priceChange > 0) {
ctx.fillStyle = "rgba(5, 222, 5, 0.15)";
ctx.strokeStyle = "rgba(5, 222, 5, 0.6)";
} else if (priceChange < 0) {
ctx.fillStyle = "rgba(166, 53, 53, 0.15)";
ctx.strokeStyle = "rgba(166, 53, 53, 0.8)";
} else {
ctx.fillStyle = "rgba(255, 255, 89, 0.069)";
ctx.strokeStyle = "rgba(187, 231, 11, 0.443)";
}
ctx.beginPath();
ctx.arc(pos.x, pos.y, size / 2, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
ctx.drawImage(
image,
pos.x - size / 5,
pos.y - size / 2.5,
size / 2.5,
size / 2.5
);
ctx.font = `${size / 6}px Acme`;
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, pos.x, pos.y + size / 6.5);
ctx.font = `${size / 6.5}px Acme`;
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(priceChange + "%", pos.x, pos.y + size / 3);
}
});
// fit the render viewport to the scene
window.addEventListener("onload", () => {
Render.lookAt(render, {
min: { x: 0, y: 0 },
max: { x: 100000, y: 10000 },
});
});
// run the engine
Runner.run(engine);
// run the renderer
Render.run(render);
return () => {
// stop the engine and renderer
Engine.clear(engine);
Render.stop(render);
Runner.stop(engine);
// remove the canvas from the DOM
canvas.remove();
};
}, [coins]);
return <></>;
};
export default Bubbles;
after each update of the coins props, I deleted the canvas and stopped the engine, but still the speed worsened
return () => {
// stop the engine and renderer
Engine.clear(engine);
Render.stop(render);
Runner.stop(engine);
// remove the canvas from the DOM
canvas.remove();
};
I want to either stop re-creating the engine and the matter js world, canvas with images.
Only without window.location.reload(), because I need to save props like "Top" and "Earth, Trampoline", etc.
I solved the problem. I didn't properly clear the js canvas value and also didn't set events.off when unmounting the component.
Here is the solution to my problem
return () => {
//set Matter Js events off. drawCanvas - afterUpdate event function
Events.off(engine, 'afterUpdate', drawCanvas);
Render.stop(render);
Composite.clear(engine.world);
Runner.stop(engine)
Engine.clear(engine);
render.canvas.remove();
render.canvas = null;
render.context = null;
// remove the canvas from the DOM
canvas.remove();
};

how to bind property of visibility of dropdown

I want to change the color of label when dropdown is shown and return the color of label to normal when dropdown hides. How can I do it with binding?
so there will be a property
property: {
icon: {
check: Boolean,
apply: handleVisibility
}
}
this.handleVisibility = function(value) {}
this.bind()?
my code
var label = new qx.ui.basic.Label("Default");
label.setFont("bold");
this.getRoot().add(label, {
left: 20,
top: 20,
});
// create a combo box
var comboBox = new qx.ui.form.ComboBox();
// fill the combo box with some stuff
for (var i = 1; i < 31; i++) {
var tempItem = new qx.ui.form.ListItem(
"2^ " + i + " = " + Math.pow(2, i)
);
comboBox.add(tempItem);
}
// add the combobox to the documents root
this.getRoot().add(comboBox, {
left: 20,
top: 40,
});
I've appended about 20 lines to your code, here, to show how you might do it. You can plug this into https://playground.qooxdoo.org to test it.
var label = new qx.ui.basic.Label("Default");
label.setFont("bold");
this.getRoot().add(label, {
left: 20,
top: 20,
});
// create a combo box
var comboBox = new qx.ui.form.ComboBox();
// fill the combo box with some stuff
for (var i = 1; i < 31; i++) {
var tempItem = new qx.ui.form.ListItem(
"2^ " + i + " = " + Math.pow(2, i)
);
comboBox.add(tempItem);
}
// add the combobox to the documents root
this.getRoot().add(comboBox, {
left: 20,
top: 40,
});
// Get easy access to the textfield of the comboBox and its popup list
let textfield = comboBox.getChildControl("textfield");
let popup = comboBox.getChildControl("popup");
// When the popup's `visibility` property changes, modify the textfield's
// `backgroundColor` property. The value of the `visibility` property will be
// "hidden" or "visible". We use a `converter`, to convert the value
// from one of those, to the color to be used. (A color of `null`, used in
// this example when the popup is hidden, means use the default color.)
popup.bind(
"visibility",
textfield,
"backgroundColor",
{
converter : function(data, model, source, target)
{
return data == "visible" ? "cyan" : null;
}
});

How to create and set a setMapTypeId using react-google-maps

I was looking for a way to create my own mars map in a website, using google maps.
I found this example in google map api
function initMap() {
var map = new google.maps.Map(document.getElementById('map'), {
center: {lat: 0, lng: 0},
zoom: 1,
streetViewControl: false,
mapTypeControlOptions: {
mapTypeIds: ['moon']
}
});
var moonMapType = new google.maps.ImageMapType({
getTileUrl: function(coord, zoom) {
var normalizedCoord = getNormalizedCoord(coord, zoom);
if (!normalizedCoord) {
return null;
}
var bound = Math.pow(2, zoom);
return '//mw1.google.com/mw-planetary/lunar/lunarmaps_v1/clem_bw' +
'/' + zoom + '/' + normalizedCoord.x + '/' +
(bound - normalizedCoord.y - 1) + '.jpg';
},
tileSize: new google.maps.Size(256, 256),
maxZoom: 9,
minZoom: 0,
radius: 1738000,
name: 'Moon'
});
map.mapTypes.set('moon', moonMapType);
map.setMapTypeId('moon');
}
// Normalizes the coords that tiles repeat across the x axis (horizontally)
// like the standard Google map tiles.
function getNormalizedCoord(coord, zoom) {
var y = coord.y;
var x = coord.x;
// tile range in one direction range is dependent on zoom level
// 0 = 1 tile, 1 = 2 tiles, 2 = 4 tiles, 3 = 8 tiles, etc
var tileRange = 1 << zoom;
// don't repeat across y-axis (vertically)
if (y < 0 || y >= tileRange) {
return null;
}
// repeat across x-axis
if (x < 0 || x >= tileRange) {
x = (x % tileRange + tileRange) % tileRange;
}
return {x: x, y: y};
}
/* Always set the map height explicitly to define the size of the div
* element that contains the map. */
#map {
height: 100%;
}
/* Optional: Makes the sample page fill the window. */
html, body {
height: 100%;
margin: 0;
padding: 0;
}
<div id="map"></div>
<!-- Replace the value of the key parameter with your own API key. -->
<script
async
defer
src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCkUOdZ5y7hMm0yrcCQoCvLwzdM6M8s5qk&callback=initMap">
</script>
https://jsfiddle.net/dobleuber/319kgLh4/
It works perfect, but I would like to create the same thing with react using react-google-maps.
I looked out in the react-google-maps code but I only see getters no setters for the map props:
getMapTypeId, getStreetView, ect.
Is there any way to achieve this without modify the react-google-maps code?
Thanks in advance
use props mapTypeId="moon" in react-google-maps
I've found a better way to solve this that preserve the changes on re-render, leaving it here to anyone who comes here.
there is an onLoad function that exposes a map instance, we can use this to set mapTypeId instead of passing it as an option. In this way, if the user changes the map type later, it will preserve the changes on re-render.
<GoogleMap
onLoad={(map) => {
map.setMapTypeId('moon');
}}
/>

Raycaster display a Render

I use Threejs and I would like to create a function to display a descriptive card (Render() of Satllite.js) when I click on a Sphere. (Satellite.js)
/********** Imports **********/
import React, { PureComponent } from 'react';
import * as THREE from 'three';
import satData from '../data/sat.json';
export class Satellites extends PureComponent {
constructor(props) {
super(props)
this.state = { open: true }
this.x = {};
this.mouse = {};
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
}
onDocumentMouseDown = event => {
event.preventDefault()
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
// find intersections
this.raycaster.setFromCamera(this.mouse, this.props.camera)
this.intersects = this.raycaster.intersectObjects(
this.scene.className("card"),
false
)
if (this.intersects.length > 0) {
if (this.intersects[0].object.callback)
this.intersects[0].object.callback()
this.intersects[0].className(".card")
this.particle = new THREE.Sprite(this.particleMaterial)
this.particle.position.copy(this.intersects[0].point)
this.particle.scale.x = this.particle.scale.y = 16
this.props.scene.add(this.particle)
}
}
componentDidMount() {
// Satellite Sphere
// this.geometry = new THREE.SphereGeometry( this.props.data.r, 32, 32 );
this.geometry = new THREE.SphereGeometry(10, 32, 32)
this.material = new THREE.MeshBasicMaterial({ color: 0xffff00 })
this.sphere = new THREE.Mesh(this.geometry, this.material)
this.sphere.callback = function() {
console.log('Toto!')
}
this.sphere.position.set(50, 50, 50)
this.props.scene.add(this.sphere)
document.addEventListener('mousedown', this.onDocumentMouseDown, true)
}
// componentDidUpdate() {
// // update satelite pos.
// const radius = 10;
// const scale = radius * 1;
// this.sphere.scale.x = scale;
// this.sphere.scale.y = scale;
// this.sphere.scale.z = scale;
// }
componentWillUnmount() {
document.removeEventListener('mousedown', this.onDocumentMouseDown);
}
render() {
return (
<div>
{satData.map((satDetail, index) => {
return <div key={index} className="card">
<h2>{satDetail.satName.toUpperCase()}</h2>
<div className="cardImg" >
<img src={satDetail.satImg} alt={satDetail.satAlt} />
</div>
<div>
<p>Altitude : <span>{satDetail.alt}</span> km</p>
<p>Longitude : <span>{satDetail.long}</span> °</p>
<p>Latitude : <span>{satDetail.lat}</span> °</p>
</div>
<button onClick={this.closeModal}>Fermer</button>
</div>
})}
</div>
)
}
}
More details here
I got the code of a tutorial that show white squares but I would like to display my div ".card" which is in the "Render"
What is the method ?
Thanks for help !
This answer involves many topics combined, basically you need:
Raycast the mouse click and find the object intersection,
Get info by picked object,
Display it if object was selected, or hide if nothing was picked by mouse.
Test it here: http://jsfiddle.net/mmalex/9k4qbL8s/
let cardShown = false;
function showCard(userText) {
var divElement = $("#card");
if (divElement) {
if (!cardShown) {
divElement.css({
display: "block",
opacity: 0,
height: "0px"
});
}
divElement.text("Object color: " + userText);
if (!cardShown) {
setTimeout(function() {
divElement.css({
opacity: 1,
height: "16px"
});
}, 25);
}
cardShown = true;
}
}
function hideCard() {
var divElement = $("#card");
if (divElement) {
divElement.css({
height: "0px",
opacity: 0
});
cardShown = false;
}
}
var scene = new THREE.Scene();
var raycaster = new THREE.Raycaster();
//create some camera
camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.x = 5;
camera.position.y = 5;
camera.position.z = 5;
camera.lookAt(0, 0, 0);
var controls = new THREE.OrbitControls(camera);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(new THREE.Color(0x595959));
document.body.appendChild(renderer.domElement);
// white spotlight shining from the side, casting a shadow
var spotLight = new THREE.SpotLight(0xffffff, 2.5, 25, Math.PI / 6);
spotLight.position.set(4, 10, 7);
scene.add(spotLight);
// collect objects for raycasting,
// for better performance don't raytrace all scene
var clickableObjects = [];
var colors = new RayysWebColors();
for (let k = 0; k < 12; k++) {
var size = 0.5;
var geometry = new THREE.BoxGeometry(size, 0.2, size);
var randomColor = colors.pickRandom();
var material = new THREE.MeshPhongMaterial({
color: randomColor.hex,
transparent: true,
opacity: 0.75
});
var cube = new THREE.Mesh(geometry, material);
cube.userData.userText = randomColor.name;
cube.applyMatrix(new THREE.Matrix4().makeTranslation(k % 3, 0, Math.floor(k / 3) - 1));
scene.add(cube);
clickableObjects.push(cube);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
// this will be 2D coordinates of the current mouse position, [0,0] is middle of the screen.
var mouse = new THREE.Vector2();
var clickedObj; // this objects is hovered at the moment
// Following two functions will convert mouse coordinates
// from screen to three.js system (where [0,0] is in the middle of the screen)
function updateMouseCoords(event, coordsObj) {
coordsObj.x = ((event.clientX - renderer.domElement.offsetLeft + 0.5) / window.innerWidth) * 2 - 1;
coordsObj.y = -((event.clientY - renderer.domElement.offsetTop + 0.5) / window.innerHeight) * 2 + 1;
}
function onMouseUp(event) {
updateMouseCoords(event, mouse);
latestMouseProjection = undefined;
clickedObj = undefined;
raycaster.setFromCamera(mouse, camera); {
var intersects = raycaster.intersectObjects(clickableObjects);
if (intersects.length > 0) {
latestMouseProjection = intersects[0].point;
clickedObj = intersects[0].object;
showCard(clickedObj.userData.userText);
} else {
clickedObj = undefined;
hideCard();
}
}
}
window.addEventListener('mouseup', onMouseUp, false);
animate();

Trying to place two d3 line graphs inside one Ionic2 page

I am trying to have two (or more) similiar graph on one page inside an Ionic2 app. I use d3-ng2-service for wrapping the d3 types for Angular2. My problem is the following: When I try to place the two graphs in two different div elements each inside their respective custom element the drawing fails for both. When I did select the first div in the page the second graph overrides the first one, but it does get drawn.
Is there a clever way to place graphs more the the one graph? The examples always give the outer container a unique id, which is, what I try to do too:
import { Component, Input, OnInit, ElementRef } from '#angular/core';
import { D3Service, D3, Selection, ScaleLinear, ScaleTime, Axis, Line } from 'd3-ng2-service'; // <-- import the D3 Service, the type alias for the d3 variable and the Selection interface
#Component({
selector: 'd3-test-app',
templateUrl: 'd3-test-app.html',
providers: [D3Service],
})
export class D3TestAppComponent {
//Time is on x-axis, value is on y-axis
#Input('timeSeries') timeSeries: Array<{isoDate: string | Date | number | {valueOf(): number}, value: number}>;
#Input('ref') ref: string;
/* the size input defines, how the component is drawn */
#Input('size') size: string;
private d3: D3;
private margin: {top: number, right: number, bottom: number, left: number};
private width: number;
private height: number;
private d3ParentElement: Selection<any, any, any, any>; // <-- Use the Selection interface (very basic here for illustration only)
constructor(element: ElementRef,
d3Service: D3Service) { // <-- pass the D3 Service into the constructor
this.d3 = d3Service.getD3(); // <-- obtain the d3 object from the D3 Service
this.d3ParentElement = element.nativeElement;
}
ngOnInit() {
let x: ScaleTime<number, number>;
let y: ScaleLinear<number, number>;
let minDate: number;
let maxDate: number;
let minValue: number = 0;
let maxValue: number;
// set the dimensions and margins of the graph
switch (this.size) {
case "large":
this.margin = {top: 20, right: 20, bottom: 30, left: 50};
this.width = 640 - this.margin.left - this.margin.right;
this.height = 480 - this.margin.top - this.margin.bottom;
break;
case "medium":
this.margin = {top: 20, right: 0, bottom: 20, left: 20};
//golden ratio
this.width = 420 - this.margin.left - this.margin.right;
this.height = 260 - this.margin.top - this.margin.bottom;
break;
case "small":
this.margin = {top: 2, right: 2, bottom: 3, left: 5};
this.width = 120 - this.margin.left - this.margin.right;
this.height = 80 - this.margin.top - this.margin.bottom;
break;
default:
this.margin = {top: 20, right: 20, bottom: 30, left: 50};
this.width = 640 - this.margin.left - this.margin.right;
this.height = 480 - this.margin.top - this.margin.bottom;
}
// ...
if (this.d3ParentElement !== null) {
let d3 = this.d3; // <-- for convenience use a block scope variable
//THIS FAILS...
let selector: string = '#' + this.ref + ' .graphContainer';
console.log(selector);
let svg = d3.select( selector).append("svg")
.attr("width", this.width + this.margin.left + this.margin.right)
.attr("height", this.height + this.margin.top + this.margin.bottom)
.append("g")
.attr("transform",
"translate(" + this.margin.left + "," + this.margin.top + ")");
this.timeSeries.forEach((d) => {
d.isoDate = +d3.isoParse(d.isoDate as string);
d.value = +d.value;
if (minDate == null || minDate >= d.isoDate) {
minDate = d.isoDate as number;
}
if (maxDate == null || maxDate <= d.isoDate) {
maxDate = d.isoDate as number;
}
// if (minValue == null || minValue >= d.value) {
// minValue = d.value as number;
// }
if (maxValue == null || maxValue <= d.value) {
maxValue = d.value as number;
}
});
// TODO magic numbers to real min max
x = d3.scaleTime().domain([minDate, maxDate]).range([0,this.width]);
y = d3.scaleLinear().domain([0, maxValue]).range([this.height, 0]);
let xAxis: Axis<number | Date | {valueOf() : number;}> = d3.axisBottom(x);
let yAxis: Axis<number | {valueOf(): number;}> = d3.axisLeft(y);
let valueLine: Line<{isoDate: number; value: number}> = d3.line<{ isoDate: number; value: number }>()
.x(function (d) { return x(d.isoDate)})
.y(function (d) { return y(d.value)});
// Add the valueline path.
svg.append("path")
.data([this.timeSeries as {isoDate: number, value: number}[]])
.attr("class", "line")
.attr("d", valueLine);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + this.height + ")")
.call(xAxis)
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
}
}
myParser() : (string) => Date {
return this.d3.utcParse("%Y-%m-%dT%H:%M:%S.%LZ");
}
}
The HTML:
<div class='graphContainer'>
</div>
The HTML file where the custom component is used:
<ion-header>
<ion-navbar #dashboardNav>
<ion-title>Dashboard</ion-title>
<button ion-button menuToggle="favMenu" right>
<ion-icon name="menu"></ion-icon>
</button>
</ion-navbar>
</ion-header>
<ion-content>
<ion-item *ngFor="let entry of dashboard">
{{ entry.name }}
<d3-test-app [id]='entry.name' [timeSeries]='entry.timeSeries' [ref]='entry.name' size='medium'></d3-test-app>
</ion-item>
</ion-content>
Hard to debug without seeing a stack trace but it looks like this is failing because of how you select the element. I will base my answer on that assumption.
Querying by ID is handy when you have to look inside the DOM for a specific element and when you are sure there is only one element with that ID. Since you are inside an Angular element you already have the reference you need, it's the element itself, no need to dynamically create ID references.
I am not an expert in ng2 at all, but take a look a at how to select the raw element in Angular and choose the best approach for the framework. Say you go for something like the example shown on this answer:
constructor(public element: ElementRef) {
this.element.nativeElement // <- your direct element reference
}
NB - looks like there are various way of achieving this... not sure this is the best/correct one, but the goal is to get the ref anyway
Then simply select it via the D3 dsl like you are already doing by passing that raw reference
// At this point this.element.nativeElement
// should contain the raw element reference
if (this.d3ParentElement !== null) {
const { d3, element } = this; // <-- for convenience use block scope variables
const svg = d3.select(element.nativeElement).append("svg")...

Resources